Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c9defa0
docs(design): typing `import a.` freezes clangd 23.1; the plan for 0.0.4
Sunrisepeak Sep 24, 2026
c5b1f75
fix(engine): clangd is never given a module name that ends in a dot a…
Sunrisepeak Sep 24, 2026
d802a97
fix(engine): a file clangd will not finish is found in seconds and cl…
Sunrisepeak Sep 24, 2026
a6b46cb
fix(plan): an import still being typed gets no stand-in, and a name n…
Sunrisepeak Sep 24, 2026
44f74c0
feat(status): a problem in the user's code is a diagnostic, not a ser…
Sunrisepeak Sep 24, 2026
69ff9b6
feat(nvim): import/module get colored, and a conflicting clangd can b…
Sunrisepeak Sep 24, 2026
d12a1e5
docs: what the status means now, the typing freeze, spin recovery and…
Sunrisepeak Sep 24, 2026
943ec7f
chore(version): 0.0.4
Sunrisepeak Sep 24, 2026
2217284
docs(design): the import-hang plan as it was built
Sunrisepeak Sep 24, 2026
5ca3d0f
fix(engine): the spin log and the rebuild patience say what they now …
Sunrisepeak Sep 24, 2026
b50fe76
feat(vscode): highlight module syntax at once, name why the status is…
Sunrisepeak Sep 24, 2026
67091b9
test(vscode): cover the injected grammar, the workaround canary, stat…
Sunrisepeak Sep 24, 2026
ca92e9c
docs(vscode): document highlighting, the semantic tokens setting and …
Sunrisepeak Sep 24, 2026
6b77d06
docs(editors): highlighting, the new VS Code setting and commands, an…
Sunrisepeak Sep 24, 2026
bee9f6b
spec(s3): issue categories, the degraded hold, and module syntax in s…
Sunrisepeak Sep 24, 2026
1cd7ad4
feat(orchestrator): semantic tokens get their own server-owned legend
Sunrisepeak Sep 24, 2026
aebbc95
feat(engine): the native engine answers semantic tokens for module sy…
Sunrisepeak Sep 24, 2026
ad7415c
feat(orchestrator): the workspace remaps and merges semantic tokens, …
Sunrisepeak Sep 24, 2026
b6ff425
feat(conformance): a semantic-tokens check kind, for both module-name…
Sunrisepeak Sep 24, 2026
c6ceb5e
spec(s3): evidence for module syntax in semantic tokens
Sunrisepeak Sep 24, 2026
cca3196
fix(orchestrator): semantic tokens advertise range only where clangd …
Sunrisepeak Sep 24, 2026
0f21c14
test(vscode): the grammar probe's export import comes before import s…
Sunrisepeak Sep 24, 2026
a9dc28f
docs(design): the import-hang plan's implementation record, and a sec…
Sunrisepeak Sep 24, 2026
d6c4bb2
docs(changelog): 0.0.4
Sunrisepeak Sep 24, 2026
b9fdc0a
fix(engine): a spin found on a file already set aside still restarts …
Sunrisepeak Sep 24, 2026
658e878
fix(conformance): on Windows clangd 23.1 crashes on `import hello.` i…
Sunrisepeak Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
669 changes: 669 additions & 0 deletions .agents/docs/2026-09-25-import-hang-status-highlight.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions .agents/docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ before anything is published (`docs/92-release.md`).
turn out to matter (0.0.3 plan §5.2).
- clangd 23.1 rejects MSVC STL's aligned allocation; the plan turns aligned allocation off for
units using MSVC STL (`msvcStlNeedsNoAlignedAllocation`) until upstream fixes it.
- Every compensation for a clangd defect is a registered workaround (`WA-CLANGD-<n>`,
`src/engine/clangd/workarounds.cpp`, import-hang plan §9); `mcppls report` lists the ones in use.
- clangd 23.1 (and main at 510126255) never finishes a file in which `import std;` comes before an
`export import` of something nothing provides, under a command that names `std`'s unit. Only a file
outside the database gets such a command, the one clangd interpolates from its nearest unit, so
only a stray file with ill-formed code reaches it. The first-diagnostics guard sets it aside after
two minutes (import-hang plan §13).
- An mcpp project built for Windows through openkal needs `--target x86_64-windows-gnu`, which no
editor setting passes to mcpp yet.
- openkal cannot lower a child's scheduling priority, so clangd's cold-start module builds compete
Expand Down Expand Up @@ -243,5 +250,12 @@ before its handshake still lets the server initialize; a loader's refusal is `en
not a crash · L the icon · S being found as mcppls · P Open VSX from CI, after local verification · A
Linux arm64 with the official LLVM clangd · X one platform table.

