package syntax import ( "testing" "codeberg.org/turbo-editors/turbo-core/theme" ) // classAt returns the class covering a rune column on a line, and whether any // span covers it at all. It is how nearly every test below asks its question. func classAt(spans [][]Span, line, col int) (Class, bool) { if line < 0 || line >= len(spans) { return 0, false } for _, s := range spans[line] { if col >= s.Start && col < s.End { return s.Class, true } } return 0, false } func TestLanguageOfRecognisesAFileByItsExtension(t *testing.T) { tests := map[string]Language{ "settings.toml": LanguageTOML, ".turbo-go/settings.toml": LanguageTOML, "Theme.TOML": LanguageTOML, "README.md": LanguageMarkdown, "notes.markdown": LanguageMarkdown, "app.js": LanguageJavaScript, "module.mjs": LanguageJavaScript, "legacy.cjs": LanguageJavaScript, "index.html": LanguageHTML, "old.HTM": LanguageHTML, "install.sh": LanguageBash, "script.bash": LanguageBash, "prompt.zsh": LanguageBash, "Makefile": LanguageNone, "go.mod": LanguageNone, "noextension": LanguageNone, "weird.md.backup": LanguageNone, "": LanguageNone, } for path, want := range tests { if got := LanguageOf(path, ""); got != want { t.Errorf("LanguageOf(%q, \"\") = %v, want %v", path, got, want) } } } func TestAShebangMakesAFileWithNoExtensionAScript(t *testing.T) { shebangs := []string{ "#!/bin/sh", "#!/bin/bash", "#!/usr/bin/env bash", "#!/usr/bin/env -S bash -e", "#! /bin/zsh", "#!/usr/bin/dash", "#!/bin/ksh", } for _, first := range shebangs { t.Run(first, func(t *testing.T) { if got := LanguageOf("configure", first); got != LanguageBash { t.Errorf("LanguageOf(\"configure\", %q) = %v, want bash", first, got) } }) } } func TestAShebangForSomethingElseIsNotAScript(t *testing.T) { notShells := []string{ "#!/usr/bin/env python3", "#!/usr/bin/perl", "#!/usr/bin/env node", "# not a shebang at all, just a comment about bash", "", "#!", } for _, first := range notShells { t.Run(first, func(t *testing.T) { if got := LanguageOf("configure", first); got != LanguageNone { t.Errorf("LanguageOf(\"configure\", %q) = %v, want none", first, got) } }) } } func TestTheExtensionWinsOverTheFirstLine(t *testing.T) { // A .md file whose first line happens to be a shebang is still Markdown. if got := LanguageOf("README.md", "#!/bin/sh"); got != LanguageMarkdown { t.Errorf("LanguageOf() = %v, want markdown", got) } } func TestAnUncolouredLanguageStillGivesOneEntryPerLine(t *testing.T) { // The editor indexes the result by line number without checking, so a file // this package cannot colour must still be as long as the file. spans := Highlight(LanguageNone, "one\ntwo\nthree\n") if len(spans) != 4 { t.Fatalf("Highlight() returned %d lines, want 4", len(spans)) } for i, line := range spans { if len(line) != 0 { t.Errorf("line %d was coloured: %v", i, line) } } } func TestLanguageString(t *testing.T) { tests := map[Language]string{ LanguageTOML: "toml", LanguageMarkdown: "markdown", LanguageJavaScript: "javascript", LanguageHTML: "html", LanguageBash: "bash", // A language this package never heard of prints its own name, because // that name is what a snippets file writes down. Language("rust"): "rust", LanguageNone: "none", } for language, want := range tests { if got := language.String(); got != want { t.Errorf("%q.String() = %q, want %q", string(language), got, want) } } } func TestClassStyleKeysAreTheThemeKeys(t *testing.T) { tests := map[Class]string{ ClassKeyword: theme.KeySyntaxKeyword, ClassString: theme.KeySyntaxString, ClassComment: theme.KeySyntaxComment, ClassPunctuation: theme.KeySyntaxPunctuation, } for class, want := range tests { if got := class.StyleKey(); got != want { t.Errorf("%v.StyleKey() = %q, want %q", class, got, want) } } if got := Class(200).StyleKey(); got != theme.KeySyntaxIdentifier { t.Errorf("an out-of-range class gives %q, want the identifier key", got) } } func TestClassString(t *testing.T) { if got := ClassKeyword.String(); got != "keyword" { t.Errorf("ClassKeyword.String() = %q", got) } if got := Class(200).String(); got != "unknown" { t.Errorf("an out-of-range class prints %q, want %q", got, "unknown") } } // The registry is the extension point an editor built on this library uses to // add its own language. The tests below drive it exactly as Turbo Go and Turbo // Rust do, and restore the registry afterwards so they leave nothing behind for // the tests in the other files here. // registerForTest adds a language and removes it again when the test ends. func registerForTest(t *testing.T, d Definition) { t.Helper() previous, existed := registry[d.Language] Register(d) t.Cleanup(func() { if existed { registry[d.Language] = previous return } delete(registry, d.Language) }) } // wholeLineAs colours every line of a document in one class, which is enough to // tell "this scanner ran" from "it did not". func wholeLineAs(class Class) func(string) [][]Span { return func(src string) [][]Span { return ScanLines(src, func(line []rune, carry struct{}) ([]Span, struct{}) { s := NewLineScanner(line) s.TakeRest(class) return s.Spans(), carry }) } } func TestARegisteredLanguageIsRecognisedByItsExtension(t *testing.T) { registerForTest(t, Definition{ Language: "rust", Extensions: []string{".rs"}, Highlight: wholeLineAs(ClassKeyword), }) if got := LanguageOf("main.rs", ""); got != "rust" { t.Errorf("LanguageOf(\"main.rs\", \"\") = %q, want rust", got) } } func TestARegisteredLanguageIsColouredByItsOwnScanner(t *testing.T) { registerForTest(t, Definition{ Language: "rust", Extensions: []string{".rs"}, Highlight: wholeLineAs(ClassKeyword), }) class, ok := classAt(Highlight("rust", "fn main() {}"), 0, 0) if !ok || class != ClassKeyword { t.Errorf("Highlight(\"rust\", …) gave %v (covered: %v), want keyword", class, ok) } } func TestARegisteredLanguageIsRecognisedByItsShebang(t *testing.T) { registerForTest(t, Definition{ Language: "python", Extensions: []string{".py"}, Shebangs: []string{"python3"}, Highlight: wholeLineAs(ClassComment), }) if got := LanguageOf("script", "#!/usr/bin/env python3"); got != "python" { t.Errorf("LanguageOf(\"script\", …) = %q, want python", got) } } func TestRegisteringALanguageTwiceReplacesIt(t *testing.T) { // An editor overriding a built-in is the point: the second registration is // the more specific statement, so it wins. registerForTest(t, Definition{ Language: LanguageMarkdown, Extensions: []string{".md"}, Highlight: wholeLineAs(ClassNumber), }) class, ok := classAt(Highlight(LanguageMarkdown, "# Title"), 0, 0) if !ok || class != ClassNumber { t.Errorf("Highlight(markdown, …) gave %v (covered: %v), want the replacement scanner", class, ok) } } func TestRegisteredListsWhatThisPackageColours(t *testing.T) { got := Registered() // Presence rather than an exact list: the examples in this package's test // binary register a language of their own, and a test that counted would // pass or fail on the order the two happened to run in. for _, want := range []Language{LanguageBash, LanguageHTML, LanguageJavaScript, LanguageMarkdown, LanguageTOML} { if !containsLanguage(got, want) { t.Errorf("Registered() = %v, want it to include %q", got, want) } } for i := 1; i < len(got); i++ { if got[i-1] >= got[i] { t.Fatalf("Registered() = %v, want it sorted", got) } } } // containsLanguage reports whether a language is in a list. func containsLanguage(list []Language, want Language) bool { for _, language := range list { if language == want { return true } } return false } func TestALanguageWithNoScannerStillGivesOneEntryPerLine(t *testing.T) { // A Definition may name a language without colouring it, which is what an // editor does when it wants a file recognised but has no scanner yet. registerForTest(t, Definition{Language: "plain", Extensions: []string{".plain"}}) spans := Highlight("plain", "one\ntwo\n") if len(spans) != 3 { t.Fatalf("Highlight() returned %d lines, want 3", len(spans)) } } func TestTheEightBuiltInLanguagesAreRegistered(t *testing.T) { // The five the library started with, plus the three ticket 8 added. A // language dropped by accident would otherwise show up as a file quietly // going plain, which nothing else here notices. got := Registered() for _, want := range []Language{ LanguageBash, LanguageDockerfile, LanguageHTML, LanguageJavaScript, LanguageMarkdown, LanguageTOML, LanguageXML, LanguageYAML, } { if !containsLanguage(got, want) { t.Errorf("Registered() = %v, want it to include %q", got, want) } } } func TestEveryBuiltInLanguageActuallyColoursSomething(t *testing.T) { // A Definition with a nil Highlight registers a name and colours nothing, // which looks identical to a scanner that is simply quiet. samples := map[Language]string{ LanguageBash: "if true; then echo hi; fi", LanguageDockerfile: "FROM alpine", LanguageHTML: "

hi

", LanguageJavaScript: "const a = 1;", LanguageMarkdown: "# Title", LanguageTOML: `a = "b"`, LanguageXML: "", LanguageYAML: "a: 1", } for language, sample := range samples { t.Run(string(language), func(t *testing.T) { if spans := Highlight(language, sample); len(spans[0]) == 0 { t.Errorf("Highlight(%q, %q) coloured nothing", language, sample) } }) } }