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