turbo-editors/turbo-jspublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-js.git
git clone ssh://git@rickub.com/turbo-editors/turbo-js.git

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

📦 Turbo JS 91999d1 · on main · k33g · 11h ago
profile_test.go · 323 lines · 11.3 KBGo Blame HistoryRaw
  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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package jslang_test

import (
	"slices"
	"strings"
	"testing"

	"rickub.com/turbo-editors/turbo-core/profile"
	"rickub.com/turbo-editors/turbo-core/syntax"

	"rickub.com/turbo-editors/turbo-js/internal/jslang"
)

// fixedMenuHotKeys are the hot keys turbo-core's own menus take. The toolchain
// menu may not claim one of them, or one of the two would be unreachable from
// the keyboard and nothing would say so.
var fixedMenuHotKeys = []rune{'F', 'E', 'S', 'R', 'C', 'O', 'W', 'N', 'H'}

func TestProfileNamesTheEditor(t *testing.T) {
	p := jslang.Profile()

	if p.Name != "Turbo JS" {
		t.Errorf("Name = %q, want %q", p.Name, "Turbo JS")
	}
	if p.Slug != "turbo-js" {
		t.Errorf("Slug = %q, want %q", p.Slug, "turbo-js")
	}
	// The About box and the status bar read this out. It names the language,
	// not the runtime: JavaScript is what the files are written in, and Node
	// is what the toolchain runs them on.
	if p.Language != "JavaScript" {
		t.Errorf("Language = %q, want %q", p.Language, "JavaScript")
	}
}

func TestSlugDerivesEveryPath(t *testing.T) {
	p := jslang.Profile()

	if got := p.ProjectDir(); got != ".turbo-js" {
		t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-js")
	}
	for name, got := range map[string]string{
		"DirEnvVar":        p.DirEnvVar(),
		"ThemeDirEnvVar":   p.ThemeDirEnvVar(),
		"SnippetDirEnvVar": p.SnippetDirEnvVar(),
	} {
		if !strings.HasPrefix(got, "TURBO_JS_") {
			t.Errorf("%s() = %q, want a TURBO_JS_ prefix", name, got)
		}
	}
}

func TestThemeDirFollowsItsEnvironmentVariable(t *testing.T) {
	p := jslang.Profile()
	want := t.TempDir()
	t.Setenv(p.ThemeDirEnvVar(), want)

	if got := p.ThemeDir(); got != want {
		t.Errorf("ThemeDir() = %q, want %q", got, want)
	}
}

func TestToolsMenuHotKeyClashesWithNoFixedMenu(t *testing.T) {
	label := jslang.Profile().ToolsMenu

	hotKey, ok := hotKeyOf(label)
	if !ok {
		t.Fatalf("ToolsMenu = %q, which marks no hot key between tildes", label)
	}
	if slices.Contains(fixedMenuHotKeys, hotKey) {
		t.Errorf("ToolsMenu hot key %q is already taken by a fixed menu", hotKey)
	}
	if got := strings.ReplaceAll(label, "~", ""); got != "JavaScript" {
		t.Errorf("ToolsMenu reads %q once the tildes are removed, want %q", got, "JavaScript")
	}
}

// hotKeyOf returns the upper-case letter a menu label marks between tildes.
func hotKeyOf(label string) (rune, bool) {
	open := strings.Index(label, "~")
	if open < 0 {
		return 0, false
	}
	rest := label[open+1:]
	shut := strings.Index(rest, "~")
	if shut != 1 {
		return 0, false
	}
	return []rune(strings.ToUpper(rest))[0], true
}

func TestTheRootMarkerIsPackageJSON(t *testing.T) {
	// package.json is a Node project's boundary, and the nearest one going up
	// is the package being edited. main_test.go checks the walk from the
	// other side.
	if got := jslang.Profile().RootMarkers; !slices.Equal(got, []string{"package.json"}) {
		t.Errorf("RootMarkers = %v, want exactly [package.json]", got)
	}
}

