diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99764d5..22ed7de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,6 @@ on: pull_request: branches: [main] -env: - CARGO_TERM_COLOR: always - jobs: ocaml: name: OCaml build (${{ matrix.os }}) @@ -21,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - uses: ocaml/setup-ocaml@v3 with: - ocaml-compiler: "5.1" + ocaml-compiler: "5.4" dune-cache: true - name: Install dune run: opam install --yes dune @@ -31,30 +28,15 @@ jobs: run: opam exec -- dune build - name: Test run: opam exec -- dune runtest - - rust: - name: Rust build (httui-lsp) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2 - - name: Build - working-directory: bin/httui-lsp - run: cargo build --all-targets - - name: Check formatting - working-directory: bin/httui-lsp - run: cargo fmt --all -- --check - - name: Clippy - working-directory: bin/httui-lsp - run: cargo clippy --all-targets -- -D warnings + - name: Format check + if: runner.os == 'Linux' + run: | + opam install --yes ocamlformat.0.28.1 + opam exec -- dune build @fmt lezer-refs: name: Lezer grammar (refs) runs-on: ubuntu-latest - if: hashFiles('grammars-lezer/lezer-httui-refs/package.json') != '' defaults: run: working-directory: grammars-lezer/lezer-httui-refs @@ -75,7 +57,6 @@ jobs: tree-sitter-refs: name: Tree-sitter grammar (refs) runs-on: ubuntu-latest - if: hashFiles('lib/grammars/tree-sitter-httui-refs/grammar.js') != '' defaults: run: working-directory: lib/grammars/tree-sitter-httui-refs diff --git a/.ocamlformat b/.ocamlformat new file mode 100644 index 0000000..141c10b --- /dev/null +++ b/.ocamlformat @@ -0,0 +1,2 @@ +version = 0.28.1 +profile = default diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17a64df..9b5a6d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,6 @@ Lezer grammars, the OCaml semantic library, and the LSP server binary. ### Prerequisites - **OCaml 5.1+** and **dune** (install via opam) -- **Rust stable** (install via rustup) - **Node.js 22+** (for grammar tooling: lezer-generator, tree-sitter CLI) - **tree-sitter CLI** (`brew install tree-sitter` or via npm) @@ -25,9 +24,9 @@ make setup-hooks # installs commit-msg, pre-push, pre-commit hooks ### Build and test ```bash -make build # OCaml lib + grammars + Rust binary +make build # OCaml lib + LSP binary + grammars make test # all tests across all languages -make lint # formatters, linters, clippy +make lint # formatters and linters ``` ## Commit style diff --git a/Makefile b/Makefile index 301dc38..fa50904 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,15 @@ # Top-level Makefile orchestrating common tasks across the OCaml library, -# Rust LSP binary, and grammars (tree-sitter + Lezer). Intended as the -# canonical entrypoint for contributors and CI. +# the OCaml LSP binary, and grammars (tree-sitter + Lezer). Intended as +# the canonical entrypoint for contributors and CI. LEZER_REFS_DIR := grammars-lezer/lezer-httui-refs TS_REFS_DIR := lib/grammars/tree-sitter-httui-refs LSP_DIR := bin/httui-lsp .PHONY: help install setup-hooks build build-ocaml build-grammars build-lsp \ - test test-ocaml test-grammars test-lsp \ - lint lint-ocaml lint-rust lint-js \ - clean clean-ocaml clean-grammars clean-lsp \ + test test-ocaml test-grammars bench \ + lint lint-ocaml lint-js \ + clean clean-ocaml clean-grammars \ regenerate-grammars audit help: ## Show this help @@ -17,46 +17,41 @@ help: ## Show this help install: ## Install all dependencies opam install --yes --deps-only --with-test . - cd $(LSP_DIR) && cargo fetch cd $(LEZER_REFS_DIR) && npm install cd $(TS_REFS_DIR) && npm install setup-hooks: ## Install git hooks (commit-msg, pre-push, pre-commit) bash scripts/setup-hooks.sh -build: build-ocaml build-grammars build-lsp ## Build everything +build: build-ocaml build-grammars ## Build everything -build-ocaml: ## Build OCaml library +build-ocaml: ## Build OCaml library + LSP binary opam exec -- dune build build-grammars: ## Build all grammars cd $(LEZER_REFS_DIR) && npm run build cd $(TS_REFS_DIR) && npx tree-sitter generate -build-lsp: ## Build LSP server binary - cd $(LSP_DIR) && cargo build --all-targets +build-lsp: ## Build only the LSP server binary + opam exec -- dune build $(LSP_DIR)/httui_lsp.exe -test: test-ocaml test-grammars test-lsp ## Run all tests +test: test-ocaml test-grammars ## Run all tests -test-ocaml: ## Run OCaml tests +test-ocaml: ## Run OCaml tests (library + LSP binary) opam exec -- dune runtest test-grammars: ## Run grammar corpus tests cd $(LEZER_REFS_DIR) && npm test cd $(TS_REFS_DIR) && npx tree-sitter test -test-lsp: ## Run Rust tests for LSP binary - cd $(LSP_DIR) && cargo test --all-targets +bench: build-ocaml ## Run the LSP transport benchmark + python3 bench/lsp_roundtrip.py -lint: lint-ocaml lint-rust lint-js ## Run all linters +lint: lint-ocaml lint-js ## Run all linters lint-ocaml: ## OCaml formatter check opam exec -- dune build @fmt -lint-rust: ## Rust fmt + clippy - cd $(LSP_DIR) && cargo fmt --all -- --check - cd $(LSP_DIR) && cargo clippy --all-targets -- -D warnings - lint-js: ## JS/TS lint via eslint + prettier cd $(LEZER_REFS_DIR) && npx eslint --max-warnings 0 src test cd $(LEZER_REFS_DIR) && npx prettier --check src test @@ -68,9 +63,8 @@ regenerate-grammars: ## Regenerate parser artifacts from grammar definitions audit: ## Run security audits across ecosystems cd $(LEZER_REFS_DIR) && npm audit --audit-level=moderate cd $(TS_REFS_DIR) && npm audit --audit-level=moderate - cd $(LSP_DIR) && cargo audit || true # cargo-audit may not be installed everywhere -clean: clean-ocaml clean-grammars clean-lsp ## Remove all build artifacts +clean: clean-ocaml clean-grammars ## Remove all build artifacts clean-ocaml: ## Clean OCaml build outputs opam exec -- dune clean @@ -78,6 +72,3 @@ clean-ocaml: ## Clean OCaml build outputs clean-grammars: ## Clean grammar build outputs rm -rf $(LEZER_REFS_DIR)/dist $(LEZER_REFS_DIR)/src/parser.js $(LEZER_REFS_DIR)/src/parser.terms.js rm -rf $(TS_REFS_DIR)/build - -clean-lsp: ## Clean Rust build outputs - cd $(LSP_DIR) && cargo clean diff --git a/README.md b/README.md index 3879e09..6259516 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ grammars-lezer/ Lezer grammars for CodeMirror 6 first-paint rendering (paired with tree-sitter for httui-owned languages) grammars-external/ Upstream tree-sitter grammars vendored as submodules (postgres, mysql, sqlite — arriving in later work) -bin/httui-lsp/ Rust binary that wraps the OCaml library via FFI - and depends on httui-core for storage and execution +bin/httui-lsp/ LSP server binary (pure OCaml) — an IO shell + around the library; storage reads only, never + touches the OS keychain spec/ Canonical specifications (token kinds vocabulary, protocol notes) bench/ Benchmark fixtures and harness @@ -25,8 +26,7 @@ OWNERS.md Per-grammar ownership declarations ## Quick start -Prerequisites: OCaml 5.1+, Rust stable, Node 22+, tree-sitter CLI, opam, -dune. +Prerequisites: OCaml 5.1+, Node 22+, tree-sitter CLI, opam, dune. ```bash git clone git@github.com:httuicom/httui-lang.git @@ -46,13 +46,12 @@ Every PR runs the following checks in CI before it can be merged: | Check | Scope | |---|---| -| OCaml build + tests | Ubuntu and macOS | -| Rust build, fmt, clippy | `bin/httui-lsp` | +| OCaml build + tests (library + LSP binary) | Ubuntu and macOS | | Lezer grammar build + corpus tests | `grammars-lezer/lezer-httui-refs` | | Tree-sitter grammar generate + corpus tests | `lib/grammars/tree-sitter-httui-refs` | | Tree-sitter sanitizers (ASan + UBSan) | scanner.c when present | | Cross-grammar sync test | shared corpus for httui-owned grammars | -| Dependency audit | npm + cargo | +| Dependency audit | npm | PRs that touch grammars also require the audit checklist from the PR template to be completed. diff --git a/bench/README.md b/bench/README.md index f7147a5..94ef5a6 100644 --- a/bench/README.md +++ b/bench/README.md @@ -14,4 +14,17 @@ Cold-start metrics tracked separately: LSP spawn-to-ready, time-to-first- completion after `didOpen`, first parse + semantic tokens, and schema cache lookup. Memory budget (RSS) tracked on both medium and large fixtures. -Bench harness implementation pending. +## Harness + +- `lsp_roundtrip.py` — transport baseline against the built `httui-lsp` + binary: spawn-to-initialize, request round-trip (framing + JSON + + dispatch), and didChange ingestion. No external dependencies. + +```bash +dune build +make bench # or: python3 bench/lsp_roundtrip.py +``` + +Feature-level operations (hover, completion, semantic tokens, +diagnostics) gain sections here as the server implements them; the +fixtures above become their inputs. diff --git a/bench/lsp_roundtrip.py b/bench/lsp_roundtrip.py new file mode 100644 index 0000000..0c9bb13 --- /dev/null +++ b/bench/lsp_roundtrip.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""LSP transport benchmark: spawn-to-initialize and request round-trip. + +Measures the httui-lsp binary over its real transport (stdio, LSP framing) +with no external dependencies. This is the harness baseline — as server +features land (hover, completion, semantic tokens), each gets a section +here and budgets gate CI. + +Usage: + python3 bench/lsp_roundtrip.py [path-to-httui-lsp] + (default: _build/default/bin/httui-lsp/httui_lsp.exe) +""" + +import json +import statistics +import subprocess +import sys +import time +from pathlib import Path + +DEFAULT_SERVER = "_build/default/bin/httui-lsp/httui_lsp.exe" +WARMUP = 200 +ITERATIONS = 5000 + + +class LspClient: + def __init__(self, server_path): + self.proc = subprocess.Popen( + [server_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + self.next_id = 1 + + def send(self, payload): + body = json.dumps(payload).encode() + frame = b"Content-Length: %d\r\n\r\n" % len(body) + body + self.proc.stdin.write(frame) + self.proc.stdin.flush() + + def recv(self): + length = None + while True: + line = self.proc.stdout.readline().strip() + if not line: + break + key, _, value = line.partition(b":") + if key.lower() == b"content-length": + length = int(value) + if length is None: + raise RuntimeError("missing content-length in server response") + return json.loads(self.proc.stdout.read(length)) + + def request(self, method, params=None): + rid = self.next_id + self.next_id += 1 + payload = {"jsonrpc": "2.0", "id": rid, "method": method} + if params is not None: + payload["params"] = params + self.send(payload) + return self.recv() + + def notify(self, method, params=None): + payload = {"jsonrpc": "2.0", "method": method} + if params is not None: + payload["params"] = params + self.send(payload) + + +def percentile(sorted_values, p): + return sorted_values[round((len(sorted_values) - 1) * p)] + + +def report(name, samples_ms): + samples_ms.sort() + print( + f"{name:32s} n={len(samples_ms):>5} " + f"p50={percentile(samples_ms, 0.50):.4f}ms " + f"p95={percentile(samples_ms, 0.95):.4f}ms " + f"p99={percentile(samples_ms, 0.99):.4f}ms " + f"max={samples_ms[-1]:.4f}ms" + ) + + +def main(): + server = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_SERVER + if not Path(server).exists(): + sys.exit(f"server binary not found: {server} (run `dune build` first)") + + t0 = time.perf_counter() + client = LspClient(server) + init = client.request( + "initialize", + {"processId": None, "rootUri": None, "capabilities": {}}, + ) + spawn_ms = (time.perf_counter() - t0) * 1e3 + name = init["result"]["serverInfo"]["name"] + print(f"server: {name} spawn -> initialize: {spawn_ms:.3f} ms") + client.notify("initialized", {}) + + doc = "# bench\n\n```http alias=req1\nGET https://api.example.com\n```\n" * 30 + client.notify( + "textDocument/didOpen", + { + "textDocument": { + "uri": "file:///tmp/bench.md", + "languageId": "markdown", + "version": 1, + "text": doc, + } + }, + ) + + # Round-trip floor: an unknown method still exercises framing, JSON + # parse and dispatch on both sides (server answers MethodNotFound). + def probe(): + client.request("httui/benchProbe") + + for _ in range(WARMUP): + probe() + samples = [] + for _ in range(ITERATIONS): + t = time.perf_counter() + probe() + samples.append((time.perf_counter() - t) * 1e3) + report("request round-trip (dispatch)", samples) + + # didChange ingestion: full-sync document replace per notification, + # measured via a probe request behind each batch (notifications have + # no response of their own). + version = 2 + samples = [] + for _ in range(1000): + t = time.perf_counter() + client.notify( + "textDocument/didChange", + { + "textDocument": {"uri": "file:///tmp/bench.md", "version": version}, + "contentChanges": [{"text": doc}], + }, + ) + probe() + samples.append((time.perf_counter() - t) * 1e3) + version += 1 + report("didChange(2KB doc) + probe", samples) + + client.request("shutdown") + client.notify("exit") + client.proc.wait(timeout=5) + + +if __name__ == "__main__": + main() diff --git a/bin/httui-lsp/Cargo.toml b/bin/httui-lsp/Cargo.toml deleted file mode 100644 index db26557..0000000 --- a/bin/httui-lsp/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "httui-lsp" -description = "LSP server for httui — wraps the OCaml language layer and depends on httui-core for storage and execution" -version = "0.1.0" -edition = "2021" -rust-version = "1.80" -license = "MIT" -authors = ["João Ferreira "] -repository = "https://github.com/httuicom/httui-lang" -homepage = "https://httui.com" -documentation = "https://github.com/httuicom/httui-lang#readme" - -[dependencies] -httui-core = { git = "https://github.com/httuicom/httui-core", rev = "aa8c44f65700933ba3e0d6476c038b5ef2f4ba9e" } -tokio = { version = "1", features = ["full"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" diff --git a/bin/httui-lsp/dune b/bin/httui-lsp/dune new file mode 100644 index 0000000..8e149e1 --- /dev/null +++ b/bin/httui-lsp/dune @@ -0,0 +1,5 @@ +(executable + (name httui_lsp) + (public_name httui-lsp) + (package httui_lang) + (libraries httui_lang lsp jsonrpc yojson)) diff --git a/bin/httui-lsp/httui_lsp.ml b/bin/httui-lsp/httui_lsp.ml new file mode 100644 index 0000000..8bba721 --- /dev/null +++ b/bin/httui-lsp/httui_lsp.ml @@ -0,0 +1,126 @@ +(* httui-lsp — Language Server for httui blocks and references. + + Pure OCaml: an IO shell (stdio framing + JSON-RPC dispatch) around the + pure [Httui_lang] library. Storage access, when it arrives, is + read-only by design; the server never touches the OS keychain — secret + values exist only on the execution path, outside this process. *) + +let docs : (string, string) Hashtbl.t = Hashtbl.create 16 + +(* --- stdio framing ----------------------------------------------------- *) + +let read_message () = + let len = ref (-1) in + let rec read_headers () = + let line = input_line stdin in + let line = + let n = String.length line in + if n > 0 && line.[n - 1] = '\r' then String.sub line 0 (n - 1) else line + in + if line = "" then () + else begin + (match String.index_opt line ':' with + | Some i -> + let key = String.lowercase_ascii (String.sub line 0 i) in + if key = "content-length" then + len := + int_of_string + (String.trim + (String.sub line (i + 1) (String.length line - i - 1))) + | None -> ()); + read_headers () + end + in + read_headers (); + if !len < 0 then failwith "missing content-length header"; + really_input_string stdin !len + +let write_packet packet = + let body = Yojson.Safe.to_string (Jsonrpc.Packet.yojson_of_t packet) in + Printf.printf "Content-Length: %d\r\n\r\n%s" (String.length body) body; + flush stdout + +let respond id json = + write_packet (Jsonrpc.Packet.Response (Jsonrpc.Response.ok id json)) + +let respond_error id code message = + write_packet + (Jsonrpc.Packet.Response + (Jsonrpc.Response.error id + (Jsonrpc.Response.Error.make ~code ~message ()))) + +(* --- handlers ----------------------------------------------------------- *) + +let params_json (params : Jsonrpc.Structured.t option) = + match params with Some p -> Jsonrpc.Structured.yojson_of_t p | None -> `Null + +let on_initialize (r : Jsonrpc.Request.t) = + let capabilities = + Lsp.Types.ServerCapabilities.create + ~textDocumentSync: + (`TextDocumentSyncOptions + (Lsp.Types.TextDocumentSyncOptions.create ~openClose:true + ~change:Lsp.Types.TextDocumentSyncKind.Full ())) + () + in + let serverInfo = + Lsp.Types.InitializeResult.create_serverInfo ~name:"httui-lsp" + ~version:Httui_lang.version () + in + let result = Lsp.Types.InitializeResult.create ~capabilities ~serverInfo () in + respond r.id (Lsp.Types.InitializeResult.yojson_of_t result) + +let shutdown_received = ref false + +let handle_request (r : Jsonrpc.Request.t) = + match r.method_ with + | "initialize" -> on_initialize r + | "shutdown" -> + shutdown_received := true; + respond r.id `Null + | m -> respond_error r.id Jsonrpc.Response.Error.Code.MethodNotFound m + +let handle_notification (n : Jsonrpc.Notification.t) = + match n.method_ with + | "initialized" -> () + | "textDocument/didOpen" -> + let p = + Lsp.Types.DidOpenTextDocumentParams.t_of_yojson (params_json n.params) + in + Hashtbl.replace docs + (Lsp.Types.DocumentUri.to_string p.textDocument.uri) + p.textDocument.text + | "textDocument/didChange" -> ( + let p = + Lsp.Types.DidChangeTextDocumentParams.t_of_yojson (params_json n.params) + in + (* Full sync: the last change event carries the whole document. *) + match List.rev p.contentChanges with + | last :: _ -> + Hashtbl.replace docs + (Lsp.Types.DocumentUri.to_string p.textDocument.uri) + last.text + | [] -> ()) + | "textDocument/didClose" -> + let p = + Lsp.Types.DidCloseTextDocumentParams.t_of_yojson (params_json n.params) + in + Hashtbl.remove docs (Lsp.Types.DocumentUri.to_string p.textDocument.uri) + | "exit" -> exit (if !shutdown_received then 0 else 1) + | _ -> () + +(* --- main loop ---------------------------------------------------------- *) + +let () = + set_binary_mode_in stdin true; + set_binary_mode_out stdout true; + try + while true do + let body = read_message () in + let json = Yojson.Safe.from_string body in + match Jsonrpc.Packet.t_of_yojson json with + | Jsonrpc.Packet.Request r -> handle_request r + | Jsonrpc.Packet.Notification n -> handle_notification n + | _ -> () + done + with End_of_file -> () diff --git a/bin/httui-lsp/src/main.rs b/bin/httui-lsp/src/main.rs deleted file mode 100644 index b579ed6..0000000 --- a/bin/httui-lsp/src/main.rs +++ /dev/null @@ -1,15 +0,0 @@ -// LSP server for httui. -// -// This binary wraps the OCaml language layer (in ../lib) via FFI and -// depends on httui-core for SQLite, keychain, and vault operations. The -// implementation is bootstrapped from the existing httui-mcp template -// (argument parsing, db pool, executor registry) and gains LSP-specific -// request handling on top. - -fn main() { - eprintln!( - "httui-lsp v{} — not yet implemented", - env!("CARGO_PKG_VERSION") - ); - std::process::exit(1); -} diff --git a/dune-project b/dune-project index 9598641..a69775d 100644 --- a/dune-project +++ b/dune-project @@ -16,4 +16,7 @@ (description "Tree-sitter and Lezer grammars, semantic analysis, type inference, and LSP support for httui executable blocks and references.") (depends (ocaml (>= 5.1.0)) - dune)) + dune + lsp + jsonrpc + yojson)) diff --git a/httui_lang.opam b/httui_lang.opam new file mode 100644 index 0000000..379c989 --- /dev/null +++ b/httui_lang.opam @@ -0,0 +1,35 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +version: "0.1.0" +synopsis: "Language layer for httui — parsing, semantics, type inference" +description: + "Tree-sitter and Lezer grammars, semantic analysis, type inference, and LSP support for httui executable blocks and references." +maintainer: ["João Ferreira "] +authors: ["João Ferreira "] +license: "MIT" +homepage: "https://httui.com" +doc: "https://github.com/httuicom/httui-lang#readme" +bug-reports: "https://github.com/httuicom/httui-lang/issues" +depends: [ + "ocaml" {>= "5.1.0"} + "dune" {>= "3.10"} + "lsp" + "jsonrpc" + "yojson" + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: "git+https://github.com/httuicom/httui-lang.git" diff --git a/lib/grammars/tree-sitter-httui-refs/grammar.js b/lib/grammars/tree-sitter-httui-refs/grammar.js index ea0cfb6..e3f4bf1 100644 --- a/lib/grammars/tree-sitter-httui-refs/grammar.js +++ b/lib/grammars/tree-sitter-httui-refs/grammar.js @@ -6,7 +6,7 @@ * layer (in httui-lang/lib) and the TUI client consume the concrete * syntax tree produced here. * - * Coverage (Slice 1 minimal): + * Initial coverage: * {{alias}} * {{alias.path}} * {{alias.path.nested}} @@ -14,8 +14,9 @@ * {{ENV_VAR}} * * Sync contract: the Lezer counterpart in - * grammars-lezer/lezer-httui-refs/ must emit equivalent token kinds - * for the same inputs. Cross-grammar corpus tests enforce alignment. + * grammars-lezer/lezer-httui-refs/ emits a lexical coarsening of these + * tokens (every Lezer token maps to a span here with an equal or more + * generic kind). Cross-grammar corpus tests enforce alignment. */ module.exports = grammar({ diff --git a/spec/token-kinds.md b/spec/token-kinds.md index 548d244..83a00ef 100644 --- a/spec/token-kinds.md +++ b/spec/token-kinds.md @@ -6,8 +6,15 @@ kinds to these names; clients (CM6, ratatui, external editors via LSP semantic tokens) consume only these names. The LSP server declares them in `SemanticTokensLegend` at `initialize`. -Clients without explicit theme rules for an `httui.*` kind fall back on the -LSP-standard kind shown in the second column. +Fallback is a **server-side downgrade**: token types are unique per range +and clients silently ignore types outside their announced capabilities, so +the server compares the `httui.*` kinds with the client's capabilities at +`initialize` and, for clients without support, emits the LSP-standard +fallback kind (second column) in both the legend and the token stream. +In VS Code, the extension registers the custom types via the +`semanticTokenTypes` contribution with `superType` pointing at the +fallback — VS Code's native degradation mechanism, aligned with this +table. ## Standard kinds (LSP 3.17 SemanticTokenTypes) @@ -81,6 +88,11 @@ A PR that adds a new `httui.*` kind without a fallback is rejected. to this spec; grammars don't ship kinds outside this list. 5. **Fallback mandatory for every `httui.*`.** Without it, an external editor with no extension renders the token uncolored. +6. **`secret` implies value masking.** No surface (hover, `inlineValue`, + completion detail, server logs) shows the real value of a secret env + var — only a masked form. The modifier governs token rendering; the + masking is a server invariant (the server has no keychain access by + design). ## Theming integration @@ -120,4 +132,7 @@ let legend = { ``` Clients announce which subsets they support via `ClientCapabilities`. -Server respects client capabilities; unknown kinds are dropped silently. +The legend above is the **full** vocab; the legend actually registered is +computed per client — `httui.*` kinds the client did not announce are +replaced by their fallback before registration, so no token ever reaches +a client as an unknown (silently dropped) kind.