turbo-editors/turbo-jspublic Fork 0
v1.0.2
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 v1.0.2 · k33g · 11h ago
jslang.go · 240 lines · 10.7 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
// Package jslang is everything about Turbo JS that is about *JavaScript*: how
// the editor names itself, which language server it talks to, what a Node.js
// project's starter files say, and how JavaScript and JSON are coloured.
//
// Everything else the editor does lives in turbo-core, which knows nothing
// about Node. This package is the whole of the difference between Turbo JS
// and Turbo Python, which is what makes a seventh editor a matter of writing
// one of these rather than forking anything.
//
//	jslang.Register()               // teach the library this editor's JavaScript, and JSON
//	editor := app.New(screen, name, jslang.Profile())
package jslang

import (
	"os"
	"path/filepath"

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

// Name and Slug are what the editor calls itself. The slug is also its binary,
// its project directory (as .turbo-js) and the stem of its environment
// variables (as TURBO_JS_…), so it is not free to change.
const (
	Name = "Turbo JS"
	Slug = "turbo-js"
)

// Language is the name JavaScript is known by: the value LanguageOf returns
// for a .js file, and what a snippets file writes in its languages key.
//
// It is turbo-core's own constant rather than a string of this package's,
// because the library already colours JavaScript for every editor in the
// family — a README's code fence, a web page's script — and this editor
// registers a fuller scanner under the **same** name. Registering the same
// name is what makes the replacement: the later registration wins, snippets
// written for "javascript" keep working, and a ```js fence in an agent window
// is coloured by the scanner this editor uses for its own files.
const Language = syntax.LanguageJavaScript

// LanguageJSON is the name JSON is known by. JSON is not one of the eight
// languages turbo-core colours, and a Node project cannot be edited without
// it: package.json is the project's manifest and its root marker.
const LanguageJSON syntax.Language = "json"

// ServerCommand is the language server Turbo JS talks to, and InstallHint the
// single command that installs it.
//
// typescript-language-server wraps tsserver, the engine behind every editor's
// JavaScript support, and it serves plain JavaScript as well as TypeScript:
// with no configuration file at all it infers a project from the files it is
// shown and answers all nine of turbo-core's questions — completion, hover,
// definition, type definition, implementation, references, the file's symbols
// and a project-wide symbol search — and publishes diagnostics unasked.
//
// The hint names both packages because the server is a thin layer over the
// typescript package and does not depend on it: installed alone it starts and
// then reports that tsserver is missing. **The @6 is not optional.** TypeScript
// 7 is the native port, and its package ships a compiler with a language
// server of its own inside it (`tsc --lsp --stdio`) but no `tsserver.js` —
// so typescript-language-server, which looks for that file, refuses to start
// beside it: "Could not find a valid TypeScript installation". TypeScript 6
// is the last version that ships it. The hint has to fit on a status bar.
//
// TypeScript 7's own server was measured against the same tests as this one:
// it answers all eight requests, four times faster, and publishes **no**
// diagnostics — it offers them pull-style, through textDocument/diagnostic,
// which turbo-core does not ask for. An editor whose gutter is blank because
// the server waits to be asked looks exactly like one with nothing to report,
// which is why the mature pair is the one named here. The day the library
// learns to pull, `tsc --lsp --stdio` from a single `npm install -g typescript`
// is the better answer.
const (
	ServerCommand = "typescript-language-server"
	InstallHint   = "npm install -g typescript-language-server typescript@6"
)

// RootMarker is the file that marks the root of a Node project. The language
// server is started in the nearest directory at or above the file being
// edited that holds one.
const RootMarker = "package.json"

// ServerArgs is what the server is started with.
//
// The --stdio is not optional: without it typescript-language-server prints
// its usage and exits, which the editor reports as a server that died at
// start-up. It is a function rather than a variable so that no caller can
// append to the package's own slice.
func ServerArgs() []string { return []string{"--stdio"} }

// Profile returns the editor Turbo JS is.
//
// It is a function rather than a variable because Server.Dirs is worked out
// from the environment, and a variable would freeze whatever NVM_BIN said when
// the package was linked — which for a Node tool is the one value most likely
// to change between two shells.
func Profile() profile.Profile {
	return profile.Profile{
		Name: Name,
		Slug: Slug,
		// What the About box and the status bar read out. The language is
		// JavaScript; Node.js is the runtime this editor's toolchain runs it
		// on, the way Go is the language and gc the compiler.
		Language: "JavaScript",
		// J is free: the fixed menus take F, E, S, R, C, O, W, N and H —
		// which rules out the S, the C and the R of JavaScript but not its
		// first letter — so the hot key lands where a reader expects it. The
		// menu is named after the language and not after npm or node, because
		// it holds whatever the project put in its tools file, and the first
		// tools file anybody writes outgrows the language's own toolchain.
		ToolsMenu: "~J~avaScript",
		// package.json is a Node project's boundary — its name, its
		// dependencies, its scripts — and in a monorepo the nearest one going
		// up is the package being edited, which is the root the server should
		// resolve imports from.
		RootMarkers: []string{RootMarker},
		Server: profile.Server{
			Command:     ServerCommand,
			Args:        ServerArgs(),
			InstallHint: InstallHint,
			Dirs:        ServerDirs(),
		},
		Templates: profile.Templates{
			Settings: settingsTemplate,
			Snippets: snippetsTemplate,
			Tools:    toolsTemplate,
			Agents:   agentsTemplate,
		},
	}
}

// Register teaches turbo-core this editor's JavaScript, and JSON.
//
// It is called explicitly at start-up rather than from an init function so that
// "which languages does this editor know?" is answered by reading main, not by
// working out which packages were imported.
//
// JavaScript is registered under the library's own name, replacing the scanner
// turbo-core ships for every editor: this one knows regular expressions, the
// hashbang line and Node's globals, which an editor *for* JavaScript owes its
// user and a Rust editor's README does not need. A shebang naming node is
// claimed too, because a command-line tool written in JavaScript is a file
// with no extension whose first line is #!/usr/bin/env node — and Node reads
// that line as a comment.
func Register() {
	syntax.Register(syntax.Definition{
		Language: Language,
		// .mjs is a module whatever package.json says, .cjs is CommonJS
		// whatever it says, and .js is whichever the nearest package.json
		// makes it. All three are the same language to a scanner.
		Extensions: []string{".js", ".mjs", ".cjs"},
		Shebangs:   []string{"node"},
		Highlight:  Highlight,
	})
	syntax.Register(syntax.Definition{
		Language: LanguageJSON,
		// .jsonc is JSON with comments, which tsconfig.json and an editor's
		// own settings file are in all but name. The scanner tolerates
		// comments in both, so the two extensions share it.
		Extensions: []string{".json", ".jsonc"},
		Highlight:  HighlightJSON,
	})
}

// DefaultInstallDirs are the two system prefixes `npm install -g` writes into
// when Node was installed system-wide: /usr/local/bin for Node's own installer
// and most Linux packages, /opt/homebrew/bin for Homebrew on Apple silicon.
// Both are on PATH on nearly every machine, and searched all the same for the
// one where they are not.
var DefaultInstallDirs = []string{"/usr/local/bin", "/opt/homebrew/bin"}

// ServerDirs returns the directories the language server is looked for in
// after PATH, most specific first.
//
// "Completion silently does nothing" is what a user sees when the editor cannot
// find a server they believe they installed, and Node has more places to put
// a global binary than most: the version manager's current version, a
// per-user npm prefix, pnpm's own home, Volta's, and the system prefix.
//
// Empty entries are skipped by the library, so a machine with no nvm and no
// pnpm simply contributes nothing here.
func ServerDirs() []string {
	dirs := []string{NvmBinDir(), NpmPrefixBinDir(), PnpmHome(), VoltaBinDir()}
	return append(dirs, DefaultInstallDirs...)
}

// NvmBinDir returns the bin directory of the Node version nvm has selected in
// this shell, or "" when nvm is not in use.
//
// nvm exports NVM_BIN whenever a version is active, and it is the directory
// `npm install -g` writes into under nvm — which makes it the most specific
// answer available, and the one that changes when the user runs `nvm use`.
// nvm's default alias is not resolved here: that means reading and following
// files under NVM_DIR, and a shell where nvm is set up has NVM_BIN already.
func NvmBinDir() string {
	return os.Getenv("NVM_BIN")
}

// NpmPrefixBinDir returns the bin directory of a per-user npm prefix:
// NPM_CONFIG_PREFIX/bin when that variable is set, and ~/.npm-global/bin
// otherwise.
//
// ~/.npm-global is the prefix npm's own documentation tells a user to
// configure so that `npm install -g` needs no sudo, and a machine set up that
// way has the server there and nowhere else. The prefix set with `npm config
// set prefix` — which lands in ~/.npmrc — is not read; the environment
// variable is the one place npm itself documents for overriding it.
func NpmPrefixBinDir() string {
	if prefix := os.Getenv("NPM_CONFIG_PREFIX"); prefix != "" {
		return filepath.Join(prefix, "bin")
	}
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	return filepath.Join(home, ".npm-global", "bin")
}

// PnpmHome returns the directory pnpm puts globally installed binaries in, or
// "" when pnpm is not set up. pnpm insists on PNPM_HOME being set and on PATH
// before it will install anything globally, so the variable is the answer.
func PnpmHome() string {
	return os.Getenv("PNPM_HOME")
}

// VoltaBinDir returns Volta's shim directory: VOLTA_HOME/bin when VOLTA_HOME
// is set, and ~/.volta/bin otherwise. A tool installed with `volta install`
// is a shim in that directory, and Volta's installer adds it to PATH — for
// the shell it was run from.
func VoltaBinDir() string {
	if home := os.Getenv("VOLTA_HOME"); home != "" {
		return filepath.Join(home, "bin")
	}
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	return filepath.Join(home, ".volta", "bin")
}