**"import-hang plan" — [2026-09-25-import-hang-status-highlight.md](2026-09-25-import-hang-status-highlight.md).**
§1 clangd 23.1 spins on a module name ending in `.` at the end of its line · §2 why the guards did
not see it · §3 WA-CLANGD-001, the same-line `;` · §4 the spin guard, a build's budget from its own
history · §5 no stand-in for an import still being typed · §6 status issue categories, the degraded
hold · §7 module syntax colored by an injected grammar and by the server's semantic tokens · §9 the
workaround registry and its canaries · §10 living with other C++ extensions.

**"tooling architecture".** 3.2 the workspace layout · 5.1 what mcpp, mcppls and devtools each do ·
5.5 how devtools finds the server it just built · M0–M6 its migration steps.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ jobs:
- platform: linux-x64
part: 1 of 2
os: ubuntu-24.04
fixtures: mcpp-split mcpp-all-cppm verify-changes mingw mcpp-split-gcc mcpp-watch multi-root mcpp-llvm mcpp-watch@polling mcpp-emit s1-two-sets failure-at-base@vscode generated-module-negotiated
fixtures: mcpp-split mcpp-all-cppm verify-changes mingw mcpp-split-gcc mcpp-watch multi-root mcpp-llvm mcpp-watch@polling mcpp-emit s1-two-sets failure-at-base@vscode generated-module-negotiated typing-import typing-import-spin workaround-canaries
- platform: linux-x64
part: 2 of 2
os: ubuntu-24.04
Expand All @@ -289,18 +289,18 @@ jobs:
- platform: darwin-arm64
os: macos-14
extras: true
fixtures: inferred engine-none module-faults untrusted mcpp-llvm mcpp-split mcpp-all-cppm verify-changes mcpp-watch multi-root failure-at-base clangd-cannot-load failure-at-base@zed generated-module generated-module-old-mcpp generated-module-negotiated
fixtures: inferred engine-none module-faults untrusted mcpp-llvm mcpp-split mcpp-all-cppm verify-changes mcpp-watch multi-root failure-at-base clangd-cannot-load failure-at-base@zed generated-module generated-module-old-mcpp generated-module-negotiated typing-import typing-import-spin workaround-canaries
# No mcpp on the arm64 runner (its tools are the cross-built ones), so the fixtures that
# need no build tool and no compiler of their own: the semantic kit, clangd and the server
# on aarch64, with module faults, a corrupt payload and polling included.
- platform: linux-arm64
os: ubuntu-24.04-arm
cross-tools: true
fixtures: inferred engine-none module-faults untrusted payload-corrupt clangd-cannot-load failure-at-base watch-polling
fixtures: inferred engine-none module-faults untrusted payload-corrupt clangd-cannot-load failure-at-base watch-polling typing-import typing-import-spin workaround-canaries
- platform: win32-x64
part: 1 of 2
os: windows-2022
fixtures: mcpp-split mcpp-all-cppm verify-changes mcpp-split-msvc cmake-msvc-std compdb-clangxx-msvc-std multi-root compdb-clang-cl-std mcpp-llvm-msvc failure-at-base@vscode
fixtures: mcpp-split mcpp-all-cppm verify-changes mcpp-split-msvc cmake-msvc-std compdb-clangxx-msvc-std multi-root compdb-clang-cl-std mcpp-llvm-msvc failure-at-base@vscode typing-import typing-import-spin workaround-canaries
- platform: win32-x64
part: 2 of 2
os: windows-2022
Expand Down
77 changes: 77 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,83 @@ release's notes are that section.
Versions are three-part semantic versions, `MAJOR.MINOR.PATCH`, and every editor plugin carries the
product version unchanged.

## [0.0.4] — 2026-09-25

Typing an `import` no longer freezes the editor, the status bar says whose problem it is, and
`import` is colored. The investigation, the plan and its measurements are in
`.agents/docs/2026-09-25-import-hang-status-highlight.md`.

### Engine

