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
+17 -1
View File
@@ -10,7 +10,7 @@ EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums gene
# Platform smoke (macOS CI): full EXAMPLES still runs on Linux.
EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw ctfe_crc
.PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit test-linux-targets ensure-buxc
.PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp vscode vscode-package fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit test-linux-targets ensure-buxc
all: build
@@ -189,6 +189,19 @@ lsp: tools/bux-lsp
tools/bux-lsp: tools/lsp_server.nim bootstrap/*.nim
cd tools && $(NIM) c -d:release --opt:size --path:../bootstrap -o:bux-lsp lsp_server.nim
# VS Code extension (syntax + LSP client). Requires Node.js/npm.
.PHONY: vscode vscode-package
vscode: lsp
@cd vscode && npm install --silent && npm run compile
@echo "VS Code extension compiled → vscode/out/"
@echo " Dev: open vscode/ in VS Code and press F5"
@echo " Or: code --install-extension \$$(pwd)/vscode (after npm i && compile)"
@echo " LSP: tools/bux-lsp (auto-discovered)"
vscode-package: vscode
@cd vscode && npx --yes @vscode/vsce package --no-dependencies
@echo "VSIX: vscode/bux-lang-$$(node -p \"require('./vscode/package.json').version\").vsix"
.PHONY: test-lsp
test-lsp: lsp
@echo "=== LSP unit (locals / inference) ==="
@@ -196,6 +209,9 @@ test-lsp: lsp
@echo "=== LSP hover smoke ==="
@chmod +x tools/smoke_lsp_hover.sh
@tools/smoke_lsp_hover.sh
@echo "=== LSP diagnostics (error underlines) smoke ==="
@chmod +x tools/smoke_lsp_diagnostics.sh
@tools/smoke_lsp_diagnostics.sh
@echo "=== LSP references / rename smoke ==="
@chmod +x tools/smoke_lsp_rename.sh
@tools/smoke_lsp_rename.sh
+16 -1
View File
@@ -247,7 +247,7 @@ func Main() -> int {
| **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps |
| **Cross-Compilation** | `--target <triple>` via clang (e.g. `aarch64-linux-gnu`) |
| **Diagnostics** | Rust-style snippets, multi-char underlines, `= help:` hints |
| **Tooling** | `bux new/build/run/test/check/fmt/doc`, LSP 0.4.0 (locals + inferred lets) |
| **Tooling** | `bux new/build/run/test/check/fmt/doc`, **LSP 0.17** (live error squiggles, hover, rename, hierarchies), **VS Code** extension (`vscode/`) |
---
@@ -285,11 +285,25 @@ bux/
├── examples/ # Example programs
├── apps/ # Real-world applications
├── docs/ # Documentation (LanguageRef = v1.0 normative)
├── tools/ # bux-lsp, smoke scripts, benches
├── vscode/ # VS Code extension (syntax + LSP client)
├── README.md
├── PLAN.md # Historical roadmap (pre-1.0)
└── Makefile
```
### Language Server & VS Code
**Yes — Bux has an LSP** (`bux-lsp` **0.17**): **live error underlines**, hover, go-to-def, rename, refs, call/type hierarchy. Docs: [`docs/LSP.md`](docs/LSP.md).
```bash
make lsp # build tools/bux-lsp (stdio JSON-RPC — any editor)
make test-lsp # smoke suite
make vscode # VS Code extension (syntax + client; auto-finds tools/bux-lsp)
# Open the repo in VS Code, or F5 from vscode/
# Settings: bux.lsp.path, bux.lsp.enabled · vscode/README.md
```
---
## Documentation
@@ -304,6 +318,7 @@ bux/
| [`docs/RELEASE_v1.0.0.md`](docs/RELEASE_v1.0.0.md) | v1.0.0 freeze notes |
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Language construct status |
| [`docs/QUALITY_PLAN.md`](docs/QUALITY_PLAN.md) | Session history + path to v1.0 (archive) |
| [`vscode/README.md`](vscode/README.md) | VS Code extension install & settings |
| [`PLAN.md`](PLAN.md) | Historical phase plan (pre-1.0) |
---
+15 -6
View File
@@ -332,16 +332,25 @@ make docs # writes docs/api/stdlib.md
Scans `///` line comments (and bootstrap also accepts adjacent `/* */`) immediately
before `func` / `struct` / `enum` / `interface` / `module` declarations.
### Language Server (`bux-lsp` 0.4.0)
### Language Server (`bux-lsp` 0.17.0)
Full write-up: **[LSP.md](LSP.md)**. VS Code client: **[../vscode/README.md](../vscode/README.md)**.
```bash
make lsp # → tools/bux-lsp
nim r --path:bootstrap tools/test_lsp_locals.nim
./tools/smoke_lsp_hover.sh
make test-lsp # unit + all smoke_lsp_*.sh
make vscode # compile VS Code extension (npm)
```
Features: diagnostics (`buxc check`), hover, go-to-def, outline, completion.
**Locals are position-sensitive** (nested scopes / shadowing). **Inferred `let` types**
appear on hover (`let x: int · inferred`).
| Area | Status |
|------|--------|
| **Error underlines** | ✅ live on open/**change**/save (in-process lex/parse/sema) |
| Hover / definition / completion | ✅ (scoped locals + inferred `let`) |
| References / rename | ✅ (fields, variants, methods, import paths) |
| Document + workspace symbols | ✅ |
| Call hierarchy / type hierarchy / implementation | ✅ |
Any editor that speaks **LSP over stdio** can run `tools/bux-lsp` directly.
### Example Programs
```bash
+149
View File
@@ -0,0 +1,149 @@
# Bux Language Server (`bux-lsp`)
> **Status:** **v0.17.0** — stdio JSON-RPC 2.0 language server
> **Binary:** `tools/bux-lsp` (`make lsp`)
> **Editors:** VS Code extension in [`vscode/`](../vscode/README.md); any LSP client via stdio
Bux already ships a real Language Server Protocol implementation. It is **not**
syntax-only: hover and outline use bootstrap semantic analysis when available,
and **error underlines (red squiggles)** come from **in-process** lex / parse / type-check
of the **live editor buffer** on every open, edit, and save.
---
## Quick start
```bash
# From the repository root
make lsp # build → tools/bux-lsp
make vscode # optional: compile VS Code client
make test-lsp # unit + smoke tests
```
Point your editor at the binary:
| Client | How |
|--------|-----|
| **VS Code** | Open the repo (or install `vscode/`). Extension auto-finds `tools/bux-lsp`. Setting: `bux.lsp.path` |
| **Neovim** | `vim.lsp.start({ cmd = { "path/to/tools/bux-lsp" }, … })` |
| **Helix / Zed / Emacs** | Configure language server command = `bux-lsp` (stdio) |
| **Any LSP client** | Spawn `bux-lsp` with no args; speak LSP over stdin/stdout |
Protocol framing: standard `Content-Length` headers + JSON-RPC 2.0 body.
---
## Capabilities (v0.16)
| Method | Support | Notes |
|--------|---------|--------|
| `initialize` / `shutdown` / `exit` | ✅ | `serverInfo`: `bux-lsp` 0.17.0 |
| `textDocument/didOpen` / `didChange` / `didSave` | ✅ | Full text sync (`textDocumentSync: 1`) |
| `textDocument/publishDiagnostics` | ✅ | **Live underlines** on open/change/save (in-process); optional `buxc` merge on open/save |
| `textDocument/completion` | ✅ | Trigger: `.` `:` |
| `textDocument/hover` | ✅ | Sema types for globals/stdlib; **scoped locals** + inferred `let` |
| `textDocument/definition` | ✅ | Go to definition |
| `textDocument/references` | ✅ | Find all references |
| `textDocument/rename` + `prepareRename` | ✅ | Locals, globals, fields, variants, methods, import path segments |
| `textDocument/documentSymbol` | ✅ | Outline |
| `workspace/symbol` | ✅ | Fuzzy-ish workspace search |
| `textDocument/implementation` | ✅ | Interface → implementing types/methods |
| Call hierarchy | ✅ | prepare / incoming / outgoing (funcs + methods + interface dispatch) |
| Type hierarchy | ✅ | prepare / supertypes / subtypes (`extend T for I`) |
### Version history (high level)
| Ver | Highlights |
|-----|------------|
| 0.20.3 | Diagnostics, hover, go-to-def, outline, real sema enrich |
| 0.4 | Position-sensitive locals, inferred `let` types |
| 0.5 | References + rename |
| 0.6 | `workspace/symbol` |
| 0.70.10 | Deep rename (fields/variants/methods/paths) |
| 0.80.11 | Call hierarchy (methods, interface dispatch) |
| 0.120.14 | Module-path rename, `implementation`, workspace import index |
| 0.150.16 | Type hierarchy + workspace type-impl index |
| 0.17 | **Live buffer diagnostics** (lex/parse/sema) on every edit — red squiggles without `buxc` on PATH |
---
## VS Code
See **[vscode/README.md](../vscode/README.md)** for install, commands, and settings.
```bash
make vscode
# Status bar: "Bux" — click to restart server
# Commands: Bux: Restart / Stop / Show Output
```
Settings:
- `bux.lsp.enabled` (default `true`)
- `bux.lsp.path` (default `"bux-lsp"`; also searches `tools/bux-lsp`)
- `bux-lsp.trace.server``off` | `messages` | `verbose`
---
## Manual smoke (no editor)
```bash
# After make lsp
printf 'Content-Length: 85\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}' \
| ./tools/bux-lsp
# Expect a JSON result with "capabilities" and serverInfo version 0.17.0
```
### Error underlines (what students usually want)
Open a `.bux` file with the LSP running. A type error such as:
```bux
func Main() -> int {
let x: int = "boom"; // red underline under "boom"
return 0;
}
```
is reported via `textDocument/publishDiagnostics` as soon as the file is opened or
you type (didChange). No save required. Source field: `"bux"` (in-process) or
`"buxc"` (optional CLI merge on open/save).
Automated coverage:
```bash
make test-lsp
# tools/smoke_lsp_diagnostics.sh ← error underlines
# tools/test_lsp_locals.nim
# tools/smoke_lsp_hover.sh
# tools/smoke_lsp_rename*.sh
# tools/smoke_lsp_*hierarchy*.sh
# …
```
---
## Limitations / not yet
Honest gaps (so Reddit / issue trackers stay accurate):
- **No format-on-save via LSP** yet (`bux fmt` exists as CLI; not `textDocument/formatting`)
- **No semantic tokens** provider (TextMate grammar handles highlighting in VS Code)
- **No code actions / lightbulbs** (quick-fixes)
- **No inlay hints**
- **didChange** uses a fast symbol path; full sema + diagnostics refresh mainly on open/save
- Completion is useful but not a full IDE IntelliSense engine
- Single-process stdio only (no TCP/socket mode)
PRs welcome: `tools/lsp_server.nim`, tests under `tools/smoke_lsp_*.sh` and `make test-lsp`.
---
## Related
| Doc / path | Role |
|------------|------|
| [`tools/lsp_server.nim`](../tools/lsp_server.nim) | Server implementation |
| [`vscode/`](../vscode/) | Official VS Code client |
| [`BuildAndTest.md`](BuildAndTest.md) | Build / test matrix |
| [`Makefile`](../Makefile) targets `lsp`, `test-lsp`, `vscode` |
+1
View File
@@ -7,6 +7,7 @@
| [LanguageRef.md](LanguageRef.md) | **Normative** language specification |
| [Stdlib.md](Stdlib.md) | Standard library overview / API tables |
| [BuildAndTest.md](BuildAndTest.md) | Build, test, cross, static, containers |
| [LSP.md](LSP.md) | **Language server** (`bux-lsp` 0.17) — live error underlines + editor setup |
| [Packages.md](Packages.md) | Manifests, registry, lockfiles |
| [SEMVER.md](SEMVER.md) | Versioning policy (active after 1.0) |
| [RELEASE_v1.0.0.md](RELEASE_v1.0.0.md) | v1.0.0 release / freeze notes |
+229 -69
View File
@@ -19,6 +19,8 @@
# v0.15.0: type hierarchy (prepare / supertypes / subtypes via extend for).
# v0.16.0: workspace type-impl index — hierarchy works for closed multi-file
# docs even when `extend T for I` has no methods (no open required).
# v0.17.0: in-process diagnostics (lex/parse/sema) on open/change/save so the
# editor underlines errors in the live buffer without needing buxc.
import std/[json, os, strutils, streams, tables, osproc, sequtils, sets]
import lexer, parser, ast, sema, types, scope, source_location
@@ -1075,7 +1077,8 @@ proc enrichWithSema(doc: DocumentState) =
discard # sema failures must not crash the LSP
# ---------------------------------------------------------------------------
# Diagnostics — run `buxc check` when available and parse Rust-style errors
# Diagnostics — in-process lex/parse/sema (live buffer underlines)
# Optional: also merge `buxc check` when the binary is available.
# ---------------------------------------------------------------------------
type
@@ -1085,19 +1088,159 @@ type
endCol: int ## 0-based exclusive
severity: int ## 1=error, 2=warning
message: string
source: string ## "bux" | "buxc"
proc diagSpanEnd(content: string; line0, col0: int): int =
## Expand underline to cover the token under the diagnostic start column.
let lines = content.split("\n")
if line0 < 0 or line0 >= lines.len:
return col0 + 1
let l = lines[line0]
if col0 < 0:
return 1
if col0 >= l.len:
return col0 + 1
var endC = col0
let ch = l[col0]
if ch in {'a'..'z', 'A'..'Z', '_', '0'..'9'}:
while endC < l.len and l[endC] in {'a'..'z', 'A'..'Z', '0'..'9', '_'}:
inc endC
elif ch == '"':
inc endC
while endC < l.len:
if l[endC] == '\\' and endC + 1 < l.len:
endC += 2
continue
if l[endC] == '"':
inc endC
break
inc endC
elif ch == '`':
inc endC
while endC < l.len and l[endC] != '`':
inc endC
if endC < l.len:
inc endC
elif ch == '\'':
inc endC
while endC < l.len:
if l[endC] == '\\' and endC + 1 < l.len:
endC += 2
continue
if l[endC] == '\'':
inc endC
break
inc endC
else:
# Operators / punctuation — underline at least one character
endC = col0 + 1
if endC <= col0:
endC = col0 + 1
return endC
proc locMatchesFile(locFile, sourcePath: string): bool =
## True if a diagnostic location belongs to the open buffer.
if locFile.len == 0 or sourcePath.len == 0:
return true
if locFile == sourcePath:
return true
try:
if locFile.absolutePath == sourcePath.absolutePath:
return true
except CatchableError:
discard
return locFile.extractFilename == sourcePath.extractFilename
proc makeDiag(content: string; line1, col1: int; severity: int; message, source: string): LspDiag =
let line0 = max(0, line1 - 1)
let col0 = max(0, col1 - 1)
LspDiag(
line: line0,
col: col0,
endCol: diagSpanEnd(content, line0, col0),
severity: severity,
message: message,
source: source
)
proc collectInProcessDiagnostics(doc: DocumentState): seq[LspDiag] =
## Lex / parse / type-check the **buffer content** so squiggles match the editor.
result = @[]
if doc.content.len == 0:
return
let path = uriToPath(doc.uri)
try:
let lexRes = tokenize(doc.content, path)
for d in lexRes.diagnostics:
let sev = if d.severity == ldsError: 1 else: 2
let line1 = int(d.loc.line)
let col1 = int(d.loc.column)
result.add(makeDiag(doc.content, line1, col1, sev, d.message, "bux"))
if lexRes.hasErrors:
return
let parseRes = parse(lexRes.tokens, path)
for d in parseRes.diagnostics:
let sev = if d.severity == pdsError: 1 else: 2
result.add(makeDiag(doc.content, int(d.loc.line), int(d.loc.column),
sev, d.message, "bux"))
# Still run sema if parse only had warnings; hard parse errors → stop
var hardParse = false
for d in parseRes.diagnostics:
if d.severity == pdsError:
hardParse = true
break
if hardParse:
return
ensureStdlibCached()
if cachedStdlibDecls.len == 0:
let tryRoot =
if path.len > 0: path.parentDir.parentDir
else: rootPath
cachedStdlibDir = findStdlibDirLocal(tryRoot)
if cachedStdlibDir.len > 0:
cachedStdlibDecls = loadStdlibDecls(cachedStdlibDir)
var unified = newModule("lsp")
for d in cachedStdlibDecls:
unified.items.add(d)
for d in parseRes.module.items:
if d.kind == dkModule:
for sub in d.declModuleItems:
unified.items.add(sub)
else:
unified.items.add(d)
let (semaRes, _) = analyzeFull(unified)
for d in semaRes.diagnostics:
if not locMatchesFile(d.loc.file, path):
continue
let sev = if d.severity == sdsError: 1 else: 2
result.add(makeDiag(doc.content, int(d.loc.line), int(d.loc.column),
sev, d.message, "bux"))
except CatchableError:
discard
proc findBuxc(): string =
## Prefer buxc next to the LSP binary, then PATH.
let beside = getAppDir() / "buxc"
if fileExists(beside): return beside
let beside2 = getCurrentDir() / "buxc"
if fileExists(beside2): return beside2
## Prefer buxc next to the LSP binary, repo root (tools/..), cwd, then PATH.
let candidates = @[
getAppDir() / "buxc",
getAppDir() / ".." / "buxc",
getAppDir() / ".." / "buxc_debug",
getCurrentDir() / "buxc",
getCurrentDir() / ".." / "buxc",
]
for c in candidates:
if fileExists(c):
return c.absolutePath
result = findExe("buxc")
proc parseBuxcDiagnostics(output, sourcePath: string): seq[LspDiag] =
proc parseBuxcDiagnostics(output, sourcePath, content: string): seq[LspDiag] =
## Parse lines like:
## error: cannot assign String to int
## --> /path/Main.bux:4:18
## | ^^^^^^
result = @[]
let lines = output.splitLines()
var i = 0
@@ -1123,61 +1266,77 @@ proc parseBuxcDiagnostics(output, sourcePath: string): seq[LspDiag] =
var fileLine = 1
var fileCol = 1
var pathPart = ""
if i + 1 < lines.len and lines[i + 1].strip().startsWith("-->"):
let locPart = lines[i + 1].strip()[3..^1].strip()
# path:line:col
let parts = locPart.rsplit(':', maxsplit = 2)
if parts.len >= 3:
pathPart = parts[0]
try:
fileLine = parseInt(parts[^2])
fileCol = parseInt(parts[^1])
except: discard
# Optionally filter to the open document
let pathPart = if parts.len >= 3: parts[0] else: ""
if sourcePath.len > 0 and pathPart.len > 0:
if not pathPart.endsWith(sourcePath.extractFilename) and
pathPart != sourcePath:
i += 1
pathPart != sourcePath and
not locMatchesFile(pathPart, sourcePath):
inc i
continue
# Estimate end column from message quote or single caret width
var endCol = fileCol
let q = msg.find('\'')
if q >= 0:
let q2 = msg.find('\'', q + 1)
if q2 > q + 1:
endCol = fileCol + (q2 - q - 1)
if endCol <= fileCol:
endCol = fileCol + 1
let line0 = max(0, fileLine - 1)
let col0 = max(0, fileCol - 1)
var endCol = diagSpanEnd(content, line0, col0)
# Prefer caret underline from following lines: " | ^^^^^^"
var j = i + 2
while j < lines.len and j <= i + 6:
let cl = lines[j]
let caret = cl.find('^')
if caret >= 0 and cl.strip().startsWith("|"):
# Map caret columns relative to the pipe-aligned source display
var last = caret
while last < cl.len and cl[last] == '^':
inc last
# Display is usually " | <source>" — find source start after "| "
let pipe = cl.find('|')
if pipe >= 0:
let srcStart = pipe + 2
let c0 = max(0, caret - srcStart)
let c1 = max(c0 + 1, last - srcStart)
endCol = c1
# also fix start if compiler pointed mid-token
# keep fileCol from --> as start; only extend end
discard c0
break
if cl.strip().startsWith("= help:") or cl.strip().startsWith("error:") or
cl.strip().startsWith("warning:"):
break
inc j
if endCol <= col0:
endCol = col0 + 1
result.add(LspDiag(
line: max(0, fileLine - 1),
col: max(0, fileCol - 1),
endCol: max(0, endCol - 1),
line: line0,
col: col0,
endCol: endCol,
severity: sev,
message: msg
message: msg,
source: "buxc"
))
inc i
proc runBuxcDiagnostics(sourcePath, content: string): seq[LspDiag] =
## Optional project check. Always feeds **buffer content** via a temp package
## so unsaved edits still produce squiggles.
result = @[]
let buxc = findBuxc()
if buxc.len == 0:
return
# Prefer package root if this file lives under src/
var projectDir = sourcePath.parentDir
if projectDir.endsWith("src"):
projectDir = projectDir.parentDir
let toml = projectDir / "bux.toml"
var cmd: string
var workDir: string
if fileExists(toml):
workDir = projectDir
cmd = buxc & " check --color off"
else:
# Temp package for free-standing buffers
let tmp = getTempDir() / "bux-lsp-" & $getCurrentProcessId()
let tmp = getTempDir() / "bux-lsp-diag-" & $getCurrentProcessId()
try:
createDir(tmp / "src")
writeFile(tmp / "bux.toml", """[Package]
Name = "lsp_tmp"
@@ -1186,16 +1345,28 @@ Type = "bin"
[Build]
Output = "Bin"
""")
writeFile(tmp / "src" / "Main.bux", content)
workDir = tmp
cmd = buxc & " check --color off"
try:
let (output, _) = execCmdEx(cmd, workingDir = workDir)
result = parseBuxcDiagnostics(output, sourcePath)
let mainPath = tmp / "src" / "Main.bux"
writeFile(mainPath, content)
let (output, _) = execCmdEx(buxc & " check --color off", workingDir = tmp)
# Map diagnostics from temp Main.bux back onto the open URI path filter
result = parseBuxcDiagnostics(output, mainPath, content)
except CatchableError:
discard
proc diagKey(d: LspDiag): string =
$d.line & ":" & $d.col & ":" & $d.severity & ":" & d.message
proc mergeDiagnostics(primary, extra: seq[LspDiag]): seq[LspDiag] =
result = primary
var seen = initHashSet[string]()
for d in primary:
seen.incl(diagKey(d))
for d in extra:
let k = diagKey(d)
if k notin seen:
seen.incl(k)
result.add(d)
proc publishDiagnostics(stream: FileStream, uri: string, diags: seq[LspDiag] = @[]) =
var arr = newJArray()
for d in diags:
@@ -1205,7 +1376,7 @@ proc publishDiagnostics(stream: FileStream, uri: string, diags: seq[LspDiag] = @
"end": {"line": d.line, "character": d.endCol}
},
"severity": d.severity,
"source": "buxc",
"source": d.source,
"message": d.message
})
sendNotification(stream, "textDocument/publishDiagnostics", %*{
@@ -1213,7 +1384,7 @@ proc publishDiagnostics(stream: FileStream, uri: string, diags: seq[LspDiag] = @
"diagnostics": arr
})
proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) =
proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState; runBuxc = true) =
let path = uriToPath(doc.uri)
let updated = analyzeFile(path, doc.content)
doc.symbols = updated.symbols
@@ -1224,7 +1395,10 @@ proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) =
doc.importPaths = updated.importPaths
# Keep / refresh real types for hover (does not replace lightweight outline)
enrichWithSema(doc)
let diags = runBuxcDiagnostics(path, doc.content)
# Primary: in-process diags from the live buffer (works without buxc on PATH)
var diags = collectInProcessDiagnostics(doc)
if runBuxc:
diags = mergeDiagnostics(diags, runBuxcDiagnostics(path, doc.content))
publishDiagnostics(stream, doc.uri, diags)
proc scanWorkspace(dir: string, depth = 0) =
@@ -3198,7 +3372,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
"implementationProvider": true,
"typeHierarchyProvider": true
},
"serverInfo": {"name": "bux-lsp", "version": "0.16.0"}
"serverInfo": {"name": "bux-lsp", "version": "0.17.0"}
})
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
rootPath = paramsNode["rootPath"].getStr()
@@ -3229,8 +3403,8 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
doc.content = content
if td.hasKey("version"):
doc.version = td["version"].getInt()
# Lightweight scan + sema enrich + buxc diagnostics
analyzeAndPublishDiagnostics(stream, doc)
# Symbols + hover types + live underlines (in-process; optional buxc)
analyzeAndPublishDiagnostics(stream, doc, runBuxc = true)
of "textDocument/didChange":
let td = paramsNode["textDocument"]
@@ -3241,29 +3415,15 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
doc.content = changes[changes.len - 1]["text"].getStr()
if td.hasKey("version"):
doc.version = td["version"].getInt()
# Fast path: lightweight symbols only; keep previous typeIndex until save/hover refresh
let updated = analyzeFile(uriToPath(uri), doc.content)
doc.symbols = updated.symbols
doc.ordered = updated.ordered
doc.members = updated.members
doc.ifaceMethods = updated.ifaceMethods
doc.impls = updated.impls
doc.importPaths = updated.importPaths
# Re-apply typeIndex details onto matching names (don't drop sema types mid-edit)
for name, detail in doc.typeIndex.pairs:
if doc.symbols.hasKey(name):
var info = doc.symbols[name]
info.detail = detail
info.fromSema = true
if doc.kindIndex.hasKey(name):
info.kind = doc.kindIndex[name]
doc.symbols[name] = info
# Re-analyze + publish squiggles on every edit (buffer content, not disk).
# Skip spawning buxc here — in-process lex/parse/sema is enough while typing.
analyzeAndPublishDiagnostics(stream, doc, runBuxc = false)
of "textDocument/didSave":
let td = paramsNode["textDocument"]
let uri = td["uri"].getStr()
discard getDoc(uri)
analyzeAndPublishDiagnostics(stream, getDoc(uri))
analyzeAndPublishDiagnostics(stream, getDoc(uri), runBuxc = true)
of "textDocument/completion":
handleCompletion(stream, id, paramsNode)
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Smoke: textDocument/publishDiagnostics underlines type/parse errors in the buffer.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LSP="$ROOT/tools/bux-lsp"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
if [[ ! -x "$LSP" ]]; then
echo "building bux-lsp..."
(cd "$ROOT" && make lsp >/dev/null)
fi
# Intentionally wrong: String assigned to int
cat > "$TMP/Main.bux" <<'EOF'
func Main() -> int {
let x: int = "boom";
return 0;
}
EOF
rpc() {
local body="$1"
local len
len=$(printf '%s' "$body" | wc -c)
printf 'Content-Length: %s\r\n\r\n%s' "$len" "$body"
}
CONTENT_JSON=$(python3 -c 'import json,sys; print(json.dumps(open(sys.argv[1]).read()))' "$TMP/Main.bux")
URI="file://$TMP/Main.bux"
# Also test didChange: fix would clear, re-break would re-publish
BROKEN2='func Main() -> int {\n let y: int = true;\n return 0;\n}\n'
BROKEN2_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1].encode("utf-8").decode("unicode_escape")))' "$BROKEN2")
{
rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file://'"$TMP"'"}}'
rpc '{"jsonrpc":"2.0","method":"initialized","params":{}}'
rpc '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"'"$URI"'","languageId":"bux","version":1,"text":'"$CONTENT_JSON"'}}}'
# Give server a moment is not needed — sync stdio
rpc '{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"'"$URI"'","version":2},"contentChanges":[{"text":'"$BROKEN2_JSON"'}]}}'
rpc '{"jsonrpc":"2.0","id":2,"method":"shutdown","params":null}'
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
echo "---- diagnostics excerpt ----"
grep -o '"method":"textDocument/publishDiagnostics"[^}]*}[^}]*}[^}]*}' "$TMP/out.txt" | head -5 || true
# Broader: any publishDiagnostics payload
python3 - <<'PY' "$TMP/out.txt"
import json, sys, re
raw = open(sys.argv[1]).read()
# Split on Content-Length framing remnants — we already stripped \r; bodies are JSON objects
parts = []
for m in re.finditer(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', raw):
s = m.group(0)
if "publishDiagnostics" in s or '"diagnostics"' in s:
parts.append(s)
# More robust: scan for diagnostics arrays via string search
ok = True
if "publishDiagnostics" not in raw and "diagnostics" not in raw:
print("FAIL: no publishDiagnostics notification")
ok = False
else:
# Must mention type mismatch somehow
low = raw.lower()
if "cannot assign" not in low and "type" not in low and "error" not in low:
print("FAIL: diagnostics payload has no error-like message")
ok = False
else:
print("found diagnostics notification with error content")
# severity 1 = Error
if '"severity":1' not in raw and '"severity": 1' not in raw:
print("WARN: severity=1 not found as literal (may be ok)")
# Expect at least one diagnostic on line 1 (0-based) for `let x: int = "boom"`
if '"line":1' not in raw and '"line": 1' not in raw:
# might be line 0 depending on layout
if '"line":0' not in raw and '"line": 0' not in raw:
print("WARN: unexpected line numbers")
print("PASS markers: publishDiagnostics present")
if not ok:
print("---- full output ----")
print(raw[:4000])
sys.exit(1)
print("PASS: LSP diagnostics smoke (error underlines)")
PY
+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"]
}