Skip to content

mcpp-language-server 0.0.6: definitions in implementation units, qt-demo's generated headers, xmake and meson, a download asked about once, one settings registry - #27

Merged
Sunrisepeak merged 28 commits into
mainfrom
release/0.0.6
Sep 27, 2026
Merged

Sunrisepeak merged 28 commits into
mainfrom
release/0.0.6

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

mcpp-language-server 0.0.6: go-to-definition reaches implementation units, qt-demo's generated headers are found, xmake and meson join mcpp and CMake, a download is asked about once and never waited for, and every setting has one definition.

The analysis, measurements, decisions (D1, D2, D4′, D5, D8, D9, D10) and the implementation plan: .agents/docs/2026-09-27-qt-demo-navigation-discovery-plan.md (§9 is the task plan). Upstream defects: #24 (UP-08 updated, UP-17, UP-M4 = mcpp-community/mcpp#724, UP-M5 = mcpp-community/mcpp#725).

What changes

Navigation

  • WA-CLANGD-008: clangd's background index compiles a module unit without building its imports, so a definition in an implementation unit was indexed apart from its declaration (every unit logs index may be incomplete). mcppls builds implementation units through clangd's foreground — relevant ones first, changed-on-disk ones again, the rest when idle (mcppls.index.primeImplementationUnits).
  • The definition search picks the units that define the name (lexical scan), and when clangd still only knows the declaration the definition found by name is the answer; on a definition the answer stays its declaration.
  • implementation-unreadable says when a unit cannot build.

Projects and build tools

  • BuildSystemProvider registry: mcpp, CMake, xmake, meson, compile-commands; nothing ever written into the workspace (xmake: private XMAKE_CONFIGDIR/--builddir, network.mode:private; meson: --wrap-mode=nodownload).
  • CMake follows CMakePresets.json, and its private configure is disconnected from the first run (BD7 withdrawn).
  • Rule inputs (.ui/.qrc/.ts) are left out; generated output is read from the project's target/, or reported missing with a build action and watched for.
  • A download is offered once in a non-blocking notification (askOnline, mcppls.describeOnline); meanwhile the project is served from its sources and the offline description retried on a backoff, so a build in the person's own terminal upgrades it by itself. Partial mcpp answers with a missing download get the same offer.
  • First model wait 2.5 s (was 10), then L4 until the build tool's model replaces it (one switch, never counted).
  • One list of project manifests for check, query and the boundaries; nested CMake/xmake/meson manifests belong to the project above.

Engine: std moves to the kit only when std's own unit fails to compile or has no unit; a report about a database clangd has not read is ignored.

Settings: src/config/settings.cppm is the one registry; command line, initializationOptions, didChangeConfiguration, the report and docs/30-settings.md (en, zh-CN) are derived from it, and tests/test_settings.cpp holds them and package.json to it. mcppls settings.

Specs: S3-4-16..21 (askOnline, non-blocking offer), project.source xmake/meson, report settings. Design record: BD5, BD7, RD1 revised; BD9, BD10, RD7–RD10.

