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") }