turbo-editors/turbo-corepublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

highlight_test.go · 317 lines · 9.6 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 19h ago1package syntax
2
3import (
4 "testing"
5
📦 Turbo Core f3ade8d k33g 11h ago6 "rickub.com/turbo-editors/turbo-core/theme"
🛟 Updated. 28d5985 k33g 19h ago7)
8
9// classAt returns the class covering a rune column on a line, and whether any
10// span covers it at all. It is how nearly every test below asks its question.
11func classAt(spans [][]Span, line, col int) (Class, bool) {
12 if line < 0 || line >= len(spans) {
13 return 0, false
14 }
15 for _, s := range spans[line] {
16 if col >= s.Start && col < s.End {
17 return s.Class, true
18 }
19 }
20 return 0, false
21}
22
23func TestLanguageOfRecognisesAFileByItsExtension(t *testing.T) {
24 tests := map[string]Language{
25 "settings.toml": LanguageTOML,
26 ".turbo-go/settings.toml": LanguageTOML,
27 "Theme.TOML": LanguageTOML,
28 "README.md": LanguageMarkdown,
29 "notes.markdown": LanguageMarkdown,
30 "app.js": LanguageJavaScript,
31 "module.mjs": LanguageJavaScript,
32 "legacy.cjs": LanguageJavaScript,
33 "index.html": LanguageHTML,
34 "old.HTM": LanguageHTML,
35 "install.sh": LanguageBash,
36 "script.bash": LanguageBash,
37 "prompt.zsh": LanguageBash,
38 "Makefile": LanguageNone,
39 "go.mod": LanguageNone,
40 "noextension": LanguageNone,
41 "weird.md.backup": LanguageNone,
42 "": LanguageNone,
43 }
44
45 for path, want := range tests {
46 if got := LanguageOf(path, ""); got != want {
47 t.Errorf("LanguageOf(%q, \"\") = %v, want %v", path, got, want)
48 }
49 }
50}
51
52func TestAShebangMakesAFileWithNoExtensionAScript(t *testing.T) {
53 shebangs := []string{
54 "#!/bin/sh",
55 "#!/bin/bash",
56 "#!/usr/bin/env bash",
57 "#!/usr/bin/env -S bash -e",
58 "#! /bin/zsh",
59 "#!/usr/bin/dash",
60 "#!/bin/ksh",
61 }
62
63 for _, first := range shebangs {
64 t.Run(first, func(t *testing.T) {
65 if got := LanguageOf("configure", first); got != LanguageBash {
66 t.Errorf("LanguageOf(\"configure\", %q) = %v, want bash", first, got)
67 }
68 })
69 }
70}
71
72func TestAShebangForSomethingElseIsNotAScript(t *testing.T) {
73 notShells := []string{
74 "#!/usr/bin/env python3",
75 "#!/usr/bin/perl",
76 "#!/usr/bin/env node",
77 "# not a shebang at all, just a comment about bash",
78 "",
79 "#!",
80 }
81
82 for _, first := range notShells {
83 t.Run(first, func(t *testing.T) {
84 if got := LanguageOf("configure", first); got != LanguageNone {
85 t.Errorf("LanguageOf(\"configure\", %q) = %v, want none", first, got)
86 }
87 })
88 }
89}
90
91func TestTheExtensionWinsOverTheFirstLine(t *testing.T) {
92 // A .md file whose first line happens to be a shebang is still Markdown.
93 if got := LanguageOf("README.md", "#!/bin/sh"); got != LanguageMarkdown {
94 t.Errorf("LanguageOf() = %v, want markdown", got)
95 }
96}
97
98func TestAnUncolouredLanguageStillGivesOneEntryPerLine(t *testing.T) {
99 // The editor indexes the result by line number without checking, so a file
100 // this package cannot colour must still be as long as the file.
101 spans := Highlight(LanguageNone, "one\ntwo\nthree\n")
102
103 if len(spans) != 4 {
104 t.Fatalf("Highlight() returned %d lines, want 4", len(spans))
105 }
106 for i, line := range spans {
107 if len(line) != 0 {
108 t.Errorf("line %d was coloured: %v", i, line)
109 }
110 }
111}
112
113func TestLanguageString(t *testing.T) {
114 tests := map[Language]string{
115 LanguageTOML: "toml", LanguageMarkdown: "markdown",
116 LanguageJavaScript: "javascript", LanguageHTML: "html", LanguageBash: "bash",
117 // A language this package never heard of prints its own name, because
118 // that name is what a snippets file writes down.
119 Language("rust"): "rust",
120 LanguageNone: "none",
121 }
122 for language, want := range tests {
123 if got := language.String(); got != want {
124 t.Errorf("%q.String() = %q, want %q", string(language), got, want)
125 }
126 }
127}
128
129func TestClassStyleKeysAreTheThemeKeys(t *testing.T) {
130 tests := map[Class]string{
131 ClassKeyword: theme.KeySyntaxKeyword,
132 ClassString: theme.KeySyntaxString,
133 ClassComment: theme.KeySyntaxComment,
134 ClassPunctuation: theme.KeySyntaxPunctuation,
135 }
136
137 for class, want := range tests {
138 if got := class.StyleKey(); got != want {
139 t.Errorf("%v.StyleKey() = %q, want %q", class, got, want)
140 }
141 }
142 if got := Class(200).StyleKey(); got != theme.KeySyntaxIdentifier {
143 t.Errorf("an out-of-range class gives %q, want the identifier key", got)
144 }
145}
146
147func TestClassString(t *testing.T) {
148 if got := ClassKeyword.String(); got != "keyword" {
149 t.Errorf("ClassKeyword.String() = %q", got)
150 }
151 if got := Class(200).String(); got != "unknown" {
152 t.Errorf("an out-of-range class prints %q, want %q", got, "unknown")
153 }
154}
155
156// The registry is the extension point an editor built on this library uses to
157// add its own language. The tests below drive it exactly as Turbo Go and Turbo
158// Rust do, and restore the registry afterwards so they leave nothing behind for
159// the tests in the other files here.
160
161// registerForTest adds a language and removes it again when the test ends.
162func registerForTest(t *testing.T, d Definition) {
163 t.Helper()
164 previous, existed := registry[d.Language]
165 Register(d)
166 t.Cleanup(func() {
167 if existed {
168 registry[d.Language] = previous
169 return
170 }
171 delete(registry, d.Language)
172 })
173}
174
175// wholeLineAs colours every line of a document in one class, which is enough to
176// tell "this scanner ran" from "it did not".
177func wholeLineAs(class Class) func(string) [][]Span {
178 return func(src string) [][]Span {
179 return ScanLines(src, func(line []rune, carry struct{}) ([]Span, struct{}) {
180 s := NewLineScanner(line)
181 s.TakeRest(class)
182 return s.Spans(), carry
183 })
184 }
185}
186
187func TestARegisteredLanguageIsRecognisedByItsExtension(t *testing.T) {
188 registerForTest(t, Definition{
189 Language: "rust",
190 Extensions: []string{".rs"},
191 Highlight: wholeLineAs(ClassKeyword),
192 })
193
194 if got := LanguageOf("main.rs", ""); got != "rust" {
195 t.Errorf("LanguageOf(\"main.rs\", \"\") = %q, want rust", got)
196 }
197}
198
199func TestARegisteredLanguageIsColouredByItsOwnScanner(t *testing.T) {
200 registerForTest(t, Definition{
201 Language: "rust",
202 Extensions: []string{".rs"},
203 Highlight: wholeLineAs(ClassKeyword),
204 })
205
206 class, ok := classAt(Highlight("rust", "fn main() {}"), 0, 0)
207 if !ok || class != ClassKeyword {
208 t.Errorf("Highlight(\"rust\", …) gave %v (covered: %v), want keyword", class, ok)
209 }
210}
211
212func TestARegisteredLanguageIsRecognisedByItsShebang(t *testing.T) {
213 registerForTest(t, Definition{
214 Language: "python",
215 Extensions: []string{".py"},
216 Shebangs: []string{"python3"},
217 Highlight: wholeLineAs(ClassComment),
218 })
219
220 if got := LanguageOf("script", "#!/usr/bin/env python3"); got != "python" {
221 t.Errorf("LanguageOf(\"script\", …) = %q, want python", got)
222 }
223}
224
225func TestRegisteringALanguageTwiceReplacesIt(t *testing.T) {
226 // An editor overriding a built-in is the point: the second registration is
227 // the more specific statement, so it wins.
228 registerForTest(t, Definition{
229 Language: LanguageMarkdown,
230 Extensions: []string{".md"},
231 Highlight: wholeLineAs(ClassNumber),
232 })
233
234 class, ok := classAt(Highlight(LanguageMarkdown, "# Title"), 0, 0)
235 if !ok || class != ClassNumber {
236 t.Errorf("Highlight(markdown, …) gave %v (covered: %v), want the replacement scanner", class, ok)
237 }
238}
239
240func TestRegisteredListsWhatThisPackageColours(t *testing.T) {
241 got := Registered()
242
243 // Presence rather than an exact list: the examples in this package's test
244 // binary register a language of their own, and a test that counted would
245 // pass or fail on the order the two happened to run in.
246 for _, want := range []Language{LanguageBash, LanguageHTML, LanguageJavaScript, LanguageMarkdown, LanguageTOML} {
247 if !containsLanguage(got, want) {
248 t.Errorf("Registered() = %v, want it to include %q", got, want)
249 }
250 }
251 for i := 1; i < len(got); i++ {
252 if got[i-1] >= got[i] {
253 t.Fatalf("Registered() = %v, want it sorted", got)
254 }
255 }
256}
257
258// containsLanguage reports whether a language is in a list.
259func containsLanguage(list []Language, want Language) bool {
260 for _, language := range list {
261 if language == want {
262 return true
263 }
264 }
265 return false
266}
267
268func TestALanguageWithNoScannerStillGivesOneEntryPerLine(t *testing.T) {
269 // A Definition may name a language without colouring it, which is what an
270 // editor does when it wants a file recognised but has no scanner yet.
271 registerForTest(t, Definition{Language: "plain", Extensions: []string{".plain"}})
272
273 spans := Highlight("plain", "one\ntwo\n")
274
275 if len(spans) != 3 {
276 t.Fatalf("Highlight() returned %d lines, want 3", len(spans))
277 }
278}
279
280func TestTheEightBuiltInLanguagesAreRegistered(t *testing.T) {
281 // The five the library started with, plus the three ticket 8 added. A
282 // language dropped by accident would otherwise show up as a file quietly
283 // going plain, which nothing else here notices.
284 got := Registered()
285
286 for _, want := range []Language{
287 LanguageBash, LanguageDockerfile, LanguageHTML, LanguageJavaScript,
288 LanguageMarkdown, LanguageTOML, LanguageXML, LanguageYAML,
289 } {
290 if !containsLanguage(got, want) {
291 t.Errorf("Registered() = %v, want it to include %q", got, want)
292 }
293 }
294}
295
296func TestEveryBuiltInLanguageActuallyColoursSomething(t *testing.T) {
297 // A Definition with a nil Highlight registers a name and colours nothing,
298 // which looks identical to a scanner that is simply quiet.
299 samples := map[Language]string{
300 LanguageBash: "if true; then echo hi; fi",
301 LanguageDockerfile: "FROM alpine",
302 LanguageHTML: "<p>hi</p>",
303 LanguageJavaScript: "const a = 1;",
304 LanguageMarkdown: "# Title",
305 LanguageTOML: `a = "b"`,
306 LanguageXML: "<a/>",
307 LanguageYAML: "a: 1",
308 }
309
310 for language, sample := range samples {
311 t.Run(string(language), func(t *testing.T) {
312 if spans := Highlight(language, sample); len(spans[0]) == 0 {
313 t.Errorf("Highlight(%q, %q) coloured nothing", language, sample)
314 }
315 })
316 }
317}