Verification (local, Linux x64)

  • mcpp test 31/31, mcpp test --workspace all members, devtools check all, validate.py (251 rules, 0 failures); cross-builds for x86_64-windows-gnu and aarch64-macos.
  • New fixtures: mcpp-partition-definition (fails N1–N5 with 0.0.5, passes now), mcpp-rules-generated, mcpp-emit-provisioned, mcpp-emit-partial-download, build-discovery-off, cmake-fetchcontent-offline; mcpp-emit-needs-download extended.
  • On mcpp's own source (2026.9.27.1): phase0_…, phase4b_… and prepare_build resolve to their .cpp (0.0.5: declarations for 420 s). On a qt-demo copy: main.cpp has no diagnostics (0.0.5: fatal missing ui_mainwindow.h). On an xmake modules project: tier 3 from xmake, clangd --check exits 0, project unchanged.
  • compdb-mixed-standards fails locally with 0.0.5 as well (this machine's clang++ is 22; CI sets CONFORMANCE_CLANGXX).
  • Local regression over 55 Linux fixtures: all pass except compdb-mixed-standards (above).
  • GalTranslPP Windows definition timeline (D8): baseline 0.0.5 (run 36317141415: of 32 call sites in the last round, 11 land on the .ixx declaration, 2 on the .cpp); the 0.0.6 round runs after the release.

Self-review fixes (final diff review)

  • xmake: whether to run xmake f again stamped a directory (always empty), and xmake keeps network.mode:private in its own configuration, so an online run stayed offline. The mode and xmake.lua's stamp are now recorded after a successful configure (real xmake: second run skips it, an edited xmake.lua configures again, project untouched).
  • N-7: a stuck priming unit that is given up now frees its slot for the next one.
  • D3: a watched batch of more than 20 files only joins the idle tier.
  • The settings docs test reads a CRLF checkout (Windows CI).
  • Release checks (real-xlings-old-mcpp STRESS1 p90 6.41 s > 3 s; 0.0.5: 2.83 s): N-7 opened implementation units while modules were still being prepared, taking clangd's workers from preparation and from requests. They now wait for preparation to finish. Reproduced locally on 4 cores: 0.0.5 1.35 s; before 2.25 / 2.44 s; after 1.52 / 1.80 s.

Sunrisepeak and others added 28 commits September 27, 2026 19:06
… units, build discovery, one settings module
… does not compile or does not exist

qt-demo lost its toolchain semantics to a race: std.cc joined the engine database and 78 ms later clangd,
still on the database it had read before, reported "Don't get the module unit for module std". That was
taken as the standard library failing, the whole project was read with libc++ and clangd restarted twice.

A report that a module has no unit is now weighed against the database clangd has actually read: before it
has read the one the unit joined, the report is about the old database and is logged, not acted on. The kit
replaces the toolchain's std only when std's own unit fails to compile or the plan has no unit for it at all
(plan 2026-09-27 Q1-1, Q1-4, D4'). The decision is a pure function, failure_action, tested with qt-demo's
sequence.
…read from the project's own build

On a rules-qt project mcpp's build database lists the Qt form, resource list and translation as translation
units with a compiler command, and names uic's and moc's output in its private planning directory, where by
its own specification no action ever runs (mcpp-community/mcpp#724). clangd then failed on the three inputs
at every scan and on every file including ui_mainwindow.h.

Units no C-family compiler reads are left out, with a notice. Include directories and sources in a
producer's planning directory are replaced, read-only, by the same paths under the project's own target/
when a build has written them; when none has yet, the status says which generated files are missing,
offers to build in a terminal, and watches where the build will put them so the model is loaded again
(plan 2026-09-27 Q1-2, Q1-3). On a copy of qt-demo, main.cpp goes from a fatal missing header to clangd
--check exiting 0.
…oreground

clangd's background index compiles a module unit without building the modules it imports: every unit of a
module project logs "Failed to compile ..., index may be incomplete", and a definition in an implementation
unit is indexed apart from its declaration, or not at all, until the unit has been open. In mcpp's own
prepare module, go-to-definition on phase0_manifest_and_workspace stayed on its declaration for as long as
it was measured (420 s).

Registered as WA-CLANGD-008, the engine now opens implementation units in clangd a few at a time, lets the
foreground build them with their modules, and closes them again; their symbols stay in clangd's index. The
units of an opened file's module and of the modules it imports go first, a unit changed on disk while not
open goes again, and the rest follow once clangd has been idle for a while. A restart of clangd starts over.
A unit that does not build (a header nothing generated yet) is told as implementation-unreadable (plan
2026-09-27 N-7, N-3). On mcpp's source, both phase functions now resolve to their .cpp from the first answer.
…sked about once, and upgrades itself

With nothing cached the build tool now has 2.5 s, not 10, to describe the project (plan D5); after that the
project is served from its scanned sources by both engines while the build tool goes on, and its model
replaces the provisional one in one switch that is never counted against clangd's restart budget.

When the offline description needs a download, the status issue carries askOnline, and mcppls.describeOnline
describes the project once with the network, the person's choice from the editor; later loads are offline
again. Needing a download is not the end of it: the offline description is asked again after 30 s, 1, 2 and
then every 5 minutes, and at once when a watched input changes, so a build or install the person runs in
their own terminal upgrades the project on its own.

The VS Code extension offers the download in a non-blocking notification that may stay unanswered forever,
once per workspace and set of missing things, never after Don't Ask Again, and does nothing if the answer
comes after the need went away (plan 2026-09-27 B-2, B-4, §9.2). Run in Terminal knows xmake and meson too.
…ed in the person's own terminal

mcpp-emit-needs-download now checks that its issue lets a client offer to fetch what is missing and that
mcppls.describeOnline describes the project once with the network, after which the model is mcpp's. The new
mcpp-emit-provisioned answers no question at all: the person builds in their own terminal, which writes
mcpp.lock, and the server upgrades from scanned sources to mcpp's model by itself. The mock producer learns
an online answer and a file that, once present, means what was missing has been fetched; the status check
learns issue-ask-online.
…definition

0.0.6 plan §9 T1/T1a/T1b: a new module mcppls.config.settings (src/config/settings.cppm
+ .cpp) holds one registry row per setting -- key, kind, allowed values, default,
command-line spelling, surface, how a change applies, category, version, an English and
a Chinese summary, and aliases -- and a Settings type that resolves the command line,
initializationOptions and workspace/didChangeConfiguration over it in that precedence,
falling back to the default and recording a problem for anything outside a row's own
vocabulary rather than ever letting an unrecognized value take effect.

mcppls.cli.commands now declares its global options by iterating the registry instead of
listing them by hand, and forwards them to a daemon the same way (which also fixes
--producer-timeout never reaching one); cli::options::session_options fills SessionOptions
from the registry's command-line layer; server::Session layers initializationOptions onto
the very same Settings object at handle_initialize_, and a new handle_configuration_change_
applies workspace/didChangeConfiguration, reloading every root's model
(Workspace::reload_with_options, a small addition alongside the existing schedule_reload)
for a `reload`-applies setting and logging that a restart is needed for a `restart` one.
SessionOptions gains fields for two settings new in 0.0.6 that other tasks implement the
behaviour of: buildDiscovery (+ .providers, .askBeforeDownload) and
index.primeImplementationUnits, plus compiler and semanticKit now that they have a
command-line spelling too (--compiler, --semantic-kit). mcppls report gains a `settings`
object (value, origin and problems per key), and a new `mcppls settings [--format
markdown|json] [--lang en|zh-CN]` subcommand prints the registry as the reference table or
its machine form.

docs/30-settings.md and its zh-CN mirror replace their hand-written tables with the exact
output of `mcppls settings --format markdown`, between `<!-- settings:begin/end -->`
markers, with new prose on precedence, what `applies` means, and how a renamed setting
would keep working. editors/vscode/package.json gets the four new settings
(mcppls.buildDiscovery and its two children, mcppls.index.primeImplementationUnits);
extension.ts passes them in initializationOptions and restarts the server when any of
them changes, same as the existing ones. tests/test_settings.cpp proves the registry's
own invariants (unique keys and aliases, defaults inside their vocabulary), precedence and
origins across all three layers, every accepted initializationOptions/didChangeConfiguration
shape (nested, dotted, mcppls-wrapped), an unknown enumeration value falling back to the
default, an alias still working, the generated docs matching the renderer byte for byte,
and every mcppls.* property of package.json matching a registry row that expects to be
there (and no server-only row that does not).

Decided: which server rows are command-line only rather than a VS Code setting --
payload/clangd/kit/mcpp/database/untrusted/discoverCompilers/logLevel/disableWorkaround/
producerTimeout/requestTimeout, plus semanticTokens.moduleType (VS Code's own extension
always declares it, fixed) -- is recorded on each row (Setting::clientConfigurable) and
proven by the same test. Zed's and CLion's docs mention no settings today, so neither
needed a change. Not done here: the actual behaviour behind buildDiscovery and
primeImplementationUnits belongs to other 0.0.6 tasks (T5, T7-T10); this only carries the
registry, the plumbing, and the fields they will read.
…generated header

mcpp-partition-definition: functions and members declared in a partition with class-type parameters and
defined in other units of the module. With 0.0.5 go-to-definition stays on the declarations for as long as
the checks wait; with the implementation units built through clangd's foreground it reaches them.
mcpp-rules-generated: a Qt form listed as a translation unit is left out, and the header uic would generate
is reported missing, with a build action, until a build writes it into the project's target/, after which
the including file has semantics.
…s, and the reference speaks to users

buildDiscovery.askBeforeDownload changed through workspace/didChangeConfiguration now reaches every running
root at once; index.primeImplementationUnits is read when clangd's engine is made, so it applies on
restart. The generated reference no longer cites internal plan sections; mcppls.index.primeImplementationUnits
says what it does and why (WA-CLANGD-008), and the settings index.primeImplementationUnits and
buildDiscovery.askBeforeDownload are wired to what they control.
…on it, and names xmake and meson

A producer-needs-download issue may carry askOnline: asked through mcppls.describeOnline, the server
describes the project once with the network, and reaches none on its own before that (S3-4-16 to S3-4-18).
A client offering it never blocks on the question, asks once per root and set of missing things, and does
nothing with an answer that comes after the need is gone (S3-4-19 to S3-4-21). project.source gains xmake
and meson, the new issue codes are named, and cxxModules/report carries the settings in effect.
…once, the kit only for a std that fails, implementation units in the index, one settings registry
…load now asks for one

S2 0.3.0 (mcpp-community/mcpp#699) lets mcpp plan a workspace's members independently: one
member's dependency needing a download offline forbids no longer fails the whole document, only
that member's own diagnostic. src/project/mcpp.cpp only ever looked at a diagnostic's severity here,
so a partial answer whose failed member carried MCPP_OFFLINE_DOWNLOAD_REQUIRED was reported as
producer-partial and nothing else -- the workspace never entered the needs-download path, and nobody
was ever asked to fetch the dependency. It now also adds a spec::NEEDS_DOWNLOAD issue with the
producer's own message, alongside producer-partial, while keeping and using the partial model exactly
as before.

conformance/fixtures/mcpp-emit-partial-download is mcpp-emit-partial's mirror image, from where the
two get their shape: one member is planned, one fails with the download-required code. The scenario
checks the status carries both issues, navigation still works from the planned member, and the
workspace stays unchanged. Added to the ci.yml Linux fixture list and conformance/README.md.
…ch, and CMake, xmake and meson join it

B-1 (2026-09-27 plan §3.2, §3.6). Every build tool's data source now implements one interface
(mcppls.project.provider): id(), detect(root) (filesystem only, cheap), existing(claim, context)
(reads already-there build output, runs nothing), describe(claim, context) (runs the build tool,
offline or not per the context, into its own private directory under the workspace's cache -- never
the project's own build directory), and watch_inputs(claim). mcppls.project.providers is the new,
small module that lists them in the registry's fixed order (mcpp, cmake, xmake, meson,
compile-commands) and is the only module that depends on every one of them; mcppls.project.detect
keeps only the vocabulary (SourceKind, tier_of) and asks the registry for everything else, with the
configured-database exception (mcppls.database) kept first and unconditional, exactly as before.
detect_project and load_project both take the registry (or a filtered subset of it,
LoadOptions::providers -- empty means every registered one, a provider left out is skipped as if its
files were not there); LoadOptions also gains buildDiscovery (false: nothing is detected, read or
run implicitly, only an explicit database is used, and a build-discovery-off notice says so -- a
notice, since nothing is reduced by an explicit setting). SourceKind gains xmake and meson, both
tier 3 like a bare compile_commands.json (exact arguments, module roles recovered by scanning rather
than said by the build system).

mcppls.project.infer gains database_from_compile_commands_file, the one thing every tier-3 provider
does once it has a compile_commands.json path: read it and tag the result with which tool wrote it.
CMake, xmake and meson (and the bare compile_commands.json case) all go through it.

fix(project): B-3(a) CMake configures offline first (2026-09-27 plan D1). BD7's "the first private
configure may download" is withdrawn: -DFETCHCONTENT_FULLY_DISCONNECTED=ON is passed whenever the
context is offline, first configure or not, and never when it is not; the configure-first network
exception is gone. An offline configure that fails because a dependency's source is not populated is
now told apart from any other configure failure and reported as needs-download, naming the
dependency (spec::NEEDS_DOWNLOAD) -- measured against a real cmake 4.4.2 with an undeclared
FetchContent dependency: the message wraps the marker phrase across a line break and reindents it,
so the parser (fetchcontent_missing_dependencies) collapses whitespace before it looks, or it would
only ever match a hand-typed one-line test string, never cmake's own output.

feat(project): B-3(b) CMakePresets.json and CMakeUserPresets.json are read (user presets win: a
preset the user file names again replaces the project's outright), inherits chains resolved, hidden
ones skipped, and the first configure preset left in file order is used: its binaryDir (macro
expansion: ${sourceDir}, ${sourceParentDir}, ${presetName}, ${sourceDirName}, $env{NAME}) as an
existing()-readable build directory, and its generator, toolchainFile and cacheVariables followed by
the private configure -- ignoring a preset's own compiler in favour of whatever the private configure
would have picked could describe semantics that do not match what the user actually builds with.

feat(project): B-5 xmake (mcppls.project.xmake). xmake project -k compile_commands does not compile,
but still does an implicit configuration and a module dependency scan -- both measured side effects
that write into the project unless isolated: XMAKE_CONFIGDIR keeps the configuration out of the
project's own .xmake/, and only xmake f can set --builddir, so a first describe() is two commands
(xmake f -c, then the compile-database command); network.mode:private on top of package.fetch_only
is what actually stops it updating its package repositories offline (package.fetch_only alone still
does, on a machine that has never fetched them). A missing package is recognized from either shape
xmake reports it in and named. Manually verified against a real xmake 3.1.1 and the hello-modules
project this plan's navigation testing already uses: the project directory is provably unchanged
(a full recursive diff before and after) in both the cold path (~6.6s configure + ~7.9s compile-
database) and the warm one, where only the second command reruns (~5.3s).

feat(project): B-6 meson (mcppls.project.meson), unmeasured at P3 (2026.9.27 plan §3.3): meson setup
--wrap-mode=nodownload into a private directory offline, --reconfigure once one is already there.
Manually verified against meson 1.12.1 and a tiny C++ project: the project directory is unchanged,
compile_commands.json is produced in the private directory, and a second describe() reconfigures it
correctly. The exact wording of a wrap-based subproject download failure was not measured against a
real one, so meson_missing_subprojects is best-effort and says so.
… that is not fetched without asking

build-discovery-off: an mcpp project whose producer would answer, with mcppls.buildDiscovery = off. Nothing
is detected, read or run; the model is its scanned sources and the report says why. cmake-fetchcontent-offline:
a CMake project whose FetchContent dependency is not on the machine. The private configure is disconnected
from its first run and stops at producer-needs-download naming the dependency, with askOnline, instead of
downloading it.
…, and a nesting one reaches its top

check, query and the project boundaries each kept their own list of the files that make a directory a
project's root, and none knew xmake.lua or meson.build, so mcppls check on an xmake project scanned its
sources instead. They share one list now, and a subdirectory's own CMakeLists.txt, xmake.lua or meson.build
belongs to the project above it: the root is the outermost of an unbroken chain. xmake.lua, meson.build,
meson_options.txt and CMakeUserPresets.json are watched build files. On an xmake modules project, mcppls
check now reads xmake's description (tier 3) and clangd --check exits 0, with nothing written.
…t just file stem (N-8)

src/engine/clangd/definition.cppm/.cpp gain a lexical scanner search_definition_'s wiring can use
to pick the right implementation unit instead of guessing by stem and directory: declared_function_at
reads a declaration's unqualified name, enclosing namespaces/classes and normalized parameters;
function_definitions finds every definition of a name in a file's text the same way, reconstructing
its qualified name from enclosing namespace blocks and the declarator's own qualification; same_function
decides whether a definition is plausibly what a declaration declares, tolerating either side spelling
fewer of the enclosing scopes; and units_defining refines units_to_search's ranking so units that
actually define the name come first, in path order.

This is what mcpp's own source needs: mcpp.build.prepare declares phase0_manifest_and_workspace in an
implementation partition and defines it in one of 17 implementation units, and the existing stem/directory
ranking (UNITS_PER_SEARCH = 4) never picks the right one. The scan is lexical, not a parse -- comments,
string/character literals (raw strings included) and preprocessor lines are skipped, but a type alias or
an unusual macro can still fool it, and it is built to fail closed (no match) rather than guess.

Not wired into search_definition_ here; that is a separate change. Covered by tests/test_definition.cpp
(22 cases, 73 assertions): the mcpp.build.prepare and prepare_build shapes verbatim from the plan, member
functions declared inside a namespace and class, overloads told apart by parameter type, defaults dropped,
constructors and destructors, comments/strings/raw-strings/preprocessor lines that must not be misread,
call sites, templates, trailing return types, and the ranking helper.

Co-authored-by: speak-agent <noreply@anthropic.com>
…son, and turning discovery off, in both languages
…e in the module's units

The search a definition request starts when clangd knows only a declaration in a module interface opened at
most four of the module's units, chosen by file name, so in mcpp's prepare module (17 units) it opened
config.cpp, driver.cpp, features.cpp and fetch.cpp and never manifest.cpp. The units that define the name,
found lexically, now come first. When clangd still answers with the declaration after they are built -- it
did for prepare_build even with driver.cpp open, its index holding no symbol for it at all -- the definition
found by name is the answer: same name, scopes that agree and the same parameter types as spelled, never a
guess. Asked on a definition, the answer stays its declaration, as every editor does it (plan 2026-09-27
N-8, O-1). On mcpp's source, prepare_build now resolves to driver.cpp from its callers and its declaration.
… a stuck priming unit frees its slot; a large watched batch only joins the rest (D3)
@Sunrisepeak
Sunrisepeak merged commit 7265cdf into main Sep 27, 2026
47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant