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.

jslang.go · 240 lines · 10.7 KBGo Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 11h ago1// Package jslang is everything about Turbo JS that is about *JavaScript*: how
2// the editor names itself, which language server it talks to, what a Node.js
3// project's starter files say, and how JavaScript and JSON are coloured.
4//
5// Everything else the editor does lives in turbo-core, which knows nothing
6// about Node. This package is the whole of the difference between Turbo JS
7// and Turbo Python, which is what makes a seventh editor a matter of writing
8// one of these rather than forking anything.
9//
10// jslang.Register() // teach the library this editor's JavaScript, and JSON
11// editor := app.New(screen, name, jslang.Profile())
12package jslang
13
14import (
15 "os"
16 "path/filepath"
17
18 "rickub.com/turbo-editors/turbo-core/profile"
19 "rickub.com/turbo-editors/turbo-core/syntax"
20)
21
22// Name and Slug are what the editor calls itself. The slug is also its binary,
23// its project directory (as .turbo-js) and the stem of its environment
24// variables (as TURBO_JS_…), so it is not free to change.
25const (
26 Name = "Turbo JS"
27 Slug = "turbo-js"
28)
29
30// Language is the name JavaScript is known by: the value LanguageOf returns
31// for a .js file, and what a snippets file writes in its languages key.
32//
33// It is turbo-core's own constant rather than a string of this package's,
34// because the library already colours JavaScript for every editor in the
35// family — a README's code fence, a web page's script — and this editor
36// registers a fuller scanner under the **same** name. Registering the same
37// name is what makes the replacement: the later registration wins, snippets
38// written for "javascript" keep working, and a ```js fence in an agent window
39// is coloured by the scanner this editor uses for its own files.
40const Language = syntax.LanguageJavaScript
41
42// LanguageJSON is the name JSON is known by. JSON is not one of the eight
43// languages turbo-core colours, and a Node project cannot be edited without
44// it: package.json is the project's manifest and its root marker.
45const LanguageJSON syntax.Language = "json"
46
47// ServerCommand is the language server Turbo JS talks to, and InstallHint the
48// single command that installs it.
49//
50// typescript-language-server wraps tsserver, the engine behind every editor's
51// JavaScript support, and it serves plain JavaScript as well as TypeScript:
52// with no configuration file at all it infers a project from the files it is
53// shown and answers all nine of turbo-core's questions — completion, hover,
54// definition, type definition, implementation, references, the file's symbols
55// and a project-wide symbol search — and publishes diagnostics unasked.
56//
57// The hint names both packages because the server is a thin layer over the
58// typescript package and does not depend on it: installed alone it starts and
59// then reports that tsserver is missing. **The @6 is not optional.** TypeScript
60// 7 is the native port, and its package ships a compiler with a language
61// server of its own inside it (`tsc --lsp --stdio`) but no `tsserver.js` —
62// so typescript-language-server, which looks for that file, refuses to start
63// beside it: "Could not find a valid TypeScript installation". TypeScript 6
64// is the last version that ships it. The hint has to fit on a status bar.
65//
66// TypeScript 7's own server was measured against the same tests as this one:
67// it answers all eight requests, four times faster, and publishes **no**
68// diagnostics — it offers them pull-style, through textDocument/diagnostic,
69// which turbo-core does not ask for. An editor whose gutter is blank because
70// the server waits to be asked looks exactly like one with nothing to report,
71// which is why the mature pair is the one named here. The day the library
72// learns to pull, `tsc --lsp --stdio` from a single `npm install -g typescript`
73// is the better answer.
74const (
75 ServerCommand = "typescript-language-server"
76 InstallHint = "npm install -g typescript-language-server typescript@6"
77)
78
79// RootMarker is the file that marks the root of a Node project. The language
80// server is started in the nearest directory at or above the file being
81// edited that holds one.
82const RootMarker = "package.json"
83
84// ServerArgs is what the server is started with.
85//
86// The --stdio is not optional: without it typescript-language-server prints
87// its usage and exits, which the editor reports as a server that died at
88// start-up. It is a function rather than a variable so that no caller can
89// append to the package's own slice.
90func ServerArgs() []string { return []string{"--stdio"} }
91
92// Profile returns the editor Turbo JS is.
93//
94// It is a function rather than a variable because Server.Dirs is worked out
95// from the environment, and a variable would freeze whatever NVM_BIN said when
96// the package was linked — which for a Node tool is the one value most likely
97// to change between two shells.
98func Profile() profile.Profile {
99 return profile.Profile{
100 Name: Name,
101 Slug: Slug,
102 // What the About box and the status bar read out. The language is
103 // JavaScript; Node.js is the runtime this editor's toolchain runs it
104 // on, the way Go is the language and gc the compiler.
105 Language: "JavaScript",
106 // J is free: the fixed menus take F, E, S, R, C, O, W, N and H —
107 // which rules out the S, the C and the R of JavaScript but not its
108 // first letter — so the hot key lands where a reader expects it. The
109 // menu is named after the language and not after npm or node, because
110 // it holds whatever the project put in its tools file, and the first
111 // tools file anybody writes outgrows the language's own toolchain.
112 ToolsMenu: "~J~avaScript",
113 // package.json is a Node project's boundary — its name, its
114 // dependencies, its scripts — and in a monorepo the nearest one going
115 // up is the package being edited, which is the root the server should
116 // resolve imports from.
117 RootMarkers: []string{RootMarker},
118 Server: profile.Server{
119 Command: ServerCommand,
120 Args: ServerArgs(),
121 InstallHint: InstallHint,
122 Dirs: ServerDirs(),
123 },
124 Templates: profile.Templates{
125 Settings: settingsTemplate,
126 Snippets: snippetsTemplate,
127 Tools: toolsTemplate,
128 Agents: agentsTemplate,
129 },
130 }
131}
132
133// Register teaches turbo-core this editor's JavaScript, and JSON.
134//
135// It is called explicitly at start-up rather than from an init function so that
136// "which languages does this editor know?" is answered by reading main, not by
137// working out which packages were imported.
138//
139// JavaScript is registered under the library's own name, replacing the scanner
140// turbo-core ships for every editor: this one knows regular expressions, the
141// hashbang line and Node's globals, which an editor *for* JavaScript owes its
142// user and a Rust editor's README does not need. A shebang naming node is
143// claimed too, because a command-line tool written in JavaScript is a file
144// with no extension whose first line is #!/usr/bin/env node — and Node reads
145// that line as a comment.
146func Register() {
147 syntax.Register(syntax.Definition{
148 Language: Language,
149 // .mjs is a module whatever package.json says, .cjs is CommonJS
150 // whatever it says, and .js is whichever the nearest package.json
151 // makes it. All three are the same language to a scanner.
152 Extensions: []string{".js", ".mjs", ".cjs"},
153 Shebangs: []string{"node"},
154 Highlight: Highlight,
155 })
156 syntax.Register(syntax.Definition{
157 Language: LanguageJSON,
158 // .jsonc is JSON with comments, which tsconfig.json and an editor's
159 // own settings file are in all but name. The scanner tolerates
160 // comments in both, so the two extensions share it.
161 Extensions: []string{".json", ".jsonc"},
162 Highlight: HighlightJSON,
163 })
164}
165
166// DefaultInstallDirs are the two system prefixes `npm install -g` writes into
167// when Node was installed system-wide: /usr/local/bin for Node's own installer
168// and most Linux packages, /opt/homebrew/bin for Homebrew on Apple silicon.
169// Both are on PATH on nearly every machine, and searched all the same for the
170// one where they are not.
171var DefaultInstallDirs = []string{"/usr/local/bin", "/opt/homebrew/bin"}
172
173// ServerDirs returns the directories the language server is looked for in
174// after PATH, most specific first.
175//
176// "Completion silently does nothing" is what a user sees when the editor cannot
177// find a server they believe they installed, and Node has more places to put
178// a global binary than most: the version manager's current version, a
179// per-user npm prefix, pnpm's own home, Volta's, and the system prefix.
180//
181// Empty entries are skipped by the library, so a machine with no nvm and no
182// pnpm simply contributes nothing here.
183func ServerDirs() []string {
184 dirs := []string{NvmBinDir(), NpmPrefixBinDir(), PnpmHome(), VoltaBinDir()}
185 return append(dirs, DefaultInstallDirs...)
186}
187
188// NvmBinDir returns the bin directory of the Node version nvm has selected in
189// this shell, or "" when nvm is not in use.
190//
191// nvm exports NVM_BIN whenever a version is active, and it is the directory
192// `npm install -g` writes into under nvm — which makes it the most specific
193// answer available, and the one that changes when the user runs `nvm use`.
194// nvm's default alias is not resolved here: that means reading and following
195// files under NVM_DIR, and a shell where nvm is set up has NVM_BIN already.
196func NvmBinDir() string {
197 return os.Getenv("NVM_BIN")
198}
199
200// NpmPrefixBinDir returns the bin directory of a per-user npm prefix:
201// NPM_CONFIG_PREFIX/bin when that variable is set, and ~/.npm-global/bin
202// otherwise.
203//
204// ~/.npm-global is the prefix npm's own documentation tells a user to
205// configure so that `npm install -g` needs no sudo, and a machine set up that
206// way has the server there and nowhere else. The prefix set with `npm config
207// set prefix` — which lands in ~/.npmrc — is not read; the environment
208// variable is the one place npm itself documents for overriding it.
209func NpmPrefixBinDir() string {
210 if prefix := os.Getenv("NPM_CONFIG_PREFIX"); prefix != "" {
211 return filepath.Join(prefix, "bin")
212 }
213 home, err := os.UserHomeDir()
214 if err != nil {
215 return ""
216 }
217 return filepath.Join(home, ".npm-global", "bin")
218}
219
220// PnpmHome returns the directory pnpm puts globally installed binaries in, or
221// "" when pnpm is not set up. pnpm insists on PNPM_HOME being set and on PATH
222// before it will install anything globally, so the variable is the answer.
223func PnpmHome() string {
224 return os.Getenv("PNPM_HOME")
225}
226
227// VoltaBinDir returns Volta's shim directory: VOLTA_HOME/bin when VOLTA_HOME
228// is set, and ~/.volta/bin otherwise. A tool installed with `volta install`
229// is a shim in that directory, and Volta's installer adds it to PATH — for
230// the shell it was run from.
231func VoltaBinDir() string {
232 if home := os.Getenv("VOLTA_HOME"); home != "" {
233 return filepath.Join(home, "bin")
234 }
235 home, err := os.UserHomeDir()
236 if err != nil {
237 return ""
238 }
239 return filepath.Join(home, ".volta", "bin")
240}