- **Typing a dotted import froze everything.** clangd 23.1 never finishes a file in which a module
name ends in `.` at the end of its line (`import hello.`, `export module a.`): it spins at a full
core, and every later version of the file waits behind it. Typing `import hello.greet;` went
through that text every time. The feature requests for the file went unanswered for 30 s at a
time, and the status turned *degraded*. On Windows, the same text crashed clangd instead. clangd is now given that line with `;` right after the
dot, which it reports at once as the error it is; nothing else about the text changes.
Measured: every keystroke answered within 0.4 s, where 0.0.3 answered nothing for 30 s.
- **A file clangd will not finish is found and recovered even while the user keeps typing.** Before,
the guards took a busy clangd for a compiling one, and an edit to the file postponed setting it
aside for two minutes; the edit that caused the hang, and the edits fixing it, kept postponing it.
- A file's build now has a budget: five times its own last build, never under 20 s.
- Past the budget, with the editor waiting on the file, clangd is restarted at once, past the
restart cap if need be.
- The file goes to mcppls's own engine, which gives module-level features, and returns to clangd
as soon as its text is anything other than the text clangd stopped on.
- Event `engine-spin`.
- **Workarounds for clangd's own defects are registered in one place**
(`src/engine/clangd/workarounds.cpp`). Each entry has the versions it applies to, the upstream
defect, and when it can go. The report lists the ones in use (`engines[].details.workarounds`),
and `--disable-workaround WA-CLANGD-<n>` turns one off.
- **Half-typed imports no longer churn the engine database.** A module nothing provides, imported
by a file changed in the last five seconds, gets its stand-in only once the file is quiet; a
unit that provides a module still gets its stand-in at once. A name that is no module name
(`hello.`, as a build tool's scan of a file saved mid-edit can report) is never planned.

### Status

- **A problem in your code is a diagnostic, not a lost feature.** A missing `;`, an import of a
module nothing provides, or a module that does not compile is reported where it is, in the
Problems list, and the status stays *ready*. *degraded* now means the server lost something, and
it says what and where, for example "clangd stopped responding on main.cpp; module-level features
only for it until it changes". Status issues carry a `category` (`code`, `engine`,
`environment`, `project`; S3).
- A change to *degraded* is shown only once it has lasted three seconds, so a condition that passes
by itself never flickers in the status bar.

### Editors

- **`import`, `module` and `export` are colored**, and so are module names:
- The server sends semantic tokens for module syntax, which clangd sends none for (a custom type
`module`, with `namespace` for clients that do not ask for it), with and without clangd.
- The VS Code extension adds a grammar that colors them as you type, since VS Code's own C++
grammar leaves `import` uncolored.
- Settings: `mcppls.semanticTokens.modules` in VS Code, `semantic_tokens_modules` in Neovim.
- **Other C++ extensions** can be turned off, or back on, at any time:
- Commands *Turn Off Other C++ Language Features* and *Restore Other C++ Language Features*, in
this workspace or everywhere.
- A notice when one becomes active later.
- The C/C++ extension's debugger keeps working.
- Neovim has `disable_conflicting`.

### Known limits

- clangd 23.1 also never finishes a file in which `import std;` comes before an `export import` of
something nothing provides. Only a file outside the project, holding such ill-formed code, gets the
command that exposes it. mcppls sets such a file aside after two minutes, and the defect is to be
reported upstream with the first. `.agents/docs/2026-09-25-import-hang-status-highlight.md` §13 has
the details.

### Testing

