| 🛟 Updated. 28d5985 k33g 18h ago | 1 | package syntax |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // tomlLine returns the spans of one line of a TOML document, as "text:class" |
| 9 | // strings, which is far easier to read in a failure than a slice of structs. |
| 10 | func tomlLine(t *testing.T, src string, line int) []string { |
| 11 | t.Helper() |
| 12 | |
| 13 | spans := Highlight(LanguageTOML, src) |
| 14 | if line >= len(spans) { |
| 15 | t.Fatalf("line %d is past the end of a %d-line document", line, len(spans)) |
| 16 | } |
| 17 | |
| 18 | text := []rune(strings.Split(src, "\n")[line]) |
| 19 | out := make([]string, 0, len(spans[line])) |
| 20 | for _, s := range spans[line] { |
| 21 | out = append(out, string(text[s.Start:s.End])+":"+s.Class.String()) |
| 22 | } |
| 23 | return out |
| 24 | } |
| 25 | |
| 26 | // wantSpans compares the spans of a line against what they should be. |
| 27 | func wantSpans(t *testing.T, src string, line int, want ...string) { |
| 28 | t.Helper() |
| 29 | |
| 30 | got := tomlLine(t, src, line) |
| 31 | if len(got) != len(want) { |
| 32 | t.Fatalf("line %d of %q gave %v, want %v", line, src, got, want) |
| 33 | } |
| 34 | for i := range want { |
| 35 | if got[i] != want[i] { |
| 36 | t.Errorf("line %d span %d = %q, want %q (whole line: %v)", line, i, got[i], want[i], got) |
| 37 | } |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | func TestTOMLColoursAComment(t *testing.T) { |
| 42 | wantSpans(t, "# just a note", 0, "# just a note:comment") |
| 43 | } |
| 44 | |
| 45 | func TestTOMLColoursATableHeader(t *testing.T) { |
| 46 | wantSpans(t, "[editor]", 0, "[:punctuation", "editor:type", "]:punctuation") |
| 47 | } |
| 48 | |
| 49 | func TestTOMLColoursAnArrayOfTablesHeader(t *testing.T) { |
| 50 | wantSpans(t, "[[servers]]", 0, |
| 51 | "[:punctuation", "[:punctuation", "servers:type", "]:punctuation", "]:punctuation") |
| 52 | } |
| 53 | |
| 54 | func TestTOMLTellsAKeyFromItsValue(t *testing.T) { |
| 55 | wantSpans(t, `theme = "turbo-dark"`, 0, "theme:identifier", "=:operator", `"turbo-dark":string`) |
| 56 | } |
| 57 | |
| 58 | func TestTOMLColoursABooleanAsAConstant(t *testing.T) { |
| 59 | wantSpans(t, "autosave = true", 0, "autosave:identifier", "=:operator", "true:constant") |
| 60 | wantSpans(t, "autosave = false", 0, "autosave:identifier", "=:operator", "false:constant") |
| 61 | } |
| 62 | |
| 63 | func TestTOMLColoursNumbers(t *testing.T) { |
| 64 | for _, value := range []string{"42", "-17", "+3", "3.14", "1_000_000", "0x1f", "1e6", "inf", "nan"} { |
| 65 | t.Run(value, func(t *testing.T) { |
| 66 | got := tomlLine(t, "n = "+value, 0) |
| 67 | last := got[len(got)-1] |
| 68 | if !strings.HasSuffix(last, ":number") { |
| 69 | t.Errorf("%q was coloured %q, want a number", value, last) |
| 70 | } |
| 71 | }) |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | func TestTOMLColoursDatesAsNumbers(t *testing.T) { |
| 76 | // A date is not a string and not a name; the number class is where it |
| 77 | // reads correctly without inventing a class no theme sets. |
| 78 | wantSpans(t, "when = 1979-05-27T07:32:00Z", 0, |
| 79 | "when:identifier", "=:operator", "1979-05-27T07:32:00Z:number") |
| 80 | } |
| 81 | |
| 82 | func TestTOMLColoursTheFourKindsOfString(t *testing.T) { |
| 83 | wantSpans(t, `a = "basic"`, 0, "a:identifier", "=:operator", `"basic":string`) |
| 84 | wantSpans(t, `a = 'literal'`, 0, "a:identifier", "=:operator", `'literal':string`) |
| 85 | wantSpans(t, `a = """one line"""`, 0, "a:identifier", "=:operator", `"""one line""":string`) |
| 86 | wantSpans(t, `a = '''one line'''`, 0, "a:identifier", "=:operator", `'''one line''':string`) |
| 87 | } |
| 88 | |
| 89 | func TestTOMLColoursAMultiLineStringOnEveryLineItCovers(t *testing.T) { |
| 90 | src := "text = \"\"\"\nfirst\nsecond\n\"\"\"\nafter = 1\n" |
| 91 | |
| 92 | wantSpans(t, src, 0, "text:identifier", "=:operator", `""":string`) |
| 93 | wantSpans(t, src, 1, "first:string") |
| 94 | wantSpans(t, src, 2, "second:string") |
| 95 | wantSpans(t, src, 3, `""":string`) |
| 96 | // The line after the string must be back to normal scanning. |
| 97 | wantSpans(t, src, 4, "after:identifier", "=:operator", "1:number") |
| 98 | } |
| 99 | |
| 100 | func TestAHashInsideAStringIsNotAComment(t *testing.T) { |
| 101 | wantSpans(t, `colour = "#ff8700"`, 0, "colour:identifier", "=:operator", `"#ff8700":string`) |
| 102 | } |
| 103 | |
| 104 | func TestAnEqualsInsideAStringIsNotAnOperator(t *testing.T) { |
| 105 | wantSpans(t, `a = "x = y"`, 0, "a:identifier", "=:operator", `"x = y":string`) |
| 106 | } |
| 107 | |
| 108 | func TestAnEscapedQuoteDoesNotEndABasicString(t *testing.T) { |
| 109 | wantSpans(t, `a = "he said \"no\""`, 0, "a:identifier", "=:operator", `"he said \"no\"":string`) |
| 110 | } |
| 111 | |
| 112 | func TestABackslashDoesNotEscapeInALiteralString(t *testing.T) { |
| 113 | // The whole point of a literal string: '\' is a backslash, and the quote |
| 114 | // after it still closes the string. |
| 115 | wantSpans(t, `path = 'C:\temp\'`, 0, "path:identifier", "=:operator", `'C:\temp\':string`) |
| 116 | } |
| 117 | |
| 118 | func TestAnUnterminatedStringIsColouredToTheEndOfTheLine(t *testing.T) { |
| 119 | // A string being typed is unterminated most of the time it is being typed; |
| 120 | // giving up on it would make the colours flicker off at every keystroke. |
| 121 | wantSpans(t, `a = "half written`, 0, "a:identifier", "=:operator", `"half written:string`) |
| 122 | } |
| 123 | |
| 124 | func TestTOMLColoursEveryPartOfADottedKey(t *testing.T) { |
| 125 | wantSpans(t, "a.b.c = 1", 0, |
| 126 | "a:identifier", ".:punctuation", "b:identifier", ".:punctuation", "c:identifier", |
| 127 | "=:operator", "1:number") |
| 128 | } |
| 129 | |
| 130 | func TestTOMLColoursAnArrayValue(t *testing.T) { |
| 131 | wantSpans(t, "list = [1, 2]", 0, |
| 132 | "list:identifier", "=:operator", "[:punctuation", |
| 133 | "1:number", ",:punctuation", "2:number", "]:punctuation") |
| 134 | } |
| 135 | |
| 136 | func TestTOMLColoursAnInlineTable(t *testing.T) { |
| 137 | wantSpans(t, `fg = { c = "red" }`, 0, |
| 138 | "fg:identifier", "=:operator", "{:punctuation", |
| 139 | "c:identifier", "=:operator", `"red":string`, "}:punctuation") |
| 140 | } |
| 141 | |
| 142 | func TestTOMLColoursAQuotedKey(t *testing.T) { |
| 143 | // Theme files are full of these: "syntax.keyword" = { … } |
| 144 | wantSpans(t, `"syntax.keyword" = "x"`, 0, |
| 145 | `"syntax.keyword":string`, "=:operator", `"x":string`) |
| 146 | } |
| 147 | |
| 148 | func TestTOMLGivesOneEntryPerLine(t *testing.T) { |
| 149 | src := "[a]\nb = 1\n\nc = 2\n" |
| 150 | |
| 151 | if got := len(Highlight(LanguageTOML, src)); got != 5 { |
| 152 | t.Errorf("Highlight() returned %d lines for a 5-line document", got) |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | func TestTOMLSpansStayInsideTheirLine(t *testing.T) { |
| 157 | src := "# a\n[t]\nk = \"\"\"\nspill\n\"\"\"\n" |
| 158 | lines := strings.Split(src, "\n") |
| 159 | |
| 160 | for i, spans := range Highlight(LanguageTOML, src) { |
| 161 | width := len([]rune(lines[i])) |
| 162 | for _, s := range spans { |
| 163 | if s.Start < 0 || s.End > width || s.End <= s.Start { |
| 164 | t.Errorf("line %d (%d runes) has span %d-%d", i, width, s.Start, s.End) |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | func TestTOMLColoursTheSettingsFileItself(t *testing.T) { |
| 171 | // The file this whole feature exists to let people edit. |
| 172 | src := "[editor]\n# which theme\ntheme = \"turbo-dark\" # ours\nautosave = false\n" |
| 173 | |
| 174 | wantSpans(t, src, 0, "[:punctuation", "editor:type", "]:punctuation") |
| 175 | wantSpans(t, src, 1, "# which theme:comment") |
| 176 | wantSpans(t, src, 2, "theme:identifier", "=:operator", `"turbo-dark":string`, "# ours:comment") |
| 177 | wantSpans(t, src, 3, "autosave:identifier", "=:operator", "false:constant") |
| 178 | } |