func TestServerIsTypeScriptLanguageServerOverStdio(t *testing.T) {
	server := jslang.Profile().Server

	if server.Command != "typescript-language-server" {
		t.Errorf("Server.Command = %q, want %q", server.Command, "typescript-language-server")
	}
	// Without --stdio the server prints its usage and exits, which the
	// editor reports as a server that died at start-up.
	if !slices.Equal(server.Args, []string{"--stdio"}) {
		t.Errorf("Server.Args = %v, want exactly [--stdio]", server.Args)
	}
	if server.InstallHint == "" {
		t.Error("Server.InstallHint is empty; a missing server would say nothing useful")
	}
	// It is shown on the status bar, so it has to fit on a narrow line.
	if len(server.InstallHint) > 72 {
		t.Errorf("InstallHint is %d characters, too long for a status bar", len(server.InstallHint))
	}
	// The server does not depend on the typescript package it wraps, so a
	// hint that installs it alone leaves a server that starts and then says
	// tsserver is missing — and so does one that installs the *current*
	// typescript, which is the native TypeScript 7 and ships no tsserver.js.
	// The pin to 6 is the whole difference between completion and nothing.
	if !strings.Contains(server.InstallHint, "typescript-language-server") || !strings.HasSuffix(server.InstallHint, " typescript@6") {
		t.Errorf("InstallHint = %q, want it to install both the server and typescript@6", server.InstallHint)
	}
}

func TestServerArgsCannotBeAppendedToByACaller(t *testing.T) {
	first := jslang.ServerArgs()
	first = append(first, "--nonsense")

	if second := jslang.ServerArgs(); slices.Contains(second, "--nonsense") {
		t.Errorf("ServerArgs() = %v after a caller appended to an earlier result", second)
	}
}

func TestServerIsLookedForInTheSystemPrefixes(t *testing.T) {
	// `npm install -g` with a system-wide Node writes into /usr/local/bin, or
	// /opt/homebrew/bin under Homebrew on Apple silicon. Both are on PATH on
	// nearly every machine, and the editor searches them anyway for the one
	// where they are not.
	dirs := jslang.Profile().Server.Dirs

	for _, want := range []string{"/usr/local/bin", "/opt/homebrew/bin"} {
		if !slices.Contains(dirs, want) {
			t.Errorf("Server.Dirs = %v, want it to contain %s", dirs, want)
		}
	}
}

func TestServerDirsFollowTheVersionManagersVariables(t *testing.T) {
	// Each variable names the one directory its tool writes global binaries
	// into. Set, it is searched; unset, the tool is not in use and nothing is
	// guessed for it.
	t.Setenv("NVM_BIN", "/versions/node/v24/bin")
	t.Setenv("PNPM_HOME", "/pnpm/home")
	t.Setenv("VOLTA_HOME", "/volta")
	t.Setenv("NPM_CONFIG_PREFIX", "/npm/prefix")

	dirs := jslang.ServerDirs()

	for _, want := range []string{"/versions/node/v24/bin", "/pnpm/home", "/volta/bin", "/npm/prefix/bin"} {
		if !slices.Contains(dirs, want) {
			t.Errorf("ServerDirs() = %v, want it to contain %s", dirs, want)
		}
	}
	// The most specific answer — the version nvm has selected — comes first,
	// because it is the one that changes when the user runs `nvm use`.
	if dirs[0] != "/versions/node/v24/bin" {
		t.Errorf("ServerDirs() starts with %q, want nvm's directory first", dirs[0])
	}
}

func TestServerDirsGuessNothingForAToolThatIsNotSetUp(t *testing.T) {
	t.Setenv("NVM_BIN", "")
	t.Setenv("PNPM_HOME", "")

	if got := jslang.NvmBinDir(); got != "" {
		t.Errorf("NvmBinDir() = %q with no NVM_BIN, want nothing", got)
	}
	if got := jslang.PnpmHome(); got != "" {
		t.Errorf("PnpmHome() = %q with no PNPM_HOME, want nothing", got)
	}
}

func TestTheNpmPrefixDefaultsToTheDocumentedPerUserOne(t *testing.T) {
	// ~/.npm-global is the prefix npm's documentation tells a user to set up
	// so that installing globally needs no sudo. It is where the install
	// hint's command lands on such a machine, so it is the one that matters
	// to somebody who followed the hint and found nothing.
	t.Setenv("NPM_CONFIG_PREFIX", "")
	t.Setenv("VOLTA_HOME", "")

	if got := jslang.NpmPrefixBinDir(); !strings.HasSuffix(got, "/.npm-global/bin") {
		t.Errorf("NpmPrefixBinDir() = %q, want it under ~/.npm-global/bin", got)
	}
	if got := jslang.VoltaBinDir(); !strings.HasSuffix(got, "/.volta/bin") {
		t.Errorf("VoltaBinDir() = %q, want it under ~/.volta/bin", got)
	}
}

func TestProfileHandsOutAFreshCopyEveryTime(t *testing.T) {
	// A caller that appends to Server.Dirs must not be appending to the
	// package's one copy, or the next caller would inherit its guess.
	first := jslang.Profile()
	first.Server.Dirs = append(first.Server.Dirs, "/somewhere/else")

	if second := jslang.Profile(); slices.Contains(second.Server.Dirs, "/somewhere/else") {
		t.Errorf("Profile().Server.Dirs = %v after a caller appended to an earlier result", second.Server.Dirs)
	}
}

