feat: live LSP error underlines + improved VS Code extension
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled

bux-lsp 0.17 publishes in-process diagnostics on open/change/save so
editors show red squiggles for lex/parse/type errors in the live buffer.
VS Code client discovers tools/bux-lsp, adds status bar/restart, richer
syntax/snippets, and docs (docs/LSP.md) for forum/editor setup.
This commit is contained in:
2026-07-28 05:51:47 +03:00
parent e87985e879
commit db7ba1dff2
20 changed files with 1627 additions and 236 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
out/
*.vsix
.vscode-test/
*.tsbuildinfo
+15
View File
@@ -0,0 +1,15 @@
.vscode/**
.vscode-test/**
src/**
.gitignore
.vscodeignore
tsconfig.json
**/*.ts
**/*.map
node_modules/**
!node_modules/vscode-languageclient/**
!node_modules/vscode-jsonrpc/**
!node_modules/vscode-languageserver-protocol/**
!node_modules/vscode-languageserver-types/**
!node_modules/semver/**
*.vsix
+89
View File
@@ -0,0 +1,89 @@
# Bux Language Support for VS Code
Syntax highlighting, snippets, editor defaults, and **Language Server Protocol** integration for the [Bux](https://github.com/katehonz/bux) programming language.
## Features
| Area | What you get |
|------|----------------|
| **Syntax** | Keywords, types, `f"..."` interpolation, raw `` `...` `` strings, C-strings, macros (`macro!` / `name!()`), attributes (`@[Checked]`), numbers (hex/bin/oct + suffixes), lifetimes |
| **Snippets** | `main`, `func`, `struct`, `enum`, `match`, `interface`, `extend`, `macro`, `checked`, … |
| **LSP** | **Live error underlines** (red squiggles on edit), completion, hover, go-to-definition, references, rename, document/workspace symbols, call hierarchy, type hierarchy, go-to-implementation |
| **Editor** | Bracket colorization, smart indent / on-enter, fold regions (`// region`) |
| **Build** | `buxc` problem matcher for Tasks |
## Requirements
1. **VS Code** ≥ 1.85
2. **`bux-lsp`** binary (from this repo):
```bash
# from the Bux repository root
make lsp
# → tools/bux-lsp
```
The extension auto-discovers the server in this order:
1. Setting `bux.lsp.path` (absolute, relative, or command name)
2. `tools/bux-lsp` under any workspace folder (and parent folders for monorepos)
3. `bux-lsp` on your `PATH`
## Install (development)
```bash
cd vscode
npm install
npm run compile
# Launch Extension Development Host: F5 in VS Code,
# or install the folder as an extension:
code --install-extension .
# or package:
npx @vscode/vsce package
code --install-extension bux-lang-0.2.0.vsix
```
Symlink into your extensions dir (Linux):
```bash
ln -sfn "$(pwd)/vscode" ~/.vscode/extensions/bux-lang.bux-lang-0.2.0
```
## Commands
| Command | Description |
|---------|-------------|
| **Bux: Restart Language Server** | Stop and start `bux-lsp` |
| **Bux: Stop Language Server** | Disconnect the client |
| **Bux: Show Output Channel** | Open the Bux log |
Status bar item **Bux** (left): click to restart. Red = missing binary / start failure.
## Settings
| Setting | Default | Description |
|---------|---------|-------------|
| `bux.lsp.enabled` | `true` | Master switch for the language server |
| `bux.lsp.path` | `"bux-lsp"` | Path or command for the server binary |
| `bux-lsp.trace.server` | `off` | LSP wire trace (`off` / `messages` / `verbose`) |
## LSP capabilities (bux-lsp)
Provided by `tools/lsp_server.nim` (see `make lsp` / `make test-lsp`):
- `textDocument/completion`, `hover`, `definition`, `references`, `rename`
- `documentSymbol`, `workspace/symbol`
- Call hierarchy, type hierarchy, `implementation`
- Diagnostics on open/save (via analyzer / `buxc`)
## Troubleshooting
1. Status bar shows **error** → run `make lsp` and ensure `tools/bux-lsp` exists and is executable.
2. **Bux: Show Output Channel** for client logs.
3. Set `"bux-lsp.trace.server": "verbose"` for JSON-RPC traffic.
4. Confirm language mode is **Bux** for `.bux` files (status bar language indicator).
## License
MIT — same as the Bux project.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
<rect width="128" height="128" rx="28" fill="#0d1117"/>
<rect x="8" y="8" width="112" height="112" rx="22" fill="#161b22" stroke="#30363d" stroke-width="2"/>
<path d="M34 32h28c12 0 20 6.5 20 16.5 0 7-3.5 12-9.5 14.5 8 2.5 12.5 8.5 12.5 16.5C85 92 76 99 62 99H34V32zm14 11v16h12c5.5 0 8.5-2.5 8.5-8s-3-8-8.5-8H48zm0 27v18h14c6.5 0 10.5-3 10.5-9.5S68.5 70 62 70H48z" fill="#6ee7b7"/>
<circle cx="96" cy="40" r="8" fill="#38bdf8" opacity=".9"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#1a1f2e"/>
<path d="M8 8h6.5c2.8 0 4.5 1.5 4.5 3.8 0 1.6-.8 2.8-2.2 3.4 1.8.6 2.9 2 2.9 3.9 0 2.7-2 4.4-5.2 4.4H8V8zm3.2 2.5v3.8h2.8c1.3 0 2-.6 2-1.9s-.7-1.9-2-1.9h-2.8zm0 6.2v4.3h3.2c1.5 0 2.4-.7 2.4-2.2s-.9-2.1-2.4-2.1h-3.2z" fill="#6ee7b7"/>
<path d="M20.5 22.5c1.8-1.2 3-3.2 3-5.5 0-3.7-2.8-6.5-6.5-6.5" stroke="#38bdf8" stroke-width="1.6" stroke-linecap="round" opacity=".85"/>
</svg>

After

Width:  |  Height:  |  Size: 511 B

+66 -14
View File
@@ -8,29 +8,81 @@
["[", "]"],
["(", ")"]
],
"colorizedBracketPairs": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"" },
{ "open": "`", "close": "`" }
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] },
{ "open": "`", "close": "`", "notIn": ["string", "comment"] },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "/*", "close": " */", "notIn": ["string"] }
],
"surroundingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"" },
{ "open": "`", "close": "`" }
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["`", "`"],
["'", "'"]
],
"autoCloseBefore": ";:.,=}])>` \n\t",
"folding": {
"markers": {
"start": "^\\s*//\\s*region\\b",
"end": "^\\s*//\\s*endregion\\b"
}
"start": "^\\s*//\\s*#?region\\b",
"end": "^\\s*//\\s*#?endregion\\b"
},
"offSide": false
},
"wordPattern": "[a-zA-Z_][a-zA-Z0-9_]*",
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
"indentationRules": {
"increaseIndentPattern": "\\{[^}]*$",
"decreaseIndentPattern": "^\\s*\\}"
}
"increaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)\\s*$",
"decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]\\)].*$"
},
"onEnterRules": [
{
"beforeText": "^\\s*/\\*(?!/).*[^*/]\\s*$",
"afterText": "^\\s*\\*/$",
"action": {
"indent": "indentOutdent",
"appendText": " * "
}
},
{
"beforeText": "^\\s*/\\*(?!/).*[^*/]\\s*$",
"action": {
"indent": "none",
"appendText": " * "
}
},
{
"beforeText": "^(\\t|(\\ \\ ))*\\ \\*(\\ ([^*]|\\*(?!/))*)?$",
"action": {
"indent": "none",
"appendText": "* "
}
},
{
"beforeText": "^(\\t|(\\ \\ ))*\\ */.*$",
"action": {
"indent": "none"
}
},
{
"beforeText": "^\\s*(func|async\\s+func|struct|enum|union|interface|extend|module|macro!|if|else|while|for|do|loop|match|switch|case)\\b.*\\{\\s*$",
"action": {
"indent": "indent"
}
},
{
"beforeText": "^\\s*.*=>\\s*$",
"action": {
"indent": "indent"
}
}
]
}
+128
View File
@@ -0,0 +1,128 @@
{
"name": "bux-lang",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bux-lang",
"version": "0.2.0",
"license": "MIT",
"dependencies": {
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/vscode": "^1.85.0",
"typescript": "^5.3.0"
},
"engines": {
"vscode": "^1.85.0"
}
},
"node_modules/@types/node": {
"version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"dev": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.125.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz",
"integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageclient": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
"dependencies": {
"minimatch": "^5.1.0",
"semver": "^7.3.7",
"vscode-languageserver-protocol": "3.17.5"
},
"engines": {
"vscode": "^1.82.0"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
}
}
}
+129 -14
View File
@@ -1,37 +1,60 @@
{
"name": "bux-lang",
"displayName": "Bux Language Support",
"description": "Syntax highlighting, snippets, and LSP support for the Bux programming language",
"version": "0.1.0",
"description": "Syntax highlighting, snippets, and full LSP support for Bux — live error underlines, hover, go-to-def, rename, call/type hierarchy",
"version": "0.2.1",
"publisher": "bux-lang",
"license": "MIT",
"icon": "icon.png",
"galleryBanner": {
"color": "#0d1117",
"theme": "dark"
},
"repository": {
"type": "git",
"url": "https://github.com/katehonz/bux"
},
"bugs": {
"url": "https://github.com/katehonz/bux/issues"
},
"homepage": "https://github.com/katehonz/bux",
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Programming Languages",
"Snippets",
"Linters"
"Linters",
"Other"
],
"keywords": [
"bux",
"bux-lang",
"programming-language",
"lsp",
"syntax"
],
"activationEvents": [
"onLanguage:bux"
"onLanguage:bux",
"workspaceContains:**/*.bux",
"workspaceContains:bux.toml"
],
"main": "./out/extension.js",
"contributes": {
"languages": [
{
"id": "bux",
"aliases": ["Bux", "bux"],
"extensions": [".bux"],
"aliases": [
"Bux",
"bux"
],
"extensions": [
".bux"
],
"configuration": "./language-configuration.json",
"icon": {
"dark": "./icon.png",
"light": "./icon.png"
"dark": "./icons/bux-file.svg",
"light": "./icons/bux-file.svg"
}
}
],
@@ -48,30 +71,122 @@
"path": "./snippets/bux.json"
}
],
"commands": [
{
"command": "bux.restartLsp",
"title": "Bux: Restart Language Server",
"category": "Bux"
},
{
"command": "bux.stopLsp",
"title": "Bux: Stop Language Server",
"category": "Bux"
},
{
"command": "bux.showOutput",
"title": "Bux: Show Output Channel",
"category": "Bux"
}
],
"menus": {
"commandPalette": [
{
"command": "bux.restartLsp"
},
{
"command": "bux.stopLsp"
},
{
"command": "bux.showOutput"
}
]
},
"configuration": {
"title": "Bux",
"properties": {
"bux.lsp.enabled": {
"type": "boolean",
"default": true,
"description": "Enable LSP server for diagnostics and autocomplete"
"description": "Enable the Bux language server (diagnostics, hover, go-to-definition, rename, hierarchy)."
},
"bux.lsp.path": {
"type": "string",
"default": "bux-lsp",
"description": "Path to the bux-lsp binary"
"description": "Path to the bux-lsp binary. Absolute path, workspace-relative path, or command on PATH. If unset/default, the extension also searches tools/bux-lsp under workspace folders."
},
"bux-lsp.trace.server": {
"type": "string",
"scope": "window",
"enum": [
"off",
"messages",
"verbose"
],
"default": "off",
"description": "Traces the communication between VS Code and the Bux language server."
}
}
}
},
"configurationDefaults": {
"[bux]": {
"editor.semanticHighlighting.enabled": true,
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"editor.quickSuggestions": {
"other": true,
"comments": false,
"strings": false
},
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.wordBasedSuggestions": "off"
}
},
"problemMatchers": [
{
"name": "buxc",
"owner": "bux",
"fileLocation": [
"relative",
"${workspaceFolder}"
],
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(error|warning|note):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
}
],
"taskDefinitions": [
{
"type": "bux",
"required": [
"task"
],
"properties": {
"task": {
"type": "string",
"description": "bux subcommand (build, run, test, check)"
}
}
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./"
"watch": "tsc -watch -p ./",
"package": "vsce package --no-dependencies 2>/dev/null || npx @vscode/vsce package --no-dependencies"
},
"dependencies": {
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/vscode": "^1.85.0",
"typescript": "^5.0.0",
"vscode-languageclient": "^9.0.0"
"typescript": "^5.3.0"
}
}
+169 -27
View File
@@ -9,11 +9,23 @@
],
"description": "Main entry point function"
},
"Hello World": {
"prefix": "hello",
"body": [
"import Std::Io::{PrintLine};",
"",
"func Main() -> int {",
" PrintLine(\"${1:Hello, Bux!}\");",
" return 0;",
"}"
],
"description": "Minimal Hello World program"
},
"Function": {
"prefix": "func",
"body": [
"func ${1:Name}(${2:params}) -> ${3:void} {",
" $4",
" $0",
"}"
],
"description": "Function declaration"
@@ -22,11 +34,20 @@
"prefix": "afunc",
"body": [
"async func ${1:Name}(${2:params}) -> ${3:void} {",
" $4",
" $0",
"}"
],
"description": "Async function declaration"
},
"Generic function": {
"prefix": "gfunc",
"body": [
"func ${1:Name}<${2:T}>(${3:param}: ${2:T}) -> ${2:T} {",
" $0",
"}"
],
"description": "Generic function"
},
"Variable (let)": {
"prefix": "let",
"body": [
@@ -41,6 +62,13 @@
],
"description": "Mutable variable"
},
"Const": {
"prefix": "const",
"body": [
"const ${1:NAME}: ${2:Type} = ${3:value};"
],
"description": "Constant declaration"
},
"Struct": {
"prefix": "struct",
"body": [
@@ -60,11 +88,52 @@
],
"description": "Enum declaration"
},
"Generic enum": {
"prefix": "genum",
"body": [
"enum ${1:Name}<${2:T}> {",
" ${3:Some}(${2:T}),",
" ${4:None},",
"}"
],
"description": "Generic enum declaration"
},
"Interface": {
"prefix": "interface",
"body": [
"interface ${1:Name} {",
" func ${2:Method}(${3:self}) -> ${4:void};",
"}"
],
"description": "Interface declaration"
},
"Extend methods": {
"prefix": "extend",
"body": [
"extend ${1:Type} {",
" func ${2:Method}(self) -> ${3:void} {",
" $0",
" }",
"}"
],
"description": "Extend block for inherent methods"
},
"Extend for interface": {
"prefix": "extendfor",
"body": [
"extend ${1:Type} for ${2:Interface} {",
" func ${3:Method}(self) -> ${4:void} {",
" $0",
" }",
"}"
],
"description": "Implement interface for type"
},
"If statement": {
"prefix": "if",
"body": [
"if ${1:condition} {",
" ${2:body}",
" $0",
"}"
],
"description": "If statement"
@@ -75,7 +144,7 @@
"if ${1:condition} {",
" ${2:body}",
"} else {",
" ${3:elseBody}",
" $0",
"}"
],
"description": "If-else statement"
@@ -84,20 +153,29 @@
"prefix": "while",
"body": [
"while ${1:condition} {",
" ${2:body}",
" $0",
"}"
],
"description": "While loop"
},
"For loop": {
"For-in loop": {
"prefix": "for",
"body": [
"for ${1:item} in ${2:iterable} {",
" ${3:body}",
" $0",
"}"
],
"description": "For-in loop"
},
"Loop": {
"prefix": "loop",
"body": [
"loop {",
" $0",
"}"
],
"description": "Infinite loop"
},
"Match": {
"prefix": "match",
"body": [
@@ -108,40 +186,61 @@
],
"description": "Pattern match expression"
},
"Switch": {
"prefix": "switch",
"body": [
"switch ${1:expr} {",
" case ${2:value}:",
" $0",
" break;",
" default:",
" break;",
"}"
],
"description": "Switch statement"
},
"Import": {
"prefix": "import",
"body": [
"import Std::${1:Module}::{${2:items}};"
"import Std::${1:Io}::{${2:PrintLine}};"
],
"description": "Import statement"
"description": "Import from Std"
},
"Module": {
"prefix": "module",
"body": [
"module ${1:Name} {",
"${2:body}",
" $0",
"}"
],
"description": "Module declaration"
},
"Extend": {
"prefix": "extend",
"body": [
"extend ${1:Type} {",
" ${2:methods}",
"}"
],
"description": "Extend block for methods"
},
"@Checked": {
"@Checked function": {
"prefix": "checked",
"body": [
"@[Checked]",
"func ${1:Name}(${2:params}) -> ${3:void} {",
" ${4:body}",
" $0",
"}"
],
"description": "Checked function with borrow checker enabled"
"description": "Checked function with borrow checker"
},
"@Release function": {
"prefix": "release",
"body": [
"@[Release]",
"func ${1:Name}(${2:params}) -> ${3:void} {",
" $0",
"}"
],
"description": "Release (unchecked) function"
},
"Defer": {
"prefix": "defer",
"body": [
"defer ${1:cleanup};"
],
"description": "Defer cleanup"
},
"PrintLine": {
"prefix": "pl",
@@ -150,13 +249,56 @@
],
"description": "Print with newline"
},
"Generic function": {
"prefix": "gfunc",
"Interpolated string": {
"prefix": "fstr",
"body": [
"func ${1:Name}<${2:T}>(${3:param}: ${2:T}) -> ${2:T} {",
" ${4:body}",
"f\"${1:Hello, }{${2:name}}\""
],
"description": "Interpolated f-string"
},
"Macro definition": {
"prefix": "macro",
"body": [
"macro! ${1:name} {",
" (\\$${2:x}:expr) => {",
" $0",
" }",
"}"
],
"description": "Generic function"
"description": "Declarative macro! definition"
},
"Test assert": {
"prefix": "tassert",
"body": [
"Test_AssertEq${1:Int}(${2:got}, ${3:want});"
],
"description": "Test equality assertion"
},
"Spawn task": {
"prefix": "spawn",
"body": [
"let ${1:handle}: *void = spawn ${2:Worker}(${3:args});"
],
"description": "Spawn green-thread task"
},
"Result Ok/Err match": {
"prefix": "resultmatch",
"body": [
"match ${1:result} {",
" Ok(${2:value}) => ${3:value},",
" Err(${4:err}) => ${5:/* handle */},",
"}"
],
"description": "Match on Result"
},
"Option Some/None match": {
"prefix": "optmatch",
"body": [
"match ${1:opt} {",
" Some(${2:value}) => ${3:value},",
" None => ${4:/* handle */},",
"}"
],
"description": "Match on Option"
}
}
+231 -33
View File
@@ -1,54 +1,252 @@
/* Bux Language Server Protocol client for VS Code
* Launches bux-lsp binary and connects via stdin/stdout.
* Discovers and launches the bux-lsp binary, connects via stdio.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind
TransportKind,
RevealOutputChannelOn,
} from 'vscode-languageclient/node';
let client: LanguageClient;
let client: LanguageClient | undefined;
let outputChannel: vscode.OutputChannel | undefined;
let statusBar: vscode.StatusBarItem | undefined;
export function activate(context: vscode.ExtensionContext) {
const config = vscode.workspace.getConfiguration('bux.lsp');
const enabled = config.get<boolean>('enabled', true);
if (!enabled) {
vscode.window.showInformationMessage('Bux LSP is disabled in settings');
return;
}
const SETTING_SECTION = 'bux';
const lspPath = config.get<string>('path', 'bux-lsp');
export async function activate(context: vscode.ExtensionContext): Promise<void> {
outputChannel = vscode.window.createOutputChannel('Bux');
context.subscriptions.push(outputChannel);
const serverOptions: ServerOptions = {
command: lspPath,
transport: TransportKind.stdio
};
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50);
statusBar.command = 'bux.restartLsp';
statusBar.tooltip = 'Bux Language Server — click to restart';
context.subscriptions.push(statusBar);
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'bux' }],
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.bux')
}
};
client = new LanguageClient(
'bux-lsp',
'Bux Language Server',
serverOptions,
clientOptions
context.subscriptions.push(
vscode.commands.registerCommand('bux.restartLsp', async () => {
await restartClient(true);
}),
vscode.commands.registerCommand('bux.stopLsp', async () => {
await stopClient();
setStatus('off', 'Bux LSP stopped');
outputChannel?.appendLine('[bux] LSP stopped by user');
}),
vscode.commands.registerCommand('bux.showOutput', () => {
outputChannel?.show(true);
}),
vscode.workspace.onDidChangeConfiguration(async (e) => {
if (
e.affectsConfiguration('bux.lsp.enabled') ||
e.affectsConfiguration('bux.lsp.path')
) {
outputChannel?.appendLine('[bux] Configuration changed — restarting LSP');
await restartClient(false);
}
})
);
client.start();
vscode.window.showInformationMessage('Bux LSP started');
await startClient();
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
return stopClient();
}
async function restartClient(userInitiated: boolean): Promise<void> {
await stopClient();
await startClient();
if (userInitiated) {
vscode.window.setStatusBarMessage('Bux LSP restarted', 3000);
}
}
async function stopClient(): Promise<void> {
if (!client) {
return;
}
const c = client;
client = undefined;
try {
await c.stop();
} catch (err) {
outputChannel?.appendLine(`[bux] Error stopping client: ${err}`);
}
}
async function startClient(): Promise<void> {
const config = vscode.workspace.getConfiguration(SETTING_SECTION);
const enabled = config.get<boolean>('lsp.enabled', true);
if (!enabled) {
setStatus('off', 'Bux LSP disabled');
outputChannel?.appendLine('[bux] LSP disabled in settings (bux.lsp.enabled)');
return;
}
const configuredPath = config.get<string>('lsp.path', 'bux-lsp') || 'bux-lsp';
const resolved = resolveLspPath(configuredPath);
if (!resolved) {
setStatus('error', 'bux-lsp not found');
const msg =
'Bux LSP binary not found. Build with `make lsp` (produces tools/bux-lsp) ' +
'or set bux.lsp.path to the full path of the binary.';
outputChannel?.appendLine(`[bux] ${msg}`);
outputChannel?.appendLine(`[bux] Configured path: ${configuredPath}`);
const choice = await vscode.window.showWarningMessage(
'Bux: language server (bux-lsp) not found',
'Open Output',
'Open Settings'
);
if (choice === 'Open Output') {
outputChannel?.show(true);
} else if (choice === 'Open Settings') {
await vscode.commands.executeCommand('workbench.action.openSettings', 'bux.lsp');
}
return;
}
outputChannel?.appendLine(`[bux] Using LSP binary: ${resolved}`);
setStatus('starting', 'Starting Bux LSP…');
const serverOptions: ServerOptions = {
command: resolved,
transport: TransportKind.stdio,
options: { env: process.env },
};
const clientOptions: LanguageClientOptions = {
documentSelector: [
{ scheme: 'file', language: 'bux' },
{ scheme: 'untitled', language: 'bux' },
],
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.{bux,toml}'),
},
outputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Error,
traceOutputChannel: outputChannel,
};
client = new LanguageClient('bux-lsp', 'Bux Language Server', serverOptions, clientOptions);
try {
await client.start();
setStatus('ready', `Bux LSP ready (${path.basename(resolved)})`);
outputChannel?.appendLine('[bux] Language server started');
} catch (err) {
setStatus('error', 'Bux LSP failed to start');
outputChannel?.appendLine(`[bux] Failed to start language server: ${err}`);
vscode.window.showErrorMessage(
`Bux LSP failed to start: ${err instanceof Error ? err.message : String(err)}`
);
client = undefined;
}
}
/**
* Resolve bux-lsp:
* 1. Configured absolute / relative path
* 2. Workspace tools/bux-lsp (and nearby monorepo parents)
* 3. Command on PATH
*/
function resolveLspPath(configured: string): string | undefined {
const candidates: string[] = [];
if (path.isAbsolute(configured)) {
candidates.push(configured);
} else if (configured.includes('/') || configured.includes('\\')) {
candidates.push(path.resolve(configured));
for (const folder of vscode.workspace.workspaceFolders ?? []) {
candidates.push(path.join(folder.uri.fsPath, configured));
}
}
for (const folder of vscode.workspace.workspaceFolders ?? []) {
const root = folder.uri.fsPath;
candidates.push(
path.join(root, 'tools', 'bux-lsp'),
path.join(root, 'tools', 'bux-lsp.exe'),
path.join(root, 'bin', 'bux-lsp'),
path.join(root, 'bux-lsp'),
path.resolve(root, '..', 'tools', 'bux-lsp'),
path.resolve(root, '..', '..', 'tools', 'bux-lsp')
);
}
for (const c of candidates) {
if (isExecutable(c)) {
return c;
}
}
// Bare name: search PATH
const bare = configured.includes('/') || configured.includes('\\') ? 'bux-lsp' : configured;
return findInPath(bare);
}
function findInPath(cmd: string): string | undefined {
const pathEnv = process.env.PATH ?? process.env.Path ?? '';
const sep = process.platform === 'win32' ? ';' : ':';
const exts =
process.platform === 'win32'
? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';')
: [''];
for (const dir of pathEnv.split(sep)) {
if (!dir) continue;
for (const ext of exts) {
const candidate = path.join(dir, cmd + ext);
if (isExecutable(candidate)) {
return candidate;
}
}
}
return undefined;
}
function isExecutable(filePath: string): boolean {
try {
const st = fs.statSync(filePath);
if (!st.isFile()) {
return false;
}
if (process.platform === 'win32') {
return true;
}
fs.accessSync(filePath, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
function setStatus(state: 'ready' | 'starting' | 'error' | 'off', text: string): void {
if (!statusBar) return;
switch (state) {
case 'ready':
statusBar.text = '$(check) Bux';
statusBar.backgroundColor = undefined;
break;
case 'starting':
statusBar.text = '$(sync~spin) Bux';
statusBar.backgroundColor = undefined;
break;
case 'error':
statusBar.text = '$(error) Bux';
statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
break;
case 'off':
statusBar.text = '$(circle-slash) Bux';
statusBar.backgroundColor = undefined;
break;
}
statusBar.tooltip = text;
statusBar.show();
}
+263 -69
View File
@@ -3,33 +3,19 @@
"name": "Bux",
"scopeName": "source.bux",
"patterns": [
{
"include": "#comments"
},
{
"include": "#strings"
},
{
"include": "#keywords"
},
{
"include": "#types"
},
{
"include": "#functions"
},
{
"include": "#constants"
},
{
"include": "#operators"
},
{
"include": "#numbers"
},
{
"include": "#attributes"
}
{ "include": "#comments" },
{ "include": "#attributes" },
{ "include": "#macros" },
{ "include": "#strings" },
{ "include": "#chars" },
{ "include": "#keywords" },
{ "include": "#types" },
{ "include": "#constants" },
{ "include": "#lifetimes" },
{ "include": "#functions" },
{ "include": "#paths" },
{ "include": "#numbers" },
{ "include": "#operators" }
],
"repository": {
"comments": {
@@ -37,20 +23,129 @@
{
"name": "comment.block.bux",
"begin": "/\\*",
"end": "\\*/"
"end": "\\*/",
"patterns": [
{ "include": "#comments" }
]
},
{
"name": "comment.line.bux",
"name": "comment.line.double-slash.bux",
"match": "//.*$"
}
]
},
"attributes": {
"patterns": [
{
"name": "meta.attribute.bux",
"begin": "@\\[",
"end": "\\]",
"beginCaptures": {
"0": { "name": "punctuation.definition.attribute.bux" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.attribute.bux" }
},
"patterns": [
{
"name": "storage.modifier.attribute.bux",
"match": "\\b(Checked|Release|Shared|Import|Drop)\\b"
}
]
},
{
"name": "storage.modifier.attribute.bux",
"match": "@(Checked|Release|Shared|Import|Drop)\\b"
}
]
},
"macros": {
"patterns": [
{
"name": "meta.macro.definition.bux",
"begin": "\\b(macro!)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"beginCaptures": {
"1": { "name": "keyword.declaration.macro.bux" },
"2": { "name": "entity.name.function.macro.bux" }
},
"end": "(?<=\\})",
"patterns": [
{ "include": "$self" }
]
},
{
"name": "meta.macro.invocation.bux",
"begin": "\\b([A-Za-z_][A-Za-z0-9_]*!)\\s*(\\()",
"beginCaptures": {
"1": { "name": "entity.name.function.macro.bux" },
"2": { "name": "punctuation.section.parens.begin.bux" }
},
"end": "\\)",
"endCaptures": {
"0": { "name": "punctuation.section.parens.end.bux" }
},
"patterns": [
{ "include": "$self" }
]
},
{
"name": "variable.other.macro.fragment.bux",
"match": "\\$([A-Za-z_][A-Za-z0-9_]*)"
},
{
"name": "keyword.operator.macro.repeat.bux",
"match": "\\$\\(|\\)\\s*[*+?]"
}
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.double.bux",
"begin": "\"",
"name": "string.interpolated.bux",
"begin": "f\"",
"end": "\"",
"beginCaptures": {
"0": { "name": "punctuation.definition.string.begin.bux" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.string.end.bux" }
},
"patterns": [
{
"name": "constant.character.escape.bux",
"match": "\\\\[ntr\\\\\"'{}]|\\\\u\\{[0-9a-fA-F]+\\}|\\\\x[0-9a-fA-F]{2}"
},
{
"name": "meta.interpolation.bux",
"begin": "\\{",
"end": "\\}",
"beginCaptures": {
"0": { "name": "punctuation.section.embedded.begin.bux" }
},
"endCaptures": {
"0": { "name": "punctuation.section.embedded.end.bux" }
},
"patterns": [
{ "include": "#keywords" },
{ "include": "#types" },
{ "include": "#constants" },
{ "include": "#numbers" },
{ "include": "#functions" },
{ "include": "#operators" }
]
}
]
},
{
"name": "string.quoted.double.cstr.bux",
"begin": "c(8|16|32)\"",
"end": "\"",
"beginCaptures": {
"0": { "name": "punctuation.definition.string.begin.bux" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.string.end.bux" }
},
"patterns": [
{
"name": "constant.character.escape.bux",
@@ -59,9 +154,40 @@
]
},
{
"name": "string.quoted.backtick.bux",
"name": "string.quoted.double.bux",
"begin": "\"",
"end": "\"",
"beginCaptures": {
"0": { "name": "punctuation.definition.string.begin.bux" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.string.end.bux" }
},
"patterns": [
{
"name": "constant.character.escape.bux",
"match": "\\\\[ntr\\\\\"']|\\\\u\\{[0-9a-fA-F]+\\}|\\\\x[0-9a-fA-F]{2}|\\\\."
}
]
},
{
"name": "string.quoted.raw.bux",
"begin": "`",
"end": "`"
"end": "`",
"beginCaptures": {
"0": { "name": "punctuation.definition.string.begin.bux" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.string.end.bux" }
}
}
]
},
"chars": {
"patterns": [
{
"name": "string.quoted.single.bux",
"match": "c?(8|16|32)?'(\\\\.|[^'\\\\])'"
}
]
},
@@ -69,55 +195,87 @@
"patterns": [
{
"name": "keyword.control.bux",
"match": "\\b(if|else|while|for|do|loop|in|break|continue|return|match|case|default|spawn|async|await|try|catch|throw|discard)\\b"
"match": "\\b(if|else|while|for|do|loop|in|break|continue|return|match|switch|case|default|spawn|async|await|try|catch|throw|discard|defer)\\b"
},
{
"name": "keyword.declaration.bux",
"match": "\\b(func|var|let|const|type|struct|enum|union|interface|extend|module|import|extern|pub)\\b"
"match": "\\b(func|var|let|const|type|struct|enum|union|interface|extend|module|import|extern|pub|macro)\\b"
},
{
"name": "keyword.operator.bux",
"name": "keyword.operator.word.bux",
"match": "\\b(as|is|sizeof|comptime|static_assert|dyn)\\b"
},
{
"name": "keyword.ownership.bux",
"match": "\\b(own|mut|borrow)\\b"
},
{
"name": "storage.modifier.bux",
"match": "@\\[(Checked|Shared|Import)\\]"
"match": "\\b(own|mut|borrow|checked)\\b"
}
]
},
"types": {
"patterns": [
{
"name": "support.type.bux",
"match": "\\b(int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|float32|float64|bool|char8|char32|String|void)\\b"
"name": "support.type.primitive.bux",
"match": "\\b(int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|float32|float64|bool|char8|char16|char32|String|void)\\b"
},
{
"name": "support.type.stdlib.bux",
"match": "\\b(Result|Option|Array|Map|Set|Slice|Channel|TaskHandle)\\b"
},
{
"name": "entity.name.type.bux",
"match": "\\b[A-Z][a-zA-Z0-9_]*\\b"
}
]
},
"functions": {
"patterns": [
{
"name": "entity.name.function.bux",
"match": "\\b([a-z_][a-zA-Z0-9_]*)(?=\\s*\\()"
"match": "\\b[A-Z][A-Za-z0-9_]*\\b"
}
]
},
"constants": {
"patterns": [
{
"name": "constant.language.bux",
"match": "\\b(true|false|null)\\b"
"name": "constant.language.boolean.bux",
"match": "\\b(true|false)\\b"
},
{
"name": "constant.numeric.bux",
"match": "\\b(true|false|null|self|super)\\b"
"name": "constant.language.null.bux",
"match": "\\bnull\\b"
},
{
"name": "variable.language.bux",
"match": "\\b(self|super)\\b"
}
]
},
"lifetimes": {
"patterns": [
{
"name": "storage.modifier.lifetime.bux",
"match": "'[A-Za-z_][A-Za-z0-9_]*\\b"
}
]
},
"functions": {
"patterns": [
{
"name": "meta.function.declaration.bux",
"match": "\\b(func|async\\s+func)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "keyword.declaration.bux" },
"2": { "name": "entity.name.function.bux" }
}
},
{
"name": "entity.name.function.bux",
"match": "\\b([A-Za-z_][A-Za-z0-9_]*)(?=\\s*[<(])"
}
]
},
"paths": {
"patterns": [
{
"name": "meta.path.bux",
"match": "\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(::)",
"captures": {
"1": { "name": "entity.name.namespace.bux" },
"2": { "name": "punctuation.accessor.bux" }
}
}
]
},
@@ -125,31 +283,67 @@
"patterns": [
{
"name": "constant.numeric.float.bux",
"match": "\\b[0-9]+\\.[0-9]+(?:[eE][+-]?[0-9]+)?\\b"
"match": "\\b[0-9]+\\.[0-9]+(?:[eE][+-]?[0-9]+)?(?:f32|f64)?\\b"
},
{
"name": "constant.numeric.hex.bux",
"match": "\\b0[xX][0-9a-fA-F]+\\b"
"match": "\\b0[xX][0-9a-fA-F_]+(?:[iu](?:8|16|32|64)?)?\\b"
},
{
"name": "constant.numeric.bux",
"match": "\\b[0-9]+\\b"
"name": "constant.numeric.octal.bux",
"match": "\\b0[oO][0-7_]+(?:[iu](?:8|16|32|64)?)?\\b"
},
{
"name": "constant.numeric.binary.bux",
"match": "\\b0[bB][01_]+(?:[iu](?:8|16|32|64)?)?\\b"
},
{
"name": "constant.numeric.integer.bux",
"match": "\\b[0-9][0-9_]*(?:[iu](?:8|16|32|64)?|f32|f64)?\\b"
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.bux",
"match": "->|=>|==|!=|<=|>=|&&|\\|\\||[+\\-*/%<>=!&|^~@]"
}
]
},
"attributes": {
"patterns": [
"name": "keyword.operator.arrow.bux",
"match": "->|=>"
},
{
"name": "storage.modifier.bux",
"match": "@(Checked|Shared|Import)"
"name": "keyword.operator.range.bux",
"match": "\\.\\.=|\\.\\.\\.|\\.\\."
},
{
"name": "keyword.operator.comparison.bux",
"match": "==|!=|<=|>=|<|>"
},
{
"name": "keyword.operator.logical.bux",
"match": "&&|\\|\\||!"
},
{
"name": "keyword.operator.assignment.bux",
"match": "\\+=|-=|\\*=|/=|%=|=|:=|\\*\\*="
},
{
"name": "keyword.operator.arithmetic.bux",
"match": "\\*\\*|[+\\-*/%]"
},
{
"name": "keyword.operator.bitwise.bux",
"match": "&|\\||\\^|~|<<|>>"
},
{
"name": "keyword.operator.try.bux",
"match": "\\?"
},
{
"name": "keyword.operator.borrow.bux",
"match": "&mut\\b|&"
},
{
"name": "punctuation.accessor.bux",
"match": "::|\\."
}
]
}
+7 -2
View File
@@ -2,12 +2,17 @@
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"lib": ["ES2020"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true,
"strict": true,
"esModuleInterop": true
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"types": ["node", "vscode"]
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
"exclude": ["node_modules", ".vscode-test", "out"]
}