| 📦 Turbo JS 91999d1 k33g 11h ago | 1 | // 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()) |
| 12 | package jslang |
| 13 | |
| 14 | import ( |
| 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. |
| 25 | const ( |
| 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. |
| 40 | const 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. |
| 45 | const 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. |
| 74 | const ( |
| 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. |
| 82 | const 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. |
| 90 | func 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. |
| 98 | func 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. |
| 146 | func 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. |
| 171 | var 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. |
| 183 | func 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. |
| 196 | func 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. |
| 209 | func 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. |
| 223 | func 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. |
| 231 | func 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 | } |