diff --git a/Makefile b/Makefile index 034df9d..faf3fcd 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 8bf4c9b..94b0651 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ func Main() -> int { | **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps | | **Cross-Compilation** | `--target ` 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) | --- diff --git a/docs/BuildAndTest.md b/docs/BuildAndTest.md index b8e4fab..ecd761c 100644 --- a/docs/BuildAndTest.md +++ b/docs/BuildAndTest.md @@ -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 diff --git a/docs/LSP.md b/docs/LSP.md new file mode 100644 index 0000000..7fdca91 --- /dev/null +++ b/docs/LSP.md @@ -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.2–0.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.7–0.10 | Deep rename (fields/variants/methods/paths) | +| 0.8–0.11 | Call hierarchy (methods, interface dispatch) | +| 0.12–0.14 | Module-path rename, `implementation`, workspace import index | +| 0.15–0.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` | diff --git a/docs/README.md b/docs/README.md index e2aec32..6dadb1a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 | diff --git a/tools/lsp_server.nim b/tools/lsp_server.nim index 62b90f2..ea32992 100644 --- a/tools/lsp_server.nim +++ b/tools/lsp_server.nim @@ -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 " | " — 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) diff --git a/tools/smoke_lsp_diagnostics.sh b/tools/smoke_lsp_diagnostics.sh new file mode 100755 index 0000000..8759a7d --- /dev/null +++ b/tools/smoke_lsp_diagnostics.sh @@ -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 diff --git a/vscode/.gitignore b/vscode/.gitignore new file mode 100644 index 0000000..6a6c8d6 --- /dev/null +++ b/vscode/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +out/ +*.vsix +.vscode-test/ +*.tsbuildinfo diff --git a/vscode/.vscodeignore b/vscode/.vscodeignore new file mode 100644 index 0000000..dcc3bfa --- /dev/null +++ b/vscode/.vscodeignore @@ -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 diff --git a/vscode/README.md b/vscode/README.md new file mode 100644 index 0000000..aeb3b46 --- /dev/null +++ b/vscode/README.md @@ -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. diff --git a/vscode/icon.png b/vscode/icon.png new file mode 100644 index 0000000..d89d51b Binary files /dev/null and b/vscode/icon.png differ diff --git a/vscode/icon.svg b/vscode/icon.svg new file mode 100644 index 0000000..97e001a --- /dev/null +++ b/vscode/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/vscode/icons/bux-file.svg b/vscode/icons/bux-file.svg new file mode 100644 index 0000000..b463699 --- /dev/null +++ b/vscode/icons/bux-file.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/vscode/language-configuration.json b/vscode/language-configuration.json index 32f42e6..e91954b 100644 --- a/vscode/language-configuration.json +++ b/vscode/language-configuration.json @@ -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" + } + } + ] } diff --git a/vscode/package-lock.json b/vscode/package-lock.json new file mode 100644 index 0000000..df6adf6 --- /dev/null +++ b/vscode/package-lock.json @@ -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==" + } + } +} diff --git a/vscode/package.json b/vscode/package.json index 9618f82..dd4afe1 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -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" } } diff --git a/vscode/snippets/bux.json b/vscode/snippets/bux.json index b396983..bf24402 100644 --- a/vscode/snippets/bux.json +++ b/vscode/snippets/bux.json @@ -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" } } diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index c96645b..df55257 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -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('enabled', true); - if (!enabled) { - vscode.window.showInformationMessage('Bux LSP is disabled in settings'); - return; - } +const SETTING_SECTION = 'bux'; - const lspPath = config.get('path', 'bux-lsp'); +export async function activate(context: vscode.ExtensionContext): Promise { + 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 | undefined { - if (!client) { - return undefined; - } - return client.stop(); + return stopClient(); +} + +async function restartClient(userInitiated: boolean): Promise { + await stopClient(); + await startClient(); + if (userInitiated) { + vscode.window.setStatusBarMessage('Bux LSP restarted', 3000); + } +} + +async function stopClient(): Promise { + 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 { + const config = vscode.workspace.getConfiguration(SETTING_SECTION); + const enabled = config.get('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('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(); } diff --git a/vscode/syntaxes/bux.tmLanguage.json b/vscode/syntaxes/bux.tmLanguage.json index c1ca00b..65c6c8d 100644 --- a/vscode/syntaxes/bux.tmLanguage.json +++ b/vscode/syntaxes/bux.tmLanguage.json @@ -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": "::|\\." } ] } diff --git a/vscode/tsconfig.json b/vscode/tsconfig.json index 5da87a1..44b048e 100644 --- a/vscode/tsconfig.json +++ b/vscode/tsconfig.json @@ -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"] }