func TestTemplatesAreAllFilledIn(t *testing.T) {
	templates := jslang.Profile().Templates

	for name, template := range map[string]string{
		"Settings": templates.Settings,
		"Snippets": templates.Snippets,
		"Tools":    templates.Tools,
		"Agents":   templates.Agents,
	} {
		if template == "" {
			t.Errorf("Templates.%s is empty; the editor would offer to write nothing", name)
		}
	}
}

func TestRegisterTeachesTheLibraryJavaScriptAndJSON(t *testing.T) {
	jslang.Register()

	for _, want := range []syntax.Language{jslang.Language, jslang.LanguageJSON} {
		if !slices.Contains(syntax.Registered(), want) {
			t.Errorf("Registered() = %v, want it to contain %q", syntax.Registered(), want)
		}
	}

	for path, want := range map[string]syntax.Language{
		"main.js":             jslang.Language,
		"lib/server.mjs":      jslang.Language,
		"config/legacy.cjs":   jslang.Language,
		"DEEP/nested/x.JS":    jslang.Language,
		"package.json":        jslang.LanguageJSON,
		"tsconfig.json":       jslang.LanguageJSON,
		".vscode/tasks.jsonc": jslang.LanguageJSON,
	} {
		if got := syntax.LanguageOf(path, ""); got != want {
			t.Errorf("LanguageOf(%q) = %q, want %q", path, got, want)
		}
	}
}

func TestRegisterReplacesTheLibrarysOwnJavaScriptScanner(t *testing.T) {
	// The library colours JavaScript too, without regular expressions: it
	// reads /x/ as two divisions. Registering under the same name is what
	// puts this editor's scanner in its place, and a regular expression
	// coming out coloured as one is the proof that it did.
	jslang.Register()

	const src = "return /x/.test(s);"
	spans := syntax.Highlight(jslang.Language, src)
	at := strings.Index(src, "/x/")

	for _, span := range spans[0] {
		if span.Start <= at && at < span.End {
			if span.Class != syntax.ClassChar {
				t.Errorf("/x/ is %v; the library's scanner is still in charge of JavaScript", span.Class)
			}
			return
		}
	}
	t.Error("nothing covers the regular expression at all")
}

func TestAScriptWithANodeShebangIsJavaScript(t *testing.T) {
	// A command-line tool written in JavaScript is a file with no extension
	// whose first line names node. Node reads that line as a comment, and
	// the editor reads it before choosing a scanner.
	jslang.Register()

	for _, firstLine := range []string{"#!/usr/bin/env node", "#!/usr/local/bin/node", "#!/usr/bin/env -S node --no-warnings"} {
		if got := syntax.LanguageOf("bin/cli", firstLine); got != jslang.Language {
			t.Errorf("LanguageOf(%q) = %q, want %q", firstLine, got, jslang.Language)
		}
	}
}

func TestRegisterLeavesOtherFilesAlone(t *testing.T) {
	jslang.Register()

	cases := map[string]syntax.Language{
		"README.md":  syntax.LanguageMarkdown,
		"Dockerfile": syntax.LanguageDockerfile,
		"index.html": syntax.LanguageHTML,
		"main.py":    syntax.LanguageNone,
		"main.rs":    syntax.LanguageNone,
		"main.go":    syntax.LanguageNone,
		// TypeScript is served by the same language server but is not
		// coloured here: it is a different language with its own keywords,
		// and a deliberate boundary the reference documents.
		"main.ts": syntax.LanguageNone,
		// A JavaScript file's neighbour with a different extension is not
		// JavaScript however JavaScript-flavoured its name.
		"javascript.toml": syntax.LanguageTOML,
	}
	for path, want := range cases {
		if got := syntax.LanguageOf(path, ""); got != want {
			t.Errorf("LanguageOf(%q) = %q, want %q", path, got, want)
		}
	}
	// A shebang naming another interpreter is not JavaScript either.
	if got := syntax.LanguageOf("script", "#!/usr/bin/env python3"); got == jslang.Language {
		t.Errorf("LanguageOf with a python shebang = %q, want anything but JavaScript", got)
	}
}

// A profile is a literal, so this example is the whole of how one is used.
func ExampleProfile() {
	jslang.Register()

	var p profile.Profile = jslang.Profile()
	_ = p.ProjectDir() // ".turbo-js"
}