diff --git a/.agents/docs/mcppls-integration.md b/.agents/docs/mcppls-integration.md index 3e85850..d3a0695 100644 --- a/.agents/docs/mcppls-integration.md +++ b/.agents/docs/mcppls-integration.md @@ -43,7 +43,7 @@ mcppls 侧**没有** `extensionDependencies`:依赖是单向的。 | `mcppls.collectReport` | 收集诊断报告 | none | | `mcppls.exportDiagnosticBundle` | 导出诊断包(服务器命令 `mcppls.exportBundle`) | none | | `mcppls.runBuildToolInTerminal` | 在集成终端运行构建工具 | confirm | -| `mcppls.turnOffOtherCppFeatures` / `mcppls.restoreOtherCppFeatures` | 关闭/恢复其它 C++ 扩展的语言特性(**会写别的扩展自己的设置**,由 mcppls 执行) | confirm | +| `mcppls.turnOffOtherCppFeatures` / `mcppls.restoreOtherCppFeatures` | 关闭/恢复其它 C++ 扩展的语言特性(**会写别的扩展自己的设置**,由 mcppls 执行;它自己先问作用范围,关掉问题即不执行)。0.7.0 起是两条能力,不再共用一条候选链 | none | | `mcppls.turnOnInWorkspace` / `mcppls.turnOffInWorkspace` | 在本工作区启用/停用 C++ Modules(**会写 `mcppls.enable`**,由 mcppls 执行) | confirm | | `mcppls.installCommandLineTools` | macOS 命令行工具 | confirm | | `mcppls.review.run` / `mcppls.review.clear` | Review Changes(**服务器广告**的命令,服务器运行后才存在;受 `mcppls.ai.enabled` 约束) | none | @@ -120,6 +120,7 @@ VS Code 命令,重名会让 mcppls 的语言客户端启动失败。mcppls 因 | `serverCommands(): string[]` | ✅(服务器未运行时为空数组) | 判断服务器是否广告了某命令 | | `environment()` | ✅ | version / vscode / appName / appHost / uiKind / platform / remote | | `waitForState()` | ✅ | 需要等待某个状态时使用 | +| `otherCppFeatures(): { active: { extensionId, displayName, settable }[]; canRestore }` | ✅(mcppls 0.0.16 起) | 「其他 C++ 语言服务」一行:哪些在工作、mcppls 能否恢复。缺失、抛错或形状不对时退回本地只读检测(`src/mcppls/otherCppFeatures.ts`,同三个候选),`canRestore` 为 false | | 计数器类(`notificationCount` 等) | ❌ 非测试模式下返回 `-1`/`0` | **不使用** | **使用纪律**(写进 `mcpp-vscode/src/mcppls/state.ts`,并有单测与 e2e 覆盖): diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ad9ff..9d41f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +## 0.7.0 - 2026-10-14 + +`build.mcpp` is C++ again, compiled by C++ Modules as the build program it is, and the project +view says when another C++ language service is answering the same files. + +### Changed + +- **`build.mcpp` is a `cpp` document.** 0.6.0 gave it a language of its own, `mcpp-build`. + VS Code prefers a filename match over an extension match, so that hid the file from C++ + Modules, which selects `cpp`: the build program got a line-by-line text check instead of a + compiler. The `mcpp-build` language, its grammar and its language configuration are gone; + this extension's build-script help now registers on `cpp` documents named `build.mcpp`, and + the C++ module grammar is injected into `source.cpp` only. +- **While C++ Modules serves `build.mcpp`, it answers what it answers better**: the + `unknownSymbol` check and the module names after `import` (completion and hover) are left to + it. The SPEC-007 rule checks, the `mcpp::…` directive completion and hover, and the snippets + stay. "Serves" means the extension is installed and enabled and `mcppls.enable` is not + `false` here; the open files are re-checked when that changes. +- **`unknownSymbol` reads the file's imports** when it does run: `import mcpp.rules.qt;` makes + `mcpp::rules::…` a name the snapshot cannot judge, so it is no longer reported + (`unknown mcpp::rules` on every Qt build program). +- The diagnostics' owner is `mcpp-build-script` (was `mcpp-build`). +- The build-script snapshot and the `mcpp.toml` schema are regenerated from mcpp 2026.10.11.1. + The schema generator reads the `[build]` keys from mcpp's key registry, where 2026.10.11.1 + moved them. + +### Added + +- **Project → Basics → Language service → "Other C++ language services".** While C/C++ + IntelliSense, clangd or ccls is active next to C++ Modules, the row shows a warning and their + names, and one click runs C++ Modules' own **Turn Off Other C++ Language Services** (it asks + whether for this workspace or everywhere, and writes the other extensions' own settings). + With none, the row is a dimmed check mark, "Only C++ Modules is serving C++". When C++ + Modules remembers what it turned off, the row's context menu offers **Restore**. The list is + C++ Modules' own answer (0.0.16+); with an older one, a read-only look at the same three + extensions. +- `mcpp.languageServer.restoreConflicts` ("C++ Modules: Restore Other C++ Language Services"). + `mcpp.languageServer.manageConflicts` is now titled "Turn Off Other C++ Language Services", + which is what it always ran. + +### Migrating + +- A setting written for the old language id, `"[mcpp-build]": { … }` in `settings.json`, no + longer applies. Move it under `"[cpp]"`. +- Turning other C++ services off or restoring them no longer asks a second time in this + extension: C++ Modules' command asks where to act, and closing that question does nothing. + ## 0.6.0 - 2026-10-02 The sidebar was reorganised around one rule: **the body shows state, the actions sit diff --git a/README.md b/README.md index 2855e55..403e047 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Opening a project never runs `mcpp` on its own and never downloads a toolchain. - **mcpp CLI and tasks** — build, run, test, clean, the quick menu, toolchains: [docs/commands.md](docs/commands.md). - **`mcpp.toml` editing** — structural completion, hovers, go-to-definition, seven structural diagnostics, no formatting: [docs/mcpp-toml.md](docs/mcpp-toml.md). -- **`build.mcpp` intelligence** — completion and hovers for `mcpp::…` and `import`, seven SPEC-007 diagnostics, never a spurious "module not found": [docs/build-script.md](docs/build-script.md). +- **`build.mcpp` help** — `build.mcpp` is a `cpp` file that C++ Modules compiles as the build program; this extension adds the `mcpp::…` directive completion, hovers and snippets and the SPEC-007 rule checks no compiler makes: [docs/build-script.md](docs/build-script.md). - **Libraries** — browse the package index already on this machine, offline; the detail page shows the real example code, the version matrix, and whether the workspace already depends on it. - **Cache views and cleanup** — project artifacts and the shared build cache, previewed before anything is deleted: [docs/cache.md](docs/cache.md). - **C++ Modules** — the other extension's state and actions, in the project view and the status-bar menu, forwarded not reimplemented: [docs/commands.md](docs/commands.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index 64351c0..cf422be 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -52,7 +52,7 @@ C++ 模块语义——诊断、补全、悬停、定义、引用、模块图— - **mcpp CLI 与任务** —— 构建、运行、测试、清理、快捷菜单、工具链:[docs/commands.md](docs/commands.md)。 - **`mcpp.toml` 编辑** —— 结构补全、悬停、跳转与七条结构诊断,不做格式化:[docs/mcpp-toml.md](docs/mcpp-toml.md)。 -- **`build.mcpp` 智能** —— `mcpp::…` 与 `import` 的补全悬停、七条 SPEC-007 诊断,且永不误报「模块找不到」:[docs/build-script.md](docs/build-script.md)。 +- **`build.mcpp` 辅助** —— `build.mcpp` 是普通的 `cpp` 文件,由 C++ Modules 当作构建程序编译;本扩展补上 `mcpp::…` 指令的补全、悬停、代码片段,以及编译器不会检查的 SPEC-007 规则:[docs/build-script.md](docs/build-script.md)。 - **库生态** —— 离线浏览本机已刷新的包索引;详情页展示真实示例代码、版本矩阵,以及工程是否已依赖它。 - **缓存视图与清理** —— 工程产物与共享缓存,删除前一律先给出预览:[docs/cache.md](docs/cache.md)。 - **C++ Modules** —— 对方扩展的状态与操作,集中在工程视图与状态栏快捷菜单,只转发不重写:[docs/commands.md](docs/commands.md)。 diff --git a/data/buildscript-api.json b/data/buildscript-api.json index f8e1027..3cb51b1 100644 --- a/data/buildscript-api.json +++ b/data/buildscript-api.json @@ -1,6 +1,6 @@ { - "sourceVersion": "2026.10.3.1", - "sourceCommit": "74bcb859e60afc71ad759540d2de5344dda400e9", + "sourceVersion": "2026.10.11.1", + "sourceCommit": "3fb2d0cc89296c527fcda710792b67bb7a481bed", "protocolVersion": 15, "cacheEpoch": 3, "roles": [ @@ -118,7 +118,8 @@ "rules": [ "R2.1", "R3.3", - "R3.6" + "R3.6", + "R3.9" ], "docsUrl": null }, @@ -131,7 +132,7 @@ "mustExist": false, "since": 4, "rules": [], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/74bcb859e60afc71ad759540d2de5344dda400e9/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/3fb2d0cc89296c527fcda710792b67bb7a481bed/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192" }, { "wire": "runner-named", @@ -262,7 +263,7 @@ "R2.1", "R9.4" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/74bcb859e60afc71ad759540d2de5344dda400e9/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/3fb2d0cc89296c527fcda710792b67bb7a481bed/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212" }, { "wire": "diagnostic", @@ -294,6 +295,7 @@ "R3.5", "R3.7", "R3.8", + "R3.9", "R5.1", "R5.3", "R6.4", @@ -312,7 +314,7 @@ "rules": [ "R2.1" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/74bcb859e60afc71ad759540d2de5344dda400e9/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/3fb2d0cc89296c527fcda710792b67bb7a481bed/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" }, { "wire": "floor", @@ -325,7 +327,7 @@ "rules": [ "R2.1" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/74bcb859e60afc71ad759540d2de5344dda400e9/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/3fb2d0cc89296c527fcda710792b67bb7a481bed/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" }, { "wire": "pack-format", @@ -374,7 +376,7 @@ "R9.2", "R9.5" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/74bcb859e60afc71ad759540d2de5344dda400e9/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/3fb2d0cc89296c527fcda710792b67bb7a481bed/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11" }, { "wire": "runtime-search-dir", diff --git a/data/i18n/zh-cn.json b/data/i18n/zh-cn.json index ce3bb1b..a326dd4 100644 --- a/data/i18n/zh-cn.json +++ b/data/i18n/zh-cn.json @@ -467,5 +467,9 @@ "Could not write the clipboard.": "无法写入剪贴板。", "The workspace is not trusted; mcpp itself was not probed.": "工作区未受信任;未探测 mcpp 本身。", "This workspace is not trusted, so mcpp is not run; the versions below come from the descriptor's text.": "工作区未受信任,因此不运行 mcpp;下面的版本来自描述符文本。", - "The workspace is not trusted; the other registries were not searched.": "工作区未受信任;未搜索其他 registry。" + "The workspace is not trusted; the other registries were not searched.": "工作区未受信任;未搜索其他 registry。", + "Only C++ Modules is serving C++": "仅 C++ Modules 在提供 C++ 语言服务", + "No other C++ language service (C/C++, clangd, ccls) is active, so no result is shown twice.": "没有其他 C++ 语言服务(C/C++、clangd、ccls)在工作,结果不会重复出现。", + "Other C++ language services": "其他 C++ 语言服务", + "They answer the same C++ files, so results appear twice. Click to turn their C++ language features off; C++ Modules asks whether for this workspace or everywhere.": "它们也在处理同一批 C++ 文件,结果会出现两次。点击即可关闭它们的 C++ 语言功能;C++ Modules 会询问只对本工作区还是对所有地方生效。" } diff --git a/data/toml-schema.json b/data/toml-schema.json index bd44b66..c5fe572 100644 --- a/data/toml-schema.json +++ b/data/toml-schema.json @@ -1,6 +1,6 @@ { - "sourceVersion": "2026.10.3.1", - "sourceCommit": "74bcb859e60afc71ad759540d2de5344dda400e9", + "sourceVersion": "2026.10.11.1", + "sourceCommit": "3fb2d0cc89296c527fcda710792b67bb7a481bed", "sections": [ { "header": "[build]", @@ -690,6 +690,11 @@ "key": "soname", "type": "string" }, + { + "key": "windows_auto_export", + "type": "string", + "unmodelled": true + }, { "key": "windows_code_page", "type": "enum", @@ -778,7 +783,7 @@ "header": "[workspace]", "name": "workspace", "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", + "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#218-workspace--tables-a-workspace-shares-202610101", "deprecatedBy": null }, { diff --git a/docs/build-script.md b/docs/build-script.md index 9913d9d..ee10298 100644 --- a/docs/build-script.md +++ b/docs/build-script.md @@ -1,38 +1,35 @@ # Editing `build.mcpp` `build.mcpp` is mcpp's build program: C++ that the engine compiles and runs to produce the -build graph. It gets its own language id, `mcpp-build` (exact filename `build.mcpp`), with the -TextMate grammar `source.mcpp-build` from `syntaxes/mcpp-build.tmLanguage.json`; the module -grammar `source.cpp.mcpp-modules` is injected into both `source.cpp` and `source.mcpp-build`. - -## Why it is not handed to the C++ language service - -Measured on a real mcppls payload with clangd 23.1.0, handing `build.mcpp` to the C++ language -service fails for **both** `import std;` and `import mcpp;`: - -```text -$ mcppls check build.mcpp --payload -root …/examples/11-features/greeter -database 6 entries, 2 standard library units, 0 left out -module build.mcpp:2: module 'mcpp' not found -E [module_not_found] Line 1: module 'std' not found -E Failed to build module mcpp; due to Don't get the module unit for module mcpp -clangd exit 3 - -$ mcppls check src/main.cpp --payload -database 6 entries, 2 standard library units, 0 left out -clangd exit 0 -``` - -The reason is in the build database: `mcpp emit build-database --spec compile-commands --format -json` lists 6 records and **none of them is `build.mcpp`** — it only appears under `data.watch`. -mcpp deliberately does not put the build program into the compilation database, and mcppls's -model has no `build.mcpp` (it handles `target/.build-mcpp/deps/…`). So this is not "only `mcpp` -is missing": `std` fails too, and VS Code has no public API to filter a diagnostic another -extension published. - -**Therefore `build.mcpp` stays in its own language id and the C++ language service never sees -it.** This extension analyses the file itself, and it never reports any import as missing. +build graph. **It is an ordinary `cpp` document** (this extension declares the filename for the +`cpp` language, and C++ Modules associates `.mcpp` with `cpp` too); the module grammar +`source.cpp.mcpp-modules` is injected into `source.cpp`. + +## Who answers what + +C++ Modules (mcppls, 0.0.12 and later) compiles `build.mcpp` as the build program it is: against +the modules it really imports, `mcpp`, `std` and the plugins named in +`[build-dependencies]`, with the toolchain the build uses. Its completion, hover, navigation and +diagnostics are a compiler's. This extension adds what no compiler says: + +| | C++ Modules serves the file | C++ Modules is disabled or switched off here | +| --- | --- | --- | +| SPEC-007 rule checks (six of the seven rules below) | this extension | this extension | +| `unknownSymbol` | off — the compiler's answer replaces it | this extension, import-aware | +| `mcpp::…` directive completion and hover, snippets | this extension | this extension | +| Module names after `import` (completion, hover) | C++ Modules | this extension | + +"Serves" is read without asking anything of the language server: the C++ Modules extension is +installed and enabled, and `mcppls.enable` is not `false` for the workspace. When that changes +(the extension is enabled, disabled, or switched off for the workspace) the open `build.mcpp` +files are re-checked at once. + +Up to 0.6.0 this extension gave `build.mcpp` a language id of its own, `mcpp-build`. VS Code +prefers a filename match over an extension match, so that hid the file from C++ Modules, which +selects `cpp` documents: the build program got a line-by-line text check instead of a compiler, +and `import mcpp.rules.qt;` followed by `mcpp::rules::qt::…` was reported as an unknown name. +A setting written for that language (`"[mcpp-build]": { … }`) no longer applies; write it under +`"[cpp]"`. ## What this extension provides @@ -42,19 +39,20 @@ work in an untrusted workspace. ### Completion -Registered for the `mcpp-build` language in `src/buildscript/providers.ts`. +Registered for `cpp` documents named `build.mcpp`, in `src/buildscript/providers.ts`. - After `mcpp::` (or `mcpp::pre`): directive names, from the 31 entries in the snapshot's `directives` array, with `-` rewritten to `_` (so the wire name `link-flag` offers `link_flag`). Kind: function. -- After `import ` or `export import `: the concrete module names from the known-module table - (the `prefix.*` patterns are not offered as literal text). Kind: module, with the module's - description as documentation. +- After `import ` or `export import `, only while C++ Modules does not serve the file: the + concrete module names from the known-module table (the `prefix.*` patterns are not offered as + literal text). Kind: module, with the module's description as documentation. ### Hover -- On a known module name in an `import`: the module name and its description (for a - `prefix.*` match, the concrete name the user wrote is shown). +- On a known module name in an `import`, only while C++ Modules does not serve the file: the + module name and its description (for a `prefix.*` match, the concrete name the user wrote is + shown). - On `mcpp::`: the wire spelling (`mcpp:=`), `slot`, `scope`, `since protocol`, the SPEC-007 rules and a link into mcpp's `docs/30-build-mcpp.md`. @@ -78,7 +76,7 @@ diagnostic's `code`; `source` is `mcpp`. | Code | What it flags | SPEC-007 | | --- | --- | --- | -| `mcpp.buildscript.unknownSymbol` | An `mcpp::` that is not a directive, an action role or a typed-API name in the snapshot | §2 | +| `mcpp.buildscript.unknownSymbol` | An `mcpp::` that is not a directive, an action role or a typed-API name in the snapshot, and not a namespace the file imports (`import mcpp.rules.qt;` covers `mcpp::rules::…`). Only while C++ Modules does not serve the file | §2 | | `mcpp.buildscript.roleLiteral` | `role = ""`; use `mcpp::roles::` so an engine that does not know it refuses to compile | R3.6 | | `mcpp.buildscript.prepareNeedsOutputDir` | A `prepare` action with no `output_dir(…)` | R3.3 | | `mcpp.buildscript.rpathInLinkFlag` | `-Wl,-rpath` in `link_flag`; use `mcpp::runtime_search_dir(…)` | R4.4 | @@ -113,8 +111,8 @@ the raw-string and block-comment state carries across lines. | Field | Value | | --- | --- | -| `sourceVersion` | `2026.10.1.3` | -| `sourceCommit` | `4d81d062` | +| `sourceVersion` | `2026.10.11.1` | +| `sourceCommit` | `3fb2d0cc` | | `protocolVersion` | `15` | | `cacheEpoch` | `3` | | `roles` | `source`, `check`, `object`, `artifact`, `prepare` | diff --git a/docs/commands.md b/docs/commands.md index 0c96bbb..b3d7902 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -77,11 +77,21 @@ behaviour. The capability table is `src/mcppls/contract.ts`, the probe is | `mcpp.languageServer.collectReport` | `command.mcpp.languageServer.collectReport.title` | `mcppls.collectReport` | none | | `mcpp.languageServer.exportDiagnosticBundle` | `command.mcpp.languageServer.exportDiagnosticBundle.title` | `mcppls.exportDiagnosticBundle` | none | | `mcpp.languageServer.runBuildToolInTerminal` | `command.mcpp.languageServer.runBuildToolInTerminal.title` | `mcppls.runBuildToolInTerminal` | confirm | -| `mcpp.languageServer.manageConflicts` | `command.mcpp.languageServer.manageConflicts.title` | `mcppls.turnOffOtherCppFeatures` / `mcppls.restoreOtherCppFeatures`, called with `true` | confirm | +| `mcpp.languageServer.manageConflicts` | `command.mcpp.languageServer.manageConflicts.title` | `mcppls.turnOffOtherCppFeatures` (it asks where: this workspace or user settings) | none | +| `mcpp.languageServer.restoreConflicts` | `command.mcpp.languageServer.restoreConflicts.title` | `mcppls.restoreOtherCppFeatures` | none | | `mcpp.languageServer.toggleInWorkspace` | `command.mcpp.languageServer.toggleInWorkspace.title` | `mcppls.turnOffInWorkspace` / `mcppls.turnOnInWorkspace`, called with `!mcppls.enable` | confirm | | `mcpp.languageServer.installTools` | `command.mcpp.languageServer.installTools.title` | `mcppls.installCommandLineTools` | confirm | | `mcpp.languageServer.reviewChanges` | `command.mcpp.languageServer.reviewChanges.title` | `mcppls.review.run` / `mcppls.review.clear`, called with `false` | none | +**Project → Basics → Language service** carries one row for the other C++ language services +(C/C++ IntelliSense, clangd, ccls). While one is active it shows a warning and their names, and +clicking it runs `mcpp.languageServer.manageConflicts`; with none it is a dimmed check mark, +"Only C++ Modules is serving C++", with nothing to click. When mcppls remembers settings it +turned off, the row's context menu offers `mcpp.languageServer.restoreConflicts`. The list comes +from mcppls itself (`otherCppFeatures()` on its extension API, 0.0.16+) and, from an older +mcppls, from a read-only look at the same three extensions and their settings. This extension +never writes those settings: mcppls's command does, at their own keys. + `refresh` for the build path uses a candidate chain (`mcppls.reloadBuildDescription`, then `mcppls.restartServer`) and is single-flight, so two builds cannot restart the language server twice. `mcpp.languageService.refreshAfterBuild` is diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index ae2ad3e..a326dd4 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -363,7 +363,6 @@ "Workspace settings": "工作区设置", "added": "已添加", "convention {0}": "约定 {0}", - "dev": "dev", "dev dependency": "dev 依赖", "git · {0}": "git · {0}", "host default": "host 默认", @@ -460,5 +459,17 @@ "{0}{1}": "{0}{1}", "{0}–{1} day(s)": "{0}–{1} 天", "{0}–{1} day(s) ago": "{0}–{1} 天前", - "✓ Has examples": "✓ 有示例" + "✓ Has examples": "✓ 有示例", + "Bring it into your code": "在代码里引入", + "Copy": "复制", + "Copied to the clipboard.": "已复制到剪贴板。", + "Copying…": "复制中…", + "Could not write the clipboard.": "无法写入剪贴板。", + "The workspace is not trusted; mcpp itself was not probed.": "工作区未受信任;未探测 mcpp 本身。", + "This workspace is not trusted, so mcpp is not run; the versions below come from the descriptor's text.": "工作区未受信任,因此不运行 mcpp;下面的版本来自描述符文本。", + "The workspace is not trusted; the other registries were not searched.": "工作区未受信任;未搜索其他 registry。", + "Only C++ Modules is serving C++": "仅 C++ Modules 在提供 C++ 语言服务", + "No other C++ language service (C/C++, clangd, ccls) is active, so no result is shown twice.": "没有其他 C++ 语言服务(C/C++、clangd、ccls)在工作,结果不会重复出现。", + "Other C++ language services": "其他 C++ 语言服务", + "They answer the same C++ files, so results appear twice. Click to turn their C++ language features off; C++ Modules asks whether for this workspace or everywhere.": "它们也在处理同一批 C++ 文件,结果会出现两次。点击即可关闭它们的 C++ 语言功能;C++ Modules 会询问只对本工作区还是对所有地方生效。" } diff --git a/package-lock.json b/package-lock.json index 0156104..562b7fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.6.0", + "version": "0.7.0", "license": "Apache-2.0", "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/package.json b/package.json index f90d2ad..1e279de 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "%displayName%", "description": "%description%", - "version": "0.6.0", + "version": "0.7.0", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "media/logo.png", @@ -22,8 +22,7 @@ ], "activationEvents": [ "workspaceContains:mcpp.toml", - "onLanguage:mcpp-toml", - "onLanguage:mcpp-build" + "onLanguage:mcpp-toml" ], "categories": [ "Programming Languages", @@ -71,6 +70,12 @@ "command": "mcpp.openMcpplsSettings", "when": "false" } + ], + "view/item/context": [ + { + "command": "mcpp.languageServer.restoreConflicts", + "when": "view == mcpp.project && viewItem =~ /^mcppOtherCppFeatures.*Restorable$/" + } ] }, "commands": [ @@ -246,6 +251,11 @@ "title": "%command.mcpp.languageServer.manageConflicts.title%", "category": "%category%" }, + { + "command": "mcpp.languageServer.restoreConflicts", + "title": "%command.mcpp.languageServer.restoreConflicts.title%", + "category": "%category%" + }, { "command": "mcpp.languageServer.toggleInWorkspace", "title": "%command.mcpp.languageServer.toggleInWorkspace.title%", @@ -990,15 +1000,10 @@ }, "languages": [ { - "id": "mcpp-build", - "aliases": [ - "mcpp build script", - "build.mcpp" - ], + "id": "cpp", "filenames": [ "build.mcpp" - ], - "configuration": "./syntaxes/mcpp-build-language-configuration.json" + ] }, { "id": "mcpp-toml", @@ -1020,11 +1025,6 @@ } }, "grammars": [ - { - "language": "mcpp-build", - "scopeName": "source.mcpp-build", - "path": "./syntaxes/mcpp-build.tmLanguage.json" - }, { "language": "mcpp-toml", "scopeName": "source.toml.mcpp", @@ -1033,8 +1033,7 @@ { "scopeName": "source.cpp.mcpp-modules", "injectTo": [ - "source.cpp", - "source.mcpp-build" + "source.cpp" ], "path": "./syntaxes/mcpp-modules.tmLanguage.json" } diff --git a/package.nls.json b/package.nls.json index 2108ab2..eef1192 100644 --- a/package.nls.json +++ b/package.nls.json @@ -16,12 +16,13 @@ "command.mcpp.languageServer.collectReport.title": "C++ Modules: Collect a Diagnostic Report", "command.mcpp.languageServer.exportDiagnosticBundle.title": "C++ Modules: Export a Diagnostic Bundle", "command.mcpp.languageServer.installTools.title": "C++ Modules: Install Command Line Tools", - "command.mcpp.languageServer.manageConflicts.title": "C++ Modules: Manage Other C++ Language Features", + "command.mcpp.languageServer.manageConflicts.title": "C++ Modules: Turn Off Other C++ Language Services", "command.mcpp.languageServer.openLogFolder.title": "Open the C++ Modules log folder", "command.mcpp.languageServer.refreshState.title": "C++ Modules: Refresh This View", "command.mcpp.languageServer.resetWorkspaceCache.title": "C++ Modules: Reset This Workspace's Cache", "command.mcpp.languageServer.restart.title": "C++ Modules: Restart the Language Server", "command.mcpp.languageServer.restartEngine.title": "C++ Modules: Restart the Semantic Engine", + "command.mcpp.languageServer.restoreConflicts.title": "C++ Modules: Restore Other C++ Language Services", "command.mcpp.languageServer.revealBundle.title": "Show the last diagnostic bundle", "command.mcpp.languageServer.reviewChanges.title": "C++ Modules: Review Workspace Changes", "command.mcpp.languageServer.runBuildToolInTerminal.title": "C++ Modules: Run the Build Tool in a Terminal", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 7236cee..33cc3ad 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -16,12 +16,13 @@ "command.mcpp.languageServer.collectReport.title": "C++ Modules: 收集诊断报告", "command.mcpp.languageServer.exportDiagnosticBundle.title": "C++ Modules: 导出诊断包", "command.mcpp.languageServer.installTools.title": "C++ Modules: 安装命令行工具", - "command.mcpp.languageServer.manageConflicts.title": "C++ Modules: 处理其它 C++ 语言特性", + "command.mcpp.languageServer.manageConflicts.title": "C++ Modules: 关闭其他 C++ 语言服务", "command.mcpp.languageServer.openLogFolder.title": "打开 C++ Modules 日志目录", "command.mcpp.languageServer.refreshState.title": "C++ Modules: 刷新此视图", "command.mcpp.languageServer.resetWorkspaceCache.title": "C++ Modules: 重置本工作区缓存", "command.mcpp.languageServer.restart.title": "C++ Modules: 重启语言服务", "command.mcpp.languageServer.restartEngine.title": "C++ Modules: 重启语义引擎", + "command.mcpp.languageServer.restoreConflicts.title": "C++ Modules: 恢复其他 C++ 语言服务", "command.mcpp.languageServer.revealBundle.title": "显示最近一次诊断包", "command.mcpp.languageServer.reviewChanges.title": "C++ Modules: 审查工作区改动", "command.mcpp.languageServer.runBuildToolInTerminal.title": "C++ Modules: 在终端运行构建工具", diff --git a/src/buildscript/analysis.ts b/src/buildscript/analysis.ts index 4f4af8e..c2d15e2 100644 --- a/src/buildscript/analysis.ts +++ b/src/buildscript/analysis.ts @@ -411,9 +411,35 @@ function findActionBlocks(masked: readonly MaskedLine[]): ActionBlock[] { * * `lines` are the file's lines without their terminators (`text.split(/\r?\n/)`). */ +export interface AnalysisOptions { + /** + * Whether rule 1 (`unknownSymbol`) runs. Off while C++ Modules (mcppls) serves + * `build.mcpp`: it compiles the file against the modules it really imports, + * so its answer replaces this guess. Default on. + */ + unknownSymbols?: boolean; +} + +/** + * The namespaces under `mcpp::` that the file's own imports bring in: + * `import mcpp.rules.qt;` makes `mcpp::rules::…` a name this snapshot cannot + * judge, so rule 1 leaves it alone. Comments and strings are already masked. + */ +function importedNamespaces(masked: readonly MaskedLine[]): Set { + const namespaces = new Set(); + const imported = /\bimport\s+mcpp\.([A-Za-z_][A-Za-z0-9_]*)/g; + for (const line of masked) { + for (let match = imported.exec(line.code); match !== null; match = imported.exec(line.code)) { + namespaces.add(match[1]); + } + } + return namespaces; +} + export function analyseBuildScript( lines: readonly string[], severity: Exclude, + options: AnalysisOptions = {}, ): BuildScriptDiagnostic[] { const masked: MaskedLine[] = []; const state: ScanState = { blockComment: false, rawDelim: null }; @@ -433,11 +459,12 @@ export function analyseBuildScript( }); }; - // 1. `mcpp::` that the engine does not have. - for (let i = 0; i < masked.length; i += 1) { + // 1. `mcpp::` that the engine does not have, and no import of the file brings in. + const imported = options.unknownSymbols === false ? undefined : importedNamespaces(masked); + for (let i = 0; imported !== undefined && i < masked.length; i += 1) { const symbols = /\bmcpp::([A-Za-z_][A-Za-z0-9_]*)/g; for (let match = symbols.exec(masked[i].code); match !== null; match = symbols.exec(masked[i].code)) { - if (KNOWN_SYMBOLS.has(match[1])) { + if (KNOWN_SYMBOLS.has(match[1]) || imported.has(match[1])) { continue; } report( diff --git a/src/buildscript/modules.ts b/src/buildscript/modules.ts index d4f255f..a626743 100644 --- a/src/buildscript/modules.ts +++ b/src/buildscript/modules.ts @@ -2,9 +2,9 @@ * The modules a `build.mcpp` can legitimately `import`, and a scanner that finds * the import sites in one. * - * §3.2.2 of the plugin-optimisation plan: `build.mcpp` keeps its own - * `mcpp-build` language id and the C++ language service never sees it, so this - * extension is the only thing that can explain what an import means. The rule is + * Since 0.7.0 `build.mcpp` is a `cpp` document and C++ Modules (mcppls) + * compiles it as the build program it is; while it serves the file it explains + * the imports itself, and this table is used only when it does not. The rule is * absolute — **we never report "module not found"**. This module therefore only * *recognises*; it never validates, because a build program's imports are * resolved by mcpp's own module graph (`docs/specs/build-plugins.md` §9, diff --git a/src/buildscript/providers.ts b/src/buildscript/providers.ts index df1f428..0e63778 100644 --- a/src/buildscript/providers.ts +++ b/src/buildscript/providers.ts @@ -2,9 +2,11 @@ * The one-line integration point for `build.mcpp` intelligence. * * `src/extension.ts` stays the only assembly point: it calls this function with - * its real context and a severity reader, and gets completion, hover and - * diagnostics for the `mcpp-build` language id in return. The severity reader is - * the one switch `src/extension.ts` already owns; the four boolean switches + * its real context, a severity reader and a "does C++ Modules serve this + * file" reader, and gets completion, hover and diagnostics for `build.mcpp` + * in return. `build.mcpp` is an ordinary `cpp` document (C++ Modules compiles + * it like any other unit), so the providers register on the `cpp` language with + * a `build.mcpp` filename pattern. The severity reader is the one switch `src/extension.ts` already owns; the four boolean switches * (`mcpp.buildScript.{intelligence,diagnostics,snippets,imports.knownModules}`) * are resolved here through `settings.ts`, because that file is the only place * this extension can be told about them. @@ -18,7 +20,12 @@ * disappears, while hover keeps recognising `std`/`std.compat`/`mcpp.*` and * diagnostics never report any import as missing. * - * This build registers no document-symbol provider for `mcpp-build`, so there is + * While C++ Modules serves the file, what it answers better is left to it: the + * `unknownSymbol` rule (it compiles the file against the modules it really + * imports) and the known-module completion and hover after `import`. The + * SPEC-007 rule checks and the snippets stay: no compiler says those. + * + * This build registers no document-symbol provider for `build.mcpp`, so there is * nothing there to suppress; if one is added it must consult `intelligence` too. * * Everything shown comes from `api.ts` (the generated snapshot), `modules.ts` or @@ -40,7 +47,14 @@ import { snippetsForScope } from "./snippets"; // here rather than injected. const vscodeApi = require("vscode") as typeof vscode; -const LANGUAGE = "mcpp-build"; +const LANGUAGE = "cpp"; +const FILE_NAME = "build.mcpp"; +const SELECTOR: vscode.DocumentFilter = { language: LANGUAGE, pattern: `**/${FILE_NAME}` }; + +/** `build.mcpp` by name, as a `cpp` document. */ +function isBuildScript(document: vscode.TextDocument): boolean { + return document.languageId === LANGUAGE && document.uri.path.split("/").pop() === FILE_NAME; +} const VS_SEVERITY: Record<"error" | "warning" | "info", vscode.DiagnosticSeverity> = { error: vscodeApi.DiagnosticSeverity.Error, @@ -51,8 +65,10 @@ const VS_SEVERITY: Record<"error" | "warning" | "info", vscode.DiagnosticSeverit function toVscodeDiagnostics( document: vscode.TextDocument, severity: "warning" | "info", + servedByMcppls: boolean, ): vscode.Diagnostic[] { - return analyseBuildScript(document.getText().split(/\r?\n/), severity).map((entry) => { + const options = { unknownSymbols: !servedByMcppls }; + return analyseBuildScript(document.getText().split(/\r?\n/), severity, options).map((entry) => { const diagnostic = new vscodeApi.Diagnostic( // The analyser reports 1-based line/column; `vscode.Position` is 0-based. new vscodeApi.Range( @@ -88,13 +104,14 @@ function markdown(lines: readonly string[]): vscode.MarkdownString { export function registerBuildScriptProviders( context: { subscriptions: { push(...items: unknown[]): unknown } }, severity: () => "warning" | "info" | "off", + servedByMcppls: () => boolean, ): void { - const selector: vscode.DocumentSelector = { language: LANGUAGE }; - const collection = vscodeApi.languages.createDiagnosticCollection(LANGUAGE); + const selector: vscode.DocumentSelector = SELECTOR; + const collection = vscodeApi.languages.createDiagnosticCollection("mcpp-build-script"); const contributions = () => buildScriptContributions(read, severity); const refresh = (document: vscode.TextDocument): void => { - if (document.languageId !== LANGUAGE) { + if (!isBuildScript(document)) { return; } const { diagnosticSeverity } = contributions(); @@ -102,7 +119,12 @@ export function registerBuildScriptProviders( collection.delete(document.uri); return; } - collection.set(document.uri, toVscodeDiagnostics(document, diagnosticSeverity)); + collection.set(document.uri, toVscodeDiagnostics(document, diagnosticSeverity, servedByMcppls())); + }; + const refreshAll = (): void => { + for (const document of vscodeApi.workspace.textDocuments) { + refresh(document); + } }; const completion = vscodeApi.languages.registerCompletionItemProvider(selector, { @@ -116,8 +138,9 @@ export function registerBuildScriptProviders( const afterImport = /\b(?:export\s+)?import\s+([A-Za-z0-9_.]*)$/.exec(before); if (afterImport !== null) { // Only the *list* is optional. Recognition itself (`knownModule`, hover, - // the no-diagnostic rule) is unconditional. - if (!completesKnownModules) { + // the no-diagnostic rule) is unconditional. C++ Modules completes module + // names from what the build can really see, so it answers instead. + if (!completesKnownModules || servedByMcppls()) { return undefined; } return knownModules() @@ -173,7 +196,8 @@ export function registerBuildScriptProviders( } const lines = document.getText().split(/\r?\n/); - for (const site of scanImports(lines)) { + // An import's hover is C++ Modules' while it serves the file. + for (const site of servedByMcppls() ? [] : scanImports(lines)) { if ( site.line === position.line && position.character >= site.startCharacter && @@ -213,9 +237,15 @@ export function registerBuildScriptProviders( vscodeApi.workspace.onDidOpenTextDocument(refresh), vscodeApi.workspace.onDidChangeTextDocument((event) => refresh(event.document)), vscodeApi.workspace.onDidCloseTextDocument((document) => collection.delete(document.uri)), + // Whether C++ Modules serves the file can change at any time: installed, + // enabled, or switched off for this workspace. + vscodeApi.extensions.onDidChange(refreshAll), + vscodeApi.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("mcppls.enable") || event.affectsConfiguration("mcpp.buildScript")) { + refreshAll(); + } + }), ]; - for (const document of vscodeApi.workspace.textDocuments) { - refresh(document); - } + refreshAll(); context.subscriptions.push(...subscriptions); } diff --git a/src/buildscript/settings.ts b/src/buildscript/settings.ts index 86c095a..258cb91 100644 --- a/src/buildscript/settings.ts +++ b/src/buildscript/settings.ts @@ -28,7 +28,7 @@ export type DiagnosticSeveritySetting = Exclude; */ export type SettingsReader = (key: string) => T; -/** What the `mcpp-build` providers contribute, given the current settings. */ +/** What the `build.mcpp` providers contribute, given the current settings. */ export interface BuildScriptContributions { /** Completion and hover for `mcpp::` symbols and known module names. */ intelligence: boolean; diff --git a/src/commands/ids.ts b/src/commands/ids.ts index fc08ce9..f0a5b12 100644 --- a/src/commands/ids.ts +++ b/src/commands/ids.ts @@ -49,7 +49,9 @@ export const LANGUAGE_SERVER_COMMANDS = { revealBundle: "mcpp.languageServer.revealBundle", openLogFolder: "mcpp.languageServer.openLogFolder", runBuildToolInTerminal: "mcpp.languageServer.runBuildToolInTerminal", + /** Turns the other C++ language services off (the id is from 0.6.0). */ manageConflicts: "mcpp.languageServer.manageConflicts", + restoreConflicts: "mcpp.languageServer.restoreConflicts", toggleInWorkspace: "mcpp.languageServer.toggleInWorkspace", installTools: "mcpp.languageServer.installTools", reviewChanges: "mcpp.languageServer.reviewChanges", diff --git a/src/extension.ts b/src/extension.ts index db251be..f4ab0ec 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,6 +19,7 @@ import { describeState } from "./mcppls/state"; import { languageServerEnabled, languageServerInstalled, + languageServerServes, languageServerVersion, readLanguageServerState, } from "./mcppls/stateSource"; @@ -423,7 +424,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi return "off"; } return read<"warning" | "info" | "off">("mcpp.buildScript.diagnostics.severity"); - }); + }, languageServerServes); extensionContext.subscriptions.push( output, diff --git a/src/mcppls/bridge.ts b/src/mcppls/bridge.ts index 55a7738..8ff8369 100644 --- a/src/mcppls/bridge.ts +++ b/src/mcppls/bridge.ts @@ -99,7 +99,7 @@ export function createLanguageServerBridge(environment: CapabilityEnvironment): refreshLanguageServerAfterBuild, ...forwarded, // The two commands whose *argument* selects the direction. - manageConflicts: (restore = false) => invoke("manageConflicts", restore), + manageConflicts: (restore = false) => invoke(restore ? "restoreOtherCppFeatures" : "turnOffOtherCppFeatures"), toggleInWorkspace: (enable) => invoke("toggleInWorkspace", enable), reviewChanges: (clear = false) => invoke("review", clear), }; diff --git a/src/mcppls/contract.ts b/src/mcppls/contract.ts index 67bbffa..923b682 100644 --- a/src/mcppls/contract.ts +++ b/src/mcppls/contract.ts @@ -172,15 +172,28 @@ export const CAPABILITIES: readonly Capability[] = [ degradedHint: "Installed C++ Modules does not offer this action.", confirmHint: "The build tool runs in an integrated terminal, with its normal side effects.", }, + // Two capabilities, not one with a candidate chain: a chain is for a renamed + // command, and "turn off" falling back to "restore" would do the opposite. + // `danger: "none"` because mcppls's own command asks where to act (this + // workspace or user settings) and does nothing when that question is closed; + // the settings it writes are the other extensions' own keys, never `mcppls.*`. { - key: "manageConflicts", + key: "turnOffOtherCppFeatures", kind: "forward", - title: "Manage other C++ language features", - commands: ["mcppls.turnOffOtherCppFeatures", "mcppls.restoreOtherCppFeatures"], + title: "Turn off other C++ language services", + commands: ["mcppls.turnOffOtherCppFeatures"], required: false, - danger: "confirm", - degradedHint: "Installed C++ Modules does not offer conflict handling.", - confirmHint: "This changes settings belonging to other C++ extensions, at their own keys. C++ Modules performs the change.", + danger: "none", + degradedHint: "Installed C++ Modules does not offer this; turn the other extension's language features off in its own settings.", + }, + { + key: "restoreOtherCppFeatures", + kind: "forward", + title: "Restore other C++ language services", + commands: ["mcppls.restoreOtherCppFeatures"], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not offer this; turn the other extension's language features on in its own settings.", }, { key: "toggleInWorkspace", diff --git a/src/mcppls/otherCppFeatures.ts b/src/mcppls/otherCppFeatures.ts new file mode 100644 index 0000000..827b2e6 --- /dev/null +++ b/src/mcppls/otherCppFeatures.ts @@ -0,0 +1,107 @@ +/** + * Which other C++ language services are serving next to C++ Modules, normalised. + * + * Pure, like `./state.ts`: no `vscode`, nothing here throws. Two sources: + * + * - **mcppls's own answer** (0.0.16+): `otherCppFeatures()` on the object its + * `activate()` returned. It uses the same candidate table and the same "is it + * active" decision as mcppls's own turn-off command, and it knows whether that + * command has something to restore. Preferred whenever it is there. + * - **A local, read-only fallback** for an older mcppls: the same three + * candidates, read from the installed extensions and their settings. It can + * never know what mcppls remembered, so `canRestore` is `false`. + * + * Either way this extension only *reads*. Turning a service off or restoring it + * is mcppls's command (`mcppls.turnOffOtherCppFeatures`, which asks where), and + * the settings it writes are the other extensions' own, never `mcppls.*`. + */ + +export interface OtherCppService { + extensionId: string; + displayName: string; + /** `true` when a setting turns its language features off; `false` (ccls) means disabling the extension. */ + settable: boolean; +} + +export interface OtherCppFeatures { + active: OtherCppService[]; + /** mcppls remembers settings it changed and can put them back. */ + canRestore: boolean; + /** Who answered: mcppls itself, or this extension's local read. */ + source: "mcppls" | "local"; +} + +/** A candidate the local read knows; `section`/`key`/`off` is how its language features are turned off. */ +interface Candidate { + extensionId: string; + displayName: string; + setting?: { section: string; key: string; off: unknown }; +} + +/** The same three mcppls checks (its `conflictCandidates.ts`), for when it cannot be asked. */ +export const OTHER_CPP_CANDIDATES: readonly Candidate[] = [ + { extensionId: "ms-vscode.cpptools", displayName: "C/C++", setting: { section: "C_Cpp", key: "intelliSenseEngine", off: "disabled" } }, + { extensionId: "llvm-vs-code-extensions.vscode-clangd", displayName: "clangd", setting: { section: "clangd", key: "enable", off: false } }, + { extensionId: "ccls-project.ccls", displayName: "ccls" }, +]; + +/** The settings whose change can make a candidate active or inactive. */ +export const OTHER_CPP_SETTINGS: readonly string[] = OTHER_CPP_CANDIDATES.flatMap((candidate) => + candidate.setting === undefined ? [] : [`${candidate.setting.section}.${candidate.setting.key}`], +); + +/** + * The local read. `installed` answers for installed *and enabled* extensions + * (`vscode.extensions.getExtension`); `value` is the effective setting. + */ +export function localOtherCppFeatures( + installed: (extensionId: string) => boolean, + value: (section: string, key: string) => unknown, +): OtherCppFeatures { + const active = OTHER_CPP_CANDIDATES.filter( + (candidate) => + installed(candidate.extensionId) && + (candidate.setting === undefined || value(candidate.setting.section, candidate.setting.key) !== candidate.setting.off), + ).map((candidate) => ({ + extensionId: candidate.extensionId, + displayName: candidate.displayName, + settable: candidate.setting !== undefined, + })); + return { active, canRestore: false, source: "local" }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function service(value: unknown): OtherCppService | undefined { + if (!isRecord(value) || typeof value.extensionId !== "string" || value.extensionId.length === 0) { + return undefined; + } + return { + extensionId: value.extensionId, + displayName: typeof value.displayName === "string" && value.displayName.length > 0 ? value.displayName : value.extensionId, + settable: value.settable !== false, + }; +} + +/** + * mcppls's answer, or `undefined` when the object has no such function, it + * throws, or what it returns has the wrong shape — the caller then reads locally. + */ +export function otherCppFeaturesFromExports(exportsValue: unknown): OtherCppFeatures | undefined { + if (!isRecord(exportsValue) || typeof exportsValue.otherCppFeatures !== "function") { + return undefined; + } + let answer: unknown; + try { + answer = (exportsValue.otherCppFeatures as () => unknown).call(exportsValue); + } catch { + return undefined; + } + if (!isRecord(answer) || !Array.isArray(answer.active)) { + return undefined; + } + const active = answer.active.map(service).filter((entry): entry is OtherCppService => entry !== undefined); + return { active, canRestore: answer.canRestore === true, source: "mcppls" }; +} diff --git a/src/mcppls/stateSource.ts b/src/mcppls/stateSource.ts index a63e92d..7dd74b6 100644 --- a/src/mcppls/stateSource.ts +++ b/src/mcppls/stateSource.ts @@ -16,6 +16,7 @@ import * as vscode from "vscode"; import { read } from "../config/access"; import { MCPPLS_EXTENSION_ID } from "./contract"; +import { localOtherCppFeatures, otherCppFeaturesFromExports, type OtherCppFeatures } from "./otherCppFeatures"; import { readStateFromExports, type McpplsStateView } from "./state"; /** Why a state read did not happen. The view prints it verbatim. */ @@ -43,6 +44,15 @@ export function languageServerEnabled(): boolean { return vscode.workspace.getConfiguration("mcppls").get("enable", true) !== false; } +/** + * Whether C++ Modules serves this workspace's C++ files, `build.mcpp` included: + * installed and enabled in VS Code, and not switched off here (`mcppls.enable`). + * Read-only, like everything else in this file. + */ +export function languageServerServes(): boolean { + return languageServerInstalled() && languageServerEnabled(); +} + export function readLanguageServerState(): McpplsStateView { const extension = dependency(); const meta = { @@ -69,3 +79,22 @@ export function readLanguageServerState(): McpplsStateView { }; } } + +/** + * The other C++ language services serving next to C++ Modules: mcppls's own + * answer when it gives one (and reading its exports is allowed), else a local + * read-only look at the same three candidates. + */ +export function readOtherCppFeatures(): OtherCppFeatures { + const extension = dependency(); + if (extension !== undefined && extension.isActive && read("mcpp.languageService.readState") !== false) { + const answer = otherCppFeaturesFromExports(extension.exports); + if (answer !== undefined) { + return answer; + } + } + return localOtherCppFeatures( + (extensionId) => vscode.extensions.getExtension(extensionId) !== undefined, + (section, key) => vscode.workspace.getConfiguration(section).get(key), + ); +} diff --git a/src/views/languageServerView.ts b/src/views/languageServerView.ts index 6baa138..1ac83f3 100644 --- a/src/views/languageServerView.ts +++ b/src/views/languageServerView.ts @@ -36,7 +36,9 @@ import { languageServerInstalled, languageServerVersion, readLanguageServerState, + readOtherCppFeatures, } from "../mcppls/stateSource"; +import { OTHER_CPP_SETTINGS } from "../mcppls/otherCppFeatures"; import { PollTimer } from "../mcppls/timers"; import { refreshTimerDecision } from "../cache/cacheState"; import type { LanguageServiceBlock } from "./models"; @@ -162,6 +164,7 @@ export function registerLanguageServerCommands( enabled: languageServerEnabled(), version: languageServerVersion(), state: readLanguageServerState(), + otherCppFeatures: languageServerInstalled() ? readOtherCppFeatures() : undefined, }; }; @@ -279,7 +282,8 @@ export function registerLanguageServerCommands( // answer is the zip's path, and the caller has to keep it. forward(LANGUAGE_SERVER_COMMANDS.runBuildToolInTerminal, "runBuildTool"); forward(LANGUAGE_SERVER_COMMANDS.installTools, "installTools"); - forward(LANGUAGE_SERVER_COMMANDS.manageConflicts, "manageConflicts", () => [true]); + forward(LANGUAGE_SERVER_COMMANDS.manageConflicts, "turnOffOtherCppFeatures"); + forward(LANGUAGE_SERVER_COMMANDS.restoreConflicts, "restoreOtherCppFeatures"); forward(LANGUAGE_SERVER_COMMANDS.reviewChanges, "review", () => [false]); forward(LANGUAGE_SERVER_COMMANDS.toggleInWorkspace, "toggleInWorkspace", () => [!languageServerEnabled()]); @@ -310,7 +314,9 @@ export function registerLanguageServerCommands( if ( event.affectsConfiguration("mcppls.enable") || event.affectsConfiguration("mcpp.views.languageServer") || - event.affectsConfiguration("mcpp.languageService") + event.affectsConfiguration("mcpp.languageService") || + // Another C++ service turned on or off, by mcppls's command or by hand. + OTHER_CPP_SETTINGS.some((setting) => event.affectsConfiguration(setting)) ) { refresh(); applyTimer(); diff --git a/src/views/models.ts b/src/views/models.ts index 95028d1..e65c4ad 100644 --- a/src/views/models.ts +++ b/src/views/models.ts @@ -49,9 +49,9 @@ export interface TreeNode { /** * A `ThemeColor` id the icon is drawn in, e.g. `"charts.blue"`. * - * Only ids the colour registry always carries are used (`charts.*`), and a - * theme that omits one leaves the icon in the normal foreground colour rather - * than making the row unreadable — colour is a hint here, never the state. + * Only ids the colour registry always carries are used (`charts.*`, + * `disabledForeground`), and a theme that omits one leaves the icon in the + * normal foreground colour rather than making the row unreadable — colour is a hint here, never the state. */ iconColor?: string; /** Consumed by `when` clauses in package.json for inline actions. */ @@ -526,6 +526,11 @@ export interface LanguageServiceBlock { state?: string; issues?: readonly LanguageServiceIssue[]; }; + /** Other C++ language services serving next to it (C/C++, clangd, ccls). */ + otherCppFeatures?: { + active: readonly { extensionId: string; displayName: string; settable: boolean }[]; + canRestore: boolean; + }; } /** The provider line the block always carries, so "who owns this" stays answerable. */ @@ -818,6 +823,9 @@ function languageServiceChildren(block: LanguageServiceBlock): TreeNode[] { icon: "beaker", }, ]; + if (block.otherCppFeatures !== undefined) { + nodes.push(otherCppFeaturesNode(block.otherCppFeatures)); + } (state?.issues ?? []).forEach((issue, index) => { nodes.push({ @@ -850,6 +858,38 @@ function languageServiceChildren(block: LanguageServiceBlock): TreeNode[] { return nodes; } +/** + * 「其他 C++ 语言服务」: another service answering the same files shows every + * result twice, so while one is active the row says which and one click hands + * the decision to mcppls (`mcppls.turnOffOtherCppFeatures`, which asks where). + * With none, the row stays as a quiet, dimmed confirmation and does nothing. + * When mcppls remembers settings it changed, the row's context menu offers to + * put them back (`package.json`: `viewItem =~ /Restorable$/`). + */ +function otherCppFeaturesNode(features: NonNullable): TreeNode { + const restorable = features.canRestore ? "Restorable" : ""; + if (features.active.length === 0) { + return { + id: "project.languageService.otherCppFeatures", + label: plain("Only C++ Modules is serving C++"), + icon: "pass", + iconColor: "disabledForeground", + tooltip: plain("No other C++ language service (C/C++, clangd, ccls) is active, so no result is shown twice."), + contextValue: `mcppOtherCppFeatures${restorable}`, + }; + } + const label = plain("Other C++ language services"); + return { + id: "project.languageService.otherCppFeatures", + label, + description: { key: "{0}", args: [features.active.map((entry) => entry.displayName).join(", ")] }, + icon: "warning", + tooltip: plain("They answer the same C++ files, so results appear twice. Click to turn their C++ language features off; C++ Modules asks whether for this workspace or everywhere."), + contextValue: `mcppOtherCppFeatures${restorable}`, + command: { command: "mcpp.languageServer.manageConflicts", title: label }, + }; +} + function actionNode(action: ActionRow): TreeNode { const label = plain(action.label); return { diff --git a/syntaxes/mcpp-build-language-configuration.json b/syntaxes/mcpp-build-language-configuration.json deleted file mode 100644 index b49c932..0000000 --- a/syntaxes/mcpp-build-language-configuration.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "comments": { - "lineComment": "//", - "blockComment": ["/*", "*/"] - }, - "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] - ], - "autoClosingPairs": [ - {"open": "{", "close": "}"}, - {"open": "[", "close": "]"}, - {"open": "(", "close": ")"}, - {"open": "\"", "close": "\""}, - {"open": "'", "close": "'"} - ], - "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"] - ] -} diff --git a/syntaxes/mcpp-build.tmLanguage.json b/syntaxes/mcpp-build.tmLanguage.json deleted file mode 100644 index 3635680..0000000 --- a/syntaxes/mcpp-build.tmLanguage.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "mcpp build script", - "scopeName": "source.mcpp-build", - "patterns": [ - { - "include": "source.cpp" - } - ] -} diff --git a/syntaxes/mcpp-modules.tmLanguage.json b/syntaxes/mcpp-modules.tmLanguage.json index 28c69e3..39bd45d 100644 --- a/syntaxes/mcpp-modules.tmLanguage.json +++ b/syntaxes/mcpp-modules.tmLanguage.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "name": "mcpp C++ Modules", "scopeName": "source.cpp.mcpp-modules", - "injectionSelector": "L:source.cpp, L:source.mcpp-build", + "injectionSelector": "L:source.cpp", "patterns": [ { "include": "#module-declaration" diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index d710dbd..f3b39f3 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { readdirSync, readFileSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import test from "node:test"; @@ -68,7 +68,6 @@ test("declares mcpp-language-server as the C++ modules language service", () => assert.deepEqual(manifest.activationEvents, [ "workspaceContains:mcpp.toml", "onLanguage:mcpp-toml", - "onLanguage:mcpp-build", ]); assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); assert.equal(manifest.capabilities?.untrustedWorkspaces?.description, "%untrustedWorkspaces.description%"); @@ -348,31 +347,36 @@ test("shows editor title buttons only inside mcpp projects", () => { assert.ok(!commands.some((command) => command.command === "mcpp.inProject")); }); -test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => { +test("build.mcpp is a cpp document, not a language of its own", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; const associations = manifest.contributes?.configurationDefaults?.["files.associations"] as | Record | undefined; - const language = manifest.contributes?.languages?.find((item) => item.id === "mcpp-build"); - const grammar = manifest.contributes?.grammars?.find((item) => item.language === "mcpp-build"); - + // 0.6.0 claimed `build.mcpp` as `mcpp-build`. A filename match beats an + // extension match in VS Code, so that hid the file from C++ Modules (which + // selects `cpp`), and the build program got a text check instead of a compiler. + const languages = manifest.contributes?.languages ?? []; + assert.equal(languages.find((item) => item.id === "mcpp-build"), undefined); + assert.deepEqual(languages.find((item) => item.id === "cpp"), { id: "cpp", filenames: ["build.mcpp"] }); + assert.equal(manifest.contributes?.grammars?.find((item) => item.language === "mcpp-build"), undefined); assert.equal(associations?.["build.mcpp"], undefined); assert.equal(associations?.["*.mcpp"], undefined); - assert.deepEqual(language, { - id: "mcpp-build", - aliases: ["mcpp build script", "build.mcpp"], - filenames: ["build.mcpp"], - configuration: "./syntaxes/mcpp-build-language-configuration.json", - }); - assert.deepEqual(grammar, { - language: "mcpp-build", - scopeName: "source.mcpp-build", - path: "./syntaxes/mcpp-build.tmLanguage.json", - }); + assert.ok(!existsSync(path.join(root, "syntaxes/mcpp-build.tmLanguage.json"))); + assert.ok(!existsSync(path.join(root, "syntaxes/mcpp-build-language-configuration.json"))); +}); - const grammarText = readFileSync(path.join(root, "syntaxes/mcpp-build.tmLanguage.json"), "utf8"); - const grammarDocument = JSON.parse(grammarText) as { patterns?: Array<{ include?: string }> }; - assert.ok(grammarDocument.patterns?.some((pattern) => pattern.include === "source.cpp")); +test("restoring other C++ services is offered only on a row that says it can be", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as { + contributes?: { menus?: Record> }; + }; + const entries = manifest.contributes?.menus?.["view/item/context"] ?? []; + const restore = entries.find((entry) => entry.command === "mcpp.languageServer.restoreConflicts"); + assert.ok(restore?.when?.includes("view == mcpp.project")); + const pattern = /viewItem =~ \/(.*)\/$/.exec(restore?.when ?? ""); + assert.ok(pattern, "the menu matches the row's contextValue"); + const matches = new RegExp(pattern[1]); + assert.ok(matches.test("mcppOtherCppFeaturesRestorable")); + assert.ok(!matches.test("mcppOtherCppFeatures")); }); test("ships TOML highlighting for the exact mcpp.toml filename", () => { @@ -424,7 +428,7 @@ test("ships an injection grammar with module-specific scopes", () => { const grammar = manifest.contributes?.grammars?.find((item) => item.scopeName === "source.cpp.mcpp-modules"); assert.deepEqual(grammar, { scopeName: "source.cpp.mcpp-modules", - injectTo: ["source.cpp", "source.mcpp-build"], + injectTo: ["source.cpp"], path: "./syntaxes/mcpp-modules.tmLanguage.json", }); @@ -433,7 +437,7 @@ test("ships an injection grammar with module-specific scopes", () => { injectionSelector?: string; repository?: Record; }; - assert.match(grammarDocument.injectionSelector ?? "", /source\.mcpp-build/); + assert.equal(grammarDocument.injectionSelector, "L:source.cpp"); for (const key of ["module-declaration", "import-declaration", "module-name"]) { assert.ok(grammarDocument.repository?.[key], `missing grammar rule: ${key}`); } diff --git a/test/buildscript/analysis.test.ts b/test/buildscript/analysis.test.ts index ea3cb4b..96358cd 100644 --- a/test/buildscript/analysis.test.ts +++ b/test/buildscript/analysis.test.ts @@ -197,3 +197,37 @@ test("each rule id is the SPEC-007 code the plan names", () => { "mcpp.buildscript.actionNeedsRole", ]); }); + +test("rule 1 leaves a namespace the file imports to the module that brings it", () => { + // The qt-demo shape: `import mcpp.rules.qt;` and then `mcpp::rules::qt::…`. + const lines = [ + "import std;", + "import mcpp;", + "import mcpp.rules.qt;", + "", + "int main() {", + " auto qt = mcpp::rules::qt::options{};", + " mcpp::rules::qt::apply(qt);", + " mcpp::nope();", + "}", + ]; + assert.deepEqual(codes(lines), [DIAGNOSTIC_CODES.unknownSymbol]); + assert.match(analyseBuildScript(lines, "warning")[0].message, /mcpp::nope/); +}); + +test("rule 1 does not count an import that is commented out", () => { + const lines = ["// import mcpp.rules.qt;", "int main() { mcpp::rules::qt::apply(); }"]; + assert.deepEqual(codes(lines), [DIAGNOSTIC_CODES.unknownSymbol]); +}); + +test("with rule 1 off, the SPEC-007 rule checks still run", () => { + const lines = [ + "import mcpp;", + "int main() {", + " mcpp::nope();", + ' mcpp::link_flag("-Wl,-rpath,/opt/lib");', + "}", + ]; + const found = analyseBuildScript(lines, "warning", { unknownSymbols: false }).map((entry) => entry.code); + assert.deepEqual(found, [DIAGNOSTIC_CODES.rpathInLinkFlag]); +}); diff --git a/test/commands/ids.test.ts b/test/commands/ids.test.ts index eebe552..ed3713d 100644 --- a/test/commands/ids.test.ts +++ b/test/commands/ids.test.ts @@ -68,6 +68,7 @@ test("the C++ Modules group lists every id it registers", () => { "mcpp.languageServer.openLogFolder", "mcpp.languageServer.runBuildToolInTerminal", "mcpp.languageServer.manageConflicts", + "mcpp.languageServer.restoreConflicts", "mcpp.languageServer.toggleInWorkspace", "mcpp.languageServer.installTools", "mcpp.languageServer.reviewChanges", diff --git a/test/e2e/fixtures/project/build.mcpp b/test/e2e/fixtures/project/build.mcpp new file mode 100644 index 0000000..a2a79f8 --- /dev/null +++ b/test/e2e/fixtures/project/build.mcpp @@ -0,0 +1,9 @@ +import std; +import mcpp; +import mcpp.rules.qt; + +int main() { + auto qt = mcpp::rules::qt::options{}; + mcpp::link_flag("-Wl,-rpath,/opt/lib"); + return 0; +} diff --git a/test/e2e/runTest.ts b/test/e2e/runTest.ts index fe0cae7..7c0596b 100644 --- a/test/e2e/runTest.ts +++ b/test/e2e/runTest.ts @@ -41,6 +41,7 @@ async function runVariant(repositoryRoot: string, fixtureRoot: string, variant: writeFileSync(join(mcpplsStub, "extension.js"), stubExtensionJs(variant)); copyFileSync(join(fixtureRoot, "project/mcpp.toml"), join(workspaceDir, "mcpp.toml")); copyFileSync(join(fixtureRoot, "project/main.cpp"), join(workspaceDir, "main.cpp")); + copyFileSync(join(fixtureRoot, "project/build.mcpp"), join(workspaceDir, "build.mcpp")); // `MCPP_E2E_CODE` runs the suite against an already-installed VS Code, which is // what makes the Extension Host tests runnable without the 135 MB download. diff --git a/test/e2e/suite/extension.e2e.ts b/test/e2e/suite/extension.e2e.ts index 2dde6c1..481cd99 100644 --- a/test/e2e/suite/extension.e2e.ts +++ b/test/e2e/suite/extension.e2e.ts @@ -124,4 +124,24 @@ suite(`mcpp extension smoke (mcppls stub: ${STUB})`, () => { test("opening the settings panel does not throw", async () => { await vscode.commands.executeCommand("mcpp.openSettings"); }); + + test("build.mcpp opens as cpp, and C++ Modules is left the names it answers", async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, "the fixture workspace should be open"); + const uri = vscode.Uri.joinPath(workspaceFolder.uri, "build.mcpp"); + const document = await vscode.workspace.openTextDocument(uri); + assert.equal(document.languageId, "cpp", "build.mcpp must reach the C++ language service"); + + // The stub is installed and enabled, so the rule checks run and `unknownSymbol` does not. + const deadline = Date.now() + 10_000; + let codes: string[] = []; + while (Date.now() < deadline) { + codes = vscode.languages.getDiagnostics(uri).map((diagnostic) => String(diagnostic.code)); + if (codes.length > 0) { + break; + } + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); + } + assert.deepEqual(codes, ["mcpp.buildscript.rpathInLinkFlag"]); + }); }); diff --git a/test/mcppls/bridge.test.ts b/test/mcppls/bridge.test.ts index 20cbf54..f732696 100644 --- a/test/mcppls/bridge.test.ts +++ b/test/mcppls/bridge.test.ts @@ -133,13 +133,23 @@ test("coalesces concurrent build refresh requests into one call", async () => { assert.deepEqual(calls, ["mcppls.reloadBuildDescription"]); }); -test("directional commands pass their argument through", async () => { +test("turning other C++ services off and restoring them are two commands, never one falling back to the other", async () => { const { bridge, calls } = harness(); + await bridge.manageConflicts(); await bridge.manageConflicts(true); + assert.deepEqual(calls, [ + { command: "mcppls.turnOffOtherCppFeatures", args: [] }, + { command: "mcppls.restoreOtherCppFeatures", args: [] }, + ]); + assert.equal(bridge.dangerOf("turnOffOtherCppFeatures"), "none"); + assert.equal(bridge.dangerOf("restoreOtherCppFeatures"), "none"); +}); + +test("directional commands pass their argument through", async () => { + const { bridge, calls } = harness(); await bridge.toggleInWorkspace(false); await bridge.reviewChanges(true); assert.deepEqual(calls, [ - { command: "mcppls.turnOffOtherCppFeatures", args: [true] }, { command: "mcppls.turnOffInWorkspace", args: [false] }, { command: "mcppls.review.run", args: [true] }, ]); diff --git a/test/mcppls/otherCppFeatures.test.ts b/test/mcppls/otherCppFeatures.test.ts new file mode 100644 index 0000000..8f18ecd --- /dev/null +++ b/test/mcppls/otherCppFeatures.test.ts @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + OTHER_CPP_SETTINGS, + localOtherCppFeatures, + otherCppFeaturesFromExports, +} from "../../src/mcppls/otherCppFeatures"; + +const installed = (...ids: string[]) => (id: string) => ids.includes(id); +const settings = (values: Record) => (section: string, key: string) => values[`${section}.${key}`]; + +test("the local read finds each candidate that is installed and not turned off", () => { + const found = localOtherCppFeatures( + installed("ms-vscode.cpptools", "llvm-vs-code-extensions.vscode-clangd", "ccls-project.ccls"), + settings({ "C_Cpp.intelliSenseEngine": "default" }), + ); + assert.deepEqual(found, { + active: [ + { extensionId: "ms-vscode.cpptools", displayName: "C/C++", settable: true }, + { extensionId: "llvm-vs-code-extensions.vscode-clangd", displayName: "clangd", settable: true }, + { extensionId: "ccls-project.ccls", displayName: "ccls", settable: false }, + ], + canRestore: false, + source: "local", + }); +}); + +test("the local read leaves out what is turned off by its own setting, or not installed", () => { + const found = localOtherCppFeatures( + installed("ms-vscode.cpptools", "llvm-vs-code-extensions.vscode-clangd"), + settings({ "C_Cpp.intelliSenseEngine": "disabled", "clangd.enable": false }), + ); + assert.deepEqual(found.active, []); + assert.deepEqual(OTHER_CPP_SETTINGS, ["C_Cpp.intelliSenseEngine", "clangd.enable"]); +}); + +test("mcppls's own answer is taken as it is, including whether it can restore", () => { + const exportsValue = { + otherCppFeatures: () => ({ + active: [{ extensionId: "llvm-vs-code-extensions.vscode-clangd", displayName: "clangd", settable: true }], + canRestore: true, + }), + }; + assert.deepEqual(otherCppFeaturesFromExports(exportsValue), { + active: [{ extensionId: "llvm-vs-code-extensions.vscode-clangd", displayName: "clangd", settable: true }], + canRestore: true, + source: "mcppls", + }); +}); + +test("an older mcppls, a throwing one, or a malformed answer means the local read is used", () => { + assert.equal(otherCppFeaturesFromExports(undefined), undefined); + assert.equal(otherCppFeaturesFromExports({ lastStatus: () => undefined }), undefined); + assert.equal( + otherCppFeaturesFromExports({ + otherCppFeatures: () => { + throw new Error("boom"); + }, + }), + undefined, + ); + assert.equal(otherCppFeaturesFromExports({ otherCppFeatures: () => ({ active: "clangd" }) }), undefined); + // Entries without an id are dropped rather than rendered as blanks. + assert.deepEqual( + otherCppFeaturesFromExports({ otherCppFeatures: () => ({ active: [{}, { extensionId: "x.y" }] }) }), + { active: [{ extensionId: "x.y", displayName: "x.y", settable: true }], canRestore: false, source: "mcppls" }, + ); +}); diff --git a/test/views/projectLayout.test.ts b/test/views/projectLayout.test.ts index 546a81f..1c4378a 100644 --- a/test/views/projectLayout.test.ts +++ b/test/views/projectLayout.test.ts @@ -414,3 +414,47 @@ test("the standalone C++ Modules view is gone", () => { assert.doesNotMatch(source, /createTreeView/); assert.match(source, /export function registerLanguageServerCommands\(/); }); + +test("other C++ language services: active ones are named and one click turns them off", () => { + const block: LanguageServiceBlock = { + ...HEALTHY, + otherCppFeatures: { + active: [ + { extensionId: "ms-vscode.cpptools", displayName: "C/C++", settable: true }, + { extensionId: "llvm-vs-code-extensions.vscode-clangd", displayName: "clangd", settable: true }, + ], + canRestore: false, + }, + }; + const service = find(buildProjectTree({ root: "/w" }, { languageService: block }), "project.languageService"); + // Right under the provider, so "who answers my C++ files" reads top to bottom. + assert.deepEqual(ids(service?.children ?? []).slice(0, 3), [ + "project.languageService.status", + "project.languageService.provider", + "project.languageService.otherCppFeatures", + ]); + const row = find(service?.children ?? [], "project.languageService.otherCppFeatures"); + assert.equal(text(row?.label), "Other C++ language services"); + assert.equal(text(row?.description), "C/C++, clangd"); + assert.equal(row?.icon, "warning"); + assert.equal(row?.iconColor, undefined); + assert.equal(row?.command?.command, "mcpp.languageServer.manageConflicts"); + assert.equal(row?.contextValue, "mcppOtherCppFeatures"); +}); + +test("other C++ language services: with none, the row is a dimmed confirmation with nothing to click", () => { + const block: LanguageServiceBlock = { ...HEALTHY, otherCppFeatures: { active: [], canRestore: true } }; + const service = find(buildProjectTree({ root: "/w" }, { languageService: block }), "project.languageService"); + const row = find(service?.children ?? [], "project.languageService.otherCppFeatures"); + assert.equal(text(row?.label), "Only C++ Modules is serving C++"); + assert.equal(row?.icon, "pass"); + assert.equal(row?.iconColor, "disabledForeground"); + assert.equal(row?.command, undefined); + // What mcppls turned off can still be put back, from the row's context menu. + assert.equal(row?.contextValue, "mcppOtherCppFeaturesRestorable"); +}); + +test("other C++ language services: no row when there is nothing to read", () => { + const service = find(buildProjectTree({ root: "/w" }, { languageService: HEALTHY }), "project.languageService"); + assert.equal(find(service?.children ?? [], "project.languageService.otherCppFeatures"), undefined); +}); diff --git a/tools/generate-toml-schema.mjs b/tools/generate-toml-schema.mjs index ad402f7..bfd4167 100644 --- a/tools/generate-toml-schema.mjs +++ b/tools/generate-toml-schema.mjs @@ -12,6 +12,8 @@ // checks (`kKnownPackageKeys`, ...). A // section mcpp stops accepting therefore // disappears from the schema. +// modules/manifest/src/key_registry.cppm — the [build] keys, once the parser +// reads them from the registry. // mcpp.toml — [package] version -> sourceVersion. // git rev-parse --short HEAD — sourceCommit (or "unknown"). // @@ -60,6 +62,7 @@ const DOC_BASE = "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp- const SPEC_FILE = "docs/specs/manifest-semantics.md"; const FIELD_DOC_FILE = "docs/04-mcpp-toml.md"; const PARSER_FILE = "modules/manifest/src/toml.cppm"; +const REGISTRY_FILE = "modules/manifest/src/key_registry.cppm"; function fail(message) { process.stderr.write(`generate-toml-schema: ${message}\n`); @@ -460,7 +463,15 @@ function extractStringArray(parserText, arrayName) { const block = new RegExp( `(?:static\\s+)?constexpr\\s+std::string_view\\s+${arrayName}\\s*\\[\\]\\s*=\\s*\\{([\\s\\S]*?)\\};`, ).exec(parserText); - if (!block) fail(`${PARSER_FILE}: key list ${arrayName}[] not found`); + if (!block) { + // mcpp 2026.10.11+ states the [build] keys once, in the key registry, and the + // parser reads them from there: `const auto kKnownBuildKeys = registry_keys("build");`. + const fromRegistry = new RegExp( + `\\b${arrayName}\\s*=\\s*registry_keys\\(\\s*"([^"]+)"\\s*\\)`, + ).exec(parserText); + if (!fromRegistry) fail(`${PARSER_FILE}: key list ${arrayName}[] not found`); + return registryKeys(fromRegistry[1]); + } const values = []; const literal = /"([^"]*)"/g; let match; @@ -471,6 +482,22 @@ function extractStringArray(parserText, arrayName) { return values; } +// The keys of one table in mcpp's key registry: every `{"", "", …}` +// row of `kKeyRegistry`, the table-as-a-whole row (`""`) left out. +function registryKeys(table) { + const text = readSource(REGISTRY_FILE); + const block = /kKeyRegistry\s*\[\]\s*=\s*\{([\s\S]*?)\n\};/.exec(text); + if (!block) fail(`${REGISTRY_FILE}: kKeyRegistry[] not found`); + const values = []; + const row = /\{\s*"([^"]*)"\s*,\s*"([^"]*)"/g; + let match; + while ((match = row.exec(block[1])) !== null) { + if (match[1] === table && match[2] !== "") values.push(match[2]); + } + if (values.length === 0) fail(`${REGISTRY_FILE}: no keys for table ${table}`); + return values; +} + // ─── assemble ─────────────────────────────────────────────────────────────── function buildKey(section, key) {