- Conformance kinds `type-text` (a line typed one key at a time, each step answered in time, the
status never turning *degraded*) and `clangd-check` (a workaround's canary).
- Fixtures on every platform:
- `typing-import`;
- `typing-import-spin`: the real spin, with the workaround off, recovered within its budget;
- `workaround-canaries`: it fails once a clangd update fixes the defect.
- `module-faults` and `failure-at-base` now expect *ready*, naming their code issues.

## [0.0.3] — 2026-09-24

Linux arm64 is a platform, the extension is on Open VSX and is found by searching *mcppls*, and a
Expand Down
14 changes: 12 additions & 2 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ checks fail at once with that reason instead of each waiting out its timeout.
| `s1-two-sets` | A workspace carrying its own S1 build database (`--database`, usable plan W9.2): two sets compile the same file under `-DVARIANT=1` and `-DVARIANT=2`; `cxxModules/setContext` switches which one answers |
| `watch-polling` | Run with `--no-dynamic-watch` (usable plan W9.3): a new module interface written straight into the workspace must still reach the module graph within seconds, through the polling fallback rather than a client-driven `workspace/didChangeWatchedFiles` |
| `clangd-cannot-load` | 0.0.3 plan B1: its `prepare` step puts a stand-in clangd in the workspace (mcppls-mock-mcpp with an `unavailable` config) that writes a loader's message, a `GLIBCXX` version not found, to standard error and exits 1; `--clangd` points the server at it. `initialize` must be answered within 20 s (it used to wait for good), the status must reach `error` with issue `engine-incompatible`, and mcppls's own module features must work |
| `typing-import` | Import-hang plan §8: `import hello.greet;` in `main.cpp` and `export module hello.greet;` in its interface typed one key at a time, through `import hello.` and `export module hello.`, which clangd 23.1 never finishes building (WA-CLANGD-001); again with every step saved, as autosave does. Every request is answered within 5 s, the status never turns `degraded`, hover works right after, and nothing restarts clangd or is set aside |
| `typing-import-spin` | The same typing with WA-CLANGD-001 turned off (`--disable-workaround`), so clangd really spins (Linux, macOS; on Windows the same text crashes it instead): a spin is found within its 20 s budget (event `engine-spin`), the file set aside with that text remembered and clangd restarted, a crash is restarted as before (`engine-exit`), and either way features come back while typing goes on (import-hang plan §4). If a clangd update removes the defect, its T2 check fails as well |
| `workaround-canaries` | Import-hang plan §9: one `clangd-check` per registered workaround with a canary, run against the payload's clangd. A failure here means a clangd update fixed that defect and the workaround it names can be removed |
| `payload-corrupt` | Its `prepare` step copies the payload the runner was given and truncates clangd in the copy (usable plan W9.4); `server-arguments` then points `--payload` at that broken copy, and status must reach `error` with issue `payload-corrupt` |
| `multi-root` | Two workspace folders (usable plan W9.1): an `inferred` root and an mcpp-built `mcpp-llvm` root (level 3, from mcpp's own build database), each getting its own project model and clangd, each `cxxModules/status` telling them apart by `project.root` |

Expand Down Expand Up @@ -115,9 +118,13 @@ sees it names `["{conformance}", "prepare", "<kind>", ...]`: the generators live
(`mcppls-conformance prepare --help`), so a conformance host needs nothing the runner does not
bring — no interpreter. Positions
are `[line, character]`, zero-based, UTF-16. A check with `"text"` opens its file with that unsaved
content; a check with `"optional": true` reports `SKIP` instead of failing, and `"timeout": SECONDS`
content; a check with `"only-on": ["linux", "macos", "windows"]` runs only on those operating systems and reports `SKIP` elsewhere; a check with `"optional": true` reports `SKIP` instead of failing, and `"timeout": SECONDS`
waits less than the run's `--timeout`. `"file"` and `"folder"` on a check, like every other path a
scenario names, are relative to the fixture's own root, never to a specific workspace folder.
`"initialization-options"` on the scenario is an object merged into the runner's own
`initializationOptions` (over whatever `--client` profile set), so a fixture can ask for something
`--client` does not, such as `{"semanticTokens": {"moduleType": true}}` (design doc 2026-09-25
K/§7).
`"initialize-within": SECONDS` on the scenario fails the run when `initialize` is answered later
than that (the runner itself waits up to 120 s): a server that answers eventually is not enough
where the point is that it answers at once (`clangd-cannot-load`).
Expand All @@ -144,7 +151,7 @@ always has been.

| Kind | Passes when |
|---|---|
| `status` | `cxxModules/status` reaches `ready`, `degraded` or `error` and matches `source`, `profile-kind`, `state`, `level`, `tier` (`project.tier`, the README's L1..L4, real-project plan RP3.2), `issue-code` (with `issue-command`, that issue's command; with `issue-message`, a part of its message), `notice-code` and `engine-name`/`engines-include` when given, and a `profile-compiler` prefix (a settled status that does not match yet is looked at again for up to three seconds, since a server coalesces changes that keep its state); `"folder"` picks one root's own status in a multi-root fixture (usable plan W9.1), absent picks whichever root's arrived most recently |
| `status` | `cxxModules/status` reaches `ready`, `degraded` or `error` and matches `source`, `profile-kind`, `state`, `level`, `tier` (`project.tier`, the README's L1..L4, real-project plan RP3.2), `issue-code` (with `issue-command`, that issue's command; with `issue-message`, a part of its message; with `issue-category`, its S3 category: `code`, `engine`, `environment` or `project`), `notice-code` and `engine-name`/`engines-include` when given, and a `profile-compiler` prefix (a settled status that does not match yet is looked at again for up to three seconds, since a server coalesces changes that keep its state); `"folder"` picks one root's own status in a multi-root fixture (usable plan W9.1), absent picks whichever root's arrived most recently |
| `workspace-unchanged` | no file under the workspace was added, changed or removed after the prepare steps |
| `responds` | a request (`method`, default `textDocument/definition`) at `at` is answered, empty answers included, within the check's time |
| `module-cache-reused` | every file clangd published for `module` (default `std`) before the server started is still there unchanged, and none was added (SC4); passes on a cold start unless `--expect-warm` |
Expand All @@ -156,6 +163,7 @@ always has been.
| `completion-contains` | a completion label starts with `expect`; `insert: [line, text]` adds a line first, `edit` changes another open buffer without saving it |
| `references-span` | the references include every path in `expect` |
| `document-symbol-contains` | the outline has a top-level symbol named `expect` |
| `semantic-tokens` | `textDocument/semanticTokens/full` (or `/range`, with `"range"`) for `"file"` (optionally with an unsaved `"text"`), decoded with the legend `initialize` gave, has every entry of `"expect"` (`{"line", "text", "type", "modifiers"?}`; `"modifiers"` is a list, and optional) among its tokens (design doc 2026-09-25 K/§7) |
| `module-graph-contains` | `cxxModules/graph` lists module `expect`; retries within the check's own timeout, so it doubles as "a change reaches the graph within N seconds" (usable plan W9.3's `watch-polling`) |
| `set-context` | sends `cxxModules/setContext` with `"context"` (usable plan W9.2), then a hover at `"at"` contains `expect`, retried the same way as `hover-contains` |
| `write-file` | writes `"content"` (default: a fresh `export module <module>;`; `"content-from"` copies another workspace file) to `"file"` directly, the way a file system watcher — or, without one, the server's own polling fallback — would notice it, without the runner opening it as a document (usable plan W9.3); with `"expect-reload": true`, also waits for the status to pass through `loading` again (S2-5-1) |
Expand All @@ -164,6 +172,8 @@ always has been.
| `execute-command` | `workspace/executeCommand` with `"command"` and `"arguments"` is answered without an error (the editor's review commands, design 7.7) |
| `cli` | S5 section 7: `mcppls <args>` with the runner's payload and the fixture's server arguments, run to completion in the workspace, exits with `"exit"` (default 0) and prints one JSON document meeting `"expect"` |
| `stress` | real-project stress testing (real-project plan RP0): seeded random use — see below — meets every key present in `"budget"` |
| `type-text` | line `line` of `file` takes each of `steps` in turn, `interval-ms` apart (default 120), the whole buffer sent each time; after each, `request` (default `textDocument/documentSymbol`) is answered within `answer-within` seconds (default 5); with `save`, each step is also written to disk and reported as saved and changed, as autosave does; fails when the status turned to a state listed in `states-never` meanwhile (import-hang plan §8) |
| `clangd-check` | the runner's own clangd (`--clangd`, else the payload's) run with `--check` on `file` does not finish (`expect: "hangs"`: not finished after `seconds`, default 10, or crashed) or finishes normally (`"finishes"`); a workaround's canary expects its defect, and fails with `says` once a clangd update fixed it (import-hang plan §9) |
| `report` | robustness design O3: `cxxModules/report` meets `"expect"`, retried within the check's time like an `mcp`/`cli` result (a plan or an engine may still be on its way) |

An expectation of `mcp`, `cli` and `report` names a JSON pointer in `"path"`, where a `*` segment stands for every
Expand Down
7 changes: 6 additions & 1 deletion conformance/fixtures/engine-none/scenario.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
"expect": [{ "path": "/partitions", "contains": "hello.greet:detail" }] },
{ "id": "D1-daemon-status", "kind": "cli", "args": ["daemon", "status"], "expect": [{ "path": "/sessions", "equals": 1 }, { "path": "/status/engine/name", "equals": "none" }] },
{ "id": "D1-daemon-stop", "kind": "cli", "args": ["daemon", "stop"], "expect": [{ "path": "/ok", "equals": true }] },
{ "id": "R1-elsewhere", "kind": "responds", "file": "src/main.cpp", "at": [4, 10], "timeout": 5 }
{ "id": "R1-elsewhere", "kind": "responds", "file": "src/main.cpp", "at": [4, 10], "timeout": 5 },
{ "id": "K1-semantic-tokens", "kind": "semantic-tokens", "file": "src/main.cpp", "expect": [
{ "line": 0, "text": "import", "type": "keyword" },
{ "line": 1, "text": "import", "type": "keyword" },
{ "line": 1, "text": "hello.greet", "type": "namespace" }
] }
]
}
Loading