A Language Server Protocol (LSP) implementation for the Grayscale programming language.
- Diagnostics — inline errors and warnings powered by
gray - Completion — keywords, types, builtins, stdlib modules, and in-file symbols
- Hover docs — documentation for all Grayscale keywords, types, and builtins
- Go to definition — jump to
mut,const,func,struct, andenumdeclarations within the current file
- Node.js v18 or later
grayon your$PATH(required for diagnostics — all other features work without it)
Verify both:
node --version
graygit clone https://github.com/grayscale-lang/grayls
cd grayls
npm install
npm run buildThe compiled server lands in out/server.js.
Install the extension once as a .vsix package. After that it activates automatically whenever you open a .gray file — no extra steps.
cd /path/to/grayls
npm install -g @vscode/vsce
npm run build
vsce package # produces grayls-0.1.0.vsix
code --install-extension grayls-0.1.0.vsixOr install via the VS Code UI: Extensions → ⋯ → Install from VSIX…
After updating server code: rebuild and reinstall:
npm run build && vsce package && code --install-extension grayls-0.1.0.vsixThen reload VS Code (Cmd+Shift+P → Reload Window).
For extension development only: press F5 in the grayls folder to open an Extension Development Host window. This is for debugging the extension itself, not for daily use.
Zed requires a small dev extension (in the zed-extension/ folder of this repo) to register the Grayscale language and syntax highlighting.
Step 1: Build grayls:
npm run buildStep 2: Install the dev extension in Zed:
Cmd+Shift+P→ "zed: install dev extension"- Select the
zed-extension/folder inside this repo - Wait ~30 seconds for Zed to compile the Rust extension
The extension auto-detects your node installation (NVM or Homebrew) and assumes grayls is cloned to ~/code/grayls.
Step 3 (only if grayls is cloned somewhere other than ~/code/grayls): Override the server path in ~/.config/zed/settings.json:
{
"lsp": {
"grayls": {
"binary": {
"path": "node",
"arguments": ["/absolute/path/to/grayls/out/server.js", "--stdio"]
}
}
}
}Important: Use the full absolute path — do not use
~.
Zed spawns the server automatically when you open any .gray file. No .zed/settings.json is needed in your project — the extension handles language registration.
After updating server code: run npm run build, then close and reopen the .gray file.
To confirm the server is running: Cmd+Shift+P → "zed: open log", search for grayls.
Step 1: Install nvim-lspconfig if you haven't already (e.g. via lazy.nvim):
{ "neovim/nvim-lspconfig" }Step 2: Add this to your Neovim config:
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
if not configs.grayls then
configs.grayls = {
default_config = {
cmd = { 'node', '/Users/you/code/grayls/out/server.js', '--stdio' },
filetypes = { 'gray' },
root_dir = lspconfig.util.root_pattern('.git', '*.gray'),
single_file_support = true,
},
}
end
lspconfig.grayls.setup({})Step 3: Register the gray filetype:
vim.filetype.add({ extension = { gray = 'gray' } })Neovim attaches the server automatically when you open a .gray file. Run :LspInfo to confirm.
After updating server code: run npm run build, then :LspRestart inside Neovim (or close and reopen the file).
On every file open, change, and save, grayls writes the buffer to a temp file and runs:
gray /tmp/gray-lsp-XXXX.grayThe output is parsed for error and warning lines of the form:
error[E3018]: type mismatch in 'when'; comparing 'int' with 'string'
--> myfile.gray:42:10
Each becomes an inline diagnostic at the correct line and column, debounced 300 ms.
If gray is not on your $PATH, diagnostics are silently skipped — completion, hover, and go to definition still work.
grayls/
src/
extension.ts VS Code extension entry point
server.ts LSP server (stdio transport)
features/
diagnostics.ts gray integration + output parser
completion.ts keyword / type / builtin / symbol completion
hover.ts hover docs for keywords, types, builtins, symbols
definition.ts go-to-definition (single-file)
utils/
gray-data.ts all Grayscale keywords, types, builtins, and docs
symbols.ts in-file symbol scanner
zed-extension/ Zed dev extension (registers Grayscale language + grayls)
out/ compiled output (generated by npm run build)
package.json
tsconfig.json
This section covers how to extend grayls when the Grayscale language itself changes, or when you want to add new LSP features.
All static language data lives in src/utils/gray-data.ts. It has three arrays and two doc maps:
| Export | What to update |
|---|---|
KEYWORDS |
Add reserved words that appear in control flow or declarations |
TYPES |
Add new primitive or sized types |
BUILTINS |
Add new builtin functions |
STDLIB_MODULES |
Add new @module names |
DOCS |
Add hover documentation for any of the above |
MODULE_FUNCTION_DOCS |
Add hover docs for module.function calls |
Example — adding a new builtin format:
- Add
'format'to theBUILTINSarray - Add an entry to
DOCS:'format': '**`format(template string, ...args) -> string`** — Format a string with substitutions.',
Rebuild (npm run build) and reopen a .gray file — the new builtin appears in completion and hover immediately.
Add an entry to MODULE_FUNCTION_DOCS in gray-data.ts. The key is "module.function":
'arrays.my_new_fn': '**`arrays.my_new_fn(arr [T], n int) -> T`** — Description here.',The scanner lives in src/utils/symbols.ts. Two functions handle multi-line bodies:
scanEnumMembers(body: string[])— parses variant names and their values (integer or string)scanStructFields(body: string[])— parses field names and types
If Grayscale adds a new enum or struct syntax (e.g. associated values, visibility modifiers), update the relevant regex patterns in those functions.
The diagnostic parser is in src/features/diagnostics.ts. It uses one regex against gray output:
const DIAG_PATTERN =
/^(error|warning)\[([EW]\d+)\]:\s+(.+)\n\s+-->\s+[^:]+:(\d+):(\d+)/gm;If gray's error output format changes (e.g. new severity levels, different arrow syntax), update this regex. The capture groups map to: severity, code, message, line, column.
-
Create
src/features/myfeature.tsand export aprovideXfunction that takes(params, documents)and returns the appropriate LSP type. -
Register it in
src/server.ts:import { provideMyFeature } from './features/myfeature'; // Inside onInitialize, add the capability: myFeatureProvider: true, // Then register the handler: connection.onMyFeature((params) => provideMyFeature(params, documents));
The vscode-languageserver package provides types and handler names for all standard LSP features (semantic tokens, code actions, rename, references, etc.).
npm run build # compile TypeScript + bundle with esbuild
npm run compile # TypeScript only (no bundle) — fast type-checkAlways rebuild before testing changes. The server binary at out/server.js is what all editors load.