diff --git a/.agents/docs/2026-09-27-qt-demo-navigation-discovery-plan.md b/.agents/docs/2026-09-27-qt-demo-navigation-discovery-plan.md new file mode 100644 index 0000000..69743c2 --- /dev/null +++ b/.agents/docs/2026-09-27-qt-demo-navigation-discovery-plan.md @@ -0,0 +1,640 @@ +# mcppls 优化方案:qt-demo 链式报错、跳到声明而不是实现、构建工具无感探测 + +状态:方案第 3 版(对照 mcpp 2026.9.27.1 复核;问题 2 找到根因——clangd 的后台索引不为模块单元准备依赖,方案随之重排; +D1、D2、D5 已定,D4′ 待再确认)· 2026-09-27 · 基于 mcppls `main` 2e80b85(0.0.5)、mcpp `main` b439fd97(2026.9.27.1, +本机从源码构建:xlings 索引与 GitHub Release 尚未发布该版本) + +依据: + +- qt-demo(`/home/speak/test/mcpp/qt-demo`,mcpp 2026.9.26.2 + `mcpp:plugins` 0.15.2 `rules-qt`,gcc 16.1.0,Qt 6.11.1): + 用户三次 VS Code 会话的日志(07:10、12:03、16:29,`~/.config/Code/logs/*/window*/exthost/sunrisepeak.mcpp-language-server/`), + `mcppls check`、`mcpp emit build-database` 的原始输出、引擎数据库;以及在副本上验证修法的端到端运行(§1.4); +- 跳转实测:自写 LSP 客户端(每秒请求一次 `textDocument/definition`,记录结果的变化)跑 mcppls 0.0.5 + payload 中的 + clangd 23.1.0,项目为 hello 系列、本仓库、GalTranslPP(`/home/speak/workspace/scode/GalTranslPP`); + clangd 行为对照 llvm-project 23.1.0 源码; +- 构建工具实测:cmake 4.4.2、xmake v3.1.1(含其 Lua 源码)在本机的离线行为和耗时; +- mcpp 2026.9.27.1(#719:#704–#716、`prepare.cppm` 拆成 16 个实现单元 + 实现分区 `:state`):qt-demo 的 emit 复测; + mcpp 自身源码作为大型"接口 + 实现单元 + 分区"项目做跳转实测(引擎数据库 507 条)。 + +编号:问题 1 为 Q1-*,问题 2 为 N-*(navigation),问题 3 为 B-*(build discovery),需要 mcpp / rules-qt 处理的为 M-*。 + +## 0. 摘要 + +### 0.1 三个结论 + +1. **qt-demo:`ui_mainwindow.h` 是持续存在的根因,两边都要修,mcppls 这边能独立解决。** mcpp 的 emit 数据库把生成物的 + `-I` 指向 emit 私有目录(`~/.mcpp/cache/build-database//target/.build-mcpp/out/qt`),那里没有 `ui_mainwindow.h`; + 同时把 `.ui/.qrc/.ts` 写成了 C++ 翻译单元(M-1、M-2)。16:29 的会话里 std 已经不再误判,**剩下的就是这一条**。 + 已验证修法:把私有目录换成项目自己的 `target/.build-mcpp/out/qt` 并剔除非 C++ 条目后,`main.cpp` 的 + `clangd --check` 退出码 0、零错误(Q1-2 + Q1-3,升为 P0)。另外 12:15 那次会话里 mcppls 把 std 的 + "找不到提供者" 误判成 "编不过",把整个项目切到了 libc++(Q1-1)。**mcpp 2026.9.27.1 复测:M-1、M-2 原样存在** + (`.ui/.qrc/.ts` 仍是翻译单元,`-I` 仍指向私有目录;emit 0.25 s,不写项目)。M-2 按 mcpp 规范是有意为之 + (emit 不写工程、不运行 action,R2.1/R2.5),所以向 mcpp 提的要求改为"在 S1 里标明生成物",mcppls 的 Q1-3 长期保留。 +2. **跳到声明:根因找到了,比第 2 版估计的严重。** clangd 23.1.0 的**后台索引编译模块单元时不准备模块依赖** + (`BackgroundIndex::index()` 不经过 ModulesBuilder,源码可证;日志里每个模块单元都是 `Failed to compile …, index may be + incomplete`,hello13 的 16 个项目单元全部如此,`math.cpp` 索引出 0 个符号)。实现单元里的定义能不能进索引,全看 clang 的 + 错误恢复能保住多少:签名只用内建类型、限定名能解析的,常常碰巧对得上(所以最小 hello 基本正常);签名里有模块里的类型 + (`PrepareState&`、`const std::string&`)、或成员函数属于分区里的类,就对不上——**索引里声明和定义变成两个符号** + (`workspace/symbol` 能同时看到 `state.cppm:495` 与 `graph.cpp:61` 两条)。实测:**mcpp 2026.9.27.1 自身**拆出来的 + `prepare` 模块,从 `driver.cpp` 点 `phase0_…`、`phase4b_…`,从 `cmd_build.cppm` 点 `prepare_build`,420 s 内三处全部停在 + 声明。打开过的文件走前台路径(会构建模块依赖),所以"打开过一次就好了"。**修法在 mcpp 源码上验证了三处中的两处**:让 clangd + 临时打开 `manifest.cpp`、`graph.cpp` 再关闭,5 s 内到 `.cpp`,关闭后 120 s 内一直正确(N-7)。第三处 `prepare_build` 另有原因: + 定义文件一直开着,hover 也知道两者是同一个函数,但索引里根本没有这个符号(`workspace/symbol` 为空),N-7 修不了,列为待查 O-1。GalTranslPP 的实现 `.cpp` 全是模块单元,同一机制适用, + 预计同样受影响;Windows 上的确认仍需 V-W。第 2 版里的 (a) 磁盘改动、(b) 解析不了、(d) `.ixx` 不被优先索引都还成立, + 但它们是这个根因之上的附加情况。 +3. **构建工具探测收敛成 `BuildSystemProvider`,并且可以关掉。** 只看文件的探测 → 读已有产物 → 离线、私有目录、有时限地 + 问构建工具 → 需要下载时问一次,同意后后台联网,期间 L4 顶上,拿到后无感切换。xmake 用的正是专门的 + `xmake project -k compile_commands`,**不编译**;但它会隐式配置并做**模块依赖扫描**,默认写进项目的 `build/`, + 所以要私有 `--builddir`;加上 `--policies=package.fetch_only,network.mode:private` 后不再联网更新仓库(已验证)。 + 新增设置 `mcppls.buildDiscovery`(`auto` / `off`)和按提供者的开关(B-7)。对照 mcpp 2026.9.27.1:emit 已声明自己的 + 副作用(`effects`: `read-project`、`write-global-cache`、`exec-build-script`),缺依赖有稳定代码 + `MCPP_OFFLINE_DOWNLOAD_REQUIRED`(mcppls 已按代码识别);但**部分回答里某个成员需要下载时,mcppls 只报 + `producer-partial`,不会进入询问流程**(B-8,新发现)。 + +### 0.2 已定的决策 + +| # | 决定 | +|---|---| +| D1 | CMake 首次配置改为离线优先(修订 BD7):不问就不联网 | +| D2 | 联网获取按工作区一次性同意;提供"不再询问";不改全局设置 | +| D5 | 首个模型的等待 10 s → 2.5 s,之后 L4 先上、后台继续 | +| D4′ | **待再确认**:原提案"tier 1/2 只按文件回退"做不到(§7 R1),改为"只在确认是 std 自身编译失败时整体切换,并同时移除工具链的 std 单元" | + +### 0.3 条目总表 + +| 主题 | # | 修什么 | 证据 | 归属 | 优先级 | 估算 | +|---|---|---|---|---|---|---| +| **qt-demo** | Q1-3 | 生成物目录:emit 私有目录缺文件时,只读地改用项目自己 `target/` 下的同一相对路径;都没有就报 `generated-files-missing`,按钮"在终端运行 mcpp build" | 副本端到端:`clangd --check` 退出码 0 | mcppls | **P0** | 1.5 天 | +| | Q1-2 | 引擎数据库剔除非 C/C++ 条目,计入 "left out" 并记日志 | 同上;clangd `'linker' input unused`、`Couldn't build compiler invocation` | mcppls | P0 | 0.5 天 | +| | Q1-1 | std 的 `unresolved` 不触发 kit 回退;回退只认 std 单元自身的编译失败(D4′) | 12:15:52.944 加入 `std.cc`,78 ms 后被判失败 | mcppls | P0 | 1 天 | +| | Q1-4 | 数据库代际:clangd 读到新一代之前报的模块失败只记日志 | 同上 | mcppls | P0 | 1 天(与 Q1-1 合做) | +| | M-1 | emit 不应把规则输入(`.ui/.qrc/.ts`)写成翻译单元(已提 mcpp#724 §1) | emit 原始输出 | mcpp | — | — | +| | M-2 | emit 的生成物路径指向从未填充的私有目录(规范 R2.1/R2.5 有意不运行 action):请 mcpp 在 S1 里标明哪些 `-I` 目录与单元是生成物、由哪个规则动作产生,必要时附上 `mcpp build` 会放到的工程内路径 | 2026.9.27.1 复测仍在;已提 mcpp#724 §2(含可选的"只跑无副作用生成器"模式) | mcpp / rules-qt | — | — | +| | M-3 | 离线 emit 缺依赖时整份失败 → 返回能规划的部分 + 缺什么(S2 `producer-partial` 已有形状) | 07:10 会话只拿到 inferred | mcpp | — | — | +| **跳转** | N-7 | **模块感知的索引补全**:mcppls 让 clangd 以前台路径(带模块依赖)逐个打开再关闭实现单元,补上后台索引建错的定义;三级队列——按需(definition 只拿到声明时,N-2)、相关(打开的文件所在模块与直接导入模块的实现单元,N-5)、其余(空闲时全部);clangd 每次重启后重做 | mcpp 源码:5 s 到 `.cpp`,关闭后 120 s 内稳定;hello B 场景 2 s | mcppls | **P0** | 3 天 | +| | N-1 | 未打开的源文件在磁盘上变了:送进 N-7 的"相关"级队列 | 修前 125 s 不恢复 | mcppls | P0 | 0.5 天(并入 N-7) | +| | N-6 | 上游:后台索引为模块单元准备依赖(`BackgroundIndex::index()` 接入 ModulesBuilder);登记到 #24,附 hello13 最小复现;另做一个探索:让 mcppls 的 module-hints 路径指向 clangd 自己构建的 BMI,后台索引即可成功(hello13:16/17 单元编译成功、9 个定义全部正确),但在 mcpp 源码上粗糙的链接方式让 clangd 12 s 内崩溃,需要设计 | §2.3 | 上游 / 探索 | P2 | 1 天(登记 + 复现);探索另计 | +| | N-3 | 解析不了的实现单元进状态栏(`implementation-unreadable`) | hello5、GalTranslPP(Linux) | mcppls | P1 | 1 天 | +| | N-4 | 夹具:分区里声明、签名含类类型的函数(最小复现),磁盘改动、新文件、坏实现单元 | §5 | mcppls | P1 | 1 天 | +| | V-W | GalTranslPP 的 Windows CI 跳转实测(修前 / 修后) | §2.4 | 验证 | P1 | 0.5 天 + CI 约 1 h | +| **构建探测** | B-1 | `BuildSystemProvider` 接口与注册表,现有 mcpp / CMake / compdb / inferred 平移 | §3.2 | mcppls | P1 | 3 天 | +| | B-2 | 需要下载时的询问 + 一次性联网获取 + L4 顶替 + 无感切换 | §3.5 | mcppls + 插件 | P1 | 3 天 | +| | B-3 | CMake 首次配置离线优先(D1);读 `CMakePresets.json` | 实测 5.5 s 失败、消息可识别 | mcppls | P1 | 1.5 天 | +| | B-4 | 首个模型等待 2.5 s(D5) | BD5 | mcppls | P1 | 0.5 天 | +| | B-5 | xmake 提供者 | 实测:离线配方有效、不写项目 | mcppls | P1 | 2 天 | +| | B-7 | 无感探测的开关:`mcppls.buildDiscovery`、按提供者启停、是否询问下载 | 用户要求 | mcppls + 插件 | P1 | 1 天 | +| | B-8 | mcpp 的部分回答里带 `MCPP_OFFLINE_DOWNLOAD_REQUIRED` 时:用已规划的部分,同时进入询问流程;`MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED`(note)并入 `generated-files-missing` 的说明 | `src/project/mcpp.cpp:312` 只看 error、只报 partial | mcppls | P1 | 0.5 天 | +| | B-6 | meson 提供者(`--wrap-mode=nodownload`) | 未实测 | mcppls | P3 | 1 天 | + +mcppls 侧合计约 **24–26 人日**(不含 B-6 与 N-6 的探索)。 + +### 0.4 版本划分建议 + +| 版本 | 内容 | 估算 | +|---|---|---| +| **0.0.6 "准"** | Q1-1~Q1-4、N-7(含 N-1/N-2/N-5)、N-3、N-4、N-6 登记、V-W;同时向 mcpp 提 M-1/M-2/M-3 | 约 12 天 | +| **0.0.7 "顺"** | B-1、B-7、B-4、B-2、B-8、B-3、B-5 | 约 11.5 天 | +| 之后 | B-6(meson) | 约 1 天 | + +## 1. 问题 1:qt-demo 的链式报错 + +### 1.1 现场 + +项目:`[build] sources = ["src/*.cpp", "ui/*.ui", "res/*.qrc", "i18n/*.ts"]`,`build.mcpp` 调 `mcpp::rules::qt::compile` +(moc、uic、rcc、lupdate/lrelease),`src/main.cpp` 第 2 行 `#include "ui_mainwindow.h"`,之后 `import std;`。 + +三次会话: + +| 会话 | 模型 | 现象 | +|---|---|---| +| 07:10 | 先 inferred(`producer-needs-download`:`xim:qt-base@6.11.1` 未安装),07:11:40 起 mcpp 模型 | `ui_mainwindow.h` 找不到;`.ui/.qrc/.ts` 索引失败 | +| 12:03 | mcpp 模型(缓存),12:15 重读后加入 std 单元 | 同上,**加上** std 误判 → 整项目切 libc++ kit → 重启 | +| 16:29 | mcpp 模型,一开始就有 2 个 std 单元 | 只剩 `ui_mainwindow.h` 找不到(`IncludeCleaner` 刷屏)和三个非 C++ 条目;没有 kit 回退 | + +12:03 会话的关键行: + +``` +12:15:49.833 engine database changed: 6 compiled otherwise ...; restarting clangd in 2 s +12:15:52.944 engine database: 10 entries (2 standard library units) — 2 added std.cc, std.compat.cc +12:15:53.022 warning: argument unused during compilation: '-I …' '-O0' … (非 C++ 条目) +12:15:53.022 Failed to build module std; due to Don't get the module unit for module std +12:15:53.022 clangd could not build the standard library module …; reading the project with the semantic kit +12:15:53.830 engine database changed: 2 added std.compat.cppm, std.cppm, 2 removed std.cc, std.compat.cc, 6 compiled otherwise +12:15:58.764 clangd could not scan …/gcc/16.1.0/include/c++/16.1.0/bits/std.cc: fatal error: 'bits/stdc++.h' file not found +12:16:01.833 restarting clangd: a module's unit left the engine database (std) +``` + +### 1.2 链条 + +``` +M-2 生成物 -I 指向空目录 ──► main.cpp 致命失败(ui_mainwindow.h)──► main.cpp 无语义、无法跳转(每次会话都有) +M-1 .ui/.qrc/.ts 成了翻译单元 ──► 三条扫描、索引失败,日志刷屏(每次会话都有) +数据库刚加入 std.cc,clangd 仍在用旧一代 ──► "Don't get the module unit for module std"(只在 12:15 出现) + └─► mcppls 判为 std 编不过(Q1-1)──► 整项目切 kit ──► gcc 的 std.cc 用 kit 参数 → bits/stdc++.h 找不到 ──► 再重启 +``` + +### 1.3 责任划分 + +| 环节 | 归属 | 理由 | +|---|---|---| +| `.ui/.qrc/.ts` 成为 TU(`g++ … -c ui/mainwindow.ui -o …/mainwindow.ui.o`) | **mcpp**(M-1) | S1 的翻译单元是 C/C++ 编译命令;`rules-qt` 已用 `device_extensions` 声明了这些扩展名 | +| 生成物目录为空(私有目录里只有两个 0 字节文件,没有 `ui_mainwindow.h`) | **mcpp / rules-qt**(M-2) | emit 在私有目录(`$MCPP_HOME/cache/build-database/`)规划,按规范不写工程(R2.1)、不运行 action(R2.5,2026.9.27.1 起连宿主工具也不构建,记 note `MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED`),所以 uic 的产物永远不会出现在那里;项目自己的 `target/.build-mcpp/out/qt/ui_mainwindow.h` 是存在的。能要求 mcpp 的是**标明**这些路径是生成物,而不是去生成 | +| 缺依赖时只能拿到 inferred | **mcpp**(M-3,改进) | 离线时整份失败 | +| std 误判 → 整项目 kit | **mcppls**(Q1-1、Q1-4) | `src/engine/clangd.cpp:2001` 的 `stdFailed && kind != FailureKind::other` 把 `unresolved`(`src/engine/clangd/process.cpp:220`)也算作失败 | +| 非 C++ 条目进了引擎数据库 | **mcppls**(Q1-2,防御) | `is_cxx_source_name`(`src/project/scan.cpp:500`)已有扩展名表 | +| 生成头文件缺失没有提示,也不去找已构建好的 | **mcppls**(Q1-3) | 设计 P7;设计 2.1 对生成模块已经"去构建留下的地方找",头文件目录没有覆盖 | + +### 1.4 mcppls 的修法 + +**Q1-3 生成物目录(P0,已端到端验证)。** + +1. 数据库里的 `-I` 目录或源文件落在 mcpp 的 build-database 私有根下(`~/.mcpp/cache/build-database//target/…`, + `src/project/generated.cpp:70` 已认识这个根)时,取 `target/` 之后的相对路径,去 `<项目根>/target/` 下找; + 存在且含有被引用的文件就**只读地**替换。与设计 2.1 对生成模块的做法一致,扩展到头文件目录和生成的源文件。 +2. 都没有(从未构建)→ issue `generated-files-missing`:"`ui_mainwindow.h` 由构建规则生成,尚不存在;运行一次 + `mcpp build` 后可用",按钮"在终端运行"。只挂在引用它的文件上,只报一次。项目构建后 watch 到 `target/` 下对应文件出现, + 重新计划。 +3. 不受信任的工作区不做 1(与设计 2.1 的 untrusted 规则一致)。 +4. 这是临时对策:它依赖 mcpp 私有目录与 `target/` 的对应关系。M-2 落地后(S1 里标明生成物及其动作)改为按声明处理; + 对应关系对不上时只报 issue,不猜。 + +验证:在 qt-demo 副本上,把 emit 输出中的私有目录换成副本自己的 `target/.build-mcpp`、去掉 `.ui/.qrc/.ts` 三条,用 +`mcppls --database` 读取:`database 5 entries, 2 standard library units`,`clangd exit 0`,无任何错误 +(修前同样的 `check` 退出码 1,`ui_mainwindow.h` 找不到)。 + +**Q1-2 剔除非 C/C++ 条目。** 进入引擎数据库前按扩展名(`is_cxx_source_name` 加上 `.c`、`.m`、`.mm`)和 S1 的语言字段过滤, +剔除的计入 `left out`,日志写明 "3 entries are not C or C++ (mainwindow.ui, …); left out"。mcppls 自己的模块索引也不扫它们。 + +**Q1-1 std 回退只认"std 单元自身的编译失败"(D4′)。** + +- `unresolved`(找不到提供者)只说明 clangd 眼里的数据库没有 std 的单元。处理:核对 clangd 当前读到的那一代数据库里有没有 + std 的提供者。没有 → 代际竞态,等它读到新一代(Q1-4);有 → 按 `module-unresolved` 报在导入它的文件上,不切 kit。 +- 只有 `compile` 类失败、且失败的源文件就是 std 的单元时才切 kit。切换时**同时从数据库移除工具链的 std 单元** + (12:15:58 的 `bits/stdc++.h` 就是 gcc 的 `std.cc` 被套上 kit 参数造成的),issue 里写明原因,并提供"重试工具链的 std"。 +- 不做"按文件回退",原因见 §7 R1。 + +**Q1-4 数据库代际。** 每次写引擎数据库记一个代号;clangd 重启或重读后才算"读到了这一代"。之前报出的模块失败、扫描失败, +记日志但不触发回退、不建事故、不算入重启预算。 + +### 1.5 `windows.h` + +qt-demo 在 Linux 上三次会话都没有出现 `windows.h`。GalTranslPP 的 `Tool.ixx`、`Tool.cpp` 等在全局模块片段里 +`#include `,这类 Windows 项目在 Linux 上打开必然报这个错,属于预期(它们的依赖也只装在 Windows 上); +mcppls 应做的是 N-3:说清楚"这些单元读不了、为什么",而不是让错误连锁。如果 qt-demo 在 Windows 上也见到 +`windows.h`,仍需要那边的问题包(`mcppls.exportBundle`)才能判断。 + +## 2. 问题 2:点击函数跳到声明而不是实现 + +### 2.1 行业惯例 + +| 工具 | 默认点击 / F12 | 声明 | 在定义上再点 | +|---|---|---|---| +| LSP | `textDocument/definition`:定义(函数体) | `textDocument/declaration` | — | +| VS Code | Ctrl+Click、F12 = Go to Definition;Go to Declaration 单独命令;Ctrl+F12 = Go to Implementation(虚函数覆写) | | | +| clangd | 索引里有定义就给定义,否则给声明 | 给声明 | 在定义上 → 声明 | +| Visual Studio | F12 到 `.cpp` 里的函数体 | Ctrl+F12 | | +| Qt Creator | F2 Follow Symbol:到定义;在定义上 → 声明 | | 切换 | + +模块项目的约定:**`.cppm`/`.ixx` 相当于头文件,实现单元相当于源文件;Ctrl+Click 到实现,Go to Declaration 到接口, +在函数体上再点回到接口。** mcppls 的 `mcpp-split` 夹具已按此约定;mcppls 自己的引擎只回答模块名位置 +(`src/engine/native/index.cpp:187`),函数的定义完全取决于 clangd 的索引。问题在索引什么时候不全。 + +### 2.2 实测(Linux) + +| 场景 | 结果 | 结论 | +|---|---|---| +| 最小 hello(llvm@22 / gcc@16 / L4);分区成员、全局模块片段、`hello::add` 限定写法 | 3.4–4.5 s 首次应答即到 `.cpp`;`.cppm` 声明 ↔ `.cpp` 定义互相切换 | 正常(签名简单,碰巧对得上,见 §2.3) | +| 本仓库冷启动,只开 `src/project/model.cpp` | 15 s 内超时(clangd 在建模块);21–26 s 起四个函数全部到 `.cpp`,之后 7 分钟稳定 | 正常 | +| A:在打开着的 `math.cpp` 里写实现,保存后关闭 | 2 s 内到 `math.cpp`,关闭后仍正确 | 正常 | +| **B:未打开的 `types.cpp` 在磁盘上加了实现**(git pull、agent、外部编辑) | **一直是声明,125 s 不恢复** | 缺陷 → N-1 | +| C:新建 `extra.cpp` | 约 8 s 后可达(模型重载把它加进数据库) | 可接受 | +| **D:`greet.cpp` 包含不存在的头文件** | `bump` 始终是声明;同模块其他实现单元正常 | 缺陷 → N-3 | +| B 之后让 clangd 临时打开再关闭 `types.cpp` | 2 s 后到 `types.cpp`,80 s 内一直正确 | N-1 修法有效 | +| GalTranslPP(Linux),`ApiPool.cpp` 里的 `wide2Ascii`、`Tool.ixx` 里的声明 | 150 s 内一直为空;`Tool.cpp` 报 `bit7z/bitarchivereader.hpp` 找不到,其余单元同类(`Windows.h`、vcpkg 头文件) | 只能说明 D;Windows 需实测 | +| **mcpp 2026.9.27.1 源码**,只开 `driver.cpp`、`cmd_build.cppm`(冷启动,引擎数据库 507 条) | `phase0_…`→`state.cppm:490`、`phase4b_…`→`state.cppm:495`、`prepare_build`→`prepare.cppm:723`,420 s 不变;分片早已生成(`manifest.cpp.*.idx`、`graph.cpp.*.idx` 都在) | **缺陷 → N-7**;不是"还没轮到" | +| 同上,热启动,再打开 `manifest.cpp` | 立即到 `manifest.cpp:47`;`workspace/symbol` 对 `phase4b_graph_worklist` 返回**两条**:`state.cppm:495` 与 `graph.cpp:61` | 索引里声明与定义是两个符号 | +| 同上,打开 `manifest.cpp`、`graph.cpp` 再关闭 | 5 s 内两处都到 `.cpp`;关闭后 30/60/90/120 s 仍正确 | **N-7 修法有效** | +| 同上,`prepare_build`(主接口 `prepare.cppm` 里导出、带默认参数;定义在一直打开着的 `driver.cpp`) | 从调用处、从声明处都只到 `prepare.cppm:723`(100 s 不变);`driver.cpp` 0 条诊断;hover 在定义处显示 "provided by prepare.cppm";`workspace/symbol` 查 `prepare_build` **为空** | **另一个缺陷,O-1**:前台 AST 知道两者是同一个函数,但动态索引没记下这个符号 | +| hello8/9/11/13:分区里声明 `phase_a(State&)`、`phase_p(const Point&)`、`phase_s(const std::string&)`、`Box::f(const std::string&)`,实现单元里定义 | 全部停在声明;同一分区的 `plain_c(int)`、`Box::g(int)`(定义在无显式 import 的文件里)到 `.cpp`;加了 `:state` 分区后,原本正常的 `Point::sum`、`twice` 也开始停在声明 | 结果取决于错误恢复,不稳定 | +| hello13 的 clangd 日志(`--log-level debug`) | 16 个项目单元的后台索引全部 `Failed to compile …, index may be incomplete`;`math.cpp` 0 个符号 | 根因的直接证据 | + +B 的根因:clangd 的后台索引只在启动时(按摘要检查过期分片)和编译命令变化时重建文件;`didChangeWatchedFiles` 在 +clangd 里只用于编译数据库。mcppls 在 `Workspace::handle_watched_files`(`src/orchestrator/workspace.cpp:1827`)转发了 +事件,但 clangd 不会因此重建索引。打开的文件走动态索引,所以"在编辑器里改"是好的,"在别处改"是坏的;agent 改代码越多, +B 越常见。 + +D 的根因:`#include` 找不到是致命错误,clang 停止解析,该翻译单元不产生符号。 + +### 2.3 根因:后台索引不为模块单元准备依赖 + +clangd 23.1.0 的 `BackgroundIndex::index()`(`clang-tools-extra/clangd/index/Background.cpp:254` 起): +`buildCompilerInvocation(Inputs, IgnoreDiags)` → `prepareCompilerInstance(CI, /*Preamble=*/nullptr, …)` → +`createStaticIndexingAction`。整个过程没有 ModulesBuilder:前台打开文件时 clangd 会先构建该文件导入的模块的 BMI 并把 +`-fmodule-file=` 交给编译器,后台索引不做这一步。于是每个模块单元在后台都是"找不到导入的模块"地编译 +(`HadErrors = hasUncompilableErrorOccurred()` → 日志 `Failed to compile …, index may be incomplete`)。 + +后果按 clang 的错误恢复分三种: + +- 定义的签名和限定名都不依赖导入的东西(`int plain_c(int)` 这类):生成的 USR 与声明一致,碰巧能跳; +- 签名里有来自模块的类型、或所属的类来自模块:这些类型成了错误类型,定义生成了另一个 USR,**索引里声明和定义是两个符号**; +- 整个实现单元解析不下去:0 个符号(hello13 的 `math.cpp`)。 + +所以第 2 版"最小 hello 正常"的结论只对"签名简单、不用分区"的写法成立;拆分区、用自定义类型当参数(mcpp 的 `prepare`、 +GalTranslPP 的 `NormalJsonTranslator:TransAgent`、`DictionaryGenerator:ReviewAgent` 都是这种写法)就会稳定地跳到声明。 +打开的文件走前台路径(有 ModulesBuilder),它的动态索引是对的,而且实测关闭后仍保留(至少 120 s;clangd 重启后丢失)。 + +mcppls 自己的 WA-CLANGD-004(module hints)给每条命令加了 `-fmodule-file=<名>=/<名>.pcm`,设计上"路径从不写", +只为让 clangd 找到提供者。关掉它(`--disable-workaround WA-CLANGD-004`)结果不变,说明它不是原因;但它提供了一个上游修复之外的 +思路(N-6 探索):如果这些路径真的指向 clangd 前台构建出的 BMI(`/.cache/clangd/modules/<源>-//<名>.pcm`, +文件名与 hint 同名),后台索引就能加载模块。hello13 上手工链接后 16/17 个单元编译成功、9 个定义全部正确;但在 mcpp 源码上把 +271 个 BMI(多轮、多种命令留下的)粗暴链接过去,clangd 12 s 内崩溃——要做只能链接"当前这一代命令"构建出的那一份,且要处理 +BMI 过期,风险高,放在上游修复之后再评估。 + +上游:clangd/clangd#2569(模块内跨文件引用、重命名不可用)是同一类症状,但没有指出原因;N-6 把"后台索引缺 ModulesBuilder" +连同 hello13 的最小复现登记到 #24,并补充到上游。 + +### 2.4 冷启动:模块项目没有"优先索引" + +clangd 23.1.0(`clang-tools-extra/clangd/index/Background.cpp:178`): + +```cpp +void BackgroundIndex::boostRelated(llvm::StringRef Path) { + if (isHeaderFile(Path)) + Queue.boost(filenameWithoutExtension(Path), IndexBoostedFile); +} +``` + +`isHeaderFile`(`SourceCode.cpp:1240`)要求扩展名的驱动类型"只有预编译阶段"。`.cppm`/`.ccm` 是 `TY_CXXModule` +(有编译阶段),`.ixx` 在 `clang/lib/Driver/Types.cpp` 里没有登记(`TY_INVALID`),两者都不算头文件。结果:打开 `Tool.ixx` +或导入它的文件,`Tool.cpp` 不会被提前索引,只能等后台队列按顺序轮到。小项目几秒就排完(hello、本仓库都没感觉), +GalTranslPP 这种 227 个单元、每个实现单元都带重型全局模块片段的项目,窗口会长到几分钟,期间 Ctrl+Click 都落在 `.ixx`。 +它会放大 §2.3 的问题:本来打开一次就能补上的定义,也要等到用户真的打开那个实现单元。 + +### 2.5 V-W:GalTranslPP 的 Windows 实测(待同意) + +复用 Sunrisepeak/GalTranslPP 的临时 PR #1(`ci/mcppls-issue23-probe`,windows-2025,上次一轮约 40 分钟):探针脚本 +增加 definition 时间线——选 20 个"在 `.ixx` 声明、在 `.cpp` 定义"的调用点,从打开起每 5 s 请求一次,记录首次到 `.cpp` +的时间和一直停在 `.ixx` 的调用点;跑两轮:0.0.5 与带 N-7 的构建。调用点里要包含分区 `NormalJsonTranslator:TransAgent`、 +`DictionaryGenerator:ReviewAgent` 里声明的成员。**这会向该仓库的临时分支推送提交并触发 CI,需要你同意。** + +### 2.6 方案 + +**N-7 模块感知的索引补全(P0,替代第 2 版的 N-5、吸收 N-1 与 N-2)。** 既然后台索引建不对模块单元、前台打开能建对且关闭后保留, +mcppls 就替 clangd 把实现单元"过一遍前台":用已有的后台文档机制(`background_`,prime 单元用的那套)让 clangd `didOpen` +(磁盘文本)→ 等该文件 idle(`textDocument/clangd.fileStatus`,上限 10 s)→ `didClose`。一个队列,三级优先: + +| 级别 | 什么进队列 | 何时 | +|---|---|---| +| 按需(原 N-2) | definition 只回答一个位置、落在接口单元(含分区)里、且与 declaration 的回答相同时,该声明所在模块的实现单元 | 请求到来时;限时 1.5 s 后重问一次,仍是声明就原样返回(有 N-3 标记时附原因) | +| 相关(原 N-5、N-1) | 编辑器打开的文件所在模块、及它直接导入的模块的实现单元(不递归,单次最多 16 个);watch 到磁盘变化、编辑器没打开、摘要确实变了的源文件 | 打开文件、磁盘变化时 | +| 其余 | 数据库里其余的模块实现单元(`module X;`、实现分区) | clangd 空闲、没有前台请求时,逐个 | + +- 并发最多 2;已经过一遍且摘要未变的跳过;解析失败(致命错误)的记给 N-3,不重试直到它的输入变化。 +- clangd 重启(计划、恢复、崩溃)后动态索引丢失,"相关"级立即重做,"其余"级在空闲时重做。 +- 单次 watch 事件超过 20 个文件(git checkout、rebase;D3)时,这些文件只进"其余"级,不插队。 +- 成本:每个单元一次前台 AST 构建(mcpp 源码上两个实现单元一起 5 s)。"其余"级只针对实现单元:mcpp 源码的引擎数据库 + 507 条里,接口单元 181、实现单元 16(`prepare` 拆出来的)、导入模块的普通单元 310,所以要补的只有 16 个;实现单元多的项目 + (GalTranslPP 每个 `.ixx` 配一个 `.cpp`)按 V-W 的实测定并发与上限。 +- 范围:N-7 修的是**跳到定义**。同一根因也让 310 个导入模块的普通单元在后台索引里残缺,跨文件的 Find References、 + Call Hierarchy、Rename 只能看到打开过的文件(clangd/clangd#2569 的症状)。把这些单元也过一遍前台代价大,不在 N-7 内, + 留给 N-6(上游修复或 BMI 方案)。 +- 不改编译命令去"骗" clangd 重建(会重建 BMI);不为了索引重启 clangd。 +- 上游修好(N-6)或 N-6 的 BMI 方案成熟后,"其余"级可以关掉,按需与相关两级仍保留(它们也解决 `.ixx`/`.cppm` 不被优先索引的问题)。 + +**N-3 解析不了的实现单元进状态(P1)。** clangd 报出的致命错误(`fatal error: … file not found`、模块扫描失败)落在实现单元上时, +记为 `implementation-unreadable`:状态栏写"1 个实现单元无法读取:`greet.cpp`(`ui_generated.h` 找不到),其中的定义不可跳转"; +是生成物时与 Q1-3 合并。 + +**文档。** 用户文档加一段约定:Ctrl+Click / F12 到实现,Go to Declaration 到接口,Peek 同时看两边;"全 `.cppm`"写法的项目, +定义本来就在接口里。 + +## 3. 问题 3:构建工具无感探测的抽象层 + +### 3.1 现状与差距 + +| 现状(0.0.5) | 位置 | 差距 | +|---|---|---| +| `detect_project` 固定顺序:配置的数据库 > `mcpp.toml` > `CMakeLists.txt` > `compile_commands.json` > inferred | `src/project/detect.cpp` | 每加一个工具要改多处 switch;不认 xmake、meson | +| mcpp:离线 emit(`MCPP_OFFLINE=1`),需要下载时只有 "Run in Terminal" | `src/project/mcpp.cpp:327`,`src/orchestrator/workspace.cpp:1524` | 没有"同意后由 mcppls 后台联网";联网只能把 `mcppls.buildTool` 设成 `online`(永久、全局) | +| CMake:有构建目录就读;否则在私有目录配置,首次允许下载(BD7) | `src/project/cmake.cpp:163` | 与 D1 冲突;不读 `CMakePresets.json`,私有配置可能选到和用户不同的编译器 | +| 没有模型时等 producer 10 s(BD5) | `src/orchestrator/workspace.cpp` | D5:2.5 s | +| 离线只靠 `MCPP_OFFLINE` 这个约定的环境变量 | `modules/platform/src/toolrun.cpp:81` | 每个工具需要自己的离线配方 | +| 无感探测没有总开关;`mcppls.buildTool = off` 只管"不执行",已有产物照读 | — | B-7 | + +### 3.2 抽象:`BuildSystemProvider` + +```cpp +// 只看文件、不执行:< 50 ms。多个提供者都认领时取 confidence 最高的(mcpp.toml 与 CMakeLists.txt 并存等)。 +struct Claim { std::string provider; int confidence; std::string manifest; std::vector markers; }; + +enum class Outcome { ok, partial, needs_download, failed, timed_out }; +struct Answer { + Outcome outcome; + std::optional database; // ok / partial + std::vector missing; // needs_download:缺什么(包名、FetchContent 名) + std::string terminalCommand; // 用户在终端里自己跑的等价命令 + std::string reason; // failed:构建工具自己的诊断 +}; + +class BuildSystemProvider { +public: + virtual std::string_view id() const = 0; // "mcpp" "cmake" "xmake" "meson" "compile-commands" + virtual std::optional detect(std::string_view root) const = 0; // 纯文件系统 + virtual std::optional existing(const Claim&) const = 0; // 读已有产物,不执行 + virtual Answer describe(const Claim&, const DescribeContext&) const = 0; // 执行构建工具;context 说明是否离线 + virtual std::vector watch_inputs(const Claim&) const = 0; // 触发重新描述的文件 + virtual std::vector fingerprint(const Claim&) const = 0; // 模型缓存的输入 +}; +``` + +`DescribeContext` 沿用 `ProviderContext`(trusted、runner、offline、soft/hard 时限、`onSlow`),另加 `privateDirectory` +(每个工作区、每个提供者一份,在 mcppls 缓存目录下)。inferred 不是提供者,是所有提供者都不可用时的兜底。 + +**副作用契约(每个提供者都要满足,conformance 的 `workspace-unchanged` 覆盖全部):** + +1. 不写工作区,所有状态在 `privateDirectory`。 +2. 离线配方由提供者给出(环境变量、参数),`toolrun` 统一记录 offline、网络观察、耗时、stderr 尾部。 +3. 失败分类:`needs_download`(可以询问)、`failed`(构建脚本错,报构建工具原话)、`timed_out`。 +4. 不交互:离线阶段绝不从标准输入读确认。 + +### 3.3 各工具的实现要点 + +| 提供者 | detect | existing(不执行) | describe 离线 | 同意后联网 | 实测 | +|---|---|---|---|---|---| +| **mcpp** | `mcpp.toml` | — | `mcpp emit build-database --format json`,`MCPP_OFFLINE=1` | 同一命令去掉离线 | qt-demo:2026.9.26.2 0.6 s,2026.9.27.1 0.25 s | +| **CMake** | `CMakeLists.txt`(+ `CMakePresets.json`) | `build*/`、`out/build/*`、`cmake-build-*`、preset 的 `binaryDir` 里的 `build_database.json` / `compile_commands.json` | 私有目录配置,`-DFETCHCONTENT_FULLY_DISCONNECTED=ON`;有 preset 时跟随其 generator、toolchainFile、cacheVariables | 私有目录去掉 `FULLY_DISCONNECTED` 重新配置,之后回到离线 | 缺 fmt:5.5 s 失败,消息 `FETCHCONTENT_FULLY_DISCONNECTED … requires the source directory … populated`,项目目录无变化 | +| **xmake** | `xmake.lua` | 项目根或 `.vscode/` 下的 `compile_commands.json` | 见 3.4 | 去掉 `fetch_only` 与 `network.mode:private`,加 `-y` | 见 3.4 | +| **meson** | `meson.build` | `builddir/`、`build/` 下的 `compile_commands.json` | 私有目录 `meson setup --wrap-mode=nodownload` | 去掉 `nodownload` | 未测 | +| **compile-commands** | `compile_commands.json` / `build/compile_commands.json` | 就是它 | — | — | — | + +CMake 跟随 preset 很重要:私有配置不跟随 preset 时,可能选到另一个编译器,给出的语义和用户实际构建的对不上。 + +### 3.4 mcpp 2026.9.27.1 对照 + +| mcpp 的事实 | 来源 | 对本方案的影响 | +|---|---|---| +| emit 不写工程、在 `$MCPP_HOME/cache/build-database/` 规划;运行构建程序,但不运行 action;不构建宿主工具,库中没有的记 note `MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED`(#707) | mcpp `docs/specs/build-database.md` R2.1–R2.5 | 副作用契约对 mcpp 天然成立;生成物缺失是常态而非异常(Q1-3、M-2);被推迟的宿主工具产生的文件也属于"生成物缺失",B-8 把这条 note 并进 `generated-files-missing` 的说明 | +| 信封声明副作用:`effects` = `read-project`、`write-global-cache`、`exec-build-script`(`--protocol-version` 同样声明,R2.6) | qt-demo 实测信封 | 提供者契约直接读它:出现 `write-project` 视为违约(记事故、不用该结果);`exec-build-script` 表示会运行项目自己的 `build.mcpp`,只在受信任工作区执行(与现状一致) | +| 缺依赖的稳定代码 `MCPP_OFFLINE_DOWNLOAD_REQUIRED`(e2e 735) | mcpp 测试 | mcppls 已按代码识别(`src/spec/discovery.cpp:91`),旧版本的文字匹配保留作兼容 | +| 按成员规划,失败成员给 `error` 诊断、其余成员照常给 `data`(2026.9.26.2 起,#699) | CHANGELOG | **缺口 B-8**:mcppls 只在"整份失败"时识别需要下载;部分回答里某成员的错误是 `MCPP_OFFLINE_DOWNLOAD_REQUIRED` 时只报 `producer-partial`(`src/project/mcpp.cpp:312`),不会询问。B-8:用已规划的部分,同时按 §3.6 询问 | +| 工作空间成员继承根的 `[xlings.workspace]`(#713、#714) | CHANGELOG | GalTranslPP 这种工作空间,成员现在也会因为根声明的包未安装而需要下载,走部分回答 + B-8 | +| 离线时"记录为已安装但载荷已删除"的包被拒绝并点名(#712、#716) | CHANGELOG | 同属 `needs_download`;询问文字用 mcpp 自己的消息 | + +### 3.5 xmake:专门的 CDB 命令不编译,但仍需两处隔离 + +`xmake project -k compile_commands ` 是 xmake 专门的编译数据库生成命令,**不编译**。实测它做两件事: +隐式配置(工具链探测、flag 检查,冷 5.4 s、热 3.9 s)和 **C++ 模块依赖扫描**(对每个模块文件调用 g++ 做扫描,结果写进 +`build/.gens/…/rules/bmi/cache/scans/*.json` 和 `build/.deps/…/*.d`)。所以: + +1. **不加隔离会写项目**:只跑 `xmake project -k compile_commands` 时,项目里多出 `build/`(`.gens`、`.deps`,已实测); + 不设 `XMAKE_CONFIGDIR` 时配置写进项目的 `.xmake/`(xmake 的默认行为,本次实测都设了私有目录)。 + `xmake project` 没有 builddir 参数,builddir 只能由 `xmake f` 设定,所以首次需要两步: + ``` + XMAKE_CONFIGDIR=/config xmake f -c --confirm=no \ + --policies=package.fetch_only,network.mode:private --builddir=/build + XMAKE_CONFIGDIR=/config xmake project -k compile_commands /out + ``` + 之后只跑第二条(配置已缓存在私有目录)。实测两步 5–7 s、只跑第二条 3.9 s,项目目录无变化。 +2. **离线要显式关网络**:只加 `package.fetch_only` 时,本机仓库未拉取过,xmake 先 `updating repositories`(联网 11 s、写 + `~/.xmake`)。xmake 源码(`modules/private/action/require/impl/repository.lua` 的 `pulled()`)在 + `network.mode` 策略为 `private` 时跳过仓库更新;加上 `network.mode:private` 后实测不再联网,缺包时 5.8 s 内报 + `The packages(xxhash) not found`,归为 `needs_download`。 +3. 生成的命令带 `-fmodule-mapper=/tmp/.xmake…/*.mapper.txt`(gcc),这些是 BMI 参数,mcppls 的规范化(P3)本就会去掉; + 模块角色由扫描恢复,层级为 L3。 + +耗时超过 2.5 s 的时限属于正常,按 D5 先上 L4、拿到后切换。 + +### 3.6 状态机与交互 + +``` +打开工作区 + │ buildDiscovery = off? → 只用 mcppls.database(若配置),否则 L4;结束 + │ 各提供者 detect(纯文件,< 50 ms),按 buildDiscovery.providers 过滤 + ├─ 有模型缓存 → 立即用(BD4),后台离线确认 + ├─ existing 有产物 → 立即用 + └─ 离线 describe,最多等 2.5 s(D5) + ├─ 在时限内成功 → 用它 + ├─ 超时限 → L4 先上(状态栏"正在读取构建描述(cmake,4 s)"),继续等到 hard 上限,成功后切换 + ├─ failed → L4 + 构建工具原话(现有 model-fallback) + ├─ needs_download → L4 + 询问(askBeforeDownload = false 时只进状态栏) + └─ partial 且其中有成员 needs_download → 用已规划的部分 + 询问(B-8) + +询问(非模态通知;每个工作区只问一次,除非缺的东西变了): + "qt-demo 需要下载依赖才能拿到完整的构建信息(xim:qt-base@6.11.1)。现在先按源码扫描提供基础功能。" + [下载并继续] [在终端运行] [不再询问] + ├─ 下载并继续 → 后台联网 describe(Network::allowed,hard 10 min,workDoneProgress,可取消) + │ ├─ 成功 → 一次计划内 clangd 重启(不计入重启预算,RD6),打开的文档保持;之后回到离线 + │ └─ 失败/取消 → 保持 L4,issue 是构建工具原话,按钮"在终端运行" + ├─ 在终端运行 → 现有 mcppls.runBuildToolInTerminal;watch 到输入变化后离线重试 + └─ 不再询问 → 记在工作区状态;状态栏仍显示 producer-needs-download +``` + +要点: + +- **同意是一次性的、只对这个工作区(D2)**;不改 `mcppls.buildTool`。 +- **L4 的质量**:qt-demo 这种依赖大型 SDK 的项目,L4 拿不到 Qt 头文件,"临时可用"只对模块本身成立;M-3(离线时返回部分数据库) + 落地后,等待期间就有已安装依赖的全部语义(mcppls 已支持 `producer-partial`)。 +- **RD1 的例外写明**:RD1 是为"producer 慢但会成功"设的。按 D5,超过 2.5 s 就 L4 先上,拿到构建模型后切换一次; + 离线失败或需要下载时同样 L4 先上。RD1 改写为"构建模型到达后只切换一次,切换不计入重启预算"。 +- **CMake 联网获取的代价**:FetchContent 下载到私有目录,与用户自己的构建目录各下一份;在询问文字里写明。 + +### 3.7 开关(B-7) + +| 设置 / 参数 | 取值 | 作用 | +|---|---|---| +| `mcppls.buildDiscovery` / `--build-discovery` | `auto`(默认)、`off` | `off`:不探测构建系统、不读已有产物、不执行、不询问;只用显式配置的 `mcppls.database`,否则 L4 | +| `mcppls.buildDiscovery.providers` | 数组,默认 `["mcpp", "cmake", "xmake", "meson", "compile-commands"]` | 从中去掉某个提供者即停用它(例如只想用 CMake 已有的构建目录、不要 xmake) | +| `mcppls.buildDiscovery.askBeforeDownload` | `true`(默认)、`false` | `false`:需要下载时只在状态栏说明,不弹询问 | +| `mcppls.buildTool`(已有) | `offline`(默认)、`online`、`off` | 不变:管"构建工具怎么执行"。`off` 仍会探测、仍读已有产物,只是不执行;与 `buildDiscovery = off` 的区别写进设置说明 | + +不受信任的工作区等同 `buildDiscovery = off`(与现在一致)。设置变化时重新加载模型(`extension.ts` 已对 `mcppls.buildTool` +这样处理)。服务端参数、VS Code 设置、Zed/CLion 的 initializationOptions 三处同步;`cxxModules/status` 的 `project` +字段报告当前取值(S3 需加字段,见 §7 R6)。 + +### 3.8 编辑器插件侧 + +- VS Code:`cxxModules/status` 里出现 `producer-needs-download` 且带 `askOnline: true` 时弹通知(每工作区一次),按钮调用 + 服务端命令 `mcppls.describeOnline`(参数为工作区根)。进度走 LSP 的 `window/workDoneProgress`,不依赖插件。 +- Zed、CLion:没有通知按钮时,issue 文本里给出等价命令;`mcppls.describeOnline` 也能从命令面板触发。 + +## 4. 与现有设计的关系 + +| 设计条目 | 变化 | +|---|---| +| BD5 | 10 s → 2.5 s(D5) | +| BD7 | 删除:CMake 首次配置也离线(D1) | +| RD1 | 改写为"构建模型到达后只切换一次,不计入重启预算" | +| P1(故障只影响它所在的地方) | Q1-1/Q1-4 修掉一个违反点 | +| 设计 2.1 "生成模块去构建留下的地方找" | 扩展到生成的头文件目录和源文件(Q1-3),长期保留 | +| 设计 2(引擎抽象:clangd 负责 C++ 语义) | 新增:mcppls 负责让 clangd 的索引覆盖模块实现单元(N-7),直到上游后台索引支持模块 | +| WA-CLANGD-004(module hints) | 不变;登记"hint 路径指向真实 BMI"作为 N-6 的探索方向 | +| 新增 | `BuildSystemProvider` 与副作用契约;`mcppls.buildDiscovery` | + +## 5. 验证计划 + +| 夹具 / 检查 | 覆盖 | 类型 | +|---|---|---| +| `mcpp-rules-generated`(新) | mock mcpp 给出 M-1/M-2 形状的 S1:非 C++ 条目剔除且计入 left out;生成物目录缺失时 `generated-files-missing`;项目 `target/` 有生成物时被只读使用、`main.cpp` 零诊断;不回退 kit;工作区不变 | conformance(mock) | +| `std-generation-race`(新) | 数据库加入 std 单元后到 clangd 重读前报的 `unresolved` 不触发 `std-fallback-kit` | conformance(mock) | +| `mcpp-partition-definition`(新,N-7) | hello13 的形状:分区里声明、签名含类类型的函数与成员,定义在实现单元;只打开调用方,definition 在 10 s 内到 `.cpp`(修前停在声明);clangd 重启后同样 | conformance(真 mcpp) | +| `mcpp-split` 加三项 | B:未打开的实现单元在磁盘上加定义,10 s 内到 `.cpp`;C:新建实现单元;D:解析不了的单元给 `implementation-unreadable` | conformance(真 mcpp) | +| `mcpp-emit-partial-download`(新,B-8) | mock mcpp 的部分回答里一个成员报 `MCPP_OFFLINE_DOWNLOAD_REQUIRED`:其余成员被使用,状态带询问(`askOnline`) | conformance(mock) | +| mcpp 源码 | 实机:`driver.cpp` 的 `phase0_…`、`phase4b_…` 与 `cmd_build.cppm` 的 `prepare_build` 冷启动后到 `.cpp` | 手工 | +| `cmake-fetchcontent-offline`(新) | 首次配置离线、缺依赖 → `producer-needs-download`,工作区不变;`mcppls.describeOnline` 在私有目录完成(CI 用本地 git 仓库代替网络) | conformance | +| `xmake-modules`(新,B-5) | 私有配置目录与 builddir、工作区不变、`network.mode:private` 下缺包归为 `needs_download` | conformance | +| `build-discovery-off`(新,B-7) | `off` 时不执行任何程序、不读已有 `compile_commands.json`、状态为 L4 | conformance | +| qt-demo 实机 | 修后:`main.cpp` 零诊断(已构建过时),或只有一条 `generated-files-missing`(未构建时);没有 `std-fallback-kit`;没有非 C++ 条目的日志 | 手工 | +| V-W | GalTranslPP Windows:修前 / 修后首次到 `.cpp` 的时间与停在 `.ixx` 的调用点数 | CI(待同意) | + +## 6. 待决策 + +| # | 问题 | 建议 | +|---|---|---| +| D3 | N-1 批量变化的阈值 | 单次 > 20 个文件时改为空闲时计划重启(先实测重启后的分片重建) | +| D4′ | kit 回退的范围(修正版) | 只在 std 单元自身编译失败时整体切换,同时移除工具链 std 单元,提供"重试工具链的 std";不做按文件回退 | +| D6 | Q1-3 读取项目 `target/` 里的生成物 | 只在受信任的工作区、只读 | +| D7 | xmake / meson 优先级 | xmake 提到 P1(离线配方已验证);meson P3 | +| D8 | V-W 是否推送到 GalTranslPP 的临时分支跑 Windows CI | 建议跑,结果决定 N-7 的并发与上限 | +| D9 | N-7 的"其余"级(空闲时把所有实现单元过一遍前台)是否默认开启 | 开启;实现单元多时由 V-W 定上限;提供 `mcppls.index.primeImplementationUnits`(`auto`/`off`)关掉 | +| D10 | N-6 的 BMI 方案是否在 0.0.7 做 | 不做;先登记上游,等上游态度与 N-7 的实际开销再定 | + +待查: + +| # | 问题 | 下一步 | +|---|---|---| +| O-1 | mcpp 的 `prepare_build`:主接口导出、默认参数、定义在实现单元,定义文件打开时索引里也没有这个符号(`workspace/symbol` 为空),跳转只到声明 | 缩成最小复现(主接口导出函数 + 实现单元定义 + 实现单元 `import :partition`);看 clangd 的 SymbolCollector 对"规范声明来自 BMI"的符号是否跳过;结果并入 N-6 的上游登记。0.0.6 内完成定位,修法另定 | + +## 7. 自我 review + +| # | 问题 | 处理 | +|---|---|---| +| R1 | **第 1 版的 D4"tier 1/2 只按文件回退"做不到。** 一个 clangd 的数据库里,模块名到提供者只能有一个映射;让一部分文件用 kit 的 `std`、另一部分用 gcc 的 `std`,就要同时放两个 `std` 提供者,clangd 只会取其一,另一半文件静默地用错 std | 改为 D4′:整体切换只在确认 std 自身编译失败时发生,并移除工具链 std 单元。**需要你再确认** | +| R2 | 第 1 版把 Q1-3 放在 P1,但 16:29 的会话证明它是 qt-demo 唯一持续的问题 | 升为 P0,并做了端到端验证 | +| R3 | Q1-3 依赖 mcpp 私有目录与 `target/` 的路径对应,属于耦合 mcpp 的实现细节 | 写明为临时对策;对应不上时只报 issue、不猜;M-2 落地后按 S1 声明处理 | +| R4 | N-7 的临时打开会让 clangd 为该单元构建模块依赖,大项目里每个几秒 CPU | 并发上限 2、相关级单次上限 16、已过一遍且未变化的跳过;V-W 用实测数据定上限 | +| R5 | 第 1 版的 xmake 结论("私有 XMAKE_CONFIGDIR + --builddir")没说清楚为什么需要配置步骤,且离线配方有漏洞(仍联网更新仓库) | §3.4 重写:CDB 命令本身不编译,隔离是因为模块扫描写 builddir;离线配方补上 `network.mode:private` 并验证 | +| R6 | B-2 的 `askOnline`、B-7 的新设置会改变 S3(`cxxModules/status` 的 issue 与 `project` 字段) | 按规范流程改 S3、更新 `conformance/traceability.json`,和实现同一个 PR | +| R7 | D5(2.5 s 后 L4 先上)意味着慢一点的 producer(2.5–60 s)会多一次切换重启,L4 期间的索引白做 | 接受(用户已定);切换不计入重启预算;有模型缓存时不受影响(缓存优先) | +| R8 | `buildDiscovery = off` 与 `buildTool = off` 容易混淆 | 设置说明里并列写出两者的差别;`off` 优先 | +| R9 | 第 1 版把 `windows.h` 推测为 std 误判导致 | 撤回推测:Linux 三次会话均无;GalTranslPP 这类 Windows 项目在 Linux 上缺 `Windows.h` 属预期 | +| R10 | N-2 依赖"definition 与 declaration 相同"来判断"只知道声明",会多一次 declaration 请求 | 只在 definition 结果落在接口单元时才发这一次;有 N-7 的相关级之后命中率应该很低,保留作为兜底 | +| R11 | **第 2 版对问题 2 的判断错了。** 它说"路由正确、hello 正常,问题只在索引没轮到",实际是后台索引对模块单元建错;hello 正常是因为签名简单、碰巧对得上(只开 `main.cpp` 的 hello7 也全对,所以不是探测方式的问题;但第 2 版有几组探测同时打开了实现文件,那几组本来就不能说明后台索引) | §2.3 重写,N-7 取代 N-5;新增按"分区 + 类类型参数"的最小复现与夹具 | +| R12 | 第 3 版一度用"直接跑 clangd、不经 mcppls"做对照,想证明是纯上游问题;但那组对照里连 `Point::sum`、`twice` 也停在声明(去掉 BMI 参数后后台索引同样失败,且更糟),不是干净对照 | 不用它下结论;根因以 clangd 源码 + mcppls 下的 debug 日志为准 | +| R13 | 第 2 版向 mcpp 要"emit 生成出生成物"(M-2),与 mcpp 规范 R2.1/R2.5(不写工程、不运行 action)冲突,mcpp 不会接受 | M-2 改为"在 S1 里标明生成物";Q1-3 不再是临时对策,长期保留 | +| R14 | 部分回答 + 需要下载的组合没有覆盖(B-8);mcppls 丢弃 note 级诊断,`HOST_TOOL_DEFERRED` 看不到 | 新增 B-8 | +| R15 | N-7 靠"关闭后动态索引仍保留"这一观察(最长验证 120 s),不是 clangd 的承诺 | 夹具里检查关闭后 10 分钟仍正确;clangd 重启后重做;若某版 clangd 关闭即丢,改为保持打开(占内存,设上限) | +| R16 | N-7 只修跳到定义;Find References、Call Hierarchy、Rename 同样受后台索引缺陷影响 | 写明范围(§2.6),留给 N-6;状态或文档里说明"跨文件引用只覆盖打开过的文件" | +| R17 | 本版开始时 mcpp 2026.9.27.1 尚未发布,复测用的是本机从 b439fd97 构建的二进制;会话后段本机的 `mcpp` 已是 2026.9.27.1,fresh 副本上的复测用的是它 | 结论不变;发布版上的 mcpp 源码跳转实测留到 V-W 一起重跑 | +| R19 | 复测时在 qt-demo 原项目里跑了 2026.9.27.1 的 emit,它改写了 `qt-demo/.mcpp/.xlings.json`(mtime 17:28),而当时只比对了项目根目录的列表,没发现 | 已告知;这也是 mcpp 的缺陷(emit 写工程,违反 R2.1),列在 mcpp#724 的附带发现里。之后的实测只在副本上跑 | +| R18 | 第 3 版一开始把 N-7 写成"在 mcpp 源码上验证有效",实际三个探测点只验证了两个,第三个(`prepare_build`)另有原因 | 改为"三处中的两处",新增 O-1;N-7 的夹具只断言已验证的形状 | + +## 8. 附:复现 + +- qt-demo:在项目目录 `mcppls --payload check src/main.cpp`;`mcpp emit build-database` 看 M-1/M-2 的原始输出; + 修法验证:emit 输出中私有目录替换为 `<副本>/target/.build-mcpp`、去掉 `.ui/.qrc/.ts` 后 `mcppls --database db.json check src/main.cpp`。 +- 跳转:hello 系列在会话临时目录,用自写 LSP 客户端对 `mcppls serve` 每秒请求一次 definition;B = 启动后改写未打开的 + `types.cpp` 并发 `didChangeWatchedFiles`;D = 在 `greet.cpp` 的全局模块片段里 `#include "ui_generated.h"`。N-4 会把它们 + 固化进 `mcpp-split` 夹具。 +- clangd 源码:`llvm-project-23.1.0.src.tar.xz`(`.payload-cache/`)中的 `clangd/index/Background.cpp:178`、 + `clangd/SourceCode.cpp:1240`、`clang/lib/Driver/Types.cpp`。 +- CMake:`FetchContent_Declare(fmt …)` + `-G Ninja -DFETCHCONTENT_FULLY_DISCONNECTED=ON`,私有构建目录。 +- xmake:见 §3.4 的两条命令;对照组为不加 `network.mode:private`(出现 `updating repositories`)和不加 `--builddir`(出现项目 `build/`)。 +- 后台索引根因:hello13 = hello2 + 分区 `:state`(`struct State`、`int phase_a(State&)`、`int phase_p(const Point&)`、 + `std::string phase_s(const std::string&)`、`struct Box { int f(const std::string&); int g(int); }`),定义分在 + `phase_a.cpp`、`phase_b.cpp`、`box.cpp`;只打开 `main.cpp`、`driver.cpp`,`mcppls --log-level debug` 的日志里看 + `Failed to compile …, index may be incomplete`。 +- mcpp 源码:`b439fd97` 检出到临时目录、`mcpp build`,`mcppls serve --mcpp <新 mcpp>`,只打开 `src/build/prepare/driver.cpp` + 与 `src/cli/cmd_build.cppm`;再临时打开 / 关闭 `manifest.cpp`、`graph.cpp` 验证 N-7。 +- clangd 源码:`clangd/index/Background.cpp:254` 起的 `BackgroundIndex::index()`(无 ModulesBuilder)。 + +## 9. 0.0.6 实施计划(单 PR) + +决定(2026-09-27):D1、D2、D4′、D5、D8、D9 同意;D10 改为"上游缺陷凡是 mcppls 侧能做的,直接在 mcppls 侧实现"(混合引擎: +mcppls 自己的引擎做一部分,clangd 做一部分),同时登记上游。另加一项:**把 mcppls 的全部可配置项收拢成一个配置模块**,并配一章文档。 +全部进 0.0.6,一个 PR。 + +### 9.1 实施时的新发现(改变了第 3 版的做法) + +- mcppls **已有**"按需找定义"(`search_definition_`,`src/engine/clangd.cpp`):clangd 只给出接口里的声明时,打开该模块的其他单元再问一次。 + 它在 mcpp 源码上失败,是因为只打开 4 个单元(`UNITS_PER_SEARCH`),且只按文件名 stem 和目录排序,不看名字定义在哪:`phase0_…` + 声明在 `state.cppm`,模块 `mcpp.build.prepare` 有 17 个其他单元,挑中的 4 个里没有 `manifest.cpp`。所以 N-8 不是另起一套, + 而是让选单元这一步**按名字**(先词法扫描哪些单元里有这个名字的定义),并在 clangd 仍对不上时(O-1)直接给出词法找到的位置。 +- mcpp 2026.9.27.1 构建不了本仓库(mcpp#725,已提):CI 固定 2026.9.26.1,不受影响;nightly 会先遇到。本 PR 不改清单,等 mcpp 修复; + 在 #24 登记 UP-M。 + +### 9.2 用户体验的硬规则(用户要求,2026-09-27) + +1. **插件侧的提示一律非阻塞**:VS Code 右下角的通知,用户可以一直不点;不用模态框,不在激活、启动或任何请求的路径上 `await` 它。 +2. **提示不影响 mcppls 的后续功能**:弹出询问时,L4 已经在工作;用户点不点、什么时候点,都只影响"是否联网获取"这一件事。 +3. **自动分级**:L4 → L3 → L1/L2 的升级都是自动的,不需要用户操作;更好的模型到达时计划内切换一次(不计入重启预算)。 +4. **环境自己变好也要接得住**:用户在终端里自己跑了构建、装好了依赖(`mcpp build`、`xlings install`、`cmake`),mcppls 通过监视构建输入与 + 后台退避重试(需要下载时 30 s、1 min、2 min、5 min,之后每 5 min;监视到的输入变化立即重试)发现环境完善,自动升级; + 此时尚未点的询问失效(插件收到新状态后撤回或忽略),不再重复弹出。 +5. 同一件事每个工作区只问一次(D2);"不再询问"记在工作区状态里;缺的东西变了才会再问。 + +### 9.3 任务与依赖 + +| # | 任务 | 依赖 | 角度 | +|---|---|---|---| +| T0 | 分支 `release/0.0.6`;本计划 | — | — | +| **T1 配置模块** `src/config/settings.cppm`(`mcppls.config.settings`):每个设置一行——键、类型、取值、默认、命令行拼写、所在位置(服务端 / 客户端)、生效方式(重载模型 / 重启)、起始版本、说明、旧名(升级映射);解析 命令行 > initializationOptions > 默认,未知取值回落默认并记为问题;`mcppls settings [--format markdown\|json]` 输出参考表;`report` 带 `settings`(生效值、来源、问题) | T0 | 架构、一致性、无感升级 | +| T1a | 命令行全局选项、`session_options`、`handle_initialize_` 都改为从 T1 读;`workspace/didChangeConfiguration` 对"重载模型"类设置直接生效 | T1 | 架构、优雅 | +| T1b | 文档 `docs/30-settings.md`(及 zh-CN)的参考表由 `mcppls settings --format markdown` 生成;单元测试比对注册表与文档、VS Code `package.json` 三者一致 | T1 | 一致性 | +| T2 | Q1-2 剔除非 C/C++ 条目;Q1-3 生成物目录(项目 `target/` 只读替换,`generated-files-missing`,监视生成物出现) | T0 | 稳定性、体验 | +| T3 | Q1-1 + Q1-4:std 回退只认 std 单元自身编译失败;数据库代际;回退时移除工具链 std 单元 | T0 | 稳定性 | +| T4 | N-8:选单元按名字(词法定义扫描,含分区与实现分区);clangd 仍只给声明时返回词法定位;`UNITS_PER_SEARCH` 以名字命中为准 | T0 | 体验、架构(混合引擎) | +| T5 | N-7:相关级(打开文件时,其模块与直接导入模块的实现单元)、磁盘变化级(未打开且摘要变了)、其余级(空闲时);设置 `index.primeImplementationUnits` | T4、T1 | 体验、稳定性(并发与上限) | +| T6 | N-3:`implementation-unreadable` | T5 | 体验 | +| T7 | B-1:`BuildSystemProvider` 注册表;mcpp / CMake / compile-commands 平移 | T0 | 架构 | +| T8 | B-3 CMake:首次配置离线(`FETCHCONTENT_FULLY_DISCONNECTED`)、预设(`CMakePresets.json` 的 `binaryDir` 与配置);B-5 xmake;B-6 meson | T7 | 跨平台、兼容性 | +| T9 | B-2 + B-8 + B-4:需要下载时询问(状态里 `askOnline`、服务端命令 `mcppls.describeOnline`、一次性联网)、部分回答 + 需要下载、首个模型等 2.5 s、需要下载时退避重试 | T7、T1 | 体验(§9.2) | +| T10 | B-7:`buildDiscovery`、`buildDiscovery.providers`、`buildDiscovery.askBeforeDownload` | T1、T7 | 体验 | +| T11 | VS Code:非阻塞询问、"不再询问"、新设置;Zed/CLion 的 initializationOptions 与文档 | T1、T9 | 体验、跨平台 | +| T12 | 规范:S3(issue 的 `askOnline`、`project.settings`、新 issue 码)+ schema/traceability;设计记录(BD5、BD7、RD1、新决定);`docs/20-projects.md`;CHANGELOG;#24(UP-M:mcpp#724、#725;UP:后台索引与模块) | 全部 | 一致性 | +| T13 | 验证:单元测试;夹具 `mcpp-rules-generated`、`std-generation-race`、`mcpp-partition-definition`、`mcpp-split`(磁盘改动)、`mcpp-emit-partial-download`、`cmake-fetchcontent-offline`、`build-discovery-off`;xmake 夹具(Linux);三平台 CI | 全部 | 稳定性、跨平台 | +| T14 | 版本 0.0.6、PR、CI 全绿、自我 review、squash 合入、Release、本地验证、交付目录 | T13 | — | + +并行:T1、T2+T3、T4、T7+T8 互不依赖,可同时进行;T5/T6 在 T4 之后,T9/T10/T11 在 T1 与 T7 之后。 + +### 9.4 各角度的检查项 + +| 角度 | 要求 | +|---|---| +| 架构 | 配置只有一个来源(T1);构建工具只经 `BuildSystemProvider`;上游缺陷的补偿都是登记过的 WA 或混合引擎的一部分 | +| 稳定性 | 新的后台打开都走现有 `background_` 的上限与超时;不为索引重启 clangd;代际检查避免竞态误判 | +| 优雅简洁 | 复用 `search_definition_`、`background_`、`toolrun`、现有 issue 通道;不引入新进程 | +| 用户体验 | §9.2 五条;状态栏说清楚层级与原因 | +| 兼容性 | 旧设置名继续被接受(注册表的旧名映射);旧客户端不认识 `askOnline` 也能工作;mcpp 旧版本的文字识别保留 | +| 跨平台 | 新代码不含平台分支(平台差异只在 `modules/os`);xmake/meson/CMake 的参数在 Windows 上同样成立;三平台 CI | +| 一致性 | 设置表、文档、`package.json` 由测试互相校验;S3 规则有 traceability | +| 无感升级 | 模型缓存格式不变;设置默认值保持旧行为(除 D1、D5 这两处已定的变化);升级后首次启动不需要用户操作 | + +### 9.5 实施结果(PR #27) + +| 任务 | 结果 | 证据 | +|---|---|---| +| T1 配置模块 | `src/config/settings.cppm` 一张注册表;命令行、`initializationOptions`、`didChangeConfiguration`、report、`mcppls settings` 与 `docs/30-settings.md`(中英)都由它生成,`tests/test_settings.cpp` 校验三方一致 | 单元测试 | +| T2 Q1-2/Q1-3 | 规则输入剔除;生成物从项目 `target/` 只读读取,缺失时 `generated-files-missing` + 监视 | qt-demo 副本 `clangd --check` 退出码 0;夹具 `mcpp-rules-generated` | +| T3 Q1-1/Q1-4 | `failure_action`:未读到的一代不算失败,kit 只在 std 自身编译失败或无单元时启用 | `test_server` 用 qt-demo 的时序 | +| T4 N-8 | 按名字选单元;clangd 仍只给声明时按名字给出定义(O-1);在定义上仍回到声明 | mcpp 源码 `prepare_build` → `driver.cpp`;夹具 N9/N10 | +| T5/T6 N-7/N-3 | WA-CLANGD-008:实现单元经前台构建(相关、磁盘变化、空闲时其余);`implementation-unreadable` | 夹具 `mcpp-partition-definition`(0.0.5 N1–N5 失败);`test_server` 引擎级测试 | +| T7/T8 | `BuildSystemProvider` 注册表;CMake 预设 + 首次即断网;xmake(私有配置/构建目录、`network.mode:private`);meson(`--wrap-mode=nodownload`);统一的工程清单与嵌套规则 | xmake 实机:tier 3、工程目录不变;夹具 `cmake-fetchcontent-offline` | +| T9/T10 | `askOnline` + `mcppls.describeOnline`;退避重试;首个模型 2.5 s;部分回答 + 下载;`buildDiscovery` 开关 | 夹具 `mcpp-emit-needs-download`(D5–D7)、`mcpp-emit-provisioned`、`mcpp-emit-partial-download`、`build-discovery-off` | +| T11 | VS Code 非阻塞询问(§9.2 五条)、Run in Terminal 认 xmake/meson | `downloadAsk.test.ts` | +| T12 | S3-4-16..21、设计记录、`docs/20-projects.md`(中英)、CHANGELOG、#24(UP-08 更新、UP-17、UP-M4、UP-M5) | `validate.py` 251 条规则 0 失败 | + +V-W(GalTranslPP,Windows,windows-2025,Sunrisepeak/GalTranslPP#1 run 36317141415,0.0.5 基线,编辑会话 15 分钟、18 轮): +最后一轮 32 个调用点里,**11 个落在 `.ixx`(声明),只有 2 个到 `.cpp`**,8 个无回答,其余是 std/Qt 的名字; +`ApiTool.ixx` 里声明的 `parseApiProtocol`、`queryApiModels` 等 15 分钟内从未到达 `.cpp`。0.0.6 发布后用同一探针重跑对比。 + +实施中另外发现并处理:mcpp 2026.9.27.1 构建不了本仓库(mcpp#725,CI 固定 2026.9.26.1);emit 会写工程 `.mcpp/.xlings.json`、 +构建程序链接失败被误报为"设备源无动作"(都写进了 mcpp#724)。 + +自我 review(合入前,全量 diff)改掉的三处:xmake 的"是否重新 `xmake f`"原来对目录取 stamp,恒为空—— +而 xmake 会把 `network.mode:private` 存进私有配置,于是"下载并继续"那一次仍然离线;改为成功配置后记下 +"模式 + xmake.lua 的 stamp",不同就重配(实机:第二次跳过、改 xmake.lua 后重配、工程目录不变)。 +N-7 卡住的单元被清掉后不再补位,队列会停到下一个事件;现在立即补位(将要重启时除外)。 +单次 watch 超过 20 个文件(D3)原来仍插队,现在只进"其余"级。 +预发布检查 `real-xlings-old-mcpp` 的 STRESS1 在 CI 超预算(p90 6.41 s,预算 3 s;0.0.5 为 2.83 s):N-7 在模块准备期间 +就开始前台构建实现单元,与准备、与用户请求抢 clangd 的 worker。改为准备期间不开新单元,准备结束再补位。本地限 4 核复现: +0.0.5 1.35 s;改前 2.25 / 2.44 s;改后 1.52 / 1.80 s。 diff --git a/.agents/docs/design.md b/.agents/docs/design.md index 62e3c2f..a512102 100644 --- a/.agents/docs/design.md +++ b/.agents/docs/design.md @@ -62,7 +62,7 @@ editor / coding agent / CI |---|---|---|---| | L1 | mcpp | `mcpp emit build-database`, run offline | every unit, its role, arguments and that toolchain's `std` (S1 level 3 after structuring) | | L2 | CMake with `FILE_SET CXX_MODULES` | the build directory's database, or a private configure | the generator's answer, `@modmap` files expanded | -| L3 | only `compile_commands.json` | the database plus scanning | arguments per file, module roles recovered by scanning, compilers probed | +| L3 | only `compile_commands.json`, or xmake / meson | the database (or the one xmake or meson writes into a private directory) plus scanning | arguments per file, module roles recovered by scanning, compilers probed | | L4 | sources only, or no usable compiler | scanning and the bundled semantic kit | modules resolve and `import std` works, with libc++ diagnostics | An untrusted workspace is L4 by definition: no build tool and no compiler runs, and — the rule an @@ -123,9 +123,21 @@ the disk or the database makes it safe. Every command clangd is given compiles o link-phase check of the driver can fail its module scan (issue #23). Restarts have a budget per cause (plan, recovery, crash) and are backed off past it, never refused (refining P3); a person's own restart and a switch of toolchain, profile or context are never counted. A crash sets aside the -file clangd names in its crash context. A project whose build system was found gets clangd only -with the build tool's model (within its bound), never with a provisional one it would have to -unlearn. +file clangd names in its crash context. With nothing cached, the build tool has 2.5 s to describe +the project; after that both engines serve it from its scanned sources (L4) while the build tool goes +on, and its model replaces the provisional one in one switch that is never counted against the budget +(0.0.6, revising 0.0.5's "clangd only with the build tool's model"). A report that a module has no +unit is weighed against the database clangd has actually read, and the kit replaces the toolchain's +`std` only when std's own unit fails to compile or does not exist (0.0.6). + +**What clangd indexes is not what it can open** (0.0.6, `.agents/docs/2026-09-27-qt-demo-navigation-discovery-plan.md`). +clangd's background index compiles a module unit without building the modules it imports, so a +definition in an implementation unit is indexed apart from its declaration, or not at all, until the +unit has been open (WA-CLANGD-008). The server builds implementation units through clangd's +foreground, a few at a time -- the units of an opened file's module and of the modules it imports +first, a unit changed on disk again, the rest once clangd is idle -- and closes them; their symbols +stay in clangd's index. A definition request that still lands on a declaration in a module interface +searches the module's units by name. **Observability.** One occurrence must be enough to see why. clangd logs at `info` into a ring in memory; crashes, stuck or spinning clangd, files set aside, backed-off restarts and a workaround @@ -141,7 +153,14 @@ gets its own process unit, soft and hard deadlines end the unit with everything are bounded, and every run is recorded (command, environment source, offline or not, duration, outcome, tail of stderr). Build tools run with the login shell's environment on POSIX; clangd keeps the editor's. Implicit runs are offline (`mcppls.buildTool = offline`); a run that needs a download -reports `producer-needs-download` with a "run it in a terminal" action. +reports `producer-needs-download` with a "run it in a terminal" action and, for a client that knows +`askOnline`, the offer to fetch it once with the network (`mcppls.describeOnline`). The offer never +blocks anything; the offline description is asked again on a backoff and whenever a watched input +changes, so a build the person runs in their own terminal upgrades the project by itself. Build +systems are providers behind one interface (detect from files, read existing output, describe into a +private directory offline or online); none writes into the workspace. Every configurable behaviour is +one row of `src/config/settings.cppm`, from which the command line, `initializationOptions`, +`workspace/didChangeConfiguration`, the report and `docs/30-settings.md` are derived. ## 4. AI-facing capabilities (`src/ai/`) @@ -182,18 +201,24 @@ before anything is published (`docs/92-release.md`). | BD1 | `mcppls.buildTool` defaults to `offline` | | BD2–3 | Build tools get the login shell's environment on POSIX; clangd keeps the editor's | | BD4 | A cached model is used immediately and confirmed in the background | -| BD5 | Deadlines: producer soft 5 s / hard 60 s, toolchain probe 20 s, login shell 10 s, 10 s wait for a producer when nothing is cached | -| BD7 | The first configure of the private CMake build directory may download (FetchContent); later ones are disconnected | +| BD5 | Deadlines: producer soft 5 s / hard 60 s, toolchain probe 20 s, login shell 10 s, 2.5 s wait for a producer when nothing is cached (10 s until 0.0.6, plan 2026-09-27 D5) | +| BD7 | Withdrawn in 0.0.6 (plan 2026-09-27 D1): every configure of the private CMake build directory is disconnected (`FETCHCONTENT_FULLY_DISCONNECTED`); fetching is the person's choice (BD9) | | BD8 | No minimum-version table for build tools; a hang, timeout or download need suggests updating | | T1 | Tooling: the server is not split internally; devtools depends on no server code and is the one entry for repository work (`mcpp run -p devtools -- ...`) | | T3 | Cache inspection belongs to the server (`mcppls cache`), since only the server knows its cache layout | | T5 | The specification schema check (`docs/specs/tools/validate.py`) is the one script kept, until a C++ JSON Schema 2020-12 validator exists | -| RD1 | A project whose build system was found gets clangd with the build tool's model only, within the producer's bound (fix plan 2026-09-26 D1) | +| RD1 | Revised in 0.0.6 (plan 2026-09-27 D5): past the first 2.5 s clangd serves the scanned-sources model too, and the build tool's model replaces it in one switch that is never counted against the restart budget | | RD2 | clangd's upstream defect behind `import a.` (UP-01) is worked around, not fixed in a clangd of our own, until upstream settles (D2) | | RD3 | clangd logs at `info` into memory; incidents on disk; nothing ever uploaded (D3, F17, F18) | | RD4 | The space is a completion trigger only after `import `, dropped by the editor elsewhere, and advertised only to clients known to drop it (D4) | | RD5 | Reports and bundles are redacted by default; a bundle with anything left is not written (F18) | | RD6 | Restarts are budgeted per cause and backed off past it, never refused; the person's restart is never counted (F14) | +| BD9 | A download the build description needs is fetched only when the person accepts, once, in a notification that never blocks; the server retries offline on its own meanwhile (plan 2026-09-27 D2, §9.2) | +| BD10 | Build systems are `BuildSystemProvider`s (mcpp, CMake, xmake, meson, compile-commands); `mcppls.buildDiscovery` turns detection off, `buildDiscovery.providers` chooses them (plan 2026-09-27 B-1, B-7) | +| RD7 | The kit replaces the toolchain's `std` only when std's own unit fails to compile or the plan has none; a report about a database clangd has not read is ignored (plan 2026-09-27 D4', Q1-1, Q1-4) | +| RD8 | Generated output a producer only names in its private planning directory is read, read-only, from the project's own `target/`; missing, it is reported with a build action and watched for (plan 2026-09-27 Q1-3, mcpp-community/mcpp#724) | +| RD9 | Implementation units are built through clangd's foreground for its index (WA-CLANGD-008), until clangd's background index builds a module unit's imports (plan 2026-09-27 N-7) | +| RD10 | Every configurable behaviour has one definition, the registry in `src/config/settings.cppm`; command line, `initializationOptions`, `didChangeConfiguration`, report and the settings chapter are derived from it and held to it by a test (plan 2026-09-27 T1) | ## 7. Known limits diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45e16d8..51933dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -277,35 +277,35 @@ jobs: - platform: linux-x64 part: 1 of 2 os: ubuntu-24.04 - fixtures: mcpp-split mcpp-all-cppm verify-changes mingw mcpp-split-gcc mcpp-watch multi-root mcpp-llvm mcpp-watch@polling mcpp-emit s1-two-sets compdb-lto-msvc failure-at-base@vscode generated-module-negotiated typing-import typing-import-spin workaround-canaries typing-autosave compdb-rejected-command compdb-mixed-standards mcpp-emit-partial + fixtures: mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mingw mcpp-split-gcc mcpp-watch multi-root mcpp-llvm mcpp-watch@polling mcpp-emit mcpp-rules-generated s1-two-sets compdb-lto-msvc failure-at-base@vscode generated-module-negotiated typing-import typing-import-spin workaround-canaries typing-autosave compdb-rejected-command compdb-mixed-standards mcpp-emit-partial mcpp-emit-partial-download - platform: linux-x64 part: 2 of 2 os: ubuntu-24.04 extras: true - fixtures: inferred inferred-bom engine-none module-faults untrusted mcpp-gcc mcpp-gcc@plain mcpp-emit-package-std mcpp-emit-broken mcpp-emit-unavailable mcpp-emit-hang mcpp-emit-needs-download mcpp-emit-watch mcpp-emit-watch@polling cmake-clang cmake-clang-bdb watch-polling payload-corrupt clangd-cannot-load generated-module generated-module@vscode generated-module-old-mcpp generated-module-old-mcpp@neovim module-faults@zed failure-at-base failure-at-base@neovim completion-keywords diagnostic-bundle clangd-crash-context mcpp-emit-wait inferred-cxx26 + fixtures: inferred inferred-bom build-discovery-off engine-none module-faults untrusted mcpp-gcc mcpp-gcc@plain mcpp-emit-package-std mcpp-emit-broken mcpp-emit-unavailable mcpp-emit-hang mcpp-emit-needs-download mcpp-emit-provisioned mcpp-emit-watch mcpp-emit-watch@polling cmake-clang cmake-clang-bdb cmake-fetchcontent-offline watch-polling payload-corrupt clangd-cannot-load generated-module generated-module@vscode generated-module-old-mcpp generated-module-old-mcpp@neovim module-faults@zed failure-at-base failure-at-base@neovim completion-keywords diagnostic-bundle clangd-crash-context mcpp-emit-wait inferred-cxx26 # generated-module{,-old-mcpp,-negotiated}'s mcpp-mock.json bakes in a POSIX driver path # (${env:HOME}/.mcpp/registry/..., no {exe}); it resolves the same way here as on Linux, so # these run on macOS but are left off win32-x64 below rather than fixed unverified. - platform: darwin-arm64 os: macos-14 extras: true - fixtures: inferred inferred-bom engine-none module-faults untrusted mcpp-llvm mcpp-split mcpp-all-cppm verify-changes mcpp-watch multi-root failure-at-base clangd-cannot-load failure-at-base@zed generated-module generated-module-old-mcpp generated-module-negotiated typing-import typing-import-spin workaround-canaries completion-keywords diagnostic-bundle typing-autosave clangd-crash-context mcpp-emit-wait inferred-cxx26 + fixtures: inferred inferred-bom build-discovery-off engine-none module-faults untrusted mcpp-llvm mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mcpp-watch multi-root failure-at-base clangd-cannot-load failure-at-base@zed generated-module generated-module-old-mcpp generated-module-negotiated typing-import typing-import-spin workaround-canaries completion-keywords diagnostic-bundle typing-autosave clangd-crash-context mcpp-emit-wait inferred-cxx26 # No mcpp on the arm64 runner (its tools are the cross-built ones), so the fixtures that # need no build tool and no compiler of their own: the semantic kit, clangd and the server # on aarch64, with module faults, a corrupt payload and polling included. - platform: linux-arm64 os: ubuntu-24.04-arm cross-tools: true - fixtures: inferred inferred-bom engine-none module-faults untrusted payload-corrupt clangd-cannot-load failure-at-base watch-polling typing-import typing-import-spin workaround-canaries completion-keywords diagnostic-bundle typing-autosave clangd-crash-context inferred-cxx26 + fixtures: inferred inferred-bom build-discovery-off engine-none module-faults untrusted payload-corrupt clangd-cannot-load failure-at-base watch-polling typing-import typing-import-spin workaround-canaries completion-keywords diagnostic-bundle typing-autosave clangd-crash-context inferred-cxx26 - platform: win32-x64 part: 1 of 2 os: windows-2022 - fixtures: mcpp-split mcpp-all-cppm verify-changes mcpp-split-msvc cmake-msvc-std compdb-clangxx-msvc-std multi-root compdb-clang-cl-std mcpp-llvm-msvc failure-at-base@vscode typing-import typing-import-spin workaround-canaries typing-autosave + fixtures: mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mcpp-split-msvc cmake-msvc-std compdb-clangxx-msvc-std multi-root compdb-clang-cl-std mcpp-llvm-msvc failure-at-base@vscode typing-import typing-import-spin workaround-canaries typing-autosave - platform: win32-x64 part: 2 of 2 os: windows-2022 extras: true - fixtures: inferred inferred-bom engine-none module-faults mcpp-emit-hang inferred-msvc untrusted mingw cmake-msvc cmake-msvc-bdb cmake-clangxx-msvc cmake-clang-cl mcpp-msvc mcpp-watch failure-at-base clangd-cannot-load completion-keywords diagnostic-bundle mcpp-emit-wait inferred-cxx26 + fixtures: inferred inferred-bom build-discovery-off engine-none module-faults mcpp-emit-hang inferred-msvc untrusted mingw cmake-msvc cmake-msvc-bdb cmake-clangxx-msvc cmake-clang-cl mcpp-msvc mcpp-watch failure-at-base clangd-cannot-load completion-keywords diagnostic-bundle mcpp-emit-wait inferred-cxx26 defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f725ba..38046f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,97 @@ release's notes are that section. Versions are three-part semantic versions, `MAJOR.MINOR.PATCH`, and every editor plugin carries the product version unchanged. +## [0.0.6] — 2026-09-27 + +Go-to-definition reaches implementation units, even when you never opened them. A Qt project's +`ui_*.h` is found, its forms and resources no longer confuse clangd, and a bad moment no longer moves +the whole project to another standard library. xmake and meson projects are described like CMake and +mcpp ones, never by writing into your project, and a build description that needs a download no longer +leaves you waiting: the project is served at once, you are asked once in a notification you may ignore +forever, and building in your own terminal is picked up by itself. Every setting now has one +definition and one generated reference. +The analysis, the measurements and the plan are `.agents/docs/2026-09-27-qt-demo-navigation-discovery-plan.md`. +Upstream defects behind this are registered in issue #24 (UP-08, UP-17, UP-M4, UP-M5). + +### Navigation + +- **Definitions in implementation units are reached.** clangd's background index compiles a module + unit without building the modules it imports, so a definition in an implementation unit was indexed + apart from its declaration, or not at all, until that file had been open: in mcpp's own `prepare` + module go-to-definition stayed on the declarations for as long as it was measured. mcppls now builds + implementation units through clangd's foreground, a few at a time (WA-CLANGD-008): the units of an + opened file's module and of the modules it imports first, a unit changed on disk (a `git pull`, another + program, a coding agent) again, the rest once clangd is idle. On mcpp's source the same requests now + land in the `.cpp` from the first answer. `mcppls.index.primeImplementationUnits = off` turns it off. +- **A definition clangd cannot link is found by name.** The search a definition request starts opens the + units that define the name first, not the first four by file name; when clangd still answers with the + declaration, the definition found in the module's units is the answer (same name, scopes and parameter + types, never a guess). Asked on a definition, you still go back to the declaration. +- **An implementation unit that does not build says so** (`implementation-unreadable`): usually a missing + header, which is why its definitions cannot be reached. + +### Projects and build tools + +- **Qt and other build rules (mcpp-community/mcpp#724).** A rule's inputs (`.ui`, `.qrc`, `.ts`) are no + longer given to clangd as C++. The files a rule generates are read from your project's own `target/` + when a build has written them; otherwise the status says which are missing and offers **Build in + Terminal**, and the model reloads by itself once they appear. On qt-demo, `main.cpp` went from a fatal + missing `ui_mainwindow.h` to no diagnostic at all. +- **xmake and meson are supported.** xmake is asked with its own `xmake project -k compile_commands` + (nothing is compiled) and meson with `meson setup`, both into mcppls's cache, offline; your project + directory stays untouched (checked file by file). Both are L3. `check`, `query` and the project + boundaries now know `xmake.lua` and `meson.build`, and a subproject's own `CMakeLists.txt`, + `xmake.lua` or `meson.build` belongs to the project above it. +- **CMake follows your presets and never downloads without asking.** A preset's `binaryDir` is looked + for, and mcppls's private configure uses the preset's generator, toolchain file and cache variables. + That configure is disconnected from its first run (`FETCHCONTENT_FULLY_DISCONNECTED`), where 0.0.5 let + the first one download. +- **A download is asked about once, and never waited for.** When the build description needs something + that is not on the machine, the project is served from its sources at once; a notification offers + **Download and Continue** (the build tool may reach the network, this once), **Run in Terminal** or + **Don't Ask Again**, and may stay unanswered forever. Meanwhile the description is asked again offline + after 30 s, 1 and 2 minutes and every 5 after, and at once when a build file changes, so a build you + run in your own terminal upgrades the project by itself. A partial answer from mcpp whose missing + member needs a download gets the same offer. +- **The first model comes sooner.** With nothing cached, the build tool has 2.5 s (was 10) before the + project is served from its sources by both engines; its model replaces that one in one switch that + never counts against clangd's restart budget. +- **`mcppls.buildDiscovery = off`** detects no build system at all; `mcppls.buildDiscovery.providers` + leaves out single ones. + +### Engine + +- **The standard library is not replaced by a bad moment.** A report that `std` has no unit, from a + clangd that had not read the database the unit had just joined, moved qt-demo from libstdc++ to the + bundled libc++ and restarted clangd twice. Such a report is now weighed against the database clangd + has read, and the kit replaces the toolchain's `std` only when std's own unit fails to compile or does + not exist. + +### Settings + +- **Every setting has exactly one definition** (`src/config/settings.cppm`), from which the command line, + `initializationOptions`, `workspace/didChangeConfiguration`, `mcppls report` and the reference in + [docs/30-settings.md](docs/30-settings.md) (both languages) are derived; a test holds them and the VS + Code settings to it. `mcppls settings` prints the reference. A value outside a setting's vocabulary + falls back to its default and is reported, never applied. New: `buildDiscovery`, + `buildDiscovery.providers`, `buildDiscovery.askBeforeDownload`, `index.primeImplementationUnits`, and + command-line spellings for `compiler` and `semanticKit`. A settings change that only needs the model + reloaded is applied without a restart. + +### Specifications + +- **S3:** a `producer-needs-download` issue may carry `askOnline`, and `mcppls.describeOnline` describes + the project once with the network; a client offering it never blocks on the question and ignores an + answer that comes after the need is gone (S3-4-16 to S3-4-21). `project.source` gains `xmake` and + `meson`; the report carries the settings in effect. All additive; protocol version 1. + +### Also + +- Conformance fixtures: `mcpp-partition-definition`, `mcpp-rules-generated`, `mcpp-emit-provisioned`, + `mcpp-emit-partial-download`, `build-discovery-off`, `cmake-fetchcontent-offline`; `mcpp-emit-needs-download` + covers the offer and the online description. +- mcpp 2026.9.27.1 cannot build this repository (mcpp-community/mcpp#725); CI stays on 2026.9.26.1. + ## [0.0.5] — 2026-09-26 Issue #23 is fixed: modules are built again for projects compiled with LTO for the MSVC ABI. An diff --git a/conformance/README.md b/conformance/README.md index 2f0f08d..7851762 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -23,20 +23,27 @@ checks fail at once with that reason instead of each waiting out its timeout. | Fixture | What it covers | |---|---| | `inferred` | Loose module sources, no build system and no compiler: the semantic kit provides libc++ semantics (design 13.5) | +| `build-discovery-off` | Plan 2026-09-27 B-7: `mcppls.buildDiscovery = off` on an mcpp project whose producer would answer: nothing is detected, read or run, the model is scanned sources (L4), the report says discovery is off, and module features still work | | `engine-none` | The `inferred` project with `--engine none` (overall design 5.6): no core engine, so mcppls's own engine alone answers module navigation, hover, outline, import completion, module-syntax keywords and module diagnostics, and a space off an import line is answered with nothing (S3-6.2); the status names `none` as the core engine and lists `mcppls` in `engines` (S3-4-5, S3-4-6). `inferred` checks the same fields with clangd, and starts a second server on the same workspace and cache, which must report the `shared-workspace` notice (design 6.3). Its S5 checks: module descriptions and interface summaries, exported symbols found without clangd, `unavailable` for references, and a file written to disk read by the next query | | `verify-changes` | `mcpp-split`'s project in a git repository, for S5's verification after an edit: snippets checked in place (passing, failing, and leaving no unsaved content behind), a partition interface renamed on disk that breaks the implementation unit using it, the working tree's changes from git, and the restored file passing again once its importer is built again | | `untrusted` | An mcpp package in an untrusted workspace: nothing is executed, the kit answers, the status is `degraded` with the reason | | `mcpp-gcc` | mcpp with GCC 16, described by mcpp's own `emit build-database` (mcpp 2026.9.15.1): level 3 once the S1 library has structured mcpp's level 2 document, GCC arguments translated for clangd (P1), libstdc++'s `std` from its manifest, a test that imports the package's module across sets, and the workspace unchanged | | `mcpp-llvm` | The same with LLVM 22: libc++ selected through include paths, BMI arguments removed (P3) | | `mcpp-split` | The shape of a project that separates interfaces from implementations: interface units and an interface partition in `.cppm`, two implementation units (`module hello.greet;`) in `.cpp`, and an implementation partition; from mcpp's own build database. Declarations and definitions are reached from importers, definitions in implementation units before any of them is open, implementation units navigate into partitions and complete module-internal names, edits reach importers, and a file outside the build (`apps/gui/main.cpp`, importing a module nothing provides) is answered by clangd. `mcpp-split-gcc` (Linux) and `mcpp-split-msvc` (Windows) are the same project with GCC 16 and with `msvc@system` | +| `mcpp-partition-definition` | Plan 2026-09-27 N-7 (WA-CLANGD-008): a module split into partitions, functions and members declared in a partition with class-type parameters and defined in other units of the module. clangd's background index builds no module a unit imports, so these definitions stayed on their declarations (0.0.5 fails N1-N5); the server builds the module's implementation units through clangd's foreground, and go-to-definition reaches them with only the callers open | | `mcpp-all-cppm` | The shape of a project whose module units are all `.cppm`, implementations written inside the interfaces: a primary interface re-exporting two partitions and a second module; navigation into partitions and through the re-exports, completion through the re-exported module, edits propagated to importers | | `mcpp-watch` | S2 5 with mcpp itself: a new module interface, a broken `mcpp.toml` and its repair are inputs mcpp names in `watch`; the broken manifest keeps the last model, `degraded` with `model-stale` and mcpp's own `MCPP_BUILD_DATABASE_PLAN_FAILED` (S2-5-9). CI also runs it as `mcpp-watch@polling` on Linux | | `mcpp-emit` | mcpp's `emit build-database --format json` (mcpp-community/mcpp#636), simulated by `mcppls-mock-mcpp` from `mcpp-mock.json`, which records what mcpp 2026.9.15.1 prints for the project (paths as `${root}` and `${env:HOME}`): a level 2 document without `ide.options`, one set per package plus `hello:test` and `mcpp:std`, each seeing every other set. The S1 library completes it to a level 3 model; a test imports the package's module across sets, and the workspace stays unchanged. The simulated fixtures cover what a real mcpp cannot be made to do on demand | | `mcpp-emit-package-std` | The same with `std` and `std.compat` provided by translation units of `mcpp:std` from a dependency package instead of by the toolchain's manifest, built in an mcpp std cache directory that does not exist yet | | `mcpp-emit-broken` | The same mcpp answering with an error: the status carries mcpp's own diagnostic, sources are scanned meanwhile, and nothing is configured in its place (the mock's `build` would leave a `compile_commands.json` that `workspace-unchanged` sees) | | `mcpp-emit-partial` | S2 0.3.0 (S2-3.4-12, S2-3.4-13; mcpp-community/mcpp#699, fix plan 2026-09-26 F10): the producer answers with every member it could plan and an `error` diagnostic whose `path` names the one it could not, exiting 1. The rest is used (the model is mcpp's, not scanned sources) and the status names the missing part (`producer-partial`) | +| `mcpp-rules-generated` | Plan 2026-09-27 Q1-2, Q1-3 (mcpp-community/mcpp#724): a rule's input (a Qt form) listed as a translation unit is left out, and a header the rule generates, named in mcpp's private planning directory where no action runs, is reported missing with a build action until a build writes it into the project's own target/, after which the model is loaded again by itself and the including file has semantics | +| `mcpp-emit-partial-download` | B-8 (2026-09-27 plan §3.4, §9.3 T9): the same partial shape, but the missing member's own diagnostic is `MCPP_OFFLINE_DOWNLOAD_REQUIRED` -- a dependency it needs is not installed and offline forbids fetching it. The status carries both `producer-partial` (the rest is used) and `producer-needs-download` (what to fetch), where before it only ever saw the former | | `mcpp-emit-unavailable` | A project whose `.xlings.json` asks for an mcpp that is not installed: xlings answers every mcpp command in its place and runs nothing, and the status carries xlings's explanation (`mcpp-no-database`) while sources are scanned | +| `mcpp-emit-needs-download` | The producer, run offline, needs a download: the status says what is missing, offers the terminal and lets a client offer to fetch it (`askOnline`, S3-4-16); `mcppls.describeOnline` then describes the project once with the network (the mock's `online` answer) and the model is mcpp's; nothing is written into the project | +| `mcpp-emit-provisioned` | Plan 2026-09-27 §9.2 rule 4: the offline description needs a download and nobody answers the offer; the person builds in their own terminal instead (the check writes `mcpp.lock`), and the server, seeing a watched build input change, asks the producer again offline and upgrades from scanned sources to mcpp's model by itself (the mock's `provisionedWhen`) | | `mcpp-emit-watch` | The inputs mcpp names in `watch` (S2 5, S2-5-1): writing one loads the model again, and an unchanged answer is recognized without rebuilding the index; when mcpp then fails the last model is kept, `degraded` with `model-stale` (S2-5-9), until it answers again. CI also runs it as `mcpp-emit-watch@polling`, with `--no-dynamic-watch` | +| `cmake-fetchcontent-offline` | Plan 2026-09-27 B-3 (D1, BD7 withdrawn): a CMake project with a FetchContent dependency not on the machine and no build directory. The private configure is disconnected from its first run, so it stops at `producer-needs-download` naming the dependency, with `askOnline`, instead of downloading; the sources are served meanwhile and nothing is written into the project | | `mingw` | A compile database for `x86_64-windows-gnu`: MinGW-w64 GCC semantics through `--sysroot` (P2) | | `cmake-clang` | CMake 3.28+ with `FILE_SET CXX_MODULES`, Clang and Ninja: `@modmap` expansion, partitions | | `cmake-clang-bdb` | The same project, but the prepare steps configure with CMake's experimental build database (`CMAKE_EXPERIMENTAL_EXPORT_BUILD_DATABASE`, CMake 4.4+ only) and build its `build_database.json` target: the model is read from that file instead of `compile_commands.json` (usable plan W4) | diff --git a/conformance/fixtures/build-discovery-off/mcpp-mock.json b/conformance/fixtures/build-discovery-off/mcpp-mock.json new file mode 100644 index 0000000..70b0ea1 --- /dev/null +++ b/conformance/fixtures/build-discovery-off/mcpp-mock.json @@ -0,0 +1,339 @@ +{ + "database": { + "ide": { + "generator": { + "name": "mcpp", + "version": "2026.9.15.1" + }, + "profile-version": "0.2.0", + "toolchains": { + "llvm-22.1.8-x86_64-unknown-linux-gnu": { + "config-files": [], + "driver": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "family": "clang", + "stdlib": { + "name": "libc++", + "version": "22.1.8" + }, + "target": "x86_64-unknown-linux-gnu", + "version": "22.1.8" + } + } + }, + "revision": 0, + "sets": [ + { + "baseline-arguments": [ + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "hello", + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "hello", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/detail.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o" + ], + "ide": { + "role": "module-partition-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o", + "private": false, + "provides": { + "hello.greet:detail": "" + }, + "requires": [ + "std" + ], + "source": "${root}/src/greet/detail.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/greet.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o", + "private": false, + "provides": { + "hello.greet": "" + }, + "requires": [ + "hello.greet:detail", + "std" + ], + "source": "${root}/src/greet/greet.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/main.cpp", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o" + ], + "ide": { + "role": "non-module" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o", + "private": false, + "provides": {}, + "requires": [ + "std", + "hello.greet" + ], + "source": "${root}/src/main.cpp", + "work-directory": "${root}" + } + ], + "visible-sets": [ + "hello:test", + "mcpp:std" + ] + }, + { + "baseline-arguments": [ + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "hello", + "ide": { + "configuration": "dev", + "kind": "test", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "hello:test", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/tests/greet_test.cpp", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet_test.o" + ], + "ide": { + "role": "non-module" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet_test.o", + "private": false, + "provides": {}, + "requires": [ + "std", + "hello.greet" + ], + "source": "${root}/tests/greet_test.cpp", + "work-directory": "${root}" + } + ], + "visible-sets": [ + "hello", + "mcpp:std" + ] + }, + { + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "mcpp:std", + "ide": { + "configuration": "dev", + "kind": "library", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "mcpp:std", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.o", + "private": false, + "provides": { + "std": "" + }, + "requires": [], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.compat.o", + "private": false, + "provides": { + "std.compat": "" + }, + "requires": [ + "std" + ], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + } + ], + "visible-sets": [ + "hello", + "hello:test" + ] + } + ], + "version": 1 + }, + "watch": [ + "${env:HOME}/.mcpp/config.toml", + "mcpp.lock", + "mcpp.toml", + "src/**/*.S", + "src/**/*.asm", + "src/**/*.c", + "src/**/*.cc", + "src/**/*.cpp", + "src/**/*.cppm", + "src/**/*.s", + "tests/**/*.cpp" + ], + "requireOffline": true +} diff --git a/conformance/fixtures/build-discovery-off/mcpp.toml b/conformance/fixtures/build-discovery-off/mcpp.toml new file mode 100644 index 0000000..79f6ceb --- /dev/null +++ b/conformance/fixtures/build-discovery-off/mcpp.toml @@ -0,0 +1,8 @@ +[package] +name = "hello" +version = "0.1.0" +description = "Conformance fixture: build-discovery-off" +license = "Apache-2.0" + +[toolchain] +default = "llvm@22.1.8" diff --git a/conformance/fixtures/build-discovery-off/scenario.json b/conformance/fixtures/build-discovery-off/scenario.json new file mode 100644 index 0000000..292625e --- /dev/null +++ b/conformance/fixtures/build-discovery-off/scenario.json @@ -0,0 +1,46 @@ +{ + "name": "build-discovery-off", + "description": "Plan 2026-09-27 B-7: mcppls.buildDiscovery = off. The workspace is an mcpp project whose producer would answer, but nothing is detected, read or run implicitly: the model is scanned from the sources (L4), the report says discovery was turned off, the producer is never started, and nothing is written into the project.", + "server-arguments": [ + "--mcpp", + "{runner-dir}/mcppls-mock-mcpp{exe}", + "--build-discovery", + "off" + ], + "checks": [ + { + "id": "B1-sources-only", + "kind": "status", + "source": "inferred", + "tier": 4 + }, + { + "id": "B2-said-so", + "kind": "report", + "expect": [ + { + "path": "/roots/0/project/notices/*/code", + "equals": "build-discovery-off" + }, + { + "path": "/roots/0/project/detected", + "equals": "inferred" + } + ] + }, + { + "id": "B3-module-features-still", + "kind": "definition", + "file": "src/main.cpp", + "at": [ + 1, + 9 + ], + "expect": "src/greet/greet.cppm" + }, + { + "id": "B4-nothing-was-written-into-the-project", + "kind": "workspace-unchanged" + } + ] +} diff --git a/conformance/fixtures/build-discovery-off/src/greet/detail.cppm b/conformance/fixtures/build-discovery-off/src/greet/detail.cppm new file mode 100644 index 0000000..56cb780 --- /dev/null +++ b/conformance/fixtures/build-discovery-off/src/greet/detail.cppm @@ -0,0 +1,5 @@ +export module hello.greet:detail; +import std; +export namespace hello::detail { + std::string prefix() { return "Hello, "; } +} diff --git a/conformance/fixtures/build-discovery-off/src/greet/greet.cppm b/conformance/fixtures/build-discovery-off/src/greet/greet.cppm new file mode 100644 index 0000000..21a6536 --- /dev/null +++ b/conformance/fixtures/build-discovery-off/src/greet/greet.cppm @@ -0,0 +1,7 @@ +export module hello.greet; +export import :detail; +import std; + +export namespace hello { + std::string greet(std::string_view who) { return detail::prefix() + std::string(who); } +} diff --git a/conformance/fixtures/build-discovery-off/src/main.cpp b/conformance/fixtures/build-discovery-off/src/main.cpp new file mode 100644 index 0000000..71169db --- /dev/null +++ b/conformance/fixtures/build-discovery-off/src/main.cpp @@ -0,0 +1,7 @@ +import std; +import hello.greet; + +int main(int argc, char* argv[]) { + std::println("{}", hello::greet("mcpp")); + return 0; +} diff --git a/conformance/fixtures/build-discovery-off/tests/greet_test.cpp b/conformance/fixtures/build-discovery-off/tests/greet_test.cpp new file mode 100644 index 0000000..c1632f7 --- /dev/null +++ b/conformance/fixtures/build-discovery-off/tests/greet_test.cpp @@ -0,0 +1,6 @@ +import std; +import hello.greet; + +int main() { + return hello::greet("test").empty() ? 1 : 0; +} diff --git a/conformance/fixtures/cmake-fetchcontent-offline/CMakeLists.txt b/conformance/fixtures/cmake-fetchcontent-offline/CMakeLists.txt new file mode 100644 index 0000000..0d71a35 --- /dev/null +++ b/conformance/fixtures/cmake-fetchcontent-offline/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(fetching LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# A dependency the build fetches: nothing of it is on the machine until someone lets it download. +include(FetchContent) +FetchContent_Declare(fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 11.0.2) +FetchContent_MakeAvailable(fmt) + +add_executable(app src/main.cpp) +target_link_libraries(app PRIVATE fmt::fmt) diff --git a/conformance/fixtures/cmake-fetchcontent-offline/scenario.json b/conformance/fixtures/cmake-fetchcontent-offline/scenario.json new file mode 100644 index 0000000..ed10afe --- /dev/null +++ b/conformance/fixtures/cmake-fetchcontent-offline/scenario.json @@ -0,0 +1,34 @@ +{ + "name": "cmake-fetchcontent-offline", + "description": "Plan 2026-09-27 B-3 (D1, BD7 withdrawn): a CMake project with no build directory, whose FetchContent dependency is not on the machine. The private configure is disconnected from the first run (FETCHCONTENT_FULLY_DISCONNECTED), so it stops at the dependency instead of downloading it: the status says a download is needed and lets a client offer it (askOnline), the sources are served meanwhile, and nothing is written into the project.", + "checks": [ + { + "id": "F1-a-download-is-needed-and-named", + "kind": "status", + "issue-code": "producer-needs-download", + "issue-message": "fmt", + "timeout": 120 + }, + { + "id": "F2-a-client-may-offer-it", + "kind": "status", + "issue-code": "producer-needs-download", + "issue-ask-online": true + }, + { + "id": "F3-served-meanwhile", + "kind": "hover-contains", + "file": "src/main.cpp", + "at": [ + 2, + 23 + ], + "expect": "answer", + "timeout": 90 + }, + { + "id": "F4-nothing-was-written-into-the-project", + "kind": "workspace-unchanged" + } + ] +} diff --git a/conformance/fixtures/cmake-fetchcontent-offline/src/main.cpp b/conformance/fixtures/cmake-fetchcontent-offline/src/main.cpp new file mode 100644 index 0000000..11076c9 --- /dev/null +++ b/conformance/fixtures/cmake-fetchcontent-offline/src/main.cpp @@ -0,0 +1,3 @@ +int answer() { return 42; } + +int main() { return answer() - 42; } diff --git a/conformance/fixtures/mcpp-emit-needs-download/mcpp-mock.json b/conformance/fixtures/mcpp-emit-needs-download/mcpp-mock.json index b450e76..aec96a6 100644 --- a/conformance/fixtures/mcpp-emit-needs-download/mcpp-mock.json +++ b/conformance/fixtures/mcpp-emit-needs-download/mcpp-mock.json @@ -6,5 +6,278 @@ "source": "mcpp", "message": "offline mode: dependency 'compat.eigen' v5.0.1 is not installed and cannot be downloaded\n run without --offline (or unset MCPP_OFFLINE) to fetch it" } - ] + ], + "online": { + "database": { + "ide": { + "generator": { + "name": "mcpp", + "version": "2026.9.15.1" + }, + "profile-version": "0.2.0", + "toolchains": { + "llvm-22.1.8-x86_64-unknown-linux-gnu": { + "config-files": [], + "driver": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "family": "clang", + "stdlib": { + "name": "libc++", + "version": "22.1.8" + }, + "target": "x86_64-unknown-linux-gnu", + "version": "22.1.8" + } + } + }, + "revision": 0, + "sets": [ + { + "baseline-arguments": [ + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "hello", + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "hello", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/detail.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o" + ], + "ide": { + "role": "module-partition-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o", + "private": false, + "provides": { + "hello.greet:detail": "" + }, + "requires": [ + "std" + ], + "source": "${root}/src/greet/detail.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/greet.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o", + "private": false, + "provides": { + "hello.greet": "" + }, + "requires": [ + "hello.greet:detail", + "std" + ], + "source": "${root}/src/greet/greet.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/main.cpp", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o" + ], + "ide": { + "role": "non-module" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o", + "private": false, + "provides": {}, + "requires": [ + "std", + "hello.greet" + ], + "source": "${root}/src/main.cpp", + "work-directory": "${root}" + } + ], + "visible-sets": [ + "mcpp:std" + ] + }, + { + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "mcpp:std", + "ide": { + "configuration": "dev", + "kind": "library", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "mcpp:std", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.o", + "private": false, + "provides": { + "std": "" + }, + "requires": [], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.compat.o", + "private": false, + "provides": { + "std.compat": "" + }, + "requires": [ + "std" + ], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + } + ], + "visible-sets": [ + "hello" + ] + } + ], + "version": 1 + }, + "watch": [ + "${env:HOME}/.mcpp/config.toml", + "mcpp.lock", + "mcpp.toml", + "src/**/*.S", + "src/**/*.asm", + "src/**/*.c", + "src/**/*.cc", + "src/**/*.cpp", + "src/**/*.cppm", + "src/**/*.s", + "tests/**/*.cpp" + ] + } } diff --git a/conformance/fixtures/mcpp-emit-needs-download/scenario.json b/conformance/fixtures/mcpp-emit-needs-download/scenario.json index a41436f..437c6a2 100644 --- a/conformance/fixtures/mcpp-emit-needs-download/scenario.json +++ b/conformance/fixtures/mcpp-emit-needs-download/scenario.json @@ -1,6 +1,6 @@ { "name": "mcpp-emit-needs-download", - "description": "Build description design 4.4: the producer ran offline, as an implicit run must, and cannot describe the build without fetching a dependency. That is not a failure of the producer and not a reason to go quiet: the status says what is missing and offers to run the build tool where the user's own proxy and credentials are, the sources are scanned meanwhile, and nothing is written into the project.", + "description": "Build description design 4.4: the producer ran offline, as an implicit run must, and cannot describe the build without fetching a dependency. That is not a failure of the producer and not a reason to go quiet: the status says what is missing and offers to run the build tool where the user's own proxy and credentials are, the sources are scanned meanwhile, and nothing is written into the project. Plan 2026-09-27 B-2: the issue lets a client offer to fetch what is missing (askOnline), and when the person accepts, mcppls.describeOnline describes the project once with the network: the model is mcpp's, and nothing is written into the project either.", "server-arguments": [ "--mcpp", "{runner-dir}/mcppls-mock-mcpp{exe}" @@ -25,10 +25,30 @@ "id": "D3-navigation-works-from-scanned-sources-meanwhile", "kind": "definition", "file": "src/main.cpp", - "at": [4, 31], + "at": [ + 4, + 31 + ], "expect": "src/greet/greet.cppm", "timeout": 60 }, + { + "id": "D5-a-client-may-offer-to-fetch-it", + "kind": "status", + "issue-code": "producer-needs-download", + "issue-ask-online": true + }, + { + "id": "D6-the-person-accepts", + "kind": "execute-command", + "command": "mcppls.describeOnline" + }, + { + "id": "D7-the-model-is-mcpp-s-once-fetched", + "kind": "status", + "source": "mcpp", + "timeout": 60 + }, { "id": "D4-nothing-was-written-into-the-project", "kind": "workspace-unchanged" diff --git a/conformance/fixtures/mcpp-emit-partial-download/mcpp-mock.json b/conformance/fixtures/mcpp-emit-partial-download/mcpp-mock.json new file mode 100644 index 0000000..76d1ea3 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-partial-download/mcpp-mock.json @@ -0,0 +1,348 @@ +{ + "database": { + "ide": { + "generator": { + "name": "mcpp", + "version": "2026.9.15.1" + }, + "profile-version": "0.2.0", + "toolchains": { + "llvm-22.1.8-x86_64-unknown-linux-gnu": { + "config-files": [], + "driver": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "family": "clang", + "stdlib": { + "name": "libc++", + "version": "22.1.8" + }, + "target": "x86_64-unknown-linux-gnu", + "version": "22.1.8" + } + } + }, + "revision": 0, + "sets": [ + { + "baseline-arguments": [ + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "hello", + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "hello", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/detail.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o" + ], + "ide": { + "role": "module-partition-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o", + "private": false, + "provides": { + "hello.greet:detail": "" + }, + "requires": [ + "std" + ], + "source": "${root}/src/greet/detail.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/greet.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o", + "private": false, + "provides": { + "hello.greet": "" + }, + "requires": [ + "hello.greet:detail", + "std" + ], + "source": "${root}/src/greet/greet.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/main.cpp", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o" + ], + "ide": { + "role": "non-module" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o", + "private": false, + "provides": {}, + "requires": [ + "std", + "hello.greet" + ], + "source": "${root}/src/main.cpp", + "work-directory": "${root}" + } + ], + "visible-sets": [ + "hello:test", + "mcpp:std" + ] + }, + { + "baseline-arguments": [ + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "hello", + "ide": { + "configuration": "dev", + "kind": "test", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "hello:test", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/tests/greet_test.cpp", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet_test.o" + ], + "ide": { + "role": "non-module" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet_test.o", + "private": false, + "provides": {}, + "requires": [ + "std", + "hello.greet" + ], + "source": "${root}/tests/greet_test.cpp", + "work-directory": "${root}" + } + ], + "visible-sets": [ + "hello", + "mcpp:std" + ] + }, + { + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "mcpp:std", + "ide": { + "configuration": "dev", + "kind": "library", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "mcpp:std", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.o", + "private": false, + "provides": { + "std": "" + }, + "requires": [], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.compat.o", + "private": false, + "provides": { + "std.compat": "" + }, + "requires": [ + "std" + ], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + } + ], + "visible-sets": [ + "hello", + "hello:test" + ] + } + ], + "version": 1 + }, + "diagnostics": [ + { + "code": "MCPP_OFFLINE_DOWNLOAD_REQUIRED", + "message": "offline mode: member 'updater' could not be planned: dependency 'xim:qt-base@6.11.1' is not installed and cannot be downloaded\n run without --offline (or unset MCPP_OFFLINE) to fetch it", + "path": "tools/updater/mcpp.toml", + "severity": "error", + "source": "mcpp" + } + ], + "requireOffline": true, + "watch": [ + "${env:HOME}/.mcpp/config.toml", + "mcpp.lock", + "mcpp.toml", + "src/**/*.S", + "src/**/*.asm", + "src/**/*.c", + "src/**/*.cc", + "src/**/*.cpp", + "src/**/*.cppm", + "src/**/*.s", + "tests/**/*.cpp" + ] +} diff --git a/conformance/fixtures/mcpp-emit-partial-download/mcpp.toml b/conformance/fixtures/mcpp-emit-partial-download/mcpp.toml new file mode 100644 index 0000000..a29d7a2 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-partial-download/mcpp.toml @@ -0,0 +1,8 @@ +[package] +name = "hello" +version = "0.1.0" +description = "Conformance fixture: mcpp-emit" +license = "Apache-2.0" + +[toolchain] +default = "llvm@22.1.8" diff --git a/conformance/fixtures/mcpp-emit-partial-download/scenario.json b/conformance/fixtures/mcpp-emit-partial-download/scenario.json new file mode 100644 index 0000000..cbd66eb --- /dev/null +++ b/conformance/fixtures/mcpp-emit-partial-download/scenario.json @@ -0,0 +1,47 @@ +{ + "name": "mcpp-emit-partial-download", + "description": "B-8 (2026-09-27 plan §3.4, §9.3 T9; mcpp-community/mcpp#699 plans members independently): a partial answer's missing member can itself be missing because it needs a download offline forbids -- MCPP_OFFLINE_DOWNLOAD_REQUIRED on one member's `error` diagnostic, the rest of the document `data`. Before this fix mcppls only ever reported `producer-partial` here and never entered the needs-download path (src/project/mcpp.cpp:312 looked at severity alone); now the workspace also gets `producer-needs-download`, so the same download prompt this producer offers when the whole run fails also fires when only one member does. The planned member's own semantics are used meanwhile, and nothing is written into the project.", + "server-arguments": [ + "--mcpp", + "{runner-dir}/mcppls-mock-mcpp{exe}" + ], + "checks": [ + { + "id": "PD1-the-rest-is-used-as-a-partial-model", + "kind": "status", + "source": "mcpp", + "level": 3, + "state": "degraded", + "issue-code": "producer-partial", + "issue-message": "tools/updater/mcpp.toml", + "timeout": 60 + }, + { + "id": "PD2-and-the-missing-member-also-asks-to-download", + "kind": "status", + "issue-code": "producer-needs-download", + "issue-message": "xim:qt-base@6.11.1" + }, + { + "id": "PD3-navigation-still-works-from-the-planned-member", + "kind": "definition", + "file": "src/main.cpp", + "at": [4, 31], + "expect": "src/greet/greet.cppm" + }, + { + "id": "PD4-in-the-report", + "kind": "report", + "expect": [ + { + "path": "/roots/0/project/source", + "equals": "mcpp" + } + ] + }, + { + "id": "PD5-nothing-was-written-into-the-project", + "kind": "workspace-unchanged" + } + ] +} diff --git a/conformance/fixtures/mcpp-emit-partial-download/src/greet/detail.cppm b/conformance/fixtures/mcpp-emit-partial-download/src/greet/detail.cppm new file mode 100644 index 0000000..56cb780 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-partial-download/src/greet/detail.cppm @@ -0,0 +1,5 @@ +export module hello.greet:detail; +import std; +export namespace hello::detail { + std::string prefix() { return "Hello, "; } +} diff --git a/conformance/fixtures/mcpp-emit-partial-download/src/greet/greet.cppm b/conformance/fixtures/mcpp-emit-partial-download/src/greet/greet.cppm new file mode 100644 index 0000000..21a6536 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-partial-download/src/greet/greet.cppm @@ -0,0 +1,7 @@ +export module hello.greet; +export import :detail; +import std; + +export namespace hello { + std::string greet(std::string_view who) { return detail::prefix() + std::string(who); } +} diff --git a/conformance/fixtures/mcpp-emit-partial-download/src/main.cpp b/conformance/fixtures/mcpp-emit-partial-download/src/main.cpp new file mode 100644 index 0000000..71169db --- /dev/null +++ b/conformance/fixtures/mcpp-emit-partial-download/src/main.cpp @@ -0,0 +1,7 @@ +import std; +import hello.greet; + +int main(int argc, char* argv[]) { + std::println("{}", hello::greet("mcpp")); + return 0; +} diff --git a/conformance/fixtures/mcpp-emit-partial-download/tests/greet_test.cpp b/conformance/fixtures/mcpp-emit-partial-download/tests/greet_test.cpp new file mode 100644 index 0000000..c1632f7 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-partial-download/tests/greet_test.cpp @@ -0,0 +1,6 @@ +import std; +import hello.greet; + +int main() { + return hello::greet("test").empty() ? 1 : 0; +} diff --git a/conformance/fixtures/mcpp-emit-provisioned/mcpp-mock.json b/conformance/fixtures/mcpp-emit-provisioned/mcpp-mock.json new file mode 100644 index 0000000..722a636 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-provisioned/mcpp-mock.json @@ -0,0 +1,284 @@ +{ + "diagnostics": [ + { + "code": "MCPP_OFFLINE_DOWNLOAD_REQUIRED", + "severity": "error", + "source": "mcpp", + "message": "offline mode: dependency 'compat.eigen' v5.0.1 is not installed and cannot be downloaded\n run without --offline (or unset MCPP_OFFLINE) to fetch it" + } + ], + "online": { + "database": { + "ide": { + "generator": { + "name": "mcpp", + "version": "2026.9.15.1" + }, + "profile-version": "0.2.0", + "toolchains": { + "llvm-22.1.8-x86_64-unknown-linux-gnu": { + "config-files": [], + "driver": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "family": "clang", + "stdlib": { + "name": "libc++", + "version": "22.1.8" + }, + "target": "x86_64-unknown-linux-gnu", + "version": "22.1.8" + } + } + }, + "revision": 0, + "sets": [ + { + "baseline-arguments": [ + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "hello", + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "hello", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/detail.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o" + ], + "ide": { + "role": "module-partition-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o", + "private": false, + "provides": { + "hello.greet:detail": "" + }, + "requires": [ + "std" + ], + "source": "${root}/src/greet/detail.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/greet.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o", + "private": false, + "provides": { + "hello.greet": "" + }, + "requires": [ + "hello.greet:detail", + "std" + ], + "source": "${root}/src/greet/greet.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/main.cpp", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o" + ], + "ide": { + "role": "non-module" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o", + "private": false, + "provides": {}, + "requires": [ + "std", + "hello.greet" + ], + "source": "${root}/src/main.cpp", + "work-directory": "${root}" + } + ], + "visible-sets": [ + "mcpp:std" + ] + }, + { + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "mcpp:std", + "ide": { + "configuration": "dev", + "kind": "library", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "mcpp:std", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.o", + "private": false, + "provides": { + "std": "" + }, + "requires": [], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.compat.o", + "private": false, + "provides": { + "std.compat": "" + }, + "requires": [ + "std" + ], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + } + ], + "visible-sets": [ + "hello" + ] + } + ], + "version": 1 + }, + "watch": [ + "${env:HOME}/.mcpp/config.toml", + "mcpp.lock", + "mcpp.toml", + "src/**/*.S", + "src/**/*.asm", + "src/**/*.c", + "src/**/*.cc", + "src/**/*.cpp", + "src/**/*.cppm", + "src/**/*.s", + "tests/**/*.cpp" + ] + }, + "provisionedWhen": "mcpp.lock" +} diff --git a/conformance/fixtures/mcpp-emit-provisioned/mcpp.toml b/conformance/fixtures/mcpp-emit-provisioned/mcpp.toml new file mode 100644 index 0000000..cd9aefc --- /dev/null +++ b/conformance/fixtures/mcpp-emit-provisioned/mcpp.toml @@ -0,0 +1,8 @@ +[package] +name = "hello" +version = "0.1.0" +description = "Conformance fixture: mcpp-emit-provisioned" +license = "Apache-2.0" + +[toolchain] +default = "llvm@22.1.8" diff --git a/conformance/fixtures/mcpp-emit-provisioned/scenario.json b/conformance/fixtures/mcpp-emit-provisioned/scenario.json new file mode 100644 index 0000000..91d9467 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-provisioned/scenario.json @@ -0,0 +1,32 @@ +{ + "name": "mcpp-emit-provisioned", + "description": "Plan 2026-09-27 \u00a79.2 rule 4: the offline description needs a download, the person answers no question and instead builds the project in their own terminal, which fetches what was missing and writes mcpp.lock. The server notices by itself -- a watched build input changed -- asks the producer again, still offline, and the project upgrades from its scanned sources to mcpp's model with nothing clicked.", + "server-arguments": [ + "--mcpp", + "{runner-dir}/mcppls-mock-mcpp{exe}" + ], + "checks": [ + { + "id": "P1-a-download-is-needed", + "kind": "status", + "source": "inferred", + "issue-code": "producer-needs-download" + }, + { + "id": "P2-nothing-was-written-into-the-project", + "kind": "workspace-unchanged" + }, + { + "id": "P3-the-person-builds-in-their-terminal", + "kind": "write-file", + "file": "mcpp.lock", + "content": "# written by the person's own mcpp build\nversion = 2\n" + }, + { + "id": "P4-the-project-upgrades-by-itself", + "kind": "status", + "source": "mcpp", + "timeout": 60 + } + ] +} diff --git a/conformance/fixtures/mcpp-emit-provisioned/src/greet/detail.cppm b/conformance/fixtures/mcpp-emit-provisioned/src/greet/detail.cppm new file mode 100644 index 0000000..56cb780 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-provisioned/src/greet/detail.cppm @@ -0,0 +1,5 @@ +export module hello.greet:detail; +import std; +export namespace hello::detail { + std::string prefix() { return "Hello, "; } +} diff --git a/conformance/fixtures/mcpp-emit-provisioned/src/greet/greet.cppm b/conformance/fixtures/mcpp-emit-provisioned/src/greet/greet.cppm new file mode 100644 index 0000000..21a6536 --- /dev/null +++ b/conformance/fixtures/mcpp-emit-provisioned/src/greet/greet.cppm @@ -0,0 +1,7 @@ +export module hello.greet; +export import :detail; +import std; + +export namespace hello { + std::string greet(std::string_view who) { return detail::prefix() + std::string(who); } +} diff --git a/conformance/fixtures/mcpp-emit-provisioned/src/main.cpp b/conformance/fixtures/mcpp-emit-provisioned/src/main.cpp new file mode 100644 index 0000000..71169db --- /dev/null +++ b/conformance/fixtures/mcpp-emit-provisioned/src/main.cpp @@ -0,0 +1,7 @@ +import std; +import hello.greet; + +int main(int argc, char* argv[]) { + std::println("{}", hello::greet("mcpp")); + return 0; +} diff --git a/conformance/fixtures/mcpp-partition-definition/mcpp.toml b/conformance/fixtures/mcpp-partition-definition/mcpp.toml new file mode 100644 index 0000000..1cede26 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/mcpp.toml @@ -0,0 +1,8 @@ +[package] +name = "hello" +version = "0.1.0" +description = "Conformance fixture: definitions in implementation units of a module split into partitions" +license = "Apache-2.0" + +[toolchain] +default = "llvm@22.1.8" diff --git a/conformance/fixtures/mcpp-partition-definition/scenario.json b/conformance/fixtures/mcpp-partition-definition/scenario.json new file mode 100644 index 0000000..a36ee13 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/scenario.json @@ -0,0 +1,108 @@ +{ + "name": "mcpp-partition-definition", + "description": "Plan 2026-09-27 N-7 (WA-CLANGD-008): clangd's background index compiles a module unit without building the modules it imports, so a definition in an implementation unit is indexed apart from its declaration -- here, functions and members declared in a partition (:state) with class-type parameters, defined in other units of the module, stayed on their declarations whatever the wait. With only the callers open, go-to-definition now reaches the implementation units, because the server builds them through clangd's foreground and closes them again. N-8: when clangd's index cannot link a declaration to its definition even with the unit open (a primary-interface function whose declaration has default arguments), the definition is found by name in the module's units; asked on a definition, the answer stays its declaration.", + "checks": [ + { + "id": "N1-a-free-function-with-a-partition-type", + "kind": "definition", + "file": "src/driver.cpp", + "at": [ + 6, + 4 + ], + "expect": "src/phase_a.cpp", + "timeout": 90 + }, + { + "id": "N2-another-unit-of-the-module", + "kind": "definition", + "file": "src/driver.cpp", + "at": [ + 7, + 4 + ], + "expect": "src/phase_b.cpp", + "timeout": 60 + }, + { + "id": "N3-a-parameter-from-another-partition", + "kind": "definition", + "file": "src/driver.cpp", + "at": [ + 11, + 26 + ], + "expect": "src/phase_b.cpp", + "timeout": 60 + }, + { + "id": "N4-a-std-parameter", + "kind": "definition", + "file": "src/driver.cpp", + "at": [ + 10, + 4 + ], + "expect": "src/phase_b.cpp", + "timeout": 60 + }, + { + "id": "N5-a-member-of-a-partition-class", + "kind": "definition", + "file": "src/driver.cpp", + "at": [ + 9, + 15 + ], + "expect": "src/phase_b.cpp", + "timeout": 60 + }, + { + "id": "N6-declaration-stays-the-declaration", + "kind": "declaration", + "file": "src/driver.cpp", + "at": [ + 6, + 4 + ], + "expect": "src/state.cppm" + }, + { + "id": "N7-from-an-importer-of-the-module", + "kind": "definition", + "file": "src/main.cpp", + "at": [ + 8, + 50 + ], + "expect": "src/driver.cpp", + "timeout": 60 + }, + { + "id": "N9-a-function-whose-declaration-has-defaults", + "kind": "definition", + "file": "src/main.cpp", + "at": [ + 9, + 30 + ], + "expect": "src/driver.cpp", + "timeout": 60 + }, + { + "id": "N10-from-the-definition-back-to-the-declaration", + "kind": "definition", + "file": "src/driver.cpp", + "at": [ + 15, + 0 + ], + "expect": "src/greet.cppm", + "timeout": 60 + }, + { + "id": "N8-nothing-was-written-into-the-project", + "kind": "workspace-unchanged" + } + ] +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/box.cpp b/conformance/fixtures/mcpp-partition-definition/src/box.cpp new file mode 100644 index 0000000..954469b --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/box.cpp @@ -0,0 +1,4 @@ +module hello.greet; +namespace hello { +int Box::g(int k) { return k; } +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/driver.cpp b/conformance/fixtures/mcpp-partition-definition/src/driver.cpp new file mode 100644 index 0000000..208b212 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/driver.cpp @@ -0,0 +1,21 @@ +module hello.greet; +import :state; +import std; +namespace hello { +int run_all() { + State s; + phase_a(s); + phase_b(s, 2); + Point pt{1, 2}; + Box bx; bx.f("y"); bx.g(1); + phase_s("x"); + return plain_c(s.n) + phase_p(pt); +} + +std::string +configure(bool verbose, + int level, + std::vector names) { + return verbose ? std::format("{} {}", level, names.size()) : std::string {}; +} +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/gmf.cpp b/conformance/fixtures/mcpp-partition-definition/src/gmf.cpp new file mode 100644 index 0000000..8da71ce --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/gmf.cpp @@ -0,0 +1,7 @@ +module; +#include +module hello.greet; +import std; +namespace hello { +std::string shout(std::string_view s) { std::string r{s}; for (auto& c : r) c = (char)std::toupper(c); return r; } +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/greet.cpp b/conformance/fixtures/mcpp-partition-definition/src/greet.cpp new file mode 100644 index 0000000..ac2e2f4 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/greet.cpp @@ -0,0 +1,10 @@ +module hello.greet; +import std; + +namespace hello { +std::string greet(std::string_view name) { + return std::format("hello, {}", name); +} +void Counter::bump() { ++n_; } +int Counter::value() const { return n_; } +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/greet.cppm b/conformance/fixtures/mcpp-partition-definition/src/greet.cppm new file mode 100644 index 0000000..3cf26b5 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/greet.cppm @@ -0,0 +1,22 @@ +export module hello.greet; +export import :types; +export import :state; +import std; + +export namespace hello { +std::string greet(std::string_view name); +int add(int a, int b); +int twice(int a); +int run_all(); +std::string configure(bool verbose, int level = 1, + std::vector names = {}); +std::string shout(std::string_view s); + +class Counter { +public: + void bump(); + int value() const; +private: + int n_ = 0; +}; +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/main.cpp b/conformance/fixtures/mcpp-partition-definition/src/main.cpp new file mode 100644 index 0000000..6b2970e --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/main.cpp @@ -0,0 +1,11 @@ +import std; +import hello.greet; + +int main() { + hello::Counter c; + c.bump(); + hello::Point p{1, 2}; + std::println("{} {} {} {} {}", hello::greet("mcpp"), hello::add(1, 2), c.value(), p.sum(), hello::shout("x")); + std::println("{} {}", hello::twice(3), hello::run_all()); + std::println("{}", hello::configure(true)); +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/math.cpp b/conformance/fixtures/mcpp-partition-definition/src/math.cpp new file mode 100644 index 0000000..e716d7c --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/math.cpp @@ -0,0 +1,4 @@ +module hello.greet; + +int hello::add(int a, int b) { return a + b; } +int hello::twice(int a) { return add(a, a); } diff --git a/conformance/fixtures/mcpp-partition-definition/src/phase_a.cpp b/conformance/fixtures/mcpp-partition-definition/src/phase_a.cpp new file mode 100644 index 0000000..af0f27b --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/phase_a.cpp @@ -0,0 +1,6 @@ +module hello.greet; +import std; +namespace hello { +int phase_a(State& s) { s.log += "a"; return ++s.n; } +int plain_c(int k) { return k * 7; } +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/phase_b.cpp b/conformance/fixtures/mcpp-partition-definition/src/phase_b.cpp new file mode 100644 index 0000000..906b626 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/phase_b.cpp @@ -0,0 +1,12 @@ +module hello.greet; +import :state; +namespace hello { +int phase_b(State& s, int k) { s.n += k; return s.n; } +} +namespace hello { +int phase_p(const Point& p) { return p.x; } +std::string phase_s(const std::string& t) { return t; } +} +namespace hello { +int Box::f(const std::string& s) { return (int)s.size(); } +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/state.cppm b/conformance/fixtures/mcpp-partition-definition/src/state.cppm new file mode 100644 index 0000000..e6855d9 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/state.cppm @@ -0,0 +1,12 @@ +export module hello.greet:state; +import std; +import :types; +export namespace hello { +struct State { int n = 0; std::string log; }; +int phase_a(State& s); +int phase_b(State& s, int k); +int plain_c(int k); +struct Box { int f(const std::string& s); int g(int k); }; +int phase_p(const Point& p); +std::string phase_s(const std::string& t); +} diff --git a/conformance/fixtures/mcpp-partition-definition/src/types.cpp b/conformance/fixtures/mcpp-partition-definition/src/types.cpp new file mode 100644 index 0000000..45be497 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/types.cpp @@ -0,0 +1,2 @@ +module hello.greet; +namespace hello { int Point::sum() const { return x + y; } } diff --git a/conformance/fixtures/mcpp-partition-definition/src/types.cppm b/conformance/fixtures/mcpp-partition-definition/src/types.cppm new file mode 100644 index 0000000..04e5a87 --- /dev/null +++ b/conformance/fixtures/mcpp-partition-definition/src/types.cppm @@ -0,0 +1,5 @@ +export module hello.greet:types; +import std; +export namespace hello { +struct Point { int x, y; int sum() const; }; +} diff --git a/conformance/fixtures/mcpp-rules-generated/mcpp-mock.json b/conformance/fixtures/mcpp-rules-generated/mcpp-mock.json new file mode 100644 index 0000000..cfc5118 --- /dev/null +++ b/conformance/fixtures/mcpp-rules-generated/mcpp-mock.json @@ -0,0 +1,309 @@ +{ + "database": { + "ide": { + "generator": { + "name": "mcpp", + "version": "2026.9.15.1" + }, + "profile-version": "0.2.0", + "toolchains": { + "llvm-22.1.8-x86_64-unknown-linux-gnu": { + "config-files": [], + "driver": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "family": "clang", + "stdlib": { + "name": "libc++", + "version": "22.1.8" + }, + "target": "x86_64-unknown-linux-gnu", + "version": "22.1.8" + } + } + }, + "revision": 0, + "sets": [ + { + "baseline-arguments": [ + "-I${env:HOME}/.mcpp/cache/build-database/7e57c0de7e57c0de/target/.build-mcpp/out/qt", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "hello", + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "hello", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-I${env:HOME}/.mcpp/cache/build-database/7e57c0de7e57c0de/target/.build-mcpp/out/qt", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/detail.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o" + ], + "ide": { + "role": "module-partition-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/detail.m.o", + "private": false, + "provides": { + "hello.greet:detail": "" + }, + "requires": [ + "std" + ], + "source": "${root}/src/greet/detail.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-I${env:HOME}/.mcpp/cache/build-database/7e57c0de7e57c0de/target/.build-mcpp/out/qt", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/greet/greet.cppm", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/greet.m.o", + "private": false, + "provides": { + "hello.greet": "" + }, + "requires": [ + "hello.greet:detail", + "std" + ], + "source": "${root}/src/greet/greet.cppm", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-I${env:HOME}/.mcpp/cache/build-database/7e57c0de7e57c0de/target/.build-mcpp/out/qt", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/src/main.cpp", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o" + ], + "ide": { + "role": "non-module" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/main.o", + "private": false, + "provides": {}, + "requires": [ + "std", + "hello.greet" + ], + "source": "${root}/src/main.cpp", + "work-directory": "${root}" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-I${env:HOME}/.mcpp/cache/build-database/7e57c0de7e57c0de/target/.build-mcpp/out/qt", + "-std=c++23", + "-fmodule-file=std=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.pcm", + "-fmodule-file=std.compat=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache/std.compat.pcm", + "-fprebuilt-module-path=${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/pcm.cache", + "-O0", + "-g", + "--no-default-config", + "-nostdinc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-c", + "${root}/ui/form.ui", + "-o", + "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/form.ui.o" + ], + "ide": { + "role": "non-module" + }, + "local-arguments": [], + "object": "${env:HOME}/.mcpp/cache/build-database/00ee0ef06d348b25/target/x86_64-linux-gnu/0ba60b7727c1b097/obj/form.ui.o", + "private": false, + "provides": {}, + "requires": [], + "source": "${root}/ui/form.ui", + "work-directory": "${root}" + } + ], + "visible-sets": [ + "mcpp:std" + ] + }, + { + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include" + ], + "family-name": "mcpp:std", + "ide": { + "configuration": "dev", + "kind": "library", + "toolchain": "llvm-22.1.8-x86_64-unknown-linux-gnu" + }, + "name": "mcpp:std", + "translation-units": [ + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "-o", + "pcm.cache/std.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.o", + "private": false, + "provides": { + "std": "" + }, + "requires": [], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + }, + { + "arguments": [ + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--no-default-config", + "-nostdinc++", + "-stdlib=libc++", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/include/x86_64-unknown-linux-gnu/c++/v1", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-glibc/2.44/include", + "-isystem${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-linux-headers/5.11.1/include", + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "ide": { + "role": "module-interface" + }, + "local-arguments": [ + "-fmodule-file=std=${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.pcm", + "--precompile", + "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "-o", + "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/pcm.cache/std.compat.pcm" + ], + "object": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73/std.compat.o", + "private": false, + "provides": { + "std.compat": "" + }, + "requires": [ + "std" + ], + "source": "${env:HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/share/libc++/v1/std.compat.cppm", + "work-directory": "${env:HOME}/.mcpp/build-cache/v1/std/c6af27b46156bf73" + } + ], + "visible-sets": [ + "hello" + ] + } + ], + "version": 1 + }, + "watch": [ + "${env:HOME}/.mcpp/config.toml", + "mcpp.lock", + "mcpp.toml", + "src/**/*.S", + "src/**/*.asm", + "src/**/*.c", + "src/**/*.cc", + "src/**/*.cpp", + "src/**/*.cppm", + "src/**/*.s", + "tests/**/*.cpp" + ] +} diff --git a/conformance/fixtures/mcpp-rules-generated/mcpp.toml b/conformance/fixtures/mcpp-rules-generated/mcpp.toml new file mode 100644 index 0000000..5ed3706 --- /dev/null +++ b/conformance/fixtures/mcpp-rules-generated/mcpp.toml @@ -0,0 +1,8 @@ +[package] +name = "hello" +version = "0.1.0" +description = "Conformance fixture: mcpp-rules-generated" +license = "Apache-2.0" + +[toolchain] +default = "llvm@22.1.8" diff --git a/conformance/fixtures/mcpp-rules-generated/scenario.json b/conformance/fixtures/mcpp-rules-generated/scenario.json new file mode 100644 index 0000000..40ee2c1 --- /dev/null +++ b/conformance/fixtures/mcpp-rules-generated/scenario.json @@ -0,0 +1,54 @@ +{ + "name": "mcpp-rules-generated", + "description": "Plan 2026-09-27 Q1-2, Q1-3 (mcpp-community/mcpp#724): a build rule's input (a Qt form) is listed as a translation unit with a compiler command, and the header the rule generates from it is named in mcpp's private planning directory, where no action ever runs. The form is left out, the status says which generated files are missing and offers to build in a terminal; once a build writes the header into the project's own target/, the model is loaded again by itself and main.cpp, which includes it, has semantics.", + "server-arguments": [ + "--mcpp", + "{runner-dir}/mcppls-mock-mcpp{exe}" + ], + "checks": [ + { + "id": "G1-the-generated-header-is-missing-and-said-so", + "kind": "status", + "source": "mcpp", + "issue-code": "generated-files-missing", + "issue-command": "mcppls.runBuildToolInTerminal" + }, + { + "id": "G2-the-form-is-no-translation-unit", + "kind": "report", + "expect": [ + { + "path": "/roots/0/project/notices/*/code", + "equals": "not-compiled-inputs" + } + ] + }, + { + "id": "G3-nothing-was-written-into-the-project", + "kind": "workspace-unchanged" + }, + { + "id": "G4-a-build-writes-the-header", + "kind": "write-file", + "file": "target/.build-mcpp/out/qt/ui_form.h", + "content": "#pragma once\nnamespace Ui { struct Form { int rows = 3; }; }\n" + }, + { + "id": "G6-into-the-generated-header", + "kind": "definition", + "file": "src/main.cpp", + "at": [ + 6, + 9 + ], + "expect": "target/.build-mcpp/out/qt/ui_form.h", + "timeout": 60 + }, + { + "id": "G5-main-cpp-has-semantics-then", + "kind": "diagnostics-empty", + "file": "src/main.cpp", + "timeout": 90 + } + ] +} diff --git a/conformance/fixtures/mcpp-rules-generated/src/greet/detail.cppm b/conformance/fixtures/mcpp-rules-generated/src/greet/detail.cppm new file mode 100644 index 0000000..56cb780 --- /dev/null +++ b/conformance/fixtures/mcpp-rules-generated/src/greet/detail.cppm @@ -0,0 +1,5 @@ +export module hello.greet:detail; +import std; +export namespace hello::detail { + std::string prefix() { return "Hello, "; } +} diff --git a/conformance/fixtures/mcpp-rules-generated/src/greet/greet.cppm b/conformance/fixtures/mcpp-rules-generated/src/greet/greet.cppm new file mode 100644 index 0000000..21a6536 --- /dev/null +++ b/conformance/fixtures/mcpp-rules-generated/src/greet/greet.cppm @@ -0,0 +1,7 @@ +export module hello.greet; +export import :detail; +import std; + +export namespace hello { + std::string greet(std::string_view who) { return detail::prefix() + std::string(who); } +} diff --git a/conformance/fixtures/mcpp-rules-generated/src/main.cpp b/conformance/fixtures/mcpp-rules-generated/src/main.cpp new file mode 100644 index 0000000..3757d74 --- /dev/null +++ b/conformance/fixtures/mcpp-rules-generated/src/main.cpp @@ -0,0 +1,10 @@ +#include "ui_form.h" + +import std; +import hello.greet; + +int main(int argc, char* argv[]) { + Ui::Form form; + std::println("{} {}", hello::greet("mcpp"), form.rows); + return 0; +} diff --git a/conformance/fixtures/mcpp-rules-generated/ui/form.ui b/conformance/fixtures/mcpp-rules-generated/ui/form.ui new file mode 100644 index 0000000..e87ac94 --- /dev/null +++ b/conformance/fixtures/mcpp-rules-generated/ui/form.ui @@ -0,0 +1,5 @@ + + + Form + + diff --git a/conformance/traceability.json b/conformance/traceability.json index bb99848..c13ccdf 100644 --- a/conformance/traceability.json +++ b/conformance/traceability.json @@ -1008,6 +1008,57 @@ "contains": "constexpr std::chrono::milliseconds DEGRADED_HOLD { 3000 };" } ], + "S3-4-16": [ + { + "check": "mcpp-emit-needs-download/D5-a-client-may-offer-to-fetch-it" + }, + { + "check": "mcpp-emit-needs-download/D6-the-person-accepts" + }, + { + "check": "mcpp-emit-needs-download/D7-the-model-is-mcpp-s-once-fetched" + } + ], + "S3-4-17": [ + { + "check": "mcpp-emit-needs-download/D3-navigation-works-from-scanned-sources-meanwhile" + }, + { + "check": "mcpp-emit-provisioned/P4-the-project-upgrades-by-itself" + } + ], + "S3-4-18": [ + { + "script": "src/orchestrator/workspace.cpp", + "contains": "const bool online { options.buildTool == \"online\" || onlineOnce };" + }, + { + "script": "src/orchestrator/workspace.cpp", + "contains": "onlineOnce = false;" + } + ], + "S3-4-19": [ + { + "script": "editors/vscode/src/downloadPrompt.ts", + "contains": "void askOnce(" + }, + { + "script": "editors/vscode/src/status.ts", + "contains": "queueMicrotask(() => {" + } + ], + "S3-4-20": [ + { + "script": "editors/vscode/src/downloadAsk.ts", + "contains": "return issue !== undefined && issue.askOnline === true && !never && !open && !askedAbout.includes(issue.message);" + } + ], + "S3-4-21": [ + { + "script": "editors/vscode/src/downloadPrompt.ts", + "contains": "if (!this.pending.has(root)) {" + } + ], "S3-5.5-1": [ { "check": "module-faults/F0-stand-in" diff --git a/docs/20-projects.md b/docs/20-projects.md index 02a8bad..5240a24 100644 --- a/docs/20-projects.md +++ b/docs/20-projects.md @@ -5,7 +5,8 @@ bar. This is what "finding it" means for each kind of project, and what you get The status bar's `L1`..`L4` is the *tier*: how the project was described — L1 a build database (mcpp's `emit build-database`, or one of your own), L2 CMake's own database, L3 a bare `compile_commands.json` -(including the one an mcpp too old to emit a build database leaves), L4 sources only (an untrusted +(including the one an mcpp too old to emit a build database leaves, and the ones xmake and meson +write), L4 sources only (an untrusted workspace is always L4, whatever else is on disk). It is not the same number as the `level` `mcppls check` and `cxxModules/status` also carry, which is [S1](specs/s1-build-database.md)'s own 1..4 for how completely a database's *document* is structured; a hand-written level-3 database and an @@ -30,8 +31,24 @@ Two things are worth knowing: - **The run is offline.** A build description is a question about the project, not an errand, so the server asks it with `MCPP_OFFLINE` set. If the project's dependencies are not on the machine - yet, mcpp says so and the status bar offers to run the build tool in your terminal — where your - proxy and credentials are. See [30-settings.md](30-settings.md) for `mcppls.buildTool`. + yet, mcpp says so, and nothing waits for you to decide anything: + - the project is served from its sources at once (L4), and whatever mcpp could describe is used; + - a notification in the corner offers **Download and Continue** (the build tool may reach the + network, this once), **Run in Terminal** (where your proxy and credentials are), or **Don't Ask + Again**. You can leave it unanswered forever; it is asked once per workspace and set of missing + things; + - the description is asked again, offline, after 30 s, 1 and 2 minutes and then every 5, and at once + when `mcpp.toml` or `mcpp.lock` changes — so if you build in your own terminal instead, the + project upgrades by itself and the question no longer applies. + + See [30-settings.md](30-settings.md) for `mcppls.buildTool` and `mcppls.buildDiscovery.askBeforeDownload`. +- **What a build rule generates.** A rule package (`mcpp:plugins`' `rules-qt`, say) turns `.ui`, + `.qrc` and `.ts` files into headers and sources when the project builds. mcpp describes the build + without running those steps, so a form's `ui_*.h` does not exist yet in what it describes. mcppls + leaves the rule's inputs out (they are not C++), reads the generated files from your project's own + `target/` when a build has written them, and otherwise says which are missing, with **Build in + Terminal**; once a build writes them, the files that include them get their semantics without a + restart. - **An older mcpp** without `emit build-database` is not the end of it: if a newer mcpp is installed elsewhere on the machine (the xlings package store, mcpp's own registry store), mcppls asks *that* one instead, read-only and offline the same way, only to describe the project — the project still @@ -52,10 +69,43 @@ If the build directory has a `build_database.json` (CMake 4.4+ with Ninja, `FILE mcppls reads it. Otherwise it reads `compile_commands.json` and expands the `@modmap` files the generator wrote. +A build directory is looked for in `build*/`, `out/build/*`, `cmake-build-*`, and where the first +configure preset of `CMakePresets.json` (or `CMakeUserPresets.json`) puts its `binaryDir`. + With no build directory at all and a trusted workspace, mcppls configures one **of its own**, under -its cache directory — never in your project. The first such configure may download what the project -declares (`FetchContent`, `ExternalProject`); every later one adds -`-DFETCHCONTENT_UPDATES_DISCONNECTED=ON`. +its cache directory — never in your project — following that preset's generator, toolchain file and +cache variables, so it describes the build you would get. That configure is **disconnected** +(`-DFETCHCONTENT_FULLY_DISCONNECTED=ON`), the first time too: a `FetchContent` dependency that is not +on the machine stops it, and you get the same non-blocking offer as for mcpp above (**Download and +Continue** configures once with the network, in that private directory). + +## xmake + +`xmake.lua` makes an xmake project. A `compile_commands.json` already at the root or in `.vscode/` +(where xmake's VS Code plugin writes one) is read as it is. Otherwise mcppls asks xmake for one with +its own command, `xmake project -k compile_commands`, which compiles nothing — but it configures and +scans modules, so mcppls points xmake's configuration and build directories at its cache +(`XMAKE_CONFIGDIR`, `--builddir`) and your project stays untouched. It runs offline +(`--policies=package.fetch_only,network.mode:private`): a package that is not installed stops it with +the same offer as above. The first description takes a few seconds (about 6–8 s measured, most of it +xmake detecting the toolchain); the project is served from its sources meanwhile. Module roles come +from scanning, so an xmake project is L3. + +## meson + +`meson.build` makes a meson project. An existing build directory (`builddir/`, `build/`, or any +directory with `meson-private/`) is read for its `compile_commands.json`. Otherwise mcppls runs +`meson setup` into its cache with `--wrap-mode=nodownload`; a subproject that would have to be +downloaded stops it with the same offer. L3, like xmake. + +## Turning discovery off + +`mcppls.buildDiscovery = off` makes mcppls detect no build system at all: nothing is read or run +implicitly, and only a database you name with `mcppls.database` is used, else the sources are scanned +(L4). `mcppls.buildDiscovery.providers` leaves out single build systems instead — for example, only +ever read an existing CMake build directory and never run xmake. `mcppls.buildTool = off` is the +narrower switch: build systems are still detected and their existing output read, only never run. See +[30-settings.md](30-settings.md). ## compile_commands.json @@ -109,4 +159,6 @@ else. The semantic kit provides the semantics and the status says why. | A compiler and standard library | The model came from your build; that toolchain's `std` is in use | | A semantic kit | No usable compiler was found, or the workspace is untrusted | | "may be stale" | The build tool could not answer this time; the last model that loaded is still in use | -| "needs a download" | Planning offline stopped at something not on the machine; there is an action to fix it | +| "needs a download" | Planning offline stopped at something not on the machine; the project is served from its sources meanwhile, and there are actions to fix it (or build in your terminal: it upgrades by itself) | +| "files the build generates … do not exist yet" | A rule's output (a Qt form's header, say) is not built yet; build once and the files that include it get their semantics | +| "implementation unit(s) cannot be read" | An implementation unit does not build (a missing header, usually), so go-to-definition cannot reach the definitions in it | diff --git a/docs/30-settings.md b/docs/30-settings.md index e9fcda4..2c24217 100644 --- a/docs/30-settings.md +++ b/docs/30-settings.md @@ -1,19 +1,91 @@ # Settings and the command line -## VS Code settings - -| Setting | Values | What it does | -|---|---|---| -| `mcppls.buildTool` | `offline` (default), `online`, `off` | How the project's build tool may be run. `offline`: run it without the network — if it then cannot describe the build without downloading something, the status says what is missing and offers to run it in your terminal. `online`: let it reach the network, with ten minutes instead of one. `off`: never run it; use the cached description or scanned sources | -| `mcppls.toolEnvironment` | `auto` (default), `editor` | Which environment build tools are started in. `auto` reads your login shell's environment once, in the background, on POSIX — an editor started from a desktop entry or a Dock icon carries none of your shell configuration, so without this the build tool it finds may not be the one your terminal finds. On Windows the editor's environment already matches the terminal's. `editor` always uses the editor process's environment | -| `mcppls.compiler` | a driver path, or `kit` | Use this compiler for module semantics instead of what was detected. `kit` forces the bundled semantic kit | -| `mcppls.semanticKit` | `auto` (default), `off` | Whether the bundled kit may be used at all | -| `mcppls.engine` | `clangd` (default), `none` | The core engine. mcppls's own module engine runs either way; `none` means module features only | -| `mcppls.ai.enabled` | `false` (default) | Whether the model-backed half of change review may be used. Off means the server makes no model calls | -| `mcppls.detectConflicts` | `true` (default) | Offer once to turn off another C++ extension's language features in this workspace, and say so when one becomes active later | -| `mcppls.semanticTokens.modules` | `true` (default) | Color `import`, `module`, `export` and module names from the server's semantic tokens. Off: only the grammar's colors | -| `mcppls.completion.triggerOnSpace` | `true` (default) | Show the module list as soon as a space is typed after `import` or `export import`. A space anywhere else never reaches the server. Other editors ask for the same with `initializationOptions.completion.triggerOnSpace` | -| `mcppls.trace.server` | `off` (default), `messages`, `verbose` | Log the LSP traffic to the C++ Modules output channel (at Trace level); `verbose` adds the server's debug log (at Debug level). Set the channel's log level to see them | +Every configurable behaviour of mcppls has exactly one definition: a row of the registry in +`src/config/settings.cppm`. The table below -- the VS Code settings, the +command-line options, and the environment variables that configure something -- is generated from +that registry (`mcppls settings --format markdown`), and `tests/test_settings.cpp` holds it, the +zh-CN mirror and `editors/vscode/package.json` to the registry so the three cannot drift apart. + +**Precedence.** The command line beats `initializationOptions` beats the default; a later +`workspace/didChangeConfiguration` updates a value `initializationOptions` (or an earlier +`didChangeConfiguration`) set, but never one the command line set. A value outside its own +vocabulary (an unknown enumeration member, say) is never applied -- it falls back to the default +and is recorded as a problem (`mcppls report`'s `settings.problems`, and `mcppls settings` itself +carries none since it only ever prints the registry). + +**Applies.** How a change to a setting already running takes effect: `restart` needs mcppls +restarted (the VS Code extension already does this for the settings below that need it); +`reload` only reloads the project's model, which happens without a restart; `immediately` needs +neither -- the next time it is read is the next time it matters. + +**A renamed setting keeps working.** A setting the registry gives an alias for is still accepted +under its earlier name (dotted key or `initializationOptions`/`didChangeConfiguration` form alike); +none of the settings below have been renamed yet, so none carry one today. + +`initializationOptions` and `didChangeConfiguration` both accept a nested object +(`{"semanticTokens": {"modules": false}}`) or a dotted key (`{"semanticTokens.modules": false}`), +either wrapped in a top-level `mcppls` object or not. + + +### Project and build tools + +| Setting | Values | Default | Command line | Applies | What it does | +|---|---|---|---|---|---| +| `mcppls.buildTool` | `offline`, `online`, `off` | `offline` | `--build-tool` | reload | How the project's build tool may be run. `offline`: run it without the network -- if it then cannot describe the build without downloading something, the status says what is missing and offers to run it in your terminal. `online`: let it reach the network, with ten minutes instead of one. `off`: never run it; the build system is still detected and its own generated files are still read (see `buildDiscovery` for turning that off too). | +| `mcppls.toolEnvironment` | `auto`, `editor` | `auto` | `--tool-environment` | restart | Which environment build tools are started in. `auto` reads your login shell's environment once, in the background, on POSIX -- an editor started from a desktop entry or a Dock icon carries none of your shell configuration, so without this the build tool it finds may not be the one your terminal finds. On Windows the editor's environment already matches the terminal's. `editor` always uses the editor process's environment. | +| `mcppls.producerTimeout` | a non-negative number of seconds | `0` | `--producer-timeout` | reload | How long a build tool may take to describe the project. `0`, the default, uses the design's own bound (a minute offline, ten minutes once `buildTool` is `online`); set it to watch that bound work, or longer for a genuinely slower build. | +| `mcppls.untrusted` | `true`, `false` | `false` | `--untrusted` | restart | Run no build tool and no compiler; an untrusted workspace is also read as though `buildDiscovery` were `off`. | +| `mcppls.discoverCompilers` | `true`, `false` | `true` | `--no-discover` | reload | Look for a compiler on the machine for a source the build description does not cover. Off: such a source uses the semantic kit instead. | +| `mcppls.buildDiscovery` | `auto`, `off` | `auto` | `--build-discovery` | reload | Whether the project's build system is detected at all. `off`: nothing is read or run implicitly -- only an explicitly configured `database`, else sources are scanned. `buildTool` still governs whether a detected build tool may be *run*; this governs whether it is looked for in the first place. | +| `mcppls.buildDiscovery.providers` | `mcpp`, `cmake`, `xmake`, `meson`, `compile-commands` (comma-separated) | `mcpp`, `cmake`, `xmake`, `meson`, `compile-commands` | `--build-discovery-providers` | reload | Which build system providers `buildDiscovery` may use; leave one out to stop mcppls from detecting it (for example, to use only a CMake build directory that already exists and never let xmake run). | +| `mcppls.buildDiscovery.askBeforeDownload` | `true`, `false` | `true` | — | immediately | When the build tool needs a download to finish describing the project, a client may offer to fetch it. Off: the status says a download is needed, and nothing asks. | + +### Engines + +| Setting | Values | Default | Command line | Applies | What it does | +|---|---|---|---|---|---| +| `mcppls.engine` | `clangd`, `none` | `clangd` | `--engine` | restart | The core semantic engine. mcppls's own module engine always runs beside it; `none` means module-level features only. | +| `mcppls.compiler` | a string | *(empty)* | `--compiler` | reload | Use this compiler for module semantics instead of what was detected: an absolute path, a name on `PATH`, or `kit` to force the bundled semantic kit. Empty means discovered automatically. | +| `mcppls.semanticKit` | `auto`, `off` | `auto` | `--semantic-kit` | reload | Whether the bundled standard library kit may be used at all: `auto`, when no compiler is found; `off`, never (without a compiler, only module-level features remain). | +| `mcppls.requestTimeout` | a non-negative number of seconds | `60` | `--request-timeout` | restart | How long an engine request may take before it is answered without the engine. A request a person waits for (hover, definition, completion and the like) waits at most 30s in all, including while clangd starts or prepares its modules, and is then answered by mcppls's own engine. | +| `MCPPLS_ENGINE_ARGUMENTS` | a string | *(empty)* | — | restart | Extra arguments appended to clangd's own command line, for troubleshooting (e.g. `-j=8 --background-index-priority=background`). | + +### Editor experience + +| Setting | Values | Default | Command line | Applies | What it does | +|---|---|---|---|---|---| +| `mcppls.semanticTokens.modules` | `true`, `false` | `true` | — | restart | Color `import`, `module`, `export` and module names from the server's semantic tokens. Off: only the grammar's colors. | +| `mcppls.semanticTokens.moduleType` | `true`, `false` | `false` | — | restart | A client declares it knows the custom `module` semantic token type and the `partition` modifier; off for every client but this one, since none else advertises it. Not a package.json setting: VS Code's own extension always declares it, fixed, because it contributes that token type itself. | +| `mcppls.completion.triggerOnSpace` | `true`, `false` | `true` | — | restart | Show the module list as soon as a space is typed after `import` or `export import`. A space anywhere else never reaches the server. A client that says nothing gets this only when it identifies itself as VS Code or a fork of it; every other client opts in with `initializationOptions.completion.triggerOnSpace: true`. | +| `mcppls.index.primeImplementationUnits` | `auto`, `off` | `auto` | `--prime-implementation-units` | restart | Build a module's implementation units in clangd in the background, a few at a time, so go-to-definition reaches a definition that only an implementation unit has, before that file was ever opened. clangd's own background index cannot see a module unit's imports (WA-CLANGD-008). `off`: only the units a definition request searches, and the files you open, are indexed for this. | +| `mcppls.detectConflicts` | `true`, `false` | `true` | — | immediately | Offer once to turn off another C++ extension's language features in this workspace, and say so when one becomes active later. VS Code only: no other client arbitrates between language servers. | + +### Diagnostics and logging + +| Setting | Values | Default | Command line | Applies | What it does | +|---|---|---|---|---|---| +| `mcppls.logLevel` | `debug`, `info`, `warning`, `error` | `info` | `--log-level` | restart | The server's own log level. | +| `mcppls.disableWorkaround` | `WA-CLANGD-` (repeatable) | *(none)* | `--disable-workaround` (repeatable) | restart | Turn off a registered clangd workaround (`WA-CLANGD-`; the register of upstream defects is issue #24), to see whether it is still needed; repeatable. `mcppls report` lists every registered workaround under `engines[].details.workarounds`. | +| `mcppls.trace.server` | `off`, `messages`, `verbose` | `off` | — | immediately | Log the LSP traffic to the C++ Modules output channel (at Trace level); `verbose` adds the server's debug log (at Debug level, by also passing `--log-level debug`). Set the channel's own log level to see them. | +| `MCPPLS_LOG_LEVEL` | `debug`, `info`, `warning`, `error` | *(empty)* | — | restart | Overrides the log level the VS Code extension starts the server with, ahead of `trace.server`. | + +### AI review + +| Setting | Values | Default | Command line | Applies | What it does | +|---|---|---|---|---|---| +| `mcppls.ai.enabled` | `true`, `false` | `false` | — | immediately | Show the AI-era features: Review Changes reviews the workspace's changes against `HEAD` with mcppls's rules and shows the findings, with their evidence, as problems. Nothing is sent to a model unless this is on and a model source is separately configured. | + +### Paths + +| Setting | Values | Default | Command line | Applies | What it does | +|---|---|---|---|---|---| +| `mcppls.database` | a path | *(empty)* | `--database` | reload | A workspace's own S1 build database, relative to its root, used instead of detecting one. | +| `mcppls.mcpp` | a path | *(empty)* | `--mcpp` | reload | The `mcpp` executable for mcpp projects; empty means found on `PATH`. | +| `mcppls.payload` | a path | *(empty)* | `--payload` | restart | Payload directory with clangd and the semantic kit; overridden per-file by `clangd` and `kit` below. | +| `mcppls.clangd` | a path | *(empty)* | `--clangd` | restart | clangd executable, overriding the one the payload carries. | +| `mcppls.kit` | a path | *(empty)* | `--kit` | restart | Semantic kit directory, overriding the one the payload carries. | +| `MCPPLS_CACHE_DIR` | a path | *(empty)* | — | restart | Overrides the whole cache directory mcppls otherwise picks under the user's cache home (workspace models, toolchain probes, logs, diagnostic bundles). | + ## Commands @@ -38,22 +110,13 @@ mcppls daemon run|start|status|stop the shared workspace daemon mcppls check the model, the profile, module diagnostics, then clangd --check mcppls report [--root DIR] [--settle SECONDS] what a bug report needs, as JSON mcppls model [--root DIR] [--export s1|compile-commands|engine] +mcppls settings [--format markdown|json] [--lang en|zh-CN] the table above, or its machine form mcppls print-environment prints this process's environment between two markers mcppls version ``` -Options that apply to every subcommand: - -| Option | Default | What it does | -|---|---|---| -| `--build-tool offline\|online\|off` | `offline` | The command-line spelling of `mcppls.buildTool` | -| `--tool-environment auto\|editor` | `auto` | The command-line spelling of `mcppls.toolEnvironment` | -| `--producer-timeout SECONDS` | 60, or 600 when online | How long the build tool may take to describe the project. Longer for a genuinely slow build; shorter to watch the bound work | -| `--request-timeout SECONDS` | 60 | How long an engine request may take before it is answered without the engine. A request a person waits for (hover, definition, completion and the like) waits at most 30 s in all, including while clangd starts or prepares its modules, and is then answered by mcppls's own engine | -| `--untrusted` | — | Run no build tool and no compiler | -| `--no-discover` | — | Do not look for compilers; loose sources use the semantic kit | -| `--log-level debug\|info\|warning\|error` | `info` | | -| `--disable-workaround WA-CLANGD-` | — | Turn off one of the registered workarounds for clangd's defects (repeatable), to see whether it is still needed; `mcppls report` lists them under `engines[].details.workarounds` | +Every global option above (`mcppls --help`) is a registered setting's command-line spelling; see the +table above for what each does, its default, and how a change takes effect. `print-environment` exists for the server itself: it is what the login shell is asked to run when `mcppls.toolEnvironment` is `auto`. diff --git a/docs/specs/CHANGELOG.md b/docs/specs/CHANGELOG.md index ffcde2c..d168398 100644 --- a/docs/specs/CHANGELOG.md +++ b/docs/specs/CHANGELOG.md @@ -2,6 +2,17 @@ Changes to the specifications in this directory. Each specification is versioned independently. +## 2026-09-27 — S3: a download the client may offer, and two more build systems + +`CxxModulesIssue` gains `askOnline` (optional): on a `producer-needs-download` issue, the server will +describe the project once with the network when the client asks with `workspace/executeCommand` +`mcppls.describeOnline` (S3-4-16). Until then, and while the download runs, the server serves the root +from what it has and reaches no network on its own (S3-4-17, S3-4-18); a client that offers the +download never blocks on the question, asks at most once per root and set of missing things, and +ignores an answer that comes after the need is gone (S3-4-19 to S3-4-21). `project.source` gains +`xmake` and `meson` (tier 3), the issue codes `producer-online`, `generated-files-missing` and +`implementation-unreadable` are named, and `cxxModules/report` carries `settings`. All additive: protocol version stays 1. + ## 2026-09-26 — S3: the standard a profile reads with `SemanticProfile` gains `standard` (optional): the C++ standard the context's module units are read diff --git a/docs/specs/s3-lsp-extensions.md b/docs/specs/s3-lsp-extensions.md index 931488b..77f935a 100644 --- a/docs/specs/s3-lsp-extensions.md +++ b/docs/specs/s3-lsp-extensions.md @@ -63,10 +63,11 @@ interface CxxModulesStatusParams { state: "starting" | "loading" | "preparing" | "ready" | "degraded" | "error"; project: { root: DocumentUri; // the workspace folder's URI exactly as the client sent it - source: "mcpp" | "cmake" | "build-database" | "compile-commands" | "inferred"; + source: "mcpp" | "cmake" | "xmake" | "meson" | "build-database" | "compile-commands" | "inferred"; level?: 1 | 2 | 3 | 4; // S1 conformance level of the project model's *document* tier?: 1 | 2 | 3 | 4; // which kind of source it came from: 1 build database, 2 CMake's - // own database, 3 compile_commands.json, 4 sources only + // own database, 3 compile_commands.json (also what xmake and meson + // write), 4 sources only }; profile: SemanticProfile; // semantic profile of the default context engine: { name: string; version: string }; // the core semantic engine, e.g. "clangd"; "none" when there is none @@ -99,9 +100,14 @@ interface CxxModulesIssue { | "file-quarantined" // the engine stopped answering for some files; they are answered from the module index until they change | "engine-incompatible" // the engine cannot run on this machine at all (its program loader refused it); module-level features remain | "modules-doomed" // modules that cannot be prepared because a module they import does not compile + | "producer-needs-download" // the build tool, run offline, cannot describe the project without a download + | "producer-online" // the build tool is describing the project with the network, as the client asked + | "generated-files-missing" // files the build generates are named by the build description but not written yet + | "implementation-unreadable" // an implementation unit does not build, so the definitions in it are not reached | string; message: string; command?: Command; // an optional action that fixes the issue + askOnline?: boolean; // producer-needs-download only: the client may offer to fetch what is missing (4, S3-4-16) category?: "code" // the user's own source is wrong: told as diagnostics where it is | "engine" // a semantic engine lost something (stopped responding, restarted too often) | "environment" // the machine, the payload or the workspace's trust @@ -131,6 +137,8 @@ Each issue **SHOULD** carry a `category` saying whose problem it is. S3-4-15 +A `producer-needs-download` issue says that the build tool, run without the network as a server runs it on its own, cannot describe the project until something is downloaded. A server **MAY** set `askOnline` on it when, asked by `workspace/executeCommand` with the command `mcppls.describeOnline`, it will describe the project once with the network allowed; every later description is without it again. S3-4-16 Until the client asks, and while the download runs, the server **MUST** go on serving the root from what it has (its sources, a partial description) S3-4-17, and **MUST NOT** reach the network on its own. S3-4-18 A client that offers the download **MUST NOT** block anything on the question: no modal dialog, and no request, activation or startup waits for the answer. S3-4-19 It **SHOULD** ask at most once per root and set of missing things. S3-4-20 It **MUST NOT** act on an answer that comes after the root's status no longer carries the issue: the person may have built the project in their own terminal meanwhile, and the server's own offline retries find that by themselves. S3-4-21 A client that does not know `askOnline` sees an issue with a command, as before. + A server whose semantic capabilities come from more than one engine **SHOULD** list each in `engines` with its role and state, and **MUST** name the engine that provides the core C++ semantics in `engine`, or `"none"` when the root has none. A client **MUST** accept engine names other than `"clangd"`. S3-4-5, S3-4-6, S3-4-7 ## 5. Requests @@ -216,6 +224,7 @@ interface CxxModulesReport { server: { name: string; version: string; platform: string; uptimeSeconds: number; logLevel: string; logFile: string }; client: { name: string; version?: string } | null; // the client's clientInfo, as it sent it roots: object[]; // one entry per workspace root + settings?: object; // the settings in effect, where each came from, and the problems applying them logTail: string[]; // the latest lines of the server's log } ``` diff --git a/docs/zh-CN/20-projects.md b/docs/zh-CN/20-projects.md index d4423e3..f574f93 100644 --- a/docs/zh-CN/20-projects.md +++ b/docs/zh-CN/20-projects.md @@ -4,7 +4,7 @@ mcppls 不需要你描述自己的构建方式。它会自己去找,然后在状态栏里告诉你找到了什么。下面说明每类工程里"找到"指什么,以及找不到时你会得到什么。 -状态栏里的 `L1`..`L4` 是 *tier*:工程是怎么被描述的——L1 是构建数据库(mcpp 的 `emit build-database`,或者你自己提供的),L2 是 CMake 自己的数据库,L3 是单独一份 `compile_commands.json`(包括版本太旧、无法 emit 构建数据库的 mcpp 留下的那份),L4 是只有源码(不受信任的工作区永远是 L4,不管磁盘上还有什么)。它和 `mcppls check`、`cxxModules/status` 里的 `level` 不是同一个数:`level` 是 [S1](../specs/s1-build-database.md) 自己的 1..4,表示数据库*文档*结构化得有多完整;手写的 level 3 数据库和 mcpp 的构建数据库都是 L1,没有 `FILE_SET CXX_MODULES` 的 CMake 工程即使 level 1 也是 L2。状态栏和编辑器插件只显示 `L`,把两者区分开。模型不会被更差 tier 的模型替换:如果构建工具之后给出的描述不如手上的完整,就保留手上的模型,并在状态里说明它可能已过期。 +状态栏里的 `L1`..`L4` 是 *tier*:工程是怎么被描述的——L1 是构建数据库(mcpp 的 `emit build-database`,或者你自己提供的),L2 是 CMake 自己的数据库,L3 是单独一份 `compile_commands.json`(包括版本太旧、无法 emit 构建数据库的 mcpp 留下的那份,以及 xmake、meson 写出的那份),L4 是只有源码(不受信任的工作区永远是 L4,不管磁盘上还有什么)。它和 `mcppls check`、`cxxModules/status` 里的 `level` 不是同一个数:`level` 是 [S1](../specs/s1-build-database.md) 自己的 1..4,表示数据库*文档*结构化得有多完整;手写的 level 3 数据库和 mcpp 的构建数据库都是 L1,没有 `FILE_SET CXX_MODULES` 的 CMake 工程即使 level 1 也是 L2。状态栏和编辑器插件只显示 `L`,把两者区分开。模型不会被更差 tier 的模型替换:如果构建工具之后给出的描述不如手上的完整,就保留手上的模型,并在状态里说明它可能已过期。 ## mcpp @@ -18,7 +18,13 @@ mcpp 给出的文档里列出了每个翻译单元、它的模块角色、它的 有两点要注意: -- **这一步是离线的。** 获取构建描述是查询工程信息,而不是替用户执行任务,所以服务端以 `MCPP_OFFLINE` 发起查询。如果项目依赖的东西机器上还没有,mcpp 会说明这一点,状态栏会提供一个选项,让你在自己的终端里跑构建工具——你的代理和凭证都在那里。`mcppls.buildTool` 见 [30-settings.md](30-settings.md)。 +- **这一步是离线的。** 获取构建描述是查询工程信息,而不是替用户执行任务,所以服务端以 `MCPP_OFFLINE` 发起查询。如果项目依赖的东西机器上还没有,mcpp 会说明这一点,而且不需要等你做任何决定: + - 工程立即按源码提供服务(L4),mcpp 能描述的部分照常使用; + - 右下角的通知提供 **Download and Continue**(这一次允许构建工具联网)、**Run in Terminal**(你的代理和凭证在那里)和 **Don't Ask Again**。它可以一直不点;每个工作区、每组缺失的东西只问一次; + - 构建描述会在 30 秒、1 分钟、2 分钟后、之后每 5 分钟离线重试一次,`mcpp.toml` 或 `mcpp.lock` 一变化就立即重试——所以你在自己的终端里构建了,工程会自己升级,那个询问也就不再适用。 + + `mcppls.buildTool` 和 `mcppls.buildDiscovery.askBeforeDownload` 见 [30-settings.md](30-settings.md)。 +- **构建规则生成的文件。** 规则包(比如 `mcpp:plugins` 的 `rules-qt`)在构建时把 `.ui`、`.qrc`、`.ts` 变成头文件和源文件。mcpp 描述构建时不运行这些步骤,所以表单的 `ui_*.h` 在它的描述里还不存在。mcppls 会去掉规则的输入(它们不是 C++),构建写过这些文件时从你工程自己的 `target/` 读取,否则说明缺了哪些,并提供 **Build in Terminal**;一旦构建写出了它们,包含它们的文件不用重启就有语义。 - **老版本的 mcpp** 没有 `emit build-database`,但这不是终点:如果机器上别处装有更新的 mcpp(xlings 的包目录、mcpp 自己的 registry 目录),mcppls 会改问*那一个*,同样只读、离线,只用来描述工程——工程本身仍用它固定的 mcpp 构建。发生这种情况时状态会说"described by mcpp X (the project pins Y)"。只有机器上没有任何 mcpp 能回答时,才会让固定的那个做配置(`mcpp build --configure-only`),这一步会写入项目;这时状态会说明,解决办法是升级 mcpp。 - **过期的数据库**——条目指向的文件已经不在了(构建曾经写过的 `target/` 后来被删掉,或者从别的机器提交进来的 `compile_commands.json`)——只使用仍然存在的部分;状态会注明它已过期,而不是直接失败。构建时才生成的模块(依赖包自己的 `std`、代码生成器的输出),mcppls 会先去构建通常留下它们的地方找——工程自己的构建目录,以及 mcpp 的 build-database 缓存(删掉 `target/` 后它仍在)——找不到时才用空的占位单元。 @@ -26,7 +32,21 @@ mcpp 给出的文档里列出了每个翻译单元、它的模块角色、它的 如果构建目录里有 `build_database.json`(CMake 4.4+ 配 Ninja、`FILE_SET CXX_MODULES`),mcppls 就读它。否则它读 `compile_commands.json`,并展开生成器写出的 `@modmap` 文件。 -如果连构建目录都没有,而且是受信任的工作区,mcppls 会**自己配置一个**,放在它自己的缓存目录下——绝不会放进你的项目。第一次这样配置可能会下载项目声明的依赖(`FetchContent`、`ExternalProject`);之后每一次都会加上 `-DFETCHCONTENT_UPDATES_DISCONNECTED=ON`。 +构建目录会在 `build*/`、`out/build/*`、`cmake-build-*`,以及 `CMakePresets.json`(或 `CMakeUserPresets.json`)第一个配置预设的 `binaryDir` 里找。 + +如果连构建目录都没有,而且是受信任的工作区,mcppls 会**自己配置一个**,放在它自己的缓存目录下——绝不会放进你的项目——并沿用那个预设的生成器、工具链文件和缓存变量,这样描述的就是你实际会得到的构建。这次配置是**断网的**(`-DFETCHCONTENT_FULLY_DISCONNECTED=ON`),第一次也一样:机器上没有的 `FetchContent` 依赖会让它停下,你会得到和上面 mcpp 一样的非阻塞询问(**Download and Continue** 会在那个私有目录里联网配置一次)。 + +## xmake + +有 `xmake.lua` 就是 xmake 工程。根目录或 `.vscode/` 里已有的 `compile_commands.json`(xmake 的 VS Code 插件写在那里)会直接读取。否则 mcppls 用 xmake 自己的命令 `xmake project -k compile_commands` 获取一份,这个命令不编译任何东西——但它会配置并扫描模块,所以 mcppls 把 xmake 的配置目录和构建目录指向自己的缓存(`XMAKE_CONFIGDIR`、`--builddir`),你的工程保持不变。它离线运行(`--policies=package.fetch_only,network.mode:private`):没有安装的包会让它停下,并给出和上面一样的询问。第一次描述要几秒钟(实测约 6–8 秒,大部分是 xmake 在探测工具链);这期间工程按源码提供服务。模块角色靠扫描得到,所以 xmake 工程是 L3。 + +## meson + +有 `meson.build` 就是 meson 工程。已有的构建目录(`builddir/`、`build/`,或任何带 `meson-private/` 的目录)会读取其中的 `compile_commands.json`。否则 mcppls 用 `--wrap-mode=nodownload` 把 `meson setup` 跑进自己的缓存;需要下载的子项目会让它停下,并给出同样的询问。和 xmake 一样是 L3。 + +## 关闭探测 + +`mcppls.buildDiscovery = off` 让 mcppls 完全不探测构建系统:不隐式读取或运行任何东西,只使用你用 `mcppls.database` 指定的数据库,否则扫描源码(L4)。`mcppls.buildDiscovery.providers` 则只去掉个别构建系统——比如只读现有的 CMake 构建目录、永远不运行 xmake。`mcppls.buildTool = off` 是更窄的开关:仍然探测构建系统、读取它们已有的输出,只是从不运行。见 [30-settings.md](30-settings.md)。 ## compile_commands.json @@ -60,4 +80,6 @@ C++26 能用到什么,取决于 clangd 23.1:包索引(pack indexing)、` | 一个编译器和标准库 | 模型来自你的构建;用的是那个工具链的 `std` | | 一个语义工具包 | 没找到可用的编译器,或者工作区不受信任 | | "may be stale" | 构建工具这次没能给出答案;用的还是上次加载的模型 | -| "needs a download" | 离线规划需要本机没有的依赖;状态栏提供了解决的操作 | +| "needs a download" | 离线规划需要本机没有的依赖;这期间工程按源码提供服务,并提供解决的操作(或者在终端里构建:它会自己升级) | +| "files the build generates … do not exist yet" | 规则的输出(比如 Qt 表单的头文件)还没构建出来;构建一次,包含它的文件就有语义 | +| "implementation unit(s) cannot be read" | 某个实现单元编译不了(通常是缺头文件),所以跳到定义到不了其中的定义 | diff --git a/docs/zh-CN/30-settings.md b/docs/zh-CN/30-settings.md index a554f1e..638f1eb 100644 --- a/docs/zh-CN/30-settings.md +++ b/docs/zh-CN/30-settings.md @@ -2,20 +2,87 @@ [English](../30-settings.md) | **简体中文** -## VS Code 设置 - -| 设置 | 取值 | 作用 | -|---|---|---| -| `mcppls.buildTool` | `offline`(默认), `online`, `off` | 项目构建工具的运行方式。`offline`:不联网运行——如果构建工具因此无法在不下载东西的情况下描述构建,状态栏会说明缺什么,并提议在你的终端里运行它。`online`:允许联网,超时时间从一分钟延长到十分钟。`off`:从不运行构建工具,使用缓存的描述或扫描到的源码 | -| `mcppls.toolEnvironment` | `auto`(默认), `editor` | 构建工具在哪个环境中启动。`auto` 会在后台读取一次你登录 shell 的环境(仅限 POSIX 系统)——从桌面项或 Dock 图标启动的编辑器不带任何 shell 配置,没有这个选项,它找到的构建工具可能就不是你终端里找到的那个。在 Windows 上,编辑器的环境本就和终端一致。`editor` 始终使用编辑器进程自身的环境 | -| `mcppls.compiler` | 编译器驱动的路径,或 `kit` | 为模块语义使用这个编译器,而不是检测到的那个。`kit` 强制使用内置的语义工具包 | -| `mcppls.semanticKit` | `auto`(默认), `off` | 内置工具包是否可以被使用 | -| `mcppls.engine` | `clangd`(默认), `none` | 核心引擎。无论如何,mcppls 自己的模块引擎都会运行;`none` 表示只提供模块相关功能 | -| `mcppls.ai.enabled` | `false`(默认) | 是否启用变更审查里依赖模型的那部分。关闭时服务端不发起任何模型调用 | -| `mcppls.detectConflicts` | `true`(默认) | 在此工作区中提议关闭另一个 C++ 扩展的语言功能(只提议一次),之后又有冲突扩展启用时会提示 | -| `mcppls.semanticTokens.modules` | `true`(默认) | 用服务端的语义 token 给 `import`、`module`、`export` 和模块名上色。关闭后只用语法文件的颜色 | -| `mcppls.completion.triggerOnSpace` | `true`(默认) | 在 `import` 或 `export import` 后输入空格时立即弹出模块列表;其他位置的空格不会发给服务端。其他编辑器用 `initializationOptions.completion.triggerOnSpace` 开启同样的行为 | -| `mcppls.trace.server` | `off`(默认), `messages`, `verbose` | 把 LSP 通信记录到 C++ Modules 输出通道(Trace 级别);`verbose` 还会打开服务端的 debug 日志(Debug 级别)。要看到它们,需把该输出通道的日志级别调到对应级别 | +mcppls 的每一个可配置行为都只有一处定义:`src/config/settings.cppm` 里注册表的一行。下面这张表——VS Code 设置、命令行选项,以及会影响行为的环境变量——是从这份注册表生成的 +(`mcppls settings --format markdown --lang zh-CN`),`tests/test_settings.cpp` 把它、英文版和 +`editors/vscode/package.json` 都与注册表互相校验,三者不会走样。 + +**优先级。** 命令行 > `initializationOptions` > 默认值;之后的 `workspace/didChangeConfiguration` +会更新 `initializationOptions`(或更早一次 `didChangeConfiguration`)设置的值,但绝不会更新命令行设置 +的值。取值超出该设置自己的取值范围(比如一个未知的枚举值)时从不生效——回落到默认值,并记为一个问题 +(`mcppls report` 的 `settings.problems`;`mcppls settings` 本身不会带问题,因为它只打印注册表)。 + +**生效方式。** 一个已经在运行的设置改变之后如何生效:`重启` 需要重启 mcppls(下面需要重启的设置, +VS Code 扩展已经会这样做);`重新加载模型` 只重新加载项目模型,不需要重启;`立即生效` 两者都不需要—— +下次被读取时就是它生效的时候。 + +**改名后的设置照常能用。** 注册表给一个设置登记了旧名时,用旧名(不管是点号写法还是 +`initializationOptions`/`didChangeConfiguration` 里的写法)依然有效;下面这些设置目前还没有改过名, +所以都没有登记旧名。 + +`initializationOptions` 和 `didChangeConfiguration` 都同时接受嵌套对象 +(`{"semanticTokens": {"modules": false}}`)和点号写法的键(`{"semanticTokens.modules": false}`), +外面套不套一层 `mcppls` 都可以。 + + +### 项目与构建工具 + +| 设置 | 取值 | 默认值 | 命令行 | 生效方式 | 作用 | +|---|---|---|---|---|---| +| `mcppls.buildTool` | `offline`, `online`, `off` | `offline` | `--build-tool` | 重新加载模型 | 项目构建工具的运行方式。`offline`:不联网运行——如果构建工具因此无法在不下载东西的情况下描述构建,状态栏会说明缺什么,并提议在终端里运行它。`online`:允许联网,超时时间从一分钟延长到十分钟。`off`:从不运行构建工具;仍会探测构建系统、仍读取它已有的产物(要连探测也关掉,见 `buildDiscovery`)。 | +| `mcppls.toolEnvironment` | `auto`, `editor` | `auto` | `--tool-environment` | 重启 | 构建工具在哪个环境中启动。`auto` 会在后台读取一次你登录 shell 的环境(仅限 POSIX 系统)——从桌面项或 Dock 图标启动的编辑器不带任何 shell 配置,没有这个选项,它找到的构建工具可能就不是你终端里找到的那个。在 Windows 上,编辑器的环境本就和终端一致。`editor` 始终使用编辑器进程自身的环境。 | +| `mcppls.producerTimeout` | 非负整数(秒) | `0` | `--producer-timeout` | 重新加载模型 | 构建工具描述项目最多可以花多长时间。默认 `0` 使用设计本身的限制(离线一分钟,`buildTool` 为 `online` 时十分钟);调短可以观察限制是否生效,构建确实慢就调长。 | +| `mcppls.untrusted` | `true`, `false` | `false` | `--untrusted` | 重启 | 不运行任何构建工具,也不运行编译器;一个不受信任的工作区也等同于 `buildDiscovery` 为 `off`。 | +| `mcppls.discoverCompilers` | `true`, `false` | `true` | `--no-discover` | 重新加载模型 | 为构建描述没有覆盖到的源码在本机查找编译器。关闭后,这类源码改用语义工具包。 | +| `mcppls.buildDiscovery` | `auto`, `off` | `auto` | `--build-discovery` | 重新加载模型 | 是否探测项目的构建系统。`off`:不隐式读取或执行任何东西——只用明确配置的 `database`,否则扫描源码。`buildTool` 管的是探测到的构建工具能不能*执行*;这个开关管的是要不要去探测它。 | +| `mcppls.buildDiscovery.providers` | `mcpp`, `cmake`, `xmake`, `meson`, `compile-commands`(逗号分隔) | `mcpp`, `cmake`, `xmake`, `meson`, `compile-commands` | `--build-discovery-providers` | 重新加载模型 | `buildDiscovery` 可以使用哪些构建系统提供者;从中去掉某个提供者即停用它的探测(例如只想用已有的 CMake 构建目录,不要 xmake)。 | +| `mcppls.buildDiscovery.askBeforeDownload` | `true`, `false` | `true` | — | 立即生效 | 当构建工具需要下载才能完成描述项目时,客户端可以提议去获取它。关闭后,状态栏说明需要下载,但不会再询问。 | + +### 引擎 + +| 设置 | 取值 | 默认值 | 命令行 | 生效方式 | 作用 | +|---|---|---|---|---|---| +| `mcppls.engine` | `clangd`, `none` | `clangd` | `--engine` | 重启 | 核心引擎。无论如何,mcppls 自己的模块引擎都会运行;`none` 表示只提供模块相关功能。 | +| `mcppls.compiler` | 字符串 | (空) | `--compiler` | 重新加载模型 | 为模块语义使用这个编译器,而不是检测到的那个:可以是绝对路径、`PATH` 上的名字,或 `kit`(强制使用内置的语义工具包)。空表示自动检测。 | +| `mcppls.semanticKit` | `auto`, `off` | `auto` | `--semantic-kit` | 重新加载模型 | 内置的标准库工具包是否可以被使用:`auto` 在没有找到编译器时使用;`off` 从不使用(没有编译器时只剩模块相关功能)。 | +| `mcppls.requestTimeout` | 非负整数(秒) | `60` | `--request-timeout` | 重启 | 一个引擎请求最多等待多久,超时后不经该引擎就给出答复。用户在等的请求(悬停、跳转、补全等)总共最多等 30 秒,clangd 启动或准备模块期间也算在内,之后由 mcppls 自己的引擎答复。 | +| `MCPPLS_ENGINE_ARGUMENTS` | 字符串 | (空) | — | 重启 | 追加到 clangd 自身命令行末尾的额外参数,用于排查问题(例如 `-j=8 --background-index-priority=background`)。 | + +### 编辑器体验 + +| 设置 | 取值 | 默认值 | 命令行 | 生效方式 | 作用 | +|---|---|---|---|---|---| +| `mcppls.semanticTokens.modules` | `true`, `false` | `true` | — | 重启 | 用服务端的语义 token 给 `import`、`module`、`export` 和模块名上色。关闭后只用语法文件的颜色。 | +| `mcppls.semanticTokens.moduleType` | `true`, `false` | `false` | — | 重启 | 客户端声明自己认得自定义的 `module` 语义 token 类型和 `partition` 修饰符;除本仓库的 VS Code 扩展外都关闭,因为没有别的客户端会声明它。不是 package.json 里的设置:VS Code 扩展自己贡献了这个 token 类型,因此固定声明为开。 | +| `mcppls.completion.triggerOnSpace` | `true`, `false` | `true` | — | 重启 | 在 `import` 或 `export import` 后输入空格时立即弹出模块列表;其他位置的空格不会发给服务端。什么都不说的客户端只有在自证是 VS Code 或其分支时才会得到这个行为;其他客户端需要用 `initializationOptions.completion.triggerOnSpace: true` 主动开启。 | +| `mcppls.index.primeImplementationUnits` | `auto`, `off` | `auto` | `--prime-implementation-units` | 重启 | 在后台让 clangd 逐个构建模块的实现单元(每次少量),这样即使实现文件从没打开过,跳到定义也能到达只在实现单元里的定义。clangd 自己的后台索引看不到模块单元的导入(WA-CLANGD-008)。`off`:只有一次跳转请求所搜索的单元和你打开的文件会为此被索引。 | +| `mcppls.detectConflicts` | `true`, `false` | `true` | — | 立即生效 | 在此工作区中提议关闭另一个 C++ 扩展的语言功能(只提议一次),之后又有冲突扩展启用时会提示。仅限 VS Code:其他客户端不会在多个语言服务端之间做取舍。 | + +### 诊断与日志 + +| 设置 | 取值 | 默认值 | 命令行 | 生效方式 | 作用 | +|---|---|---|---|---|---| +| `mcppls.logLevel` | `debug`, `info`, `warning`, `error` | `info` | `--log-level` | 重启 | 服务端自身的日志级别。 | +| `mcppls.disableWorkaround` | `WA-CLANGD-`(可重复) | (无) | `--disable-workaround`(可重复) | 重启 | 关掉一个针对 clangd 缺陷登记的规避措施(`WA-CLANGD-`;上游缺陷登记在 issue #24),用来确认它是否还有必要;可重复。`mcppls report` 在 `engines[].details.workarounds` 下列出所有登记过的规避措施。 | +| `mcppls.trace.server` | `off`, `messages`, `verbose` | `off` | — | 立即生效 | 把 LSP 通信记录到 C++ Modules 输出通道(Trace 级别);`verbose` 还会打开服务端的 debug 日志(通过附加 `--log-level debug`,Debug 级别)。要看到它们,需把该输出通道的日志级别调到对应级别。 | +| `MCPPLS_LOG_LEVEL` | `debug`, `info`, `warning`, `error` | (空) | — | 重启 | 覆盖 VS Code 扩展启动服务端时使用的日志级别,优先于 `trace.server`。 | + +### AI 评审 + +| 设置 | 取值 | 默认值 | 命令行 | 生效方式 | 作用 | +|---|---|---|---|---|---| +| `mcppls.ai.enabled` | `true`, `false` | `false` | — | 立即生效 | 显示 AI 时代的功能:Review Changes 用 mcppls 的规则对照 `HEAD` 审查工作区的变更,并把发现连同证据以问题的形式展示。除非这里开启并且另行配置了模型来源,否则不会向任何模型发送内容。 | + +### 路径 + +| 设置 | 取值 | 默认值 | 命令行 | 生效方式 | 作用 | +|---|---|---|---|---|---| +| `mcppls.database` | 路径 | (空) | `--database` | 重新加载模型 | 工作区自己的 S1 构建数据库,相对于其根目录,用它代替探测。 | +| `mcppls.mcpp` | 路径 | (空) | `--mcpp` | 重新加载模型 | mcpp 项目所用的 `mcpp` 可执行文件;空表示在 `PATH` 上查找。 | +| `mcppls.payload` | 路径 | (空) | `--payload` | 重启 | 包含 clangd 和语义工具包的 payload 目录;下面的 `clangd` 和 `kit` 可以分别覆盖其中一项。 | +| `mcppls.clangd` | 路径 | (空) | `--clangd` | 重启 | clangd 可执行文件,覆盖 payload 自带的那一份。 | +| `mcppls.kit` | 路径 | (空) | `--kit` | 重启 | 语义工具包目录,覆盖 payload 自带的那一份。 | +| `MCPPLS_CACHE_DIR` | 路径 | (空) | — | 重启 | 覆盖 mcppls 原本在用户缓存目录下选定的整个缓存目录(工作区模型、工具链探测结果、日志、诊断包)。 | + ## 命令 @@ -40,21 +107,11 @@ mcppls daemon run|start|status|stop 共享工作区的守护进程 mcppls check 模型、语义配置、模块诊断,然后运行 clangd --check mcppls report [--root DIR] [--settle SECONDS] bug 报告需要的全部信息,JSON 格式 mcppls model [--root DIR] [--export s1|compile-commands|engine] +mcppls settings [--format markdown|json] [--lang en|zh-CN] 上面这张表,或它的机器可读形式 mcppls print-environment 在两个标记之间打印本进程的环境 mcppls version ``` -对每个子命令都生效的选项: - -| 选项 | 默认值 | 作用 | -|---|---|---| -| `--build-tool offline\|online\|off` | `offline` | `mcppls.buildTool` 的命令行写法 | -| `--tool-environment auto\|editor` | `auto` | `mcppls.toolEnvironment` 的命令行写法 | -| `--producer-timeout SECONDS` | 60,联网时 600 | 构建工具描述项目最多可以花多长时间。构建确实慢就调长;想观察超时限制是否生效就调短 | -| `--request-timeout SECONDS` | 60 | 一个引擎请求最多等待多久,超时后不经该引擎就给出答复。用户在等的请求(悬停、跳转、补全等)总共最多等 30 秒,clangd 启动或准备模块期间也算在内,之后由 mcppls 自己的引擎答复 | -| `--untrusted` | — | 不运行任何构建工具,也不运行编译器 | -| `--no-discover` | — | 不查找编译器;零散源码使用语义工具包 | -| `--log-level debug\|info\|warning\|error` | `info` | | -| `--disable-workaround WA-CLANGD-` | — | 关掉一个针对 clangd 缺陷登记的规避措施(可重复),用来确认它是否还有必要;`mcppls report` 在 `engines[].details.workarounds` 下列出它们 | +上面的每一个全局选项(`mcppls --help`)都是某个注册设置的命令行写法;它的作用、默认值和生效方式见上表。 `print-environment` 是给服务端自己用的:`mcppls.toolEnvironment` 为 `auto` 时,服务端让登录 shell 运行的就是这个命令。 diff --git a/editors/claude-code/.claude-plugin/marketplace.json b/editors/claude-code/.claude-plugin/marketplace.json index 96be841..ee2a05e 100644 --- a/editors/claude-code/.claude-plugin/marketplace.json +++ b/editors/claude-code/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ "displayName": "C++ Modules Language Server", "source": "./mcppls-lsp", "description": "Registers mcppls as the language server for C and C++ sources, including C++20/23 named modules.", - "version": "0.0.5", + "version": "0.0.6", "author": { "name": "Sunrisepeak", "url": "https://github.com/Sunrisepeak/mcpp-language-server" diff --git a/editors/claude-code/mcppls-lsp/.claude-plugin/plugin.json b/editors/claude-code/mcppls-lsp/.claude-plugin/plugin.json index c68b430..25a8ad9 100644 --- a/editors/claude-code/mcppls-lsp/.claude-plugin/plugin.json +++ b/editors/claude-code/mcppls-lsp/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "mcppls-lsp", "displayName": "C++ Modules Language Server", - "version": "0.0.5", + "version": "0.0.6", "description": "Registers mcppls as the language server for C and C++ sources, including C++20/23 named modules, and its MCP tools (symbols, references, modules, verification, review). Replaces clangd-lsp for a project; do not enable both at once.", "author": { "name": "Sunrisepeak", diff --git a/editors/clion/gradle.properties b/editors/clion/gradle.properties index 90e75c0..1f0ee49 100644 --- a/editors/clion/gradle.properties +++ b/editors/clion/gradle.properties @@ -2,7 +2,7 @@ # ones within the same major line; sinceBuild/untilBuild in plugin.xml is what actually gates it. platformType = CL platformVersion = 2025.2 -pluginVersion = 0.0.5 +pluginVersion = 0.0.6 org.gradle.jvmargs = -Xmx2g # The IDE ships the Kotlin standard library; bundling a second copy in the plugin is what JetBrains # asks plugins not to do. diff --git a/editors/vscode/package.json b/editors/vscode/package.json index db0c7bb..4f52143 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "mcpp-language-server", "displayName": "C++ Modules Language Server", "description": "C++20/23 named modules that just work: go to definition, completion, hover and references across modules for any compiler, with clangd and a standard library kit built in. (mcppls)", - "version": "0.0.5", + "version": "0.0.6", "publisher": "sunrisepeak", "license": "Apache-2.0", "icon": "icon.png", @@ -273,6 +273,58 @@ "Always start build tools in the editor process's environment." ], "description": "Which environment mcppls starts your build tools in. An editor started from a desktop entry, a Dock icon or a launcher does not carry your shell configuration, so the tool it finds may not be the one your terminal finds." + }, + "mcppls.buildDiscovery": { + "type": "string", + "enum": [ + "auto", + "off" + ], + "enumDescriptions": [ + "Detect the project's build system: mcpp, CMake, xmake, meson, or an existing compile_commands.json.", + "Detect nothing and run nothing implicitly: use an explicitly configured mcppls.database, else scanned sources." + ], + "default": "auto", + "description": "Whether the project's build system is detected at all. mcppls.buildTool separately governs whether a detected build tool may be run; this governs whether it is looked for in the first place." + }, + "mcppls.buildDiscovery.providers": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mcpp", + "cmake", + "xmake", + "meson", + "compile-commands" + ] + }, + "default": [ + "mcpp", + "cmake", + "xmake", + "meson", + "compile-commands" + ], + "description": "Which build system providers mcppls.buildDiscovery may use; leave one out to stop mcppls from detecting it." + }, + "mcppls.buildDiscovery.askBeforeDownload": { + "type": "boolean", + "default": true, + "description": "When the build tool needs a download to finish describing the project, offer to fetch it. Off: the status bar says a download is needed, and nothing asks." + }, + "mcppls.index.primeImplementationUnits": { + "type": "string", + "enum": [ + "auto", + "off" + ], + "enumDescriptions": [ + "Open a module's implementation units in clangd in the background, so go-to-definition reaches a definition that only one has.", + "Only index what is opened in the editor." + ], + "default": "auto", + "description": "Whether mcppls opens a module's implementation units in the background so go-to-definition can reach a definition that only an implementation unit has." } } }, diff --git a/editors/vscode/src/commands.ts b/editors/vscode/src/commands.ts index 059c9ef..2fcc914 100644 --- a/editors/vscode/src/commands.ts +++ b/editors/vscode/src/commands.ts @@ -362,9 +362,13 @@ async function runBuildToolInTerminal(access: ServerAccess): Promise { command = 'mcpp build'; } else if (await exists('CMakeLists.txt')) { command = 'cmake -S . -B build'; + } else if (await exists('xmake.lua')) { + command = 'xmake'; + } else if (await exists('meson.build')) { + command = 'meson setup build'; } if (!command) { - void vscode.window.showWarningMessage('C++ Modules: this folder has no mcpp.toml or CMakeLists.txt to build.'); + void vscode.window.showWarningMessage('C++ Modules: this folder has no mcpp.toml, CMakeLists.txt, xmake.lua or meson.build to build.'); return; } const choice = await vscode.window.showInformationMessage( diff --git a/editors/vscode/src/downloadAsk.ts b/editors/vscode/src/downloadAsk.ts new file mode 100644 index 0000000..b5fb419 --- /dev/null +++ b/editors/vscode/src/downloadAsk.ts @@ -0,0 +1,21 @@ +// When to offer to fetch what the build description needs (plan 2026-09-27 B-2, §9.2): the decision of +// downloadPrompt.ts, in plain TypeScript so it is tested without VS Code (test/unit/downloadAsk.test.ts). + +export const NEEDS_DOWNLOAD_CODE = 'producer-needs-download'; + +export interface DownloadIssue { + code: string; + message: string; + // S3 (plan 2026-09-27 B-2): the server lets a client offer to fetch what is missing. + askOnline?: boolean; +} + +export function needsDownload(status: { issues?: { code: string; message: string }[] }): DownloadIssue | undefined { + return (status.issues ?? []).find((issue) => issue.code === NEEDS_DOWNLOAD_CODE) as DownloadIssue | undefined; +} + +// Whether to ask now: the server offers it, the person has not declined for this workspace, this set of +// missing things has not been asked about yet, and no question for this root is still open. +export function shouldAsk(issue: DownloadIssue | undefined, never: boolean, askedAbout: readonly string[], open: boolean): boolean { + return issue !== undefined && issue.askOnline === true && !never && !open && !askedAbout.includes(issue.message); +} diff --git a/editors/vscode/src/downloadPrompt.ts b/editors/vscode/src/downloadPrompt.ts new file mode 100644 index 0000000..4f1481c --- /dev/null +++ b/editors/vscode/src/downloadPrompt.ts @@ -0,0 +1,86 @@ +// The build description needs a download (plan 2026-09-27 B-2, §9.2). The server runs the build tool +// offline, and when it cannot describe the project without fetching something it says so in the status, +// with `askOnline` when a client may offer to fetch it. This is the third question the extension ever +// asks (conflicts.ts and commandLineTools.ts are the others), and it follows the rules of §9.2: +// +// - it never blocks anything: a notification in the corner, which may stay unanswered forever; nothing +// the extension or the server does waits for it, and the project is served from its sources meanwhile; +// - it is asked once per workspace for one set of missing things, and never again after "Don't Ask Again"; +// - the answer can come long after the question. By then the person may have built the project in their +// own terminal, or the server's own retries may have found everything in place; an answer to a question +// that no longer applies does nothing. + +import * as vscode from 'vscode'; +import { askOnce } from './prompt'; +import type { CxxModulesStatus } from './status'; +import { needsDownload, shouldAsk } from './downloadAsk'; + +export const DESCRIBE_ONLINE_COMMAND = 'mcppls.describeOnline'; +export const RUN_IN_TERMINAL_COMMAND = 'mcppls.runBuildToolInTerminal'; +export const NEVER_KEY = 'mcppls.downloadPrompt.never'; +export const ASKED_KEY = 'mcppls.downloadPrompt.asked'; +export const FETCH = 'Download and Continue'; +export const RUN_IN_TERMINAL = 'Run in Terminal'; +export const NEVER = "Don't Ask Again"; + +export class DownloadPromptController { + // What each root still needs, from its latest status: the answer to an old question is checked against it. + private readonly pending = new Map(); + private readonly open = new Set(); + + constructor(private readonly context: vscode.ExtensionContext, private readonly log: (line: string) => void) {} + + // Called for every cxxModules/status notification from the running server. + onStatus(status: CxxModulesStatus): void { + const root = status.project.root; + const issue = needsDownload(status); + if (issue) { + this.pending.set(root, issue.message); + } else { + this.pending.delete(root); + } + const never = this.context.workspaceState.get(NEVER_KEY, false); + const askedAbout = this.context.workspaceState.get(ASKED_KEY, []); + if (!shouldAsk(issue, never, askedAbout, this.open.has(root)) || !issue) { + return; + } + // Recorded before the answer: another status while the question is open must not ask again. + this.open.add(root); + void this.context.workspaceState.update(ASKED_KEY, [...askedAbout, issue.message].slice(-8)); + const folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(root))?.name ?? root; + this.log(`asking whether to fetch what the build description of ${folder} needs`); + void askOnce( + 'download', + `C++ Modules: the build description of ${folder} needs a download. The project is served from its sources ` + + 'meanwhile. Fetch it now (the build tool may reach the network), or run the build yourself?', + FETCH, + RUN_IN_TERMINAL, + NEVER, + ).then((answer) => this.answered(root, answer)); + } + + private answered(root: string, answer: string | undefined): void { + this.open.delete(root); + if (answer === NEVER) { + void this.context.workspaceState.update(NEVER_KEY, true); + this.log('the build description download will not be offered again in this workspace'); + return; + } + if (answer === undefined) { + return; + } + if (!this.pending.has(root)) { + // §9.2 rule 4: the environment was completed meanwhile (a build in a terminal, the server's own retry). + this.log('the build description no longer needs a download; nothing to do'); + return; + } + if (answer === FETCH) { + this.log('fetching what the build description needs'); + void vscode.commands.executeCommand(DESCRIBE_ONLINE_COMMAND).then(undefined, (error: unknown) => { + this.log(`could not ask the server to fetch it: ${error instanceof Error ? error.message : String(error)}`); + }); + } else if (answer === RUN_IN_TERMINAL) { + void vscode.commands.executeCommand(RUN_IN_TERMINAL_COMMAND); + } + } +} diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 7e5dd40..0f652cd 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -25,6 +25,7 @@ import { TransportKind, } from 'vscode-languageclient/node'; import { CommandLineToolsController, withInstallCommandFallback } from './commandLineTools'; +import { DownloadPromptController } from './downloadPrompt'; import { registerCommands, reloadBuildDescription } from './commands'; import { sendTriggeredCompletion } from './completionGate'; import { checkConflicts, ConflictCheck, watchForNewConflicts } from './conflicts'; @@ -39,6 +40,9 @@ function buildToolSetting(value: string | undefined): string { return value === 'online' || value === 'off' ? value : 'offline'; } +// mcppls.buildDiscovery.providers' own default (config registry, settings §9 T1): every provider. +const BUILD_DISCOVERY_PROVIDERS = ['mcpp', 'cmake', 'xmake', 'meson', 'compile-commands']; + const CLIENT_ID = 'mcppls'; const CLIENT_NAME = 'C++ Modules'; const RESTART_WINDOW_MS = 3 * 60 * 1000; @@ -278,6 +282,16 @@ class ServerHost implements vscode.Disposable { completion: { triggerOnSpace: configuration.get('completion.triggerOnSpace', true), }, + // 0.0.6 plan §3.7 B-7: whether the project's build system is detected at all, which + // providers may be used, and whether a needed download is ever offered. Dotted keys, + // not a nested `buildDiscovery` object: the setting `buildDiscovery` is itself a leaf + // (`auto`/`off`), so it cannot also be the object `buildDiscovery.providers` nests + // under -- the config registry's own dotted-key form (settings §9 T1) sidesteps that. + 'buildDiscovery': configuration.get('buildDiscovery') === 'off' ? 'off' : 'auto', + 'buildDiscovery.providers': configuration.get('buildDiscovery.providers', BUILD_DISCOVERY_PROVIDERS), + 'buildDiscovery.askBeforeDownload': configuration.get('buildDiscovery.askBeforeDownload', true), + // 0.0.6 plan §2.6, §9 T5: implementation units opened in the background. + 'index.primeImplementationUnits': configuration.get('index.primeImplementationUnits') === 'off' ? 'off' : 'auto', }, middleware: { // Fix plan 2026-09-26 F9 (D4 layer 1): of the completions a typed space asks for, only @@ -488,6 +502,8 @@ export function activate(context: vscode.ExtensionContext): TestApi { // ServerHost instance without restructuring construction order. let host: ServerHost; const commandLineTools = new CommandLineToolsController(context, (line) => host.log(line)); + const downloadPrompt = new DownloadPromptController(context, (line) => host.log(line)); + status.onUpdate((current) => downloadPrompt.onStatus(current)); let latestConflictCheck: Promise = Promise.resolve('none-found'); // Conflicts can appear or disappear after activation (another extension @@ -526,7 +542,11 @@ export function activate(context: vscode.ExtensionContext): TestApi { if (event.affectsConfiguration('mcppls.compiler') || event.affectsConfiguration('mcppls.semanticKit') || event.affectsConfiguration('mcppls.engine') || event.affectsConfiguration('mcppls.buildTool') || event.affectsConfiguration('mcppls.toolEnvironment') || event.affectsConfiguration('mcppls.semanticTokens.modules') - || event.affectsConfiguration('mcppls.completion.triggerOnSpace')) { + || event.affectsConfiguration('mcppls.completion.triggerOnSpace') + // 0.0.6 plan §3.7 B-7, §2.6/§9 T5: new settings, same treatment as the ones above. + || event.affectsConfiguration('mcppls.buildDiscovery') || event.affectsConfiguration('mcppls.buildDiscovery.providers') + || event.affectsConfiguration('mcppls.buildDiscovery.askBeforeDownload') + || event.affectsConfiguration('mcppls.index.primeImplementationUnits')) { void host.restart(); } }), diff --git a/editors/vscode/src/prompt.ts b/editors/vscode/src/prompt.ts index 4ef09df..2ed4532 100644 --- a/editors/vscode/src/prompt.ts +++ b/editors/vscode/src/prompt.ts @@ -32,7 +32,7 @@ import * as vscode from 'vscode'; // 'turnOffScope' and 'restoreScope' are the quick picks `mcppls.turnOffOtherCppFeatures` and // `mcppls.restoreOtherCppFeatures` (src/conflicts.ts) show for which settings scope to act on. -export type PromptKind = 'conflict' | 'commandLineTools' | 'turnOffScope' | 'restoreScope'; +export type PromptKind = 'conflict' | 'commandLineTools' | 'download' | 'turnOffScope' | 'restoreScope'; const TEST_MODE = process.env.MCPPLS_TEST === '1'; const SUBSTITUTION_GRACE_MS = 5000; diff --git a/editors/vscode/src/status.ts b/editors/vscode/src/status.ts index 5889cfd..73e0aa4 100644 --- a/editors/vscode/src/status.ts +++ b/editors/vscode/src/status.ts @@ -33,7 +33,7 @@ export interface CxxModulesStatus { state: ModuleState; project: { root: string; - source: 'mcpp' | 'cmake' | 'build-database' | 'compile-commands' | 'inferred'; + source: 'mcpp' | 'cmake' | 'xmake' | 'meson' | 'build-database' | 'compile-commands' | 'inferred'; level?: number; // How the project was described (README L1..L4; S3-4-8, S3-4-9): 1 a build database, 2 // CMake's own database, 3 a compile_commands.json, 4 sources only. Shown as @@ -207,8 +207,19 @@ export class StatusController implements vscode.Disposable { } } + // Called with every status the server sends, after the item shows it (downloadPrompt.ts listens). + private readonly listeners: ((status: CxxModulesStatus) => void)[] = []; + onUpdate(listener: (status: CxxModulesStatus) => void): void { + this.listeners.push(listener); + } + update(status: CxxModulesStatus): void { this.current = status; + queueMicrotask(() => { + for (const listener of this.listeners) { + listener(status); + } + }); this.failure = undefined; const label = describeProfile(status.profile); this.item.text = label.length > 0 ? `C++ Modules · ${label}` : 'C++ Modules'; diff --git a/editors/vscode/test/unit/downloadAsk.test.ts b/editors/vscode/test/unit/downloadAsk.test.ts new file mode 100644 index 0000000..3712458 --- /dev/null +++ b/editors/vscode/test/unit/downloadAsk.test.ts @@ -0,0 +1,33 @@ +// When the extension offers to fetch what the build description needs (plan 2026-09-27 B-2, §9.2), in +// plain Node: no VS Code, same reason test/unit/statusText.test.ts is. +import * as assert from 'assert'; +import { needsDownload, shouldAsk } from '../../src/downloadAsk'; + +suite('the build description download offer', () => { + const issue = { code: 'producer-needs-download', message: 'xim:qt-base@6.11.1 is not installed', askOnline: true }; + + test('offered when the server lets a client offer it', () => { + assert.strictEqual(shouldAsk(issue, false, [], false), true); + }); + + test('not offered when the server does not (an older server, askBeforeDownload off, a fetch already running)', () => { + assert.strictEqual(shouldAsk({ ...issue, askOnline: false }, false, [], false), false); + assert.strictEqual(shouldAsk({ code: issue.code, message: issue.message }, false, [], false), false); + }); + + test('never again after "Don\'t Ask Again", and once per set of missing things', () => { + assert.strictEqual(shouldAsk(issue, true, [], false), false); + assert.strictEqual(shouldAsk(issue, false, [issue.message], false), false); + assert.strictEqual(shouldAsk({ ...issue, message: 'fmt is not populated' }, false, [issue.message], false), true); + }); + + test('one open question per root', () => { + assert.strictEqual(shouldAsk(issue, false, [], true), false); + }); + + test('found among the status issues, absent once the build description no longer needs it', () => { + assert.deepStrictEqual(needsDownload({ issues: [{ code: 'model-stale', message: 'x' }, issue] }), issue); + assert.strictEqual(needsDownload({ issues: [{ code: 'model-stale', message: 'x' }] }), undefined); + assert.strictEqual(needsDownload({}), undefined); + }); +}); diff --git a/editors/zed/extension.toml b/editors/zed/extension.toml index 299f63d..ab5665f 100644 --- a/editors/zed/extension.toml +++ b/editors/zed/extension.toml @@ -1,6 +1,6 @@ id = "mcppls" name = "C++ Modules Language Server" -version = "0.0.5" +version = "0.0.6" schema_version = 1 description = "C++20 named modules that work on any compiler: navigation, completion and diagnostics from mcpp-language-server" repository = "https://github.com/Sunrisepeak/mcpp-language-server" diff --git a/mcpp.toml b/mcpp.toml index 89a8e7f..3c1661d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -34,7 +34,7 @@ libarchive = "3.8.7" [package] name = "mcpp-language-server" -version = "0.0.5" +version = "0.0.6" description = "Compiler-agnostic C++ modules language server" license = "Apache-2.0" authors = ["Sunrisepeak"] diff --git a/modules/base/src/version.cppm b/modules/base/src/version.cppm index f92d6db..9950d13 100644 --- a/modules/base/src/version.cppm +++ b/modules/base/src/version.cppm @@ -9,7 +9,7 @@ export namespace mcppls::base { // checked against mcpp.toml (the one source) by `mcppls-devtools version --check`, not kept in step by // hand. Three constants that lived here and nothing read were removed rather than left to drift: // the S1 profile version is spec::PROFILE_VERSION, the kit manifest version is spec::KIT_VERSION. -inline constexpr std::string_view VERSION { "0.0.5" }; +inline constexpr std::string_view VERSION { "0.0.6" }; // The clangd the payload ships. Checked against packaging/payload.lock.json by the same command. inline constexpr std::string_view CLANGD_VERSION { "23.1.0" }; // The oldest mcpp that answers `mcpp emit build-database` — the `mcpp.build-database` kind, which diff --git a/src/bin/conformance.cpp b/src/bin/conformance.cpp index 0a126af..a464816 100644 --- a/src/bin/conformance.cpp +++ b/src/bin/conformance.cpp @@ -1113,8 +1113,12 @@ class Scenario { const std::string wantedCommand { check.value("issue-command", std::string {}) }; const std::string wantedMessage { check.value("issue-message", std::string {}) }; // a part of the message const std::string wantedCategory { check.value("issue-category", std::string {}) }; // S3: code | engine | environment | project + // S3-4-16 (plan 2026-09-27 B-2): whether the issue lets a client offer to fetch what is missing. + const std::optional wantedAskOnline { check.contains("issue-ask-online") ? std::optional { check.value("issue-ask-online", false) } + : std::nullopt }; matched = matched && std::ranges::any_of(snapshot.value("issues", Json::array()), [&](const Json& issue) { if (issue.value("code", std::string {}) != issueCode->get()) return false; + if (wantedAskOnline && issue.value("askOnline", false) != *wantedAskOnline) return false; if (!wantedMessage.empty() && !issue.value("message", std::string {}).contains(wantedMessage)) return false; if (!wantedCategory.empty() && issue.value("category", std::string {}) != wantedCategory) return false; return wantedCommand.empty() || issue.value("command", Json::object()).value("command", std::string {}) == wantedCommand; diff --git a/src/bin/mockmcpp.cpp b/src/bin/mockmcpp.cpp index 94e810f..5bb57c1 100644 --- a/src/bin/mockmcpp.cpp +++ b/src/bin/mockmcpp.cpp @@ -181,6 +181,17 @@ int emit_build_database(std::span arguments) { return 1; } } + // Plan 2026-09-27 B-2: {"online": {...}} is what the producer answers when it may reach the network (no + // MCPP_OFFLINE): a fixture whose offline answer needs a download describes the project once the person lets it. + // {"provisionedWhen": ""}: once exists in the project (a build the person ran in their own terminal + // wrote it), the offline answer is the "online" one too -- what they fetched is there now (§9.2 rule 4). + const bool provisioned { recorded.contains("provisionedWhen") && recorded["provisionedWhen"].is_string() + && fs::exists(base::join_path(root, recorded["provisionedWhen"].get())) }; + if (const auto offline = mcppls::platform::env::get("MCPP_OFFLINE"); (provisioned || !offline || offline->empty() || *offline == "0") + && recorded.contains("online") && recorded["online"].is_object()) { + Json online = recorded["online"]; + recorded = std::move(online); + } expand_all(recorded, root); if (!recorded.contains("database")) { std::println("{}", envelope("mcpp.build-database", nullptr, recorded.value("diagnostics", Json::array()), Json::array({ "read-project" })).dump(2)); diff --git a/src/cli/commands.cpp b/src/cli/commands.cpp index 89c8c60..199b102 100644 --- a/src/cli/commands.cpp +++ b/src/cli/commands.cpp @@ -7,6 +7,7 @@ import mcppls.os; import mcppls.base.error; import mcppls.base.log; import mcppls.base.path; +import mcppls.project.boundary; import mcppls.base.version; import mcppls.platform.fs; import mcppls.platform.dirs; @@ -31,6 +32,8 @@ import mcppls.server.session; import mcppls.cli.options; import mcppls.cli.query; import mcppls.cli.cache; +import mcppls.cli.settings; +import mcppls.config.settings; import mcppls.orchestrator.report; import mcppls.orchestrator.kernel; import mcppls.bundle.writer; @@ -53,17 +56,7 @@ struct Loaded { }; // The root for a file: the nearest directory with a build description, else the file's directory. -std::string root_for(std::string_view file) { - std::string directory { base::parent_path(file) }; - while (true) { - for (std::string_view marker : { "mcpp.toml", "CMakeLists.txt", "compile_commands.json" }) { - if (platform::fs::is_regular_file(base::join_path(directory, marker))) return directory; - } - const std::string parent { base::parent_path(directory) }; - if (parent == directory) return base::parent_path(file); - directory = parent; - } -} +std::string root_for(std::string_view file) { return project::enclosing_project_root(base::parent_path(file)); } Loaded load(std::string_view root, const cmdline::ParsedArgs& args, bool trusted) { Loaded loaded; @@ -177,18 +170,30 @@ int command_check(const cmdline::ParsedArgs& args) { return result->exitCode == 0 && !result->timedOut ? 0 : 1; } -// The options a daemon started for an entry is started with: the entry's own. +// The options a daemon started for an entry is started with: the entry's own. Every registered +// server setting with a command-line spelling (config::settings registry, 0.0.6 plan §9 T1) is +// forwarded from whatever `args` itself carries; `model-*`, `idle-minutes` and `tool-timeout` +// configure this `mcp`/`daemon` command rather than the server the daemon starts, so they are not +// registry rows, and are forwarded by name here instead. std::vector daemon_arguments(const cmdline::ParsedArgs& args) { std::vector forwarded; - for (const std::string_view name : { "payload", "clangd", "kit", "mcpp", "database", "engine", "request-timeout", "log-level", "tool-timeout", - "model-source", "model-gateway", "model-name", "model-budget", "idle-minutes" }) { + for (const auto& row : config::settings::registry()) { + if (row.surface != config::settings::Surface::server || row.commandLine.empty()) continue; + const std::string flag { row.commandLine.substr(2) }; + if (row.kind == config::settings::Kind::boolean) { + if (args.is_flag_set(flag)) forwarded.push_back(row.commandLine); + continue; + } + if (row.kind == config::settings::Kind::list && row.commandLineRepeatable) { + for (const auto& value : args.option_or_empty(flag).values) forwarded.insert(forwarded.end(), { row.commandLine, value }); + continue; + } + if (auto value = args.value(flag)) forwarded.insert(forwarded.end(), { row.commandLine, *value }); + } + for (const std::string_view name : { "model-source", "model-gateway", "model-name", "model-budget", "idle-minutes", "tool-timeout" }) { if (auto value = args.value(name)) forwarded.insert(forwarded.end(), { std::format("--{}", name), *value }); } for (const auto& pattern : args.option_or_empty("model-exclude").values) forwarded.insert(forwarded.end(), { "--model-exclude", pattern }); - for (const auto& id : args.option_or_empty("disable-workaround").values) forwarded.insert(forwarded.end(), { "--disable-workaround", id }); - for (const std::string_view flag : { "untrusted", "no-discover" }) { - if (args.is_flag_set(flag)) forwarded.push_back(std::format("--{}", flag)); - } return forwarded; } @@ -210,21 +215,21 @@ int run(int argc, char* argv[]) { cmdline::App app { "mcppls" }; (void)app.version(std::string { base::VERSION }); (void)app.description("Compiler-agnostic C++ modules language server"); - (void)app.option("payload").takes_value().global(true).help("Payload directory with clangd and the semantic kit"); - (void)app.option("clangd").takes_value().global(true).help("clangd executable (overrides the payload)"); - (void)app.option("kit").takes_value().global(true).help("Semantic kit directory (overrides the payload)"); - (void)app.option("mcpp").takes_value().global(true).help("The mcpp executable for mcpp projects (default: found on PATH)"); - (void)app.option("database").takes_value().global(true).help("A workspace's own S1 build database, relative to its root"); - (void)app.option("untrusted").global(true).help("Do not run build tools or compilers"); - (void)app.option("no-discover").global(true).help("Do not look for compilers; loose sources use the semantic kit"); - (void)app.option("log-level").takes_value().global(true).help("debug | info | warning | error"); - (void)app.option("request-timeout").takes_value().global(true).help("Seconds before an engine request is answered without it"); - (void)app.option("build-tool").takes_value().global(true).help("How the project's build tool may be run: offline (default), online, off"); - (void)app.option("tool-environment").takes_value().global(true).help("Which environment build tools run in: auto (the login shell on POSIX) or editor"); - (void)app.option("producer-timeout").takes_value().global(true).help("Seconds a build tool may take to describe the project (default 60, or 600 when online)"); - (void)app.option("engine").takes_value().global(true).help("The core semantic engine: clangd (default) or none, mcppls's own module features only"); - (void)app.option("disable-workaround").takes_value().multiple().global(true).help("Turn off a registered clangd workaround (WA-CLANGD-, see mcppls report); repeatable"); + // Every global option below a registered server setting's command-line spelling (config::settings + // registry, 0.0.6 plan §9 T1: one definition, everything else -- `session_options`, this help + // text, `mcppls settings`, `mcppls report`, the docs -- derived from it). + for (const auto& row : config::settings::registry()) { + if (row.surface != config::settings::Surface::server || row.commandLine.empty()) continue; + const std::string flag { row.commandLine.substr(2) }; + (void)app.option(flag) + .global(true) + .help(row.summary) + .takes_value(row.kind != config::settings::Kind::boolean) + .multiple(row.kind == config::settings::Kind::list && row.commandLineRepeatable); + } // Language clients pass these by convention; this server always speaks over its standard streams. + // They are not settings (nothing about mcppls's own behaviour follows from either), so they are + // not registry rows. (void)app.option("stdio").global(true).help("Accepted for language clients; standard input and output are always used"); (void)app.option("clientProcessId").takes_value().global(true).help("Accepted for language clients; not used"); (void)app.action(serve); @@ -282,7 +287,7 @@ int run(int argc, char* argv[]) { const engine::PayloadPaths payload { engine::resolve_payload(engine::PayloadRequest { options.session.payloadDirectory, options.session.clangd, options.session.kit, options.session.engine }) }; Json report = orchestrator::make_report(std::move(roots), Json { { "name", "mcppls report" } }, options.session.engine, payload, false, - std::chrono::steady_clock::now() - started); + std::chrono::steady_clock::now() - started, options.session.settings.to_json()); const bool redact { !args.is_flag_set("no-redact") }; if (auto output = args.value("bundle")) { // Before the kernel shuts down: a second instance's private cache, with its engine database, goes with it. @@ -441,6 +446,7 @@ int run(int argc, char* argv[]) { (void)app.subcommand(impact_command(handled, status)); (void)app.subcommand(review_command(handled, status)); (void)app.subcommand(cache_command(handled, status)); + (void)app.subcommand(settings_command(handled, status)); cmdline::App versionCommand { "version" }; (void)versionCommand.description("Print the version"); diff --git a/src/cli/options.cpp b/src/cli/options.cpp index 3f61f71..684db16 100644 --- a/src/cli/options.cpp +++ b/src/cli/options.cpp @@ -4,6 +4,7 @@ import std; import mcpplibs.cmdline; import mcppls.base.log; import mcppls.base.path; +import mcppls.config.settings; import mcppls.platform.env; import mcppls.platform.fs; import mcppls.engine; @@ -33,45 +34,52 @@ orchestrator::EngineFactories engine_factories(const orchestrator::SessionOption clangd.verboseLog = options.verboseEngineLog; clangd.requestTimeout = options.requestTimeout; clangd.disabledWorkarounds = options.disabledWorkarounds; + clangd.primeImplementationUnits = options.primeImplementationUnits != "off"; return engine::clangd::make_engine(std::move(clangd)); }; return factories; } +// Every field below is read out of `options.settings` after its command-line layer (config settings +// §9 T1): the one place a flag's default, its validation and its precedence over a client are +// defined is the registry, not this function. `handle_initialize_` and `workspace/didChangeConfiguration` +// (`mcppls.server.session`) layer over the very same `Settings` object and re-derive these same +// fields the same way, through `orchestrator::Workspace::reload_with_options`. orchestrator::SessionOptions session_options(const cmdline::ParsedArgs& args) { orchestrator::SessionOptions options; - options.payloadDirectory = args.value("payload").value_or(""); - options.clangd = args.value("clangd").value_or(""); - options.kit = args.value("kit").value_or(""); - options.mcpp = args.value("mcpp").value_or(""); - options.database = args.value("database").value_or(""); - options.trusted = !args.is_flag_set("untrusted"); - options.discoverCompilers = !args.is_flag_set("no-discover"); - options.verboseEngineLog = args.value("log-level").value_or("") == "debug"; - if (auto chosen = args.value("engine")) { - options.engine = *chosen; - options.engineFromCommandLine = true; - } + options.settings.apply_command_line(args); + const auto& settings = options.settings; + options.payloadDirectory = settings.string_value("payload"); + options.clangd = settings.string_value("clangd"); + options.kit = settings.string_value("kit"); + options.mcpp = settings.string_value("mcpp"); + options.database = settings.string_value("database"); + options.compiler = settings.string_value("compiler"); + options.semanticKit = settings.string_value("semanticKit"); + options.trusted = !settings.bool_value("untrusted"); + options.discoverCompilers = settings.bool_value("discoverCompilers"); + options.verboseEngineLog = settings.string_value("logLevel") == "debug"; + options.engine = settings.string_value("engine"); options.engineFactories = engine_factories; - options.disabledWorkarounds = args.option_or_empty("disable-workaround").values; + options.disabledWorkarounds = settings.list_value("disableWorkaround"); + options.buildTool = settings.string_value("buildTool"); + options.toolEnvironment = settings.string_value("toolEnvironment"); + options.semanticTokensModules = settings.bool_value("semanticTokens.modules"); + options.semanticTokensModuleType = settings.bool_value("semanticTokens.moduleType"); + options.buildDiscovery = settings.string_value("buildDiscovery"); + options.buildDiscoveryProviders = settings.list_value("buildDiscovery.providers"); + options.buildDiscoveryAskBeforeDownload = settings.bool_value("buildDiscovery.askBeforeDownload"); + options.primeImplementationUnits = settings.string_value("index.primeImplementationUnits"); + // Design 4.2 sets this at a minute. A machine whose build tool is honestly slower needs it + // longer, and a test that means to watch the bound fire needs it much shorter. + options.producerTimeout = settings.seconds_value("producerTimeout"); + options.requestTimeout = std::chrono::duration_cast(settings.seconds_value("requestTimeout")); // This very program, for the reviews an editor asks for: named as the process started it, else found on PATH. if (const auto arguments = platform::env::arguments(); !arguments.empty()) { const std::string started { arguments.front() }; const bool hasDirectory { started.find('/') != std::string::npos || started.find('\\') != std::string::npos }; options.serverExecutable = hasDirectory ? absolute(started) : platform::env::find_executable(started).value_or(""); } - if (auto buildTool = args.value("build-tool"); buildTool && (*buildTool == "offline" || *buildTool == "online" || *buildTool == "off")) { - options.buildTool = *buildTool; - } - if (auto environment = args.value("tool-environment"); environment && (*environment == "auto" || *environment == "editor")) { - options.toolEnvironment = *environment; - } - // Design 4.2 sets this at a minute. A machine whose build tool is honestly slower needs it - // longer, and a test that means to watch the bound fire needs it much shorter. - options.producerTimeout = seconds_option(args, "producer-timeout", std::chrono::seconds { 0 }); - options.requestTimeout = seconds_option(args, "request-timeout", options.requestTimeout.count() > 0 - ? std::chrono::duration_cast(options.requestTimeout) - : std::chrono::seconds { 60 }); return options; } diff --git a/src/cli/query.cpp b/src/cli/query.cpp index 7a85aec..7a2b2e3 100644 --- a/src/cli/query.cpp +++ b/src/cli/query.cpp @@ -5,6 +5,7 @@ import nlohmann.json; import mcpplibs.cmdline; import mcppls.base.log; import mcppls.base.path; +import mcppls.project.boundary; import mcppls.base.text; import mcppls.base.uri; import mcppls.platform.fs; @@ -44,15 +45,7 @@ constexpr int EXIT_FAILED { 2 }; std::string root_of(const cmdline::ParsedArgs& args, std::string_view file) { if (auto root = args.value("root")) return absolute(*root); if (file.empty()) return platform::fs::current_directory(); - std::string directory { base::parent_path(absolute(file)) }; - while (true) { - for (std::string_view marker : { "mcpp.toml", "CMakeLists.txt", "compile_commands.json" }) { - if (platform::fs::is_regular_file(base::join_path(directory, marker))) return directory; - } - const std::string parent { base::parent_path(directory) }; - if (parent == directory) return platform::fs::current_directory(); - directory = parent; - } + return project::find_project_root(base::parent_path(absolute(file))).value_or(platform::fs::current_directory()); } struct Position { diff --git a/src/cli/settings.cpp b/src/cli/settings.cpp new file mode 100644 index 0000000..479c6db --- /dev/null +++ b/src/cli/settings.cpp @@ -0,0 +1,40 @@ +module mcppls.cli.settings; + +import std; +import nlohmann.json; +import mcpplibs.cmdline; +import mcppls.config.settings; + +namespace mcppls::cli { + +namespace { + +namespace cmdline = mcpplibs::cmdline; +namespace settings = mcppls::config::settings; + +} // namespace + +cmdline::App settings_command(bool& handled, int& status) { + cmdline::App command { "settings" }; + (void)command.description("Print the configuration registry: every setting mcppls understands, its default, and how to spell it"); + (void)command.option("format").takes_value().help("markdown (default) | json"); + (void)command.option("lang").takes_value().help("en (default) | zh-CN; markdown only"); + (void)command.action([&handled, &status](const cmdline::ParsedArgs& args) { + handled = true; + const std::string format { args.value("format").value_or("markdown") }; + const auto rows = settings::registry(); + if (format == "json") { + std::println("{}", settings::registry_to_json(rows).dump(2)); + } else if (format == "markdown") { + std::println("{}", settings::to_markdown(rows, args.value("lang").value_or("en"))); + } else { + std::println(std::cerr, "settings: unknown format {}; use markdown or json", format); + status = 2; + return; + } + status = 0; + }); + return command; +} + +} // namespace mcppls::cli diff --git a/src/cli/settings.cppm b/src/cli/settings.cppm new file mode 100644 index 0000000..a8f916c --- /dev/null +++ b/src/cli/settings.cppm @@ -0,0 +1,16 @@ +// mcppls settings [--format markdown|json] [--lang en|zh-CN] +// +// Prints the configuration registry (0.0.6 plan §9 T1): the same rows `docs/30-settings.md` and its +// zh-CN mirror embed between their `` / `` markers +// (`--format markdown`, the default, `--lang` choosing which of a row's two summaries to print), or +// every field of every row for a machine reader (`--format json`). +export module mcppls.cli.settings; + +import std; +import mcpplibs.cmdline; + +export namespace mcppls::cli { + +mcpplibs::cmdline::App settings_command(bool& handled, int& status); + +} // namespace mcppls::cli diff --git a/src/config/settings.cpp b/src/config/settings.cpp new file mode 100644 index 0000000..bbbb312 --- /dev/null +++ b/src/config/settings.cpp @@ -0,0 +1,746 @@ +module mcppls.config.settings; + +import std; +import nlohmann.json; +import mcpplibs.cmdline; +import mcppls.base.text; +import mcppls.platform.env; + +namespace mcppls::config::settings { + +using Json = nlohmann::json; +namespace cmdline = mcpplibs::cmdline; + +namespace { + +// A category is grouped and headed by this table, in this order, in both the generated docs and +// `mcppls settings --format markdown`; a row's `category` is one of these keys, never displayed +// directly. Keeping the key and the two headings together is what makes adding a category (rather +// than misspelling an existing one) the only way a row's table silently stops rendering. +struct CategoryHeading { + std::string_view key; + std::string_view en; + std::string_view zh; +}; + +constexpr std::array CATEGORIES { { + { "build", "Project and build tools", "项目与构建工具" }, + { "engines", "Engines", "引擎" }, + { "editor", "Editor experience", "编辑器体验" }, + { "diagnostics", "Diagnostics and logging", "诊断与日志" }, + { "ai", "AI review", "AI 评审" }, + { "paths", "Paths", "路径" }, +} }; + +bool is_zh(std::string_view lang) { return lang.starts_with("zh"); } + +// The registry itself (0.0.6 plan §9 T1). One row per configurable behaviour; see `Setting` in +// `settings.cppm` for what each field means. Ordered as the generated docs list it: by category in +// `CATEGORIES`' order, each category's own rows in the order below. +const std::vector& shipped_registry() { + static const std::vector rows { + // ---- Project and build tools ------------------------------------------------------ + Setting { + .key = "buildTool", .kind = Kind::enumeration, .values = { "offline", "online", "off" }, .defaultValue = "offline", + .commandLine = "--build-tool", .surface = Surface::server, .applies = Applies::reload, .category = "build", .since = "0.0.1", + .summary = "How the project's build tool may be run. `offline`: run it without the network -- if it then cannot describe " + "the build without downloading something, the status says what is missing and offers to run it in your terminal. " + "`online`: let it reach the network, with ten minutes instead of one. `off`: never run it; the build system is " + "still detected and its own generated files are still read (see `buildDiscovery` for turning that off too).", + .summaryZh = "项目构建工具的运行方式。`offline`:不联网运行——如果构建工具因此无法在不下载东西的情况下描述构建,状态栏会说明缺什么," + "并提议在终端里运行它。`online`:允许联网,超时时间从一分钟延长到十分钟。`off`:从不运行构建工具;仍会探测构建系统、" + "仍读取它已有的产物(要连探测也关掉,见 `buildDiscovery`)。", + .clientConfigurable = true, + }, + Setting { + .key = "toolEnvironment", .kind = Kind::enumeration, .values = { "auto", "editor" }, .defaultValue = "auto", + .commandLine = "--tool-environment", .surface = Surface::server, .applies = Applies::restart, .category = "build", .since = "0.0.1", + .summary = "Which environment build tools are started in. `auto` reads your login shell's environment once, in the " + "background, on POSIX -- an editor started from a desktop entry or a Dock icon carries none of your shell " + "configuration, so without this the build tool it finds may not be the one your terminal finds. On Windows the " + "editor's environment already matches the terminal's. `editor` always uses the editor process's environment.", + .summaryZh = "构建工具在哪个环境中启动。`auto` 会在后台读取一次你登录 shell 的环境(仅限 POSIX 系统)——从桌面项或 Dock 图标启动的" + "编辑器不带任何 shell 配置,没有这个选项,它找到的构建工具可能就不是你终端里找到的那个。在 Windows 上,编辑器的环境本就" + "和终端一致。`editor` 始终使用编辑器进程自身的环境。", + .clientConfigurable = true, + }, + Setting { + .key = "producerTimeout", .kind = Kind::seconds, .defaultValue = "0", .commandLine = "--producer-timeout", + .surface = Surface::server, .applies = Applies::reload, .category = "build", .since = "0.0.1", + .summary = "How long a build tool may take to describe the project. `0`, the default, uses the design's own bound (a " + "minute offline, ten minutes once `buildTool` is `online`); set it to watch that bound work, or longer for a " + "genuinely slower build.", + .summaryZh = "构建工具描述项目最多可以花多长时间。默认 `0` 使用设计本身的限制(离线一分钟,`buildTool` 为 `online` 时十分钟);" + "调短可以观察限制是否生效,构建确实慢就调长。", + }, + Setting { + .key = "untrusted", .kind = Kind::boolean, .defaultValue = "false", .commandLine = "--untrusted", .surface = Surface::server, + .applies = Applies::restart, .category = "build", .since = "0.0.1", + .summary = "Run no build tool and no compiler; an untrusted workspace is also read as though `buildDiscovery` were `off`.", + .summaryZh = "不运行任何构建工具,也不运行编译器;一个不受信任的工作区也等同于 `buildDiscovery` 为 `off`。", + }, + Setting { + .key = "discoverCompilers", .kind = Kind::boolean, .defaultValue = "true", .commandLine = "--no-discover", + .commandLineNegated = true, .surface = Surface::server, .applies = Applies::reload, .category = "build", .since = "0.0.1", + .summary = "Look for a compiler on the machine for a source the build description does not cover. Off: such a source " + "uses the semantic kit instead.", + .summaryZh = "为构建描述没有覆盖到的源码在本机查找编译器。关闭后,这类源码改用语义工具包。", + }, + Setting { + .key = "buildDiscovery", .kind = Kind::enumeration, .values = { "auto", "off" }, .defaultValue = "auto", + .commandLine = "--build-discovery", .surface = Surface::server, .applies = Applies::reload, .category = "build", + .since = "0.0.6", + .summary = "Whether the project's build system is detected at all. `off`: nothing is read or run " + "implicitly -- only an explicitly configured `database`, else sources are scanned. `buildTool` still governs " + "whether a detected build tool may be *run*; this governs whether it is looked for in the first place.", + .summaryZh = "是否探测项目的构建系统。`off`:不隐式读取或执行任何东西——只用明确配置的 `database`,否则" + "扫描源码。`buildTool` 管的是探测到的构建工具能不能*执行*;这个开关管的是要不要去探测它。", + .clientConfigurable = true, + }, + Setting { + .key = "buildDiscovery.providers", .kind = Kind::list, + .values = { "mcpp", "cmake", "xmake", "meson", "compile-commands" }, + .defaultValue = "mcpp,cmake,xmake,meson,compile-commands", .commandLine = "--build-discovery-providers", + .surface = Surface::server, .applies = Applies::reload, .category = "build", .since = "0.0.6", + .summary = "Which build system providers `buildDiscovery` may use; leave one out to stop mcppls from detecting it (for " + "example, to use only a CMake build directory that already exists and never let xmake run).", + .summaryZh = "`buildDiscovery` 可以使用哪些构建系统提供者;从中去掉某个提供者即停用它的探测(例如只想用已有的 CMake 构建目录," + "不要 xmake)。", + .clientConfigurable = true, + }, + Setting { + .key = "buildDiscovery.askBeforeDownload", .kind = Kind::boolean, .defaultValue = "true", .surface = Surface::server, + .applies = Applies::immediately, .category = "build", .since = "0.0.6", + .summary = "When the build tool needs a download to finish describing the project, a client may offer to fetch it. " + "Off: the status says a download is needed, and nothing asks.", + .summaryZh = "当构建工具需要下载才能完成描述项目时,客户端可以提议去获取它。关闭后,状态栏说明需要下载,但不会再询问。", + .clientConfigurable = true, + }, + // ---- Engines ------------------------------------------------------------------------ + Setting { + .key = "engine", .kind = Kind::enumeration, .values = { "clangd", "none" }, .defaultValue = "clangd", + .commandLine = "--engine", .surface = Surface::server, .applies = Applies::restart, .category = "engines", .since = "0.0.1", + .summary = "The core semantic engine. mcppls's own module engine always runs beside it; `none` means module-level " + "features only.", + .summaryZh = "核心引擎。无论如何,mcppls 自己的模块引擎都会运行;`none` 表示只提供模块相关功能。", + .clientConfigurable = true, + }, + Setting { + .key = "compiler", .kind = Kind::string, .defaultValue = "", .commandLine = "--compiler", .surface = Surface::server, + .applies = Applies::reload, .category = "engines", .since = "0.0.1", + .summary = "Use this compiler for module semantics instead of what was detected: an absolute path, a name on `PATH`, or " + "`kit` to force the bundled semantic kit. Empty means discovered automatically.", + .summaryZh = "为模块语义使用这个编译器,而不是检测到的那个:可以是绝对路径、`PATH` 上的名字,或 `kit`(强制使用内置的语义工具" + "包)。空表示自动检测。", + .clientConfigurable = true, + }, + Setting { + .key = "semanticKit", .kind = Kind::enumeration, .values = { "auto", "off" }, .defaultValue = "auto", + .commandLine = "--semantic-kit", .surface = Surface::server, .applies = Applies::reload, .category = "engines", + .since = "0.0.1", + .summary = "Whether the bundled standard library kit may be used at all: `auto`, when no compiler is found; `off`, " + "never (without a compiler, only module-level features remain).", + .summaryZh = "内置的标准库工具包是否可以被使用:`auto` 在没有找到编译器时使用;`off` 从不使用(没有编译器时只剩模块相关功能)。", + .clientConfigurable = true, + }, + Setting { + .key = "requestTimeout", .kind = Kind::seconds, .defaultValue = "60", .commandLine = "--request-timeout", + .surface = Surface::server, .applies = Applies::restart, .category = "engines", .since = "0.0.1", + .summary = "How long an engine request may take before it is answered without the engine. A request a person waits " + "for (hover, definition, completion and the like) waits at most 30s in all, including while clangd starts or " + "prepares its modules, and is then answered by mcppls's own engine.", + .summaryZh = "一个引擎请求最多等待多久,超时后不经该引擎就给出答复。用户在等的请求(悬停、跳转、补全等)总共最多等 30 秒," + "clangd 启动或准备模块期间也算在内,之后由 mcppls 自己的引擎答复。", + }, + Setting { + .key = "MCPPLS_ENGINE_ARGUMENTS", .kind = Kind::string, .defaultValue = "", .surface = Surface::environment, + .applies = Applies::restart, .category = "engines", .since = "0.0.1", + .summary = "Extra arguments appended to clangd's own command line, for troubleshooting " + "(e.g. `-j=8 --background-index-priority=background`).", + .summaryZh = "追加到 clangd 自身命令行末尾的额外参数,用于排查问题(例如 `-j=8 --background-index-priority=background`)。", + }, + // ---- Editor experience ---------------------------------------------------------------- + Setting { + .key = "semanticTokens.modules", .kind = Kind::boolean, .defaultValue = "true", .surface = Surface::server, + .applies = Applies::restart, .category = "editor", .since = "0.0.4", + .summary = "Color `import`, `module`, `export` and module names from the server's semantic tokens. Off: only the " + "grammar's colors.", + .summaryZh = "用服务端的语义 token 给 `import`、`module`、`export` 和模块名上色。关闭后只用语法文件的颜色。", + .clientConfigurable = true, + }, + Setting { + .key = "semanticTokens.moduleType", .kind = Kind::boolean, .defaultValue = "false", .surface = Surface::server, + .applies = Applies::restart, .category = "editor", .since = "0.0.4", + .summary = "A client declares it knows the custom `module` semantic token type and the `partition` modifier; off for " + "every client but this one, since none else advertises it. Not a package.json " + "setting: VS Code's own extension always declares it, fixed, because it contributes that token type itself.", + .summaryZh = "客户端声明自己认得自定义的 `module` 语义 token 类型和 `partition` 修饰符;除本仓库的 " + "VS Code 扩展外都关闭,因为没有别的客户端会声明它。不是 package.json 里的设置:VS Code 扩展自己贡献了这个 token " + "类型,因此固定声明为开。", + }, + Setting { + .key = "completion.triggerOnSpace", .kind = Kind::boolean, .defaultValue = "true", .surface = Surface::server, + .applies = Applies::restart, .category = "editor", .since = "0.0.5", + .summary = "Show the module list as soon as a space is typed after `import` or `export import`. A space anywhere else " + "never reaches the server. A client that says nothing gets this only when it identifies itself as VS Code or " + "a fork of it; every other client opts in with `initializationOptions.completion.triggerOnSpace: true`.", + .summaryZh = "在 `import` 或 `export import` 后输入空格时立即弹出模块列表;其他位置的空格不会发给服务端。什么都不说的客户端" + "只有在自证是 VS Code 或其分支时才会得到这个行为;其他客户端需要用 " + "`initializationOptions.completion.triggerOnSpace: true` 主动开启。", + .clientConfigurable = true, + }, + Setting { + .key = "index.primeImplementationUnits", .kind = Kind::enumeration, .values = { "auto", "off" }, .defaultValue = "auto", + .commandLine = "--prime-implementation-units", .surface = Surface::server, .applies = Applies::restart, + .category = "editor", .since = "0.0.6", + .summary = "Build a module's implementation units in clangd in the background, a few at a time, so go-to-definition " + "reaches a definition that only an implementation unit has, before that file was ever opened. clangd's own " + "background index cannot see a module unit's imports (WA-CLANGD-008). `off`: only the units a definition " + "request searches, and the files you open, are indexed for this.", + .summaryZh = "在后台让 clangd 逐个构建模块的实现单元(每次少量),这样即使实现文件从没打开过,跳到定义也能到达只在实现单元里的" + "定义。clangd 自己的后台索引看不到模块单元的导入(WA-CLANGD-008)。`off`:只有一次跳转请求所搜索的单元和你打开的" + "文件会为此被索引。", + .clientConfigurable = true, + }, + Setting { + .key = "detectConflicts", .kind = Kind::boolean, .defaultValue = "true", .surface = Surface::client, + .applies = Applies::immediately, .category = "editor", .since = "0.0.1", + .summary = "Offer once to turn off another C++ extension's language features in this workspace, and say so when one " + "becomes active later. VS Code only: no other client arbitrates between language servers.", + .summaryZh = "在此工作区中提议关闭另一个 C++ 扩展的语言功能(只提议一次),之后又有冲突扩展启用时会提示。仅限 VS Code:其他" + "客户端不会在多个语言服务端之间做取舍。", + .clientConfigurable = true, + }, + // ---- Diagnostics and logging ---------------------------------------------------------- + Setting { + .key = "logLevel", .kind = Kind::enumeration, .values = { "debug", "info", "warning", "error" }, .defaultValue = "info", + .commandLine = "--log-level", .surface = Surface::server, .applies = Applies::restart, .category = "diagnostics", + .since = "0.0.1", + .summary = "The server's own log level.", + .summaryZh = "服务端自身的日志级别。", + }, + Setting { + .key = "disableWorkaround", .kind = Kind::list, .defaultValue = "", .commandLine = "--disable-workaround", + .commandLineRepeatable = true, .surface = Surface::server, .applies = Applies::restart, .category = "diagnostics", + .since = "0.0.4", + .summary = "Turn off a registered clangd workaround (`WA-CLANGD-`; the register of upstream defects is issue #24), to " + "see whether it is still needed; repeatable. `mcppls report` lists every registered workaround under " + "`engines[].details.workarounds`.", + .summaryZh = "关掉一个针对 clangd 缺陷登记的规避措施(`WA-CLANGD-`;上游缺陷登记在 issue #24),用来确认它是否还有" + "必要;可重复。`mcppls report` 在 `engines[].details.workarounds` 下列出所有登记过的规避措施。", + }, + Setting { + .key = "trace.server", .kind = Kind::enumeration, .values = { "off", "messages", "verbose" }, .defaultValue = "off", + .surface = Surface::client, .applies = Applies::immediately, .category = "diagnostics", .since = "0.0.1", + .summary = "Log the LSP traffic to the C++ Modules output channel (at Trace level); `verbose` adds the server's debug " + "log (at Debug level, by also passing `--log-level debug`). Set the channel's own log level to see them.", + .summaryZh = "把 LSP 通信记录到 C++ Modules 输出通道(Trace 级别);`verbose` 还会打开服务端的 debug 日志(通过附加 " + "`--log-level debug`,Debug 级别)。要看到它们,需把该输出通道的日志级别调到对应级别。", + .clientConfigurable = true, + }, + Setting { + .key = "MCPPLS_LOG_LEVEL", .kind = Kind::enumeration, .values = { "debug", "info", "warning", "error" }, .defaultValue = "", + .surface = Surface::environment, .applies = Applies::restart, .category = "diagnostics", .since = "0.0.1", + .summary = "Overrides the log level the VS Code extension starts the server with, ahead of `trace.server`.", + .summaryZh = "覆盖 VS Code 扩展启动服务端时使用的日志级别,优先于 `trace.server`。", + }, + // ---- AI review -------------------------------------------------------------------- + Setting { + .key = "ai.enabled", .kind = Kind::boolean, .defaultValue = "false", .surface = Surface::client, + .applies = Applies::immediately, .category = "ai", .since = "0.0.1", + .summary = "Show the AI-era features: Review Changes reviews the workspace's changes against `HEAD` with mcppls's " + "rules and shows the findings, with their evidence, as problems. Nothing is sent to a model unless this is " + "on and a model source is separately configured.", + .summaryZh = "显示 AI 时代的功能:Review Changes 用 mcppls 的规则对照 `HEAD` 审查工作区的变更,并把发现连同证据以问题的形式" + "展示。除非这里开启并且另行配置了模型来源,否则不会向任何模型发送内容。", + .clientConfigurable = true, + }, + // ---- Paths -------------------------------------------------------------------------- + Setting { + .key = "database", .kind = Kind::path, .defaultValue = "", .commandLine = "--database", .surface = Surface::server, + .applies = Applies::reload, .category = "paths", .since = "0.0.1", + .summary = "A workspace's own S1 build database, relative to its root, used instead of detecting one.", + .summaryZh = "工作区自己的 S1 构建数据库,相对于其根目录,用它代替探测。", + }, + Setting { + .key = "mcpp", .kind = Kind::path, .defaultValue = "", .commandLine = "--mcpp", .surface = Surface::server, + .applies = Applies::reload, .category = "paths", .since = "0.0.1", + .summary = "The `mcpp` executable for mcpp projects; empty means found on `PATH`.", + .summaryZh = "mcpp 项目所用的 `mcpp` 可执行文件;空表示在 `PATH` 上查找。", + }, + Setting { + .key = "payload", .kind = Kind::path, .defaultValue = "", .commandLine = "--payload", .surface = Surface::server, + .applies = Applies::restart, .category = "paths", .since = "0.0.1", + .summary = "Payload directory with clangd and the semantic kit; overridden per-file by `clangd` and `kit` below.", + .summaryZh = "包含 clangd 和语义工具包的 payload 目录;下面的 `clangd` 和 `kit` 可以分别覆盖其中一项。", + }, + Setting { + .key = "clangd", .kind = Kind::path, .defaultValue = "", .commandLine = "--clangd", .surface = Surface::server, + .applies = Applies::restart, .category = "paths", .since = "0.0.1", + .summary = "clangd executable, overriding the one the payload carries.", + .summaryZh = "clangd 可执行文件,覆盖 payload 自带的那一份。", + }, + Setting { + .key = "kit", .kind = Kind::path, .defaultValue = "", .commandLine = "--kit", .surface = Surface::server, + .applies = Applies::restart, .category = "paths", .since = "0.0.1", + .summary = "Semantic kit directory, overriding the one the payload carries.", + .summaryZh = "语义工具包目录,覆盖 payload 自带的那一份。", + }, + Setting { + .key = "MCPPLS_CACHE_DIR", .kind = Kind::path, .defaultValue = "", .surface = Surface::environment, + .applies = Applies::restart, .category = "paths", .since = "0.0.1", + .summary = "Overrides the whole cache directory mcppls otherwise picks under the user's cache home (workspace models, " + "toolchain probes, logs, diagnostic bundles).", + .summaryZh = "覆盖 mcppls 原本在用户缓存目录下选定的整个缓存目录(工作区模型、工具链探测结果、日志、诊断包)。", + }, + }; + return rows; +} + +// A value's string form as read out of `object`, per the row's `Kind` (T1: `initializationOptions` +// and `didChangeConfiguration` may write a boolean, a number, a string or an array of strings, +// depending on the row). Null when `object`'s JSON type does not fit the row's kind at all. +std::optional json_to_text(const Setting& row, const Json& object) { + switch (row.kind) { + case Kind::boolean: + return object.is_boolean() ? std::optional { std::string { object.get() ? "true" : "false" } } : std::nullopt; + case Kind::seconds: + if (object.is_number_integer()) return std::to_string(object.get()); + if (object.is_string()) return object.get(); + return std::nullopt; + case Kind::list: { + if (object.is_string()) return object.get(); + if (!object.is_array()) return std::nullopt; + std::vector parts; + for (const auto& item : object) { + if (!item.is_string()) return std::nullopt; + parts.push_back(item.get()); + } + return base::join(parts, ","); + } + case Kind::enumeration: + case Kind::string: + case Kind::path: + return object.is_string() ? std::optional { object.get() } : std::nullopt; + } + return std::nullopt; +} + +// Validates `text` (already in the row's own string form) against its `Kind`'s vocabulary, +// returning the canonical stored text, or null with a `Problem` appended to `problems` when it is +// outside that vocabulary -- the caller then keeps the row at whatever it already was (T1: an +// unknown value never silently takes effect). +std::optional validate(const Setting& row, std::string_view text, std::vector& problems) { + switch (row.kind) { + case Kind::boolean: + if (text == "true" || text == "false") return std::string { text }; + problems.push_back({ row.key, std::format("{} is not true or false; keeping the default", text) }); + return std::nullopt; + case Kind::enumeration: + if (std::ranges::find(row.values, text) != row.values.end()) return std::string { text }; + problems.push_back( + { row.key, std::format("{} is not one of {}; keeping the default", text, base::join(row.values, ", ")) }); + return std::nullopt; + case Kind::seconds: { + // Full consumption, no sign: `stoll` alone would silently accept "10 minutes please". + if (!text.empty() && std::ranges::all_of(text, [](char c) { return c >= '0' && c <= '9'; })) { + try { + return std::to_string(std::stoll(std::string { text })); + } catch (...) { + } + } + problems.push_back({ row.key, std::format("{} is not a non-negative number of seconds; keeping the default", text) }); + return std::nullopt; + } + case Kind::list: { + std::vector members; + for (auto piece : base::split(text, ',')) { + const auto trimmed = base::trim(piece); + if (trimmed.empty()) continue; + if (!row.values.empty() && std::ranges::find(row.values, trimmed) == row.values.end()) { + problems.push_back({ row.key, + std::format("{} is not one of {}; keeping the default", trimmed, base::join(row.values, ", ")) }); + return std::nullopt; + } + members.emplace_back(trimmed); + } + return base::join(members, ","); + } + case Kind::string: + case Kind::path: + return std::string { text }; + } + return std::nullopt; +} + +// An object's own key `dottedKey` when it has one literally (covers a plain key and the dotted-key +// form, `{"semanticTokens.modules": false}`), else the same path walked as nested objects +// (`{"semanticTokens": {"modules": false}}`) -- T1 accepts either from a client. +const Json* find_dotted_or_nested(const Json& root, std::string_view dottedKey) { + if (!root.is_object()) return nullptr; + if (auto it = root.find(std::string { dottedKey }); it != root.end()) return &*it; + const Json* cursor { &root }; + std::size_t start { 0 }; + while (true) { + const auto dot = dottedKey.find('.', start); + const std::string_view segment { dottedKey.substr(start, dot == std::string_view::npos ? std::string_view::npos : dot - start) }; + if (!cursor->is_object()) return nullptr; + auto it = cursor->find(std::string { segment }); + if (it == cursor->end()) return nullptr; + if (dot == std::string_view::npos) return &*it; + cursor = &*it; + start = dot + 1; + } +} + +// `object`, or its nested `mcppls` object when it has one: both `initializationOptions` and +// `didChangeConfiguration.settings` may or may not carry that wrapper (T1). +const Json& unwrap_mcppls(const Json& object) { + if (object.is_object()) { + if (auto it = object.find("mcppls"); it != object.end() && it->is_object()) return *it; + } + return object; +} + +const Json* find_setting_json(const Json& scope, const Setting& row) { + if (const Json* found = find_dotted_or_nested(scope, row.key)) return found; + for (const auto& alias : row.aliases) { + if (const Json* found = find_dotted_or_nested(scope, alias)) return found; + } + return nullptr; +} + +Json typed_json(const Setting& row, const std::string& text) { + switch (row.kind) { + case Kind::boolean: + return text == "true"; + case Kind::seconds: + try { + return std::stoll(text); + } catch (...) { + return text; + } + case Kind::list: { + Json array = Json::array(); + for (auto piece : base::split(text, ',')) { + const auto trimmed = base::trim(piece); + if (!trimmed.empty()) array.push_back(std::string { trimmed }); + } + return array; + } + case Kind::enumeration: + case Kind::string: + case Kind::path: + return text; + } + return text; +} + +} // namespace + +std::string_view to_string(Kind kind) { + switch (kind) { + case Kind::boolean: return "boolean"; + case Kind::enumeration: return "enumeration"; + case Kind::string: return "string"; + case Kind::path: return "path"; + case Kind::seconds: return "seconds"; + case Kind::list: return "list"; + } + return "?"; +} + +std::string_view to_string(Surface surface) { + switch (surface) { + case Surface::server: return "server"; + case Surface::client: return "client"; + case Surface::environment: return "environment"; + } + return "?"; +} + +std::string_view to_string(Applies applies) { + switch (applies) { + case Applies::restart: return "restart"; + case Applies::reload: return "reload"; + case Applies::immediately: return "immediately"; + } + return "?"; +} + +std::string_view to_string(Origin origin) { + switch (origin) { + case Origin::defaulted: return "default"; + case Origin::environment: return "environment"; + case Origin::commandLine: return "command-line"; + case Origin::client: return "client"; + case Origin::clientUpdated: return "client-updated"; + } + return "?"; +} + +std::span registry() { return shipped_registry(); } + +const Setting* find(std::span rows, std::string_view key) { + for (const auto& row : rows) { + if (row.key == key) return &row; + if (std::ranges::find(row.aliases, key) != row.aliases.end()) return &row; + } + return nullptr; +} + +Settings::Settings(std::span rows) : rows_(rows) { + for (const auto& row : rows_) { + if (row.surface == Surface::environment) { + if (auto value = platform::env::get(row.key)) { + values_[row.key] = { *value, Origin::environment }; + continue; + } + } + values_[row.key] = { row.defaultValue, Origin::defaulted }; + } +} + +std::span Settings::rows() const { return rows_; } + +const Value& Settings::value(std::string_view key) const { + static const Value fallback {}; + const auto it = values_.find(key); + return it != values_.end() ? it->second : fallback; +} + +std::string Settings::string_value(std::string_view key) const { return value(key).text; } + +bool Settings::bool_value(std::string_view key) const { return value(key).text == "true"; } + +std::chrono::seconds Settings::seconds_value(std::string_view key) const { + try { + return std::chrono::seconds { std::stoll(value(key).text) }; + } catch (...) { + return std::chrono::seconds { 0 }; + } +} + +std::vector Settings::list_value(std::string_view key) const { + std::vector members; + for (auto piece : base::split(value(key).text, ',')) { + const auto trimmed = base::trim(piece); + if (!trimmed.empty()) members.emplace_back(trimmed); + } + return members; +} + +Origin Settings::origin(std::string_view key) const { return value(key).origin; } + +const std::vector& Settings::problems() const { return problems_; } + +void Settings::apply_command_line(const cmdline::ParsedArgs& args) { + for (const auto& row : rows_) { + if (row.surface == Surface::environment || row.commandLine.empty()) continue; + // Every command-line spelling here is "--name"; cmdline itself is asked about "name". + const std::string flag { row.commandLine.substr(2) }; + if (row.kind == Kind::boolean) { + if (!args.is_flag_set(flag)) continue; + values_[row.key] = { row.commandLineNegated ? "false" : "true", Origin::commandLine }; + continue; + } + if (row.kind == Kind::list && row.commandLineRepeatable) { + const auto given = args.option_or_empty(flag).values; + if (given.empty()) continue; + if (auto validated = validate(row, base::join(given, ","), problems_)) { + values_[row.key] = { *validated, Origin::commandLine }; + } + continue; + } + if (auto raw = args.value(flag)) { + if (auto validated = validate(row, *raw, problems_)) values_[row.key] = { *validated, Origin::commandLine }; + } + } +} + +void Settings::apply_initialization_options(const Json& initializationOptionsOrParams) { + const Json* init { &initializationOptionsOrParams }; + if (initializationOptionsOrParams.is_object()) { + if (auto it = initializationOptionsOrParams.find("initializationOptions"); + it != initializationOptionsOrParams.end() && it->is_object()) { + init = &*it; + } + } + if (!init->is_object()) return; + const Json& scope { unwrap_mcppls(*init) }; + for (const auto& row : rows_) { + if (row.surface == Surface::environment) continue; + if (values_[row.key].origin == Origin::commandLine) continue; + const Json* found { find_setting_json(scope, row) }; + if (found == nullptr) continue; + auto text { json_to_text(row, *found) }; + if (!text) { + problems_.push_back({ row.key, std::format("initializationOptions carries {} as the wrong kind of value; keeping {}", + row.key, values_[row.key].text) }); + continue; + } + if (auto validated = validate(row, *text, problems_)) values_[row.key] = { *validated, Origin::client }; + } +} + +ChangeResult Settings::apply_configuration_change(const Json& params) { + ChangeResult result; + const Json* settingsObject { ¶ms }; + if (params.is_object()) { + if (auto it = params.find("settings"); it != params.end()) settingsObject = &*it; + } + if (!settingsObject->is_object()) return result; + const Json& scope { unwrap_mcppls(*settingsObject) }; + for (const auto& row : rows_) { + if (row.surface == Surface::environment) continue; + auto it = values_.find(row.key); + if (it == values_.end() || it->second.origin == Origin::commandLine) continue; + const Json* found { find_setting_json(scope, row) }; + if (found == nullptr) continue; + auto text { json_to_text(row, *found) }; + if (!text) { + problems_.push_back( + { row.key, std::format("didChangeConfiguration carries {} as the wrong kind of value; keeping {}", row.key, it->second.text) }); + continue; + } + auto validated { validate(row, *text, problems_) }; + const std::string newValue { validated.value_or(row.defaultValue) }; + const bool changed { newValue != it->second.text }; + it->second = { newValue, Origin::clientUpdated }; + if (!changed) continue; + result.changedKeys.push_back(row.key); + if (row.applies == Applies::restart) result.restartKeys.push_back(row.key); + else if (row.applies == Applies::reload) result.reloadKeys.push_back(row.key); + } + return result; +} + +Json Settings::to_json() const { + Json result = Json::object(); + for (const auto& row : rows_) { + const auto& current = value(row.key); + result[row.key] = Json { { "value", typed_json(row, current.text) }, { "origin", std::string { to_string(current.origin) } } }; + } + Json problems = Json::array(); + for (const auto& problem : problems_) problems.push_back(Json { { "key", problem.key }, { "message", problem.message } }); + result["problems"] = std::move(problems); + return result; +} + +namespace { + +std::string setting_cell(const Setting& row) { + return row.surface == Surface::environment ? std::format("`{}`", row.key) : std::format("`mcppls.{}`", row.key); +} + +std::string backticked_join(std::span values, std::string_view separator) { + std::vector quoted; + quoted.reserve(values.size()); + for (const auto& value : values) quoted.push_back(std::format("`{}`", value)); + return base::join(quoted, separator); +} + +std::string values_cell(const Setting& row, bool zh) { + switch (row.kind) { + case Kind::boolean: return "`true`, `false`"; + case Kind::enumeration: return backticked_join(row.values, ", "); + case Kind::list: + if (row.values.empty()) return zh ? "`WA-CLANGD-`(可重复)" : "`WA-CLANGD-` (repeatable)"; + return backticked_join(row.values, ", ") + (row.commandLineRepeatable ? "" : (zh ? "(逗号分隔)" : " (comma-separated)")); + case Kind::seconds: return zh ? "非负整数(秒)" : "a non-negative number of seconds"; + case Kind::string: return zh ? "字符串" : "a string"; + case Kind::path: return zh ? "路径" : "a path"; + } + return "?"; +} + +std::string default_cell(const Setting& row, bool zh) { + switch (row.kind) { + case Kind::boolean: + case Kind::seconds: return std::format("`{}`", row.defaultValue); + case Kind::enumeration: + case Kind::string: + case Kind::path: return row.defaultValue.empty() ? (zh ? "(空)" : "*(empty)*") : std::format("`{}`", row.defaultValue); + case Kind::list: { + if (row.defaultValue.empty()) return zh ? "(无)" : "*(none)*"; + std::vector members; + for (auto piece : base::split(row.defaultValue, ',')) members.emplace_back(piece); + return backticked_join(members, ", "); + } + } + return "?"; +} + +std::string command_cell(const Setting& row, bool zh) { + if (row.commandLine.empty()) return "—"; + std::string cell { std::format("`{}`", row.commandLine) }; + if (row.kind == Kind::list && row.commandLineRepeatable) cell += zh ? "(可重复)" : " (repeatable)"; + return cell; +} + +std::string applies_cell(Applies applies, bool zh) { + if (!zh) return std::string { to_string(applies) }; + switch (applies) { + case Applies::restart: return "重启"; + case Applies::reload: return "重新加载模型"; + case Applies::immediately: return "立即生效"; + } + return "?"; +} + +} // namespace + +std::string to_markdown(std::span rows, std::string_view lang) { + const bool zh { is_zh(lang) }; + const std::string_view headSetting { zh ? "设置" : "Setting" }; + const std::string_view headValues { zh ? "取值" : "Values" }; + const std::string_view headDefault { zh ? "默认值" : "Default" }; + const std::string_view headCommand { zh ? "命令行" : "Command line" }; + const std::string_view headApplies { zh ? "生效方式" : "Applies" }; + const std::string_view headWhat { zh ? "作用" : "What it does" }; + + std::vector blocks; + for (const auto& info : CATEGORIES) { + std::vector members; + for (const auto& row : rows) { + if (row.category == info.key) members.push_back(&row); + } + if (members.empty()) continue; + std::string block { std::format("### {}\n\n", zh ? info.zh : info.en) }; + block += std::format("| {} | {} | {} | {} | {} | {} |\n", headSetting, headValues, headDefault, headCommand, headApplies, headWhat); + block += "|---|---|---|---|---|---|\n"; + for (const auto* row : members) { + block += std::format("| {} | {} | {} | {} | {} | {} |\n", setting_cell(*row), values_cell(*row, zh), default_cell(*row, zh), + command_cell(*row, zh), applies_cell(row->applies, zh), zh ? row->summaryZh : row->summary); + } + block.pop_back(); // the loop above leaves one trailing '\n'; blocks are joined by "\n\n" instead + blocks.push_back(std::move(block)); + } + return base::join(blocks, "\n\n"); +} + +Json registry_to_json(std::span rows) { + Json array = Json::array(); + for (const auto& row : rows) { + array.push_back(Json { + { "key", row.key }, + { "kind", std::string { to_string(row.kind) } }, + { "values", row.values }, + { "default", row.defaultValue }, + { "commandLine", row.commandLine }, + { "commandLineNegated", row.commandLineNegated }, + { "commandLineRepeatable", row.commandLineRepeatable }, + { "surface", std::string { to_string(row.surface) } }, + { "applies", std::string { to_string(row.applies) } }, + { "category", row.category }, + { "since", row.since }, + { "summary", row.summary }, + { "summaryZh", row.summaryZh }, + { "aliases", row.aliases }, + { "clientConfigurable", row.clientConfigurable }, + }); + } + return array; +} + +} // namespace mcppls::config::settings diff --git a/src/config/settings.cppm b/src/config/settings.cppm new file mode 100644 index 0000000..9242fc7 --- /dev/null +++ b/src/config/settings.cppm @@ -0,0 +1,163 @@ +// Every configurable behaviour of mcppls, in one place (0.0.6 plan §9 T1). Before this module, the +// same fact -- the default of `mcppls.buildTool`, say -- lived separately in the command line's +// `--build-tool` help text, `handle_initialize_`'s reading of `initializationOptions`, the VS Code +// extension's `package.json`, and the hand-written table in `docs/30-settings.md`; keeping them in +// step was a matter of remembering to. Here it lives once, as a row of `registry()`, and every one +// of those sites is derived from it: `mcppls.cli.commands` builds the global options from it, +// `session_options` and `handle_initialize_` fill a `Settings` from it, `mcppls settings` and +// `mcppls report` render it, and `tests/test_settings.cpp` holds the rendered docs and +// `editors/vscode/package.json` to it. +export module mcppls.config.settings; + +import std; +import nlohmann.json; +import mcpplibs.cmdline; + +export namespace mcppls::config::settings { + +using Json = nlohmann::json; + +// What kind of value a row takes. Purely descriptive for `string` and `path` (either accepts any +// text; `path` says so in the generated docs) -- the difference is real for the rest: `boolean` and +// `enumeration` are validated against a closed vocabulary, `seconds` against a non-negative +// integer, and `list` against zero or more comma-separated (or, on the command line, repeated) +// members. +enum class Kind { boolean, enumeration, string, path, seconds, list }; + +// Where a row is read: `server` is mcppls's own behaviour; `client` is read only by an editor +// plugin (kept here so the docs and `package.json` stay one table); `environment` is a variable of +// the process environment rather than a setting at all. +enum class Surface { server, client, environment }; + +// How a change to a row's value takes effect once mcppls is already running. +enum class Applies { restart, reload, immediately }; + +// Where a row's current value came from, in ascending precedence (T1's rule: command line beats +// initializationOptions beats the default; a later didChangeConfiguration updates a `client` or +// `clientUpdated` value, never one the command line set). +enum class Origin { defaulted, environment, commandLine, client, clientUpdated }; + +std::string_view to_string(Kind kind); +std::string_view to_string(Surface surface); +std::string_view to_string(Applies applies); +std::string_view to_string(Origin origin); + +// One row: everything about one configurable behaviour of mcppls. `key` is dotted, under the +// `mcppls.` namespace for a `server` or `client` row (`buildTool`, `semanticTokens.modules`); for +// an `environment` row it is the bare variable name (`MCPPLS_CACHE_DIR`). `values` is the closed +// vocabulary for `enumeration` and (when it has one) `list`; a `list` row with none accepts any +// non-empty member. `defaultValue` is the row's own string form of its default -- for `boolean`, +// `"true"` or `"false"`; for `list`, its members joined with `,`. `commandLine` is the flag's +// spelling (`"--build-tool"`), empty when there is none. `commandLineNegated` is for a boolean +// whose flag's presence means false against a true default (`--no-discover`). +// `commandLineRepeatable` is for a `list` row taken as one value per occurrence of the flag +// (`--disable-workaround`, repeated) rather than one occurrence with a comma-separated value +// (`--build-discovery-providers`). `clientConfigurable` says whether an editor's own settings UI +// is expected to expose this row at all: true for every `client` row and for a `server` row VS +// Code's `package.json` should carry a matching property for; false for one that is command-line +// only (a path to something on this machine, a workaround id, a timeout) or fixed by what a +// client's own capabilities declare (`semanticTokens.moduleType`) -- `tests/test_settings.cpp` +// reads this to know which rows to expect in `package.json` and which to expect absent. +struct Setting { + std::string key; + Kind kind { Kind::string }; + std::vector values; + std::string defaultValue; + std::string commandLine; + bool commandLineNegated { false }; + bool commandLineRepeatable { false }; + Surface surface { Surface::server }; + Applies applies { Applies::restart }; + std::string category; + std::string since; + std::string summary; + std::string summaryZh; + std::vector aliases; + bool clientConfigurable { false }; +}; + +// The shipped registry (0.0.6 plan §9 T1), in the order the generated docs list it: grouped by +// category, each category's rows in a fixed, meaningful order. +std::span registry(); + +// `key` (bare, dotted, no `mcppls.` prefix) matched against `rows`' own key or any of its +// `aliases`; null when none of `rows` answers to it. +const Setting* find(std::span rows, std::string_view key); + +// A row's effective value, in its own string form (see `Setting::defaultValue` above for what that +// form is per `Kind`), and where it came from. +struct Value { + std::string text; + Origin origin { Origin::defaulted }; +}; + +// Something wrong with one layer's attempt to set a row: an unknown key (`key` empty) or a value +// outside its kind's vocabulary (`key` names the row; the row's value was left at what it already +// was, never silently changed by a value nobody here recognizes). +struct Problem { + std::string key; + std::string message; +}; + +// What a `workspace/didChangeConfiguration` (or an equivalent later layer) actually changed, split +// by what taking it needs: a caller reloads each workspace's model for `reloadKeys`, and tells the +// person `restartKeys` needs a restart to take effect (`immediately` rows need neither: this +// module's caller reads the new value straight from `Settings` the next time it looks). +struct ChangeResult { + std::vector changedKeys; + std::vector restartKeys; + std::vector reloadKeys; +}; + +// The registry resolved against however many layers have been applied: every row's effective value, +// its origin, and the problems every layer applied so far ran into. Constructed at the row +// defaults (and an `environment` row's value read from the process environment, once); each +// `apply_*` layers a source of values over what is there, per T1's precedence. +class Settings { +public: + explicit Settings(std::span rows = registry()); + + std::span rows() const; + const Value& value(std::string_view key) const; + std::string string_value(std::string_view key) const; + bool bool_value(std::string_view key) const; + std::chrono::seconds seconds_value(std::string_view key) const; + // A `list` row's members, split from its stored comma-joined text; empty when the row's value is empty. + std::vector list_value(std::string_view key) const; + Origin origin(std::string_view key) const; + + // The command-line layer (T1a): every row with a `commandLine` spelling that `args` gives, + // validated and recorded at `Origin::commandLine`. Wins over every layer after it. + void apply_command_line(const mcpplibs::cmdline::ParsedArgs& args); + // `initializationOptions` (or the whole `initialize` params -- only that key is read): nested + // objects, dotted keys, or either wrapped in a top-level `mcppls` object, all accepted (T1). + // A row the command line already set is left alone. + void apply_initialization_options(const Json& initializationOptionsOrParams); + // `workspace/didChangeConfiguration`'s params: `params.settings.mcppls` (VS Code's own shape), + // or the same nested/dotted/wrapped forms `apply_initialization_options` accepts, directly on + // `params.settings` or `params` itself. Updates every row the command line did not set, and + // says what actually changed. + ChangeResult apply_configuration_change(const Json& params); + + const std::vector& problems() const; + + // `{"": {"value": ..., "origin": "..."}, ..., "problems": [...]}` (`mcppls report`'s + // `settings` object, T1). `value` is typed by the row's `Kind`: a JSON boolean, number or + // array of strings where that fits, a string otherwise. + Json to_json() const; + +private: + std::span rows_; + std::map> values_; + std::vector problems_; +}; + +// `mcppls settings --format markdown [--lang en|zh-CN]`: the reference table grouped by category +// (`docs/30-settings.md` and its zh-CN mirror embed this verbatim between two markers, so +// `tests/test_settings.cpp` can hold the file to the renderer byte for byte). +std::string to_markdown(std::span rows, std::string_view lang = "en"); + +// `mcppls settings --format json`: every field of every row, for a machine reader. +Json registry_to_json(std::span rows); + +} // namespace mcppls::config::settings diff --git a/src/engine/clangd.cpp b/src/engine/clangd.cpp index 72bcf67..e1d4e77 100644 --- a/src/engine/clangd.cpp +++ b/src/engine/clangd.cpp @@ -36,6 +36,7 @@ EngineTraits traits_for_version(std::string_view version, std::span, std::less<>> moduleUnits_; // module -> its units other than its interface std::map> interfaceModules_; // path key of an importable unit -> its module struct BackgroundUnit { @@ -315,14 +319,39 @@ class ClangdEngine final : public Engine { Clock::time_point usedAt; bool built { false }; std::string state; // clangd's last fileStatus state for it + bool priming { false }; // opened to put its definitions in clangd's index (N-7), not for a waiting request }; std::map> background_; // path key std::set> closedBackground_; // path keys whose closing diagnostics are still to come std::map, std::less<>> backgroundRefused_; // units that did not build in time, as they were + // Plan 2026-09-27 N-7 (WA-CLANGD-008). clangd's background index compiles a module unit without building the modules + // it imports, so a definition in an implementation unit is indexed apart from its declaration, or not at all, until + // the unit has been open. Implementation units are therefore opened here, a few at a time, built through clangd's + // foreground -- which builds their modules first -- and closed again: their symbols stay in clangd's index. + // Relevant ones first (the units of an opened file's module and of the modules it imports, and units changed on + // disk), the rest once clangd has been idle for a while. A restart of clangd loses what its index held. + struct ImplementationUnit { + std::string path; + std::string module; + }; + std::deque implementationQueue_; + std::set> implementationQueued_; // path keys + std::map, std::less<>> implementationBuilt_; // path key -> the file as it was built + std::map> implementationUnreadable_; // path key -> why it did not build (N-3) + bool implementationSeedOpen_ { true }; // the open documents' relevant units are still to be queued + bool implementationRestQueued_ { false }; + std::optional implementationRestAt_; + // Plan 2026-09-27 N-8: a declaration clangd answered a definition request with, read lexically. + struct DeclarationSite { + std::string path; + std::string module; + DeclaredFunction function; + }; struct DefinitionSearch { Json message; // the client's request Json firstAnswer; // clangd's answer before the units were built Reply reply; + std::vector sites; // what the first answer declares, for the lexical answer if clangd's stays a declaration std::set waitingFor; // path keys Clock::time_point deadline; Clock::time_point limit; // the client's request's own limit (wait_limit), which the search stays within @@ -408,6 +437,9 @@ class ClangdEngine final : public Engine { { "filesWaitingForDatabase", held_files_() }, { "fileStates", fileStatus_ }, { "backgroundUnits", background_files_() }, + // N-7 (WA-CLANGD-008): implementation units built for clangd's index, waiting, and the ones that did not build. + { "implementationIndex", Json { { "built", implementationBuilt_.size() }, { "queued", implementationQueue_.size() }, + { "unreadable", implementationUnreadable_.size() }, { "on", priming_implementations_() } } }, { "definitionSearches", searches_.size() }, { "unresolvedModules", std::move(unresolved) }, { "modulesThatDidNotCompile", std::move(compileFailures) }, @@ -693,6 +725,10 @@ class ClangdEngine final : public Engine { if (!entry.imports.empty()) fileImports_[key] = entry.imports; if (!entry.module.empty()) fileModule_[key] = entry.module; } + // N-7: the units of this plan, for the open documents first and the rest once clangd is idle again. + implementationSeedOpen_ = true; + implementationRestQueued_ = false; + if (std::erase_if(implementationUnreadable_, [&](const auto& item) { return !writtenArguments_.contains(item.first); }) > 0) update_unreadable_issue_(); std::vector backgroundLeaving; for (const auto& [key, unit] : background_) { if (!writtenArguments_.contains(key) || excluded_.contains(key) || restartNeeded) backgroundLeaving.push_back(key); @@ -743,6 +779,8 @@ class ClangdEngine final : public Engine { if (accepting_) { open_or_hold_(document, false); prepare_imports_of_(document); + queue_implementations_of_(document.path); + pump_implementations_(Clock::now()); } else if (!document.path.empty() && !writtenDatabase_.empty() && !writtenArguments_.contains(base::path_key(document.path)) && project::is_cxx_source_name(document.path) && base::is_within(document.path, host_->root_directory())) { // Opened while clangd restarts: the database it reads may not have the file yet. Before the first plan nothing is @@ -814,6 +852,21 @@ class ClangdEngine final : public Engine { // Fix plan F16: a change on disk (from another program, or an editor that does not send didSave) is // looked at the same way, and a file whose disk text would spin clangd is left out of what it is told. if (const Json* changes = lsp::find_path(message, { "params", "changes" }); changes != nullptr && changes->is_array()) { + // N-7: clangd's background index does not index a unit again when it changes on disk; a unit the editor + // does not have open (a git pull, another program, a coding agent) is built again for the index. A batch + // larger than WATCHED_BATCH_LIMIT (a checkout, a rebase) only joins the rest, behind what was queued first. + const bool first { changes->size() <= WATCHED_BATCH_LIMIT }; + for (const auto& change : *changes) { + if (!change.is_object() || change.value("type", 0) == 3) continue; + const std::string path { host_->path_of_uri(change.value("uri", std::string {})) }; + if (path.empty()) continue; + const std::string key { base::path_key(path) }; + const auto module = fileModule_.find(key); + if (module == fileModule_.end() || interfaceModules_.contains(key)) continue; + implementationBuilt_.erase(key); + queue_implementation_(path, module->second, first); + } + pump_implementations_(Clock::now()); Json kept = Json::array(); for (const auto& change : *changes) { const std::string path { change.is_object() ? host_->path_of_uri(change.value("uri", std::string {})) : std::string {} }; @@ -1059,11 +1112,14 @@ class ClangdEngine final : public Engine { for (const auto& waiting : requests) consider(waiting.limit); } for (const auto& [key, unit] : background_) consider(unit.built ? unit.usedAt + BACKGROUND_IDLE : unit.openedAt + BACKGROUND_BUILD_LIMIT); + consider(implementationRestAt_); return deadline; } void handle_timers() override { const auto now = Clock::now(); + if (implementationRestAt_ && *implementationRestAt_ <= now) queue_rest_of_implementations_(now); + else if (!implementationQueue_.empty() && !primer_.busy()) pump_implementations_(now); // preparation ended another way if (pendingExit_ && now >= pendingExit_->at + EXIT_CONTEXT_WAIT) settle_exit_(); if (!closingAfterBuild_.empty()) settle_disk_builds_(now); if (diskRecheckAt_ && *diskRecheckAt_ <= now) recheck_disk_(now); @@ -1248,6 +1304,7 @@ class ClangdEngine final : public Engine { void start_process_() { handshakeDone_ = false; + forget_implementations_(); // a new clangd's index has none of what the last one was given loadFailure_.reset(); accepting_ = false; stuck_.clear(); @@ -1721,6 +1778,7 @@ class ClangdEngine final : public Engine { } } prepare_modules_(); + pump_implementations_(Clock::now()); host_->status_changed(); } @@ -1815,10 +1873,16 @@ class ClangdEngine final : public Engine { if (const auto unit = background_.find(diagnosedKey); unit != background_.end() && !host_->has_document(uri)) { if (!unit->second.built) { unit->second.built = true; - log::info("{} built in clangd in {} ms to find definitions ({})", unit->second.path, - std::chrono::duration_cast(Clock::now() - unit->second.openedAt).count(), host_->root_directory()); + const auto took = std::chrono::duration_cast(Clock::now() - unit->second.openedAt).count(); + if (unit->second.priming) log::debug("{} built in clangd in {} ms for its index ({})", unit->second.path, took, host_->root_directory()); + else log::info("{} built in clangd in {} ms to find definitions ({})", unit->second.path, took, host_->root_directory()); } + // Every unit built here, for a search or for the index, is now in clangd's index (N-7). + implementation_built_(diagnosedKey, unit->second, params.value("diagnostics", Json::array())); + const bool priming { unit->second.priming }; unit_built_(diagnosedKey); + if (priming && !waited_on_(diagnosedKey)) close_background_(diagnosedKey); + pump_implementations_(Clock::now()); return; } // The empty list clangd sends when such a unit is closed, even when the editor opens the file right after. @@ -1998,7 +2062,24 @@ class ClangdEngine final : public Engine { const auto standard = moduleSources_.find("std"); const bool stdFailed { parsed.module == "std" || parsed.module == "std.compat" || (kind == FailureKind::compile && standard != moduleSources_.end() && base::same_path(parsed.failedSource, standard->second)) }; - if (stdFailed && kind != FailureKind::other && !stdFromKit_) { + // Plan 2026-09-27 Q1-4: "no unit for module M" from a clangd that has not read the database the unit + // joined yet is about the database it had, not the one it has now. qt-demo: std.cc joined the database + // and 78 ms later clangd, still on the previous one, answered "Don't get the module unit for module std"; + // taken at its word, the whole project was moved to the semantic kit and clangd restarted twice. + // Q1-1 (D4'): the kit replaces the toolchain's standard library only when its unit failed to compile, or + // when the plan has no unit for it at all; a unit the plan has and clangd has read but still "does not + // get" is a scanning problem of the files that import it, which the kit would not make any better. + const auto provider = moduleSources_.find(parsed.module); + const bool providerPlanned { provider != moduleSources_.end() && !generated_path_(provider->second) }; + const FailureAction action { failure_action(kind, FailureContext { .standardLibrary = stdFailed, .providerPlanned = providerPlanned, + .providerRead = engine_read_unit_of_(parsed.module, Clock::now()), + .alreadyOnKit = stdFromKit_ }) }; + if (action == FailureAction::ignore) { + log::info("clangd has not read the unit of module {} yet ({}): {}; not taken as a failure", parsed.module, host_->root_directory(), parsed.reason); + host_->record_event("module-unresolved-before-read", Json { { "module", parsed.module }, { "reason", parsed.reason } }); + return; + } + if (action == FailureAction::use_kit) { stdFromKit_ = true; log::warning("clangd could not build the standard library module ({}): {}; reading the project with the semantic kit", host_->root_directory(), parsed.reason); @@ -2481,11 +2562,8 @@ class ClangdEngine final : public Engine { if (name == "std" || name == "std.compat" || resolvedElsewhere_.contains(name)) continue; const bool fresh { planned == fileImports_.end() || std::ranges::find(planned->second, name) == planned->second.end() }; if (!fresh && !watched) continue; - const auto joined = moduleJoinedAt_.find(name); - if (joined == moduleJoinedAt_.end()) return std::format("it imports {}, which the engine database has no unit for yet (UP-02)", name); - // A clangd that has read no database yet reads this one, whole, when it is given its first file. - const bool read { !databaseRead_ || (databaseReadAt_ && *databaseReadAt_ >= joined->second) || now >= joined->second + DATABASE_REREAD }; - if (!read) return std::format("it imports {}, whose unit clangd has not read from the engine database yet (UP-02)", name); + if (!moduleJoinedAt_.contains(name)) return std::format("it imports {}, which the engine database has no unit for yet (UP-02)", name); + if (!engine_read_unit_of_(name, now)) return std::format("it imports {}, whose unit clangd has not read from the engine database yet (UP-02)", name); } return std::nullopt; } @@ -2629,6 +2707,16 @@ class ClangdEngine final : public Engine { // ---- fix plan F13, F17.3: what a plan changed ------------------------------------------------ // Stand-ins and prime units: files this server writes, which nothing but clangd's own lookups ever builds. + // Whether this clangd has read the engine database that gave `module` its current unit. A clangd that + // has read no database yet reads this one, whole, when it is given its first file; one that read an + // earlier database rereads it within DATABASE_REREAD. A module whose unit has been there since before + // this clangd started (no join recorded) is read. + bool engine_read_unit_of_(std::string_view module, Clock::time_point now) const { + const auto joined = moduleJoinedAt_.find(module); + if (joined == moduleJoinedAt_.end()) return true; + return !databaseRead_ || (databaseReadAt_ && *databaseReadAt_ >= joined->second) || now >= joined->second + DATABASE_REREAD; + } + bool generated_path_(std::string_view path) const { const auto within = [&](const std::string& directory) { return !directory.empty() && (base::is_within(path, directory) || base::is_within(path, base::path_key(directory))); @@ -2982,26 +3070,95 @@ class ClangdEngine final : public Engine { } } + // The text of `path` as the editor has it, else as it is on disk. + std::optional text_of_(const std::string& path) const { + for (const auto& document : host_->documents()) { + if (!document.path.empty() && base::same_path(document.path, path)) return std::string { document.text }; + } + auto read = platform::fs::read_file(path); + if (!read) return std::nullopt; + return std::move(*read); + } + DeclarationKind declaration_kind_at_(const std::string& path, const Json& range) const { const Json* end { lsp::find(range, "end") }; if (end == nullptr || !end->is_object()) return DeclarationKind::unknown; const base::Position position { end->value("line", 0), end->value("character", 0) }; - std::string text; - bool open { false }; - for (const auto& document : host_->documents()) { - if (!document.path.empty() && base::same_path(document.path, path)) { - text = std::string { document.text }; - open = true; - break; + const auto text = text_of_(path); + if (!text) return DeclarationKind::unknown; + const auto offset = base::offset_at(*text, position); + return offset ? declaration_kind(*text, *offset) : DeclarationKind::unknown; + } + + // Whether a definition request was made on a definition (a body follows the name it is on). + bool request_on_definition_(const Json& message) const { + const Json* params { lsp::find(message, "params") }; + const Json* uri { params != nullptr ? lsp::find_path(*params, { "textDocument", "uri" }) : nullptr }; + const Json* position { params != nullptr ? lsp::find(*params, "position") : nullptr }; + if (uri == nullptr || !uri->is_string() || position == nullptr || !position->is_object()) return false; + const std::string path { host_->path_of_uri(uri->get()) }; + const auto text = path.empty() ? std::nullopt : text_of_(path); + if (!text) return false; + auto offset = base::offset_at(*text, base::Position { position->value("line", 0), position->value("character", 0) }); + if (!offset) return false; + std::size_t end { *offset }; + while (end < text->size() && (base::is_identifier_char((*text)[end]) || (*text)[end] == '~')) ++end; + return end > *offset && declaration_kind(*text, end) == DeclarationKind::definition; + } + + // Plan 2026-09-27 N-8. When every location of `answer` is a declaration without a body in a module's interface + // (a partition's included), the functions they declare, read lexically; empty otherwise. + std::vector declaration_sites_(const Json& answer) const { + std::vector sites; + bool onlyDeclarations { true }; + std::size_t locations { 0 }; + for_each_location_(answer, [&](const std::string& uri, const Json& range) { + ++locations; + const std::string path { host_->path_of_uri(uri) }; + const auto module = path.empty() ? interfaceModules_.end() : interfaceModules_.find(base::path_key(path)); + if (module == interfaceModules_.end() || declaration_kind_at_(path, range) != DeclarationKind::declaration) { + onlyDeclarations = false; + return; + } + const Json* start { lsp::find(range, "start") }; + const auto text = text_of_(path); + if (start == nullptr || !text) return; + const auto offset = base::offset_at(*text, base::Position { start->value("line", 0), start->value("character", 0) }); + if (!offset) return; + if (auto function = declared_function_at(*text, *offset)) sites.push_back(DeclarationSite { path, module->second, std::move(*function) }); + }); + if (locations == 0 || !onlyDeclarations) sites.clear(); + return sites; + } + + // Plan 2026-09-27 N-8. The definitions of what `sites` declare, found lexically in the other units of their modules + // (the interface and its partitions included): what clangd's index could not link to the declaration (its + // background index builds no module a unit imports, WA-CLANGD-008). Only an exact match counts -- same name, + // scopes that agree, the same parameter types as spelled -- so a definition this cannot tell apart is not guessed at. + Json lexical_definitions_(const std::vector& sites) const { + Json locations = Json::array(); + std::set> seen; + for (const auto& site : sites) { + std::vector candidates; + if (const auto units = moduleUnits_.find(site.module); units != moduleUnits_.end()) { + for (const auto& unit : units->second) candidates.push_back(unit.path); + } + if (const auto interface = moduleSources_.find(site.module); interface != moduleSources_.end()) candidates.push_back(interface->second); + for (const auto& candidate : candidates) { + const auto text = text_of_(candidate); + if (!text) continue; + for (const auto& definition : function_definitions(*text, site.function.name)) { + if (!same_function(site.function, definition)) continue; + const std::string key { std::format("{}:{}", base::path_key(candidate), definition.nameOffset) }; + if (!seen.insert(key).second) continue; + const auto& range = definition.nameRange; + locations.push_back(Json { { "uri", base::path_to_uri(candidate) }, + { "range", Json { { "start", Json { { "line", range.start.line }, { "character", range.start.character } } }, + { "end", Json { { "line", range.end.line }, { "character", range.end.character } } } } } }); + } } } - if (!open) { - auto read = platform::fs::read_file(path); - if (!read) return DeclarationKind::unknown; - text = std::move(*read); - } - const auto offset = base::offset_at(text, position); - return offset ? declaration_kind(text, *offset) : DeclarationKind::unknown; + return locations; } // clangd's answer to a definition request. When every location it gives is a declaration only, in a module's interface, @@ -3013,6 +3170,13 @@ class ClangdEngine final : public Engine { reply(std::move(answer)); return; } + // Asked on a definition, clangd's answer is its declaration, which is the answer: the editors' convention of + // going back and forth between the two (plan 2026-09-27 §2.1). Nothing to search for. + std::vector sites { request_on_definition_(message) ? std::vector {} : declaration_sites_(answer.value) }; + if (sites.empty() && request_on_definition_(message)) { + reply(std::move(answer)); + return; + } std::set> modules; std::size_t locations { 0 }; bool onlyDeclarations { true }; @@ -3037,8 +3201,13 @@ class ClangdEngine final : public Engine { const auto units = moduleUnits_.find(module); if (units == moduleUnits_.end()) continue; const auto interface = moduleSources_.find(module); - for (const auto& path : units_to_search(interface == moduleSources_.end() ? std::string_view {} : std::string_view { interface->second }, - units->second, UNITS_PER_SEARCH)) { + // N-8: the units that define the name come first; the rest in the order the file names suggest. + const auto site = std::ranges::find_if(sites, [&](const DeclarationSite& each) { return each.module == module; }); + const std::vector chosen { site != sites.end() + ? units_defining(site->function.name, units->second, [this](const std::string& path) { return text_of_(path); }, UNITS_PER_SEARCH) + : units_to_search(interface == moduleSources_.end() ? std::string_view {} : std::string_view { interface->second }, units->second, + UNITS_PER_SEARCH) }; + for (const auto& path : chosen) { const std::string key { base::path_key(path) }; if (const auto uri = editor_uri_of_(key)) { // The editor has it open: clangd builds it anyway. @@ -3057,11 +3226,17 @@ class ClangdEngine final : public Engine { } } if (waiting.empty()) { + // Every unit that could define it is built already, and clangd still only knows the declaration (O-1). + if (Json found = lexical_definitions_(sites); !found.empty()) { + host_->record_event("definition-lexical", Json { { "modules", Json(std::vector { modules.begin(), modules.end() }) } }); + reply(Answer { Answer::Kind::result, std::move(found) }); + return; + } reply(std::move(answer)); return; } if (!opened.empty()) host_->record_event("definition-search", Json { { "modules", Json(std::vector { modules.begin(), modules.end() }) }, { "opened", opened } }); - searches_.push_back(DefinitionSearch { message, std::move(answer.value), std::move(reply), std::move(waiting), + searches_.push_back(DefinitionSearch { message, std::move(answer.value), std::move(reply), std::move(sites), std::move(waiting), std::min(now + DEFINITION_PATIENCE, limit), limit }); } @@ -3075,7 +3250,7 @@ class ClangdEngine final : public Engine { return std::nullopt; } - bool open_in_background_(const std::string& path, std::string_view module, Clock::time_point now) { + bool open_in_background_(const std::string& path, std::string_view module, Clock::time_point now, bool priming = false) { const std::string key { base::path_key(path) }; if (const auto refused = backgroundRefused_.find(key); refused != backgroundRefused_.end()) { if (platform::fs::stamp(path) == refused->second) return false; @@ -3106,8 +3281,9 @@ class ClangdEngine final : public Engine { if (!send_(lsp::make_notification("textDocument/didOpen", std::move(params)))) return false; note_database_read_(); closedBackground_.erase(key); - background_[key] = BackgroundUnit { path, uri, now, now, false, {} }; - log::info("opening {} in clangd to find definitions in module {} ({})", path, module, host_->root_directory()); + background_[key] = BackgroundUnit { path, uri, now, now, false, {}, priming }; + if (priming) log::debug("opening {} in clangd to index its definitions (module {}, {})", path, module, host_->root_directory()); + else log::info("opening {} in clangd to find definitions in module {} ({})", path, module, host_->root_directory()); return true; } @@ -3124,6 +3300,147 @@ class ClangdEngine final : public Engine { background_.erase(unit); } + // ---- implementation units in clangd's index (plan 2026-09-27 N-7, WA-CLANGD-008) ------------------ + + bool priming_implementations_() const { return options_.primeImplementationUnits && traits_.indexesModuleUnitsWithoutModules; } + + // Whether `path` went into the queue: not when it is there already, was built as it is, or the editor has it. + bool queue_implementation_(const std::string& path, const std::string& module, bool first) { + if (!priming_implementations_()) return false; + const std::string key { base::path_key(path) }; + if (implementationQueued_.contains(key) || !writtenArguments_.contains(key)) return false; + if (const auto built = implementationBuilt_.find(key); built != implementationBuilt_.end() && built->second == platform::fs::stamp(path)) return false; + // The editor has it open: clangd builds it anyway. + if (editor_uri_of_(key)) return false; + implementationQueued_.insert(key); + if (first) implementationQueue_.push_front(ImplementationUnit { path, module }); + else implementationQueue_.push_back(ImplementationUnit { path, module }); + return true; + } + + // The units of `path`'s own module and of the modules it imports directly: where a definition it reaches is. + void queue_implementations_of_(std::string_view path) { + if (!priming_implementations_() || path.empty()) return; + const std::string key { base::path_key(path) }; + std::vector modules; + auto add_module = [&](std::string_view name) { + const std::string primary { name.substr(0, name.find(':')) }; // a partition belongs to its module's units + if (!primary.empty() && std::ranges::find(modules, primary) == modules.end()) modules.push_back(primary); + }; + if (const auto own = fileModule_.find(key); own != fileModule_.end()) add_module(own->second); + if (const auto imports = fileImports_.find(key); imports != fileImports_.end()) { + for (const auto& name : imports->second) { + if (name.starts_with(':')) { + if (const auto own = fileModule_.find(key); own != fileModule_.end()) add_module(own->second); + } else { + add_module(name); + } + } + } + std::size_t queued { 0 }; + for (const auto& module : modules) { + const auto units = moduleUnits_.find(module); + if (units == moduleUnits_.end()) continue; + for (const auto& unit : units->second) { + if (queued >= IMPLEMENTATIONS_PER_OPEN) return; + if (base::same_path(unit.path, path)) continue; + if (queue_implementation_(unit.path, module, true)) ++queued; + } + } + } + + bool engine_idle_() const { + return accepting_ && pending_.empty() && searches_.empty() && awaitingDiagnostics_.empty() && !primer_.busy(); + } + + // Opens queued units while fewer than IMPLEMENTATIONS_AT_ONCE are building; once nothing is queued and clangd has + // been idle for Options::implementationIdle, every other unit of every module is queued (the rest). Nothing is + // opened while modules are being prepared: those BMIs are what an implementation unit is built from, and the + // workers preparation leaves free are for what a person asks (robustness design C7); finish_prime_ and + // handle_timers pump again once it is done. + void pump_implementations_(Clock::time_point now) { + if (!priming_implementations_() || !accepting_ || !handshakeDone_) return; + if (implementationSeedOpen_) { + implementationSeedOpen_ = false; + for (const auto& document : host_->documents()) queue_implementations_of_(document.path); + } + if (primer_.busy()) return; + std::size_t building { static_cast(std::ranges::count_if(background_, [](const auto& item) { return item.second.priming && !item.second.built; })) }; + while (building < IMPLEMENTATIONS_AT_ONCE && !implementationQueue_.empty()) { + ImplementationUnit unit { std::move(implementationQueue_.front()) }; + implementationQueue_.pop_front(); + const std::string key { base::path_key(unit.path) }; + implementationQueued_.erase(key); + if (background_.contains(key) || editor_uri_of_(key)) continue; + if (!open_in_background_(unit.path, unit.module, now, true)) continue; + ++building; + } + if (implementationQueue_.empty() && building == 0 && !implementationRestQueued_ && !implementationRestAt_) { + implementationRestAt_ = now + options_.implementationIdle; + } + } + + void queue_rest_of_implementations_(Clock::time_point now) { + implementationRestAt_.reset(); + if (!priming_implementations_() || implementationRestQueued_) return; + if (!engine_idle_()) { + implementationRestAt_ = now + options_.implementationIdle; + return; + } + implementationRestQueued_ = true; + for (const auto& [module, units] : moduleUnits_) { + for (const auto& unit : units) queue_implementation_(unit.path, module, false); + } + if (!implementationQueue_.empty()) log::info("building {} implementation units for clangd's index ({})", implementationQueue_.size(), host_->root_directory()); + pump_implementations_(now); + } + + // A unit opened for the index was built: clangd's index has its definitions now (WA-CLANGD-008's premise), and + // one that did not compile says why (N-3). + void implementation_built_(const std::string& key, const BackgroundUnit& unit, const Json& diagnostics) { + implementationBuilt_[key] = platform::fs::stamp(unit.path); + std::string reason; + for (const auto& diagnostic : diagnostics) { + if (diagnostic.value("severity", 0) != 1) continue; + const std::string code { diagnostic.contains("code") && diagnostic["code"].is_string() ? diagnostic["code"].get() : std::string {} }; + const std::string message { diagnostic.value("message", std::string {}) }; + if (code == "pp_file_not_found" || code == "module_not_found" || message.find("file not found") != std::string::npos) { + reason = message; + break; + } + } + const bool wasUnreadable { implementationUnreadable_.contains(key) }; + if (!reason.empty()) implementationUnreadable_[key] = reason; + else implementationUnreadable_.erase(key); + if (wasUnreadable != !reason.empty() || !reason.empty()) update_unreadable_issue_(); + } + + void update_unreadable_issue_() { + std::erase_if(issues_, [](const Issue& issue) { return issue.code == "implementation-unreadable"; }); + if (implementationUnreadable_.empty()) { + host_->status_changed(); + return; + } + const auto& [firstKey, firstReason] = *implementationUnreadable_.begin(); + std::string firstFile { firstKey }; + if (const auto unit = background_.find(firstKey); unit != background_.end()) firstFile = unit->second.path; + const std::size_t count { implementationUnreadable_.size() }; + add_issue_(Issue { "implementation-unreadable", + std::format("{} implementation {} cannot be read ({}: {}); definitions in {} are not reached by go-to-definition", + count, count == 1 ? "unit" : "units", base::file_name(firstFile), firstReason, count == 1 ? "it" : "them"), + "mcppls.showLogs", "code" }); + host_->status_changed(); + } + + void forget_implementations_() { + implementationQueue_.clear(); + implementationQueued_.clear(); + implementationBuilt_.clear(); + implementationSeedOpen_ = true; + implementationRestQueued_ = false; + implementationRestAt_.reset(); + } + void unit_built_(const std::string& key) { std::vector ready; for (auto it = searches_.begin(); it != searches_.end();) { @@ -3145,8 +3462,19 @@ class ClangdEngine final : public Engine { } // Asked again within the client's own limit, with clangd's first answer if this one finds nothing // (including when nothing of the limit is left: request_now_ answers unavailable at once). - request_now_(search.message, [first = std::move(search.firstAnswer), reply = std::move(search.reply)](Answer answer) mutable { + request_now_(search.message, [this, first = std::move(search.firstAnswer), reply = std::move(search.reply), sites = std::move(search.sites)](Answer answer) mutable { const bool found { answer.kind == Answer::Kind::result && !answer.value.is_null() && !(answer.value.is_array() && answer.value.empty()) }; + // N-8: an answer that is still only the declaration is no better than the first; the definition found + // lexically is, when there is one. + if (found && declaration_sites_(answer.value).empty()) { + reply(std::move(answer)); + return; + } + if (Json lexical = lexical_definitions_(sites); !lexical.empty()) { + host_->record_event("definition-lexical", Json {}); + reply(Answer { Answer::Kind::result, std::move(lexical) }); + return; + } if (found) reply(std::move(answer)); else reply(Answer { Answer::Kind::result, std::move(first) }); }, search.limit, false); @@ -3174,8 +3502,10 @@ class ClangdEngine final : public Engine { // Asked again with what clangd has built by now. for (auto& search : overdue) ask_definition_again_(std::move(search)); std::vector closing; + bool primingStuck { false }; for (const auto& [key, unit] : background_) { if (!unit.built && now >= unit.openedAt + BACKGROUND_BUILD_LIMIT) { + primingStuck = primingStuck || unit.priming; log::warning("{} did not build in clangd in {} minutes; it is not opened without the editor again until it changes ({}); clangd was {}", unit.path, BACKGROUND_BUILD_LIMIT.count(), host_->root_directory(), unit.state.empty() ? std::string { "in an unknown state" } : unit.state); host_->record_event("background-unit-stuck", Json { { "file", unit.path }, { "clangdState", unit.state } }); @@ -3187,6 +3517,9 @@ class ClangdEngine final : public Engine { } } for (const auto& key : closing) close_background_(key); + // A stuck unit gave up its N-7 slot: the next one takes it, unless clangd is about to restart (which seeds + // the queue again). + if (primingStuck && !restartAt_) pump_implementations_(now); } // ---- parallel module preparation ------------------------------------------------------ @@ -3333,6 +3666,7 @@ class ClangdEngine final : public Engine { lastPrimeProgressAt_ = Clock::now(); pump_primer_(); release_prime_units_if_idle_(); + if (!primer_.busy()) pump_implementations_(Clock::now()); return true; } diff --git a/src/engine/clangd.cppm b/src/engine/clangd.cppm index 9b4c22e..5e4d71e 100644 --- a/src/engine/clangd.cppm +++ b/src/engine/clangd.cppm @@ -32,6 +32,10 @@ struct Options { std::chrono::milliseconds stuckWatch { std::chrono::seconds { 5 } }; std::vector extraArguments; std::vector disabledWorkarounds; // registered workarounds turned off (import-hang plan §9) + // mcppls.index.primeImplementationUnits (plan 2026-09-27 N-7): implementation units are built through clangd's + // foreground in the background, so go-to-definition reaches them; off leaves only the search a definition request starts. + bool primeImplementationUnits { true }; + std::chrono::milliseconds implementationIdle { std::chrono::seconds { 15 } }; // how long clangd is idle before the rest are built (tests shorten it) std::function()> processFactory; // empty: a real clangd process }; diff --git a/src/engine/clangd/definition.cpp b/src/engine/clangd/definition.cpp index 003c29f..13da307 100644 --- a/src/engine/clangd/definition.cpp +++ b/src/engine/clangd/definition.cpp @@ -2,6 +2,7 @@ module mcppls.engine.clangd.definition; import std; import mcppls.base.path; +import mcppls.base.text; namespace mcppls::engine::clangd { @@ -22,6 +23,281 @@ std::size_t shared_directories(std::string_view left, std::string_view right) { return shared; } +// ---- N-8: lexical scanning shared by declared_function_at, function_definitions, units_defining ---- + +bool is_space(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == '\v'; } + +// Skips one comment, string/character literal or raw string starting at `i` (returns `i` unchanged when +// nothing starts there), and one preprocessor line when `i` is a `#` with only whitespace before it since +// the last newline. The caller resumes scanning at what this returns. +std::size_t skip_non_code(std::string_view text, std::size_t i) { + const std::size_t n { text.size() }; + if (i >= n) return i; + const char c { text[i] }; + const char next { i + 1 < n ? text[i + 1] : '\0' }; + if (c == '/' && next == '/') { + const auto nl = text.find('\n', i); + return nl == std::string_view::npos ? n : nl; + } + if (c == '/' && next == '*') { + const auto end = text.find("*/", i + 2); + return end == std::string_view::npos ? n : end + 2; + } + if (c == 'R' && next == '"') { + std::size_t p { i + 2 }; + const std::size_t delimStart { p }; + while (p < n && p - delimStart < 16 && text[p] != '(') ++p; + if (p >= n || text[p] != '(') return i + 1; // not a raw string after all -- just step past 'R' + const std::string_view delim { text.substr(delimStart, p - delimStart) }; + const std::string closer { std::string { ")" } + std::string { delim } + "\"" }; + const auto end = text.find(closer, p + 1); + return end == std::string_view::npos ? n : end + closer.size(); + } + if (c == '"' || c == '\'') { + std::size_t p { i + 1 }; + for (; p < n && text[p] != c; ++p) { + if (text[p] == '\\') ++p; + if (p < n && text[p] == '\n') break; + } + return p < n && text[p] == c ? p + 1 : p; + } + if (c == '#') { + std::size_t back { i }; + bool onlyWhitespaceBefore { true }; + while (back > 0 && text[back - 1] != '\n') { + if (!is_space(text[back - 1])) { onlyWhitespaceBefore = false; break; } + --back; + } + if (onlyWhitespaceBefore) { + std::size_t p { i }; + while (true) { + const auto nl = text.find('\n', p); + if (nl == std::string_view::npos) return n; + std::size_t last { nl }; + while (last > p && is_space(text[last - 1]) && text[last - 1] != '\\') --last; + if (last > p && text[last - 1] == '\\') { p = nl + 1; continue; } // a continued directive + return nl + 1; + } + } + } + return i; +} + +// Splits a declarator's qualification (`hello::add`, `mcpp::build::phase0_manifest_and_workspace`) into +// its identifiers, in order. Whitespace and `::` between identifiers are the only separators understood. +std::vector split_scope_name(std::string_view s) { + std::vector parts; + std::size_t i { 0 }; + while (true) { + const std::size_t start { i }; + while (i < s.size() && mcppls::base::is_identifier_char(s[i])) ++i; + if (i == start) break; + parts.push_back(std::string { s.substr(start, i - start) }); + while (i < s.size() && is_space(s[i])) ++i; + if (i + 1 < s.size() && s[i] == ':' && s[i + 1] == ':') { i += 2; while (i < s.size() && is_space(s[i])) ++i; } + else break; + } + return parts; +} + +// The names a `{` at `bracePos` opens, from the header text since `from` (the position right after the +// previous top-level `;`, `{` or `}`): what a `namespace`/`class`/`struct` header introduces, or none for +// anything else (a function body, a control-flow block, an initializer, `export { }`, `extern "C" { }`, +// ...). `export`/`inline` prefixes and one `template <...>` clause ahead of `class`/`struct` are skipped. +std::vector header_scope_segments(std::string_view text, std::size_t from, std::size_t bracePos) { + std::string_view header { mcppls::base::trim(text.substr(from, bracePos - from)) }; + while (true) { + if (header.starts_with("export") && (header.size() == 6 || is_space(header[6]))) header = mcppls::base::trim(header.substr(6)); + else if (header.starts_with("inline") && (header.size() == 6 || is_space(header[6]))) header = mcppls::base::trim(header.substr(6)); + else break; + } + if (header.empty()) return {}; // `export { ... }`, `inline { ... }` + if (header.starts_with("namespace") && (header.size() == 9 || is_space(header[9]) || header[9] == '{')) { + return split_scope_name(mcppls::base::trim(header.substr(9))); // empty for an anonymous namespace + } + if (header.starts_with("template")) { + if (const auto lt = header.find('<'); lt != std::string_view::npos) { + int depth { 0 }; + std::size_t p { lt }; + for (; p < header.size(); ++p) { + if (header[p] == '<') ++depth; + else if (header[p] == '>' && --depth == 0) { ++p; break; } + } + header = mcppls::base::trim(header.substr(std::min(p, header.size()))); + } + } + for (const std::string_view keyword : { std::string_view { "class" }, std::string_view { "struct" } }) { + if (!header.starts_with(keyword) || header.size() <= keyword.size() || !is_space(header[keyword.size()])) continue; + const std::string_view rest { mcppls::base::trim(header.substr(keyword.size())) }; + std::size_t end { 0 }; + while (end < rest.size() && mcppls::base::is_identifier_char(rest[end])) ++end; + return end == 0 ? std::vector {} : std::vector { std::string { rest.substr(0, end) } }; + } + return {}; +} + +// The running state of the brace/segment scan `function_definitions` and `scope_stack_before` share. +struct ScopeState { + std::vector scopeStack; + std::vector frameCounts; // how many of scopeStack's entries close with each open frame + std::size_t segmentStart { 0 }; +}; + +void cross_structural_char(std::string_view text, std::size_t i, char c, ScopeState& state) { + if (c == ';') { state.segmentStart = i + 1; return; } + if (c == '{') { + auto segments = header_scope_segments(text, state.segmentStart, i); + state.frameCounts.push_back(static_cast(segments.size())); + for (auto& s : segments) state.scopeStack.push_back(std::move(s)); + state.segmentStart = i + 1; + return; + } + // c == '}' + if (!state.frameCounts.empty()) { + int count { state.frameCounts.back() }; + state.frameCounts.pop_back(); + while (count-- > 0 && !state.scopeStack.empty()) state.scopeStack.pop_back(); + } + state.segmentStart = i + 1; +} + +// The namespaces and classes enclosing byte offset `pos` -- the same bookkeeping `function_definitions` +// does over a whole file, run just far enough for a single declaration. +std::vector scope_stack_before(std::string_view text, std::size_t pos) { + ScopeState state; + const std::size_t end { std::min(pos, text.size()) }; + std::size_t i { 0 }; + while (i < end) { + if (const std::size_t j { skip_non_code(text, i) }; j != i) { i = std::min(j, end); continue; } + const char c { text[i] }; + if (c == ';' || c == '{' || c == '}') cross_structural_char(text, i, c, state); + ++i; + } + return std::move(state.scopeStack); +} + +// The index one past the `)` matching the `(` at `openParen`, skipping nested parentheses, comments and +// literals; nullopt when the text ends unbalanced. +std::optional matching_close_paren(std::string_view text, std::size_t openParen) { + int depth { 0 }; + std::size_t i { openParen }; + const std::size_t n { text.size() }; + while (i < n) { + if (const std::size_t j { skip_non_code(text, i) }; j != i) { i = j; continue; } + if (text[i] == '(') ++depth; + else if (text[i] == ')' && --depth == 0) return i; + ++i; + } + return std::nullopt; +} + +bool is_type_keyword(std::string_view word) { + static const std::set> keywords { + "void", "bool", "char", "char8_t", "char16_t", "char32_t", "wchar_t", + "short", "int", "long", "signed", "unsigned", "float", "double", "auto", + }; + return keywords.contains(word); +} + +// One parameter's normalized spelling: whitespace collapsed, and no space around `*`, `&`, `&&`, `<`, +// `>`, `,`, `::` (a `,` can remain here inside a nested `<...>` the caller did not split on). +std::string normalize_spacing(std::string_view s) { + std::vector tokens; + std::size_t i { 0 }; + while (i < s.size()) { + if (const std::size_t j { skip_non_code(s, i) }; j != i) { i = j; continue; } + const char c { s[i] }; + if (is_space(c)) { ++i; continue; } + if (mcppls::base::is_identifier_char(c)) { + const std::size_t start { i }; + while (i < s.size() && mcppls::base::is_identifier_char(s[i])) ++i; + tokens.push_back(std::string { s.substr(start, i - start) }); + continue; + } + if (c == ':' && i + 1 < s.size() && s[i + 1] == ':') { tokens.emplace_back("::"); i += 2; continue; } + if (c == '&' && i + 1 < s.size() && s[i + 1] == '&') { tokens.emplace_back("&&"); i += 2; continue; } + tokens.push_back(std::string(1, c)); // braces here would pick initializer_list, not the fill constructor + ++i; + } + static const std::set> tightBefore { "*", "&", "&&", ",", "<", ">", "::", ")", "]" }; + static const std::set> tightAfter { "*", "&", "&&", "<", "::", "(", "[" }; + std::string out; + for (std::size_t k { 0 }; k < tokens.size(); ++k) { + if (k > 0 && !tightBefore.contains(std::string_view { tokens[k] }) && !tightAfter.contains(std::string_view { tokens[k - 1] })) out += ' '; + out += tokens[k]; + } + return out; +} + +// One parameter, as written between two top-level commas (or the whole list, for a single parameter): +// its default argument dropped, its name dropped when the parameter has more than one token and the +// last identifier is neither preceded by `::` (the tail of a qualified type, not a name) nor a type +// keyword, and its spacing normalized. `int (*fp)(int)` is left alone: a function-pointer parameter's +// last identifier is already its name, wrapped in the declarator, not a trailing word to drop. +std::string normalize_one_parameter(std::string_view part) { + { // Drop a default argument: the first top-level '=' (outside <>, (), [], {}). + int angle { 0 }, paren { 0 }, bracket { 0 }, brace { 0 }; + std::size_t i { 0 }; + while (i < part.size()) { + if (const std::size_t j { skip_non_code(part, i) }; j != i) { i = j; continue; } + const char c { part[i] }; + if (c == '<') ++angle; + else if (c == '>') { if (angle > 0) --angle; } + else if (c == '(') ++paren; + else if (c == ')') { if (paren > 0) --paren; } + else if (c == '[') ++bracket; + else if (c == ']') { if (bracket > 0) --bracket; } + else if (c == '{') ++brace; + else if (c == '}') { if (brace > 0) --brace; } + else if (c == '=' && angle == 0 && paren == 0 && bracket == 0 && brace == 0) { part = part.substr(0, i); break; } + ++i; + } + } + part = mcppls::base::trim(part); + if (!part.empty() && !part.contains("(*")) { + const std::size_t idEnd { part.size() }; + std::size_t idStart { idEnd }; + while (idStart > 0 && mcppls::base::is_identifier_char(part[idStart - 1])) --idStart; + if (idStart < idEnd) { + std::size_t before { idStart }; + while (before > 0 && is_space(part[before - 1])) --before; + const bool afterScope { before >= 2 && part[before - 1] == ':' && part[before - 2] == ':' }; + const std::string_view lastWord { part.substr(idStart, idEnd - idStart) }; + if (before > 0 && !afterScope && !is_type_keyword(lastWord)) part = mcppls::base::trim(part.substr(0, idStart)); + } + } + return normalize_spacing(part); +} + +// Splits a parameter list's raw text at its top-level commas (respecting `<>`, `()`, `[]`, `{}`), then +// normalizes each part. `void` alone, or an empty list, normalizes to no parameters at all. +std::vector normalize_parameters(std::string_view rawList) { + rawList = mcppls::base::trim(rawList); + if (rawList.empty() || rawList == "void") return {}; + std::vector parameters; + int angle { 0 }, paren { 0 }, bracket { 0 }, brace { 0 }; + std::size_t start { 0 }, i { 0 }; + while (i < rawList.size()) { + if (const std::size_t j { skip_non_code(rawList, i) }; j != i) { i = j; continue; } + const char c { rawList[i] }; + if (c == '<') ++angle; + else if (c == '>') { if (angle > 0) --angle; } + else if (c == '(') ++paren; + else if (c == ')') { if (paren > 0) --paren; } + else if (c == '[') ++bracket; + else if (c == ']') { if (bracket > 0) --bracket; } + else if (c == '{') ++brace; + else if (c == '}') { if (brace > 0) --brace; } + else if (c == ',' && angle == 0 && paren == 0 && bracket == 0 && brace == 0) { + parameters.push_back(normalize_one_parameter(rawList.substr(start, i - start))); + start = i + 1; + } + ++i; + } + parameters.push_back(normalize_one_parameter(rawList.substr(start))); + return parameters; +} + } // namespace DeclarationKind declaration_kind(std::string_view text, std::size_t nameEnd) { @@ -92,4 +368,123 @@ std::vector units_to_search(std::string_view interfacePath, std::sp return chosen; } +std::optional declared_function_at(std::string_view text, std::size_t nameOffset) { + if (nameOffset >= text.size() || !base::is_identifier_start(text[nameOffset])) return std::nullopt; + std::size_t nameEnd { nameOffset }; + while (nameEnd < text.size() && base::is_identifier_char(text[nameEnd])) ++nameEnd; + if (declaration_kind(text, nameEnd) != DeclarationKind::declaration) return std::nullopt; + std::size_t k { nameEnd }; + while (true) { + if (const std::size_t j { skip_non_code(text, k) }; j != k) { k = j; continue; } + if (k < text.size() && is_space(text[k])) { ++k; continue; } + break; + } + if (k >= text.size() || text[k] != '(') return std::nullopt; // the name is not a declarator at all + const auto close = matching_close_paren(text, k); + DeclaredFunction declared; + declared.name = std::string { text.substr(nameOffset, nameEnd - nameOffset) }; + declared.scopes = scope_stack_before(text, nameOffset); + declared.parameters = close ? normalize_parameters(text.substr(k + 1, *close - k - 1)) : std::vector {}; + return declared; +} + +std::vector function_definitions(std::string_view text, std::string_view name) { + std::vector found; + if (name.empty()) return found; + ScopeState state; + const std::size_t n { text.size() }; + std::size_t i { 0 }; + while (i < n) { + if (const std::size_t j { skip_non_code(text, i) }; j != i) { i = j; continue; } + const char c { text[i] }; + if (c == ';' || c == '{' || c == '}') { cross_structural_char(text, i, c, state); ++i; continue; } + const std::size_t wordStart { i }; + if (c == '~') { + if (i + 1 >= n || !base::is_identifier_start(text[i + 1])) { ++i; continue; } + ++i; // a destructor's name is "~Class" -- the identifier scan below extends the token over it + } else if (!base::is_identifier_start(c)) { + ++i; + continue; + } + while (i < n && base::is_identifier_char(text[i])) ++i; + const std::string_view word { text.substr(wordStart, i - wordStart) }; + if (word != name) continue; + // Followed, after whitespace and comments, directly by '(': a call or a declarator, never a plain + // use of the name (a type, a variable, a member access) -- `declaration_kind` cannot tell those apart. + std::size_t k { i }; + while (true) { + if (const std::size_t j { skip_non_code(text, k) }; j != k) { k = j; continue; } + if (k < n && is_space(text[k])) { ++k; continue; } + break; + } + if (k >= n || text[k] != '(') continue; + if (declaration_kind(text, i) != DeclarationKind::definition) continue; + // The declarator's own qualification: identifiers joined by "::" immediately before the name. + std::vector qualifiers; + std::size_t back { wordStart }; + while (true) { + std::size_t p { back }; + while (p > 0 && is_space(text[p - 1])) --p; + if (p < 2 || text[p - 1] != ':' || text[p - 2] != ':') break; + p -= 2; + while (p > 0 && is_space(text[p - 1])) --p; + const std::size_t qEnd { p }; + while (p > 0 && base::is_identifier_char(text[p - 1])) --p; + if (p == qEnd) break; // "::" with nothing before it -- give up rather than guess + qualifiers.push_back(std::string { text.substr(p, qEnd - p) }); + back = p; + } + std::ranges::reverse(qualifiers); + std::string qualifiedName; + for (const auto& scope : state.scopeStack) { qualifiedName += scope; qualifiedName += "::"; } + for (const auto& qualifier : qualifiers) { qualifiedName += qualifier; qualifiedName += "::"; } + qualifiedName += word; + const auto close = matching_close_paren(text, k); + FunctionDefinition definition; + definition.qualifiedName = std::move(qualifiedName); + definition.name = std::string { word }; + definition.nameOffset = wordStart; + definition.nameEnd = i; + definition.nameRange = { base::position_at(text, wordStart), base::position_at(text, i) }; + definition.parameters = close ? normalize_parameters(text.substr(k + 1, *close - k - 1)) : std::vector {}; + found.push_back(std::move(definition)); + } + return found; +} + +bool same_function(const DeclaredFunction& declaration, const FunctionDefinition& definition) { + if (declaration.name != definition.name) return false; + if (declaration.parameters != definition.parameters) return false; + std::vector defScopes { split_scope_name(definition.qualifiedName) }; + if (!defScopes.empty()) defScopes.pop_back(); // the name itself; the equality above already checked it + const std::size_t shared { std::min(declaration.scopes.size(), defScopes.size()) }; + for (std::size_t k { 0 }; k < shared; ++k) { + if (declaration.scopes[declaration.scopes.size() - 1 - k] != defScopes[defScopes.size() - 1 - k]) return false; + } + return true; +} + +std::vector units_defining(std::string_view name, std::span units, + const std::function(const std::string&)>& read, + std::size_t limit) { + std::vector defining; + std::vector rest; + for (const auto& unit : units) { + bool defines { false }; + if (const auto text = read(unit.path)) defines = !function_definitions(*text, name).empty(); + (defines ? defining : rest).push_back(&unit); + } + std::ranges::sort(defining, [](const UnitOfModule* a, const UnitOfModule* b) { return a->path < b->path; }); + std::vector chosen; + for (const auto* unit : defining) { + if (chosen.size() >= limit) return chosen; + chosen.push_back(unit->path); + } + for (const auto* unit : rest) { + if (chosen.size() >= limit) break; + chosen.push_back(unit->path); + } + return chosen; +} + } // namespace mcppls::engine::clangd diff --git a/src/engine/clangd/definition.cppm b/src/engine/clangd/definition.cppm index 821ae95..d6b97f8 100644 --- a/src/engine/clangd/definition.cppm +++ b/src/engine/clangd/definition.cppm @@ -2,9 +2,19 @@ // background index cannot build module imports (clangd 23.1), so a function declared in a module interface and defined in // an implementation unit nobody opened resolves to its declaration. The engine builds that module's other units and asks // again (robustness design C10); what is decided here is whether a location is a declaration only, and which units to build. +// +// N-8 (design plan §9.1): picking those units by file stem and directory alone misses a module laid out like mcpp's own +// `mcpp.build.prepare` (17 units, the declaration in a partition, the definition in whichever implementation unit happens +// to hold that phase) -- the module's declaration and its callers do not say which unit defines a name. The rest of this +// module is a lexical scanner that answers that, by name: `declared_function_at` reads a declaration's shape, `function_ +// definitions` finds every definition of a name in a file's text, `same_function` decides whether one matches the other, +// and `units_defining` ranks a module's units by that instead of by file stem. It is lexical, not a parse: it can be fooled +// by a type alias, an unusual macro, or a name reused across unrelated scopes, and it is built to fail closed in that case +// (no match) rather than to guess -- clangd's own answer, or a declaration-only fallback, is always what asked for it. export module mcppls.engine.clangd.definition; import std; +import mcppls.base.text; export namespace mcppls::engine::clangd { @@ -23,4 +33,57 @@ struct UnitOfModule { // and among them the ones named like the interface, then the ones nearest to it. std::vector units_to_search(std::string_view interfacePath, std::span units, std::size_t limit); +// A function or member function's declaration, as written: its unqualified name (a destructor's is `~Class`, matching how +// `function_definitions` names one; operator overloads are not recognized), the namespaces and classes enclosing it in +// that file (outer to inner, `namespace a::b {`/`export namespace`/`inline namespace` and `class X {`/`struct X {` bodies; +// an anonymous namespace or class contributes no name but still nests), and its parameter list, normalized as +// `function_definitions` normalizes one (so the two compare equal when they agree). +struct DeclaredFunction { + std::string name; + std::vector scopes; + std::vector parameters; +}; + +// `declared_function_at(text, nameOffset)` reads the declaration whose name begins at byte `nameOffset` of `text` (as +// `declaration_kind` is given the name's end); nullopt when it is not a function or member function declaration, or when +// `nameOffset` is not the start of an identifier. +std::optional declared_function_at(std::string_view text, std::size_t nameOffset); + +// One definition of a function or member function found by `function_definitions`: its name qualified by the namespace +// blocks enclosing it plus whatever qualification the declarator itself wrote (`namespace a::b { void f(...) {` -> +// `a::b::f`; `void hello::add(...) {` at file scope -> `hello::add`), the unqualified name, the name token's byte range +// and its line/character range, and its normalized parameter list. +struct FunctionDefinition { + std::string qualifiedName; + std::string name; + std::size_t nameOffset { 0 }; + std::size_t nameEnd { 0 }; + base::Range nameRange; + std::vector parameters; +}; + +// Every definition of a function or member function named `name` (unqualified; a destructor is `~Class`) found lexically +// in `text` -- a body, a constructor's `: ` initializer list, or `= default`/`= delete`, consistent with `declaration_kind`. +// Comments, string/character literals (raw strings included) and preprocessor lines are skipped, and a call, a declaration +// without a body, and a name used inside a function body are not definitions. No preprocessing is done: a name hidden or +// changed by a macro is read as written. +std::vector function_definitions(std::string_view text, std::string_view name); + +// True when `definition` is plausibly what `declaration` declares: the same name, the same parameters once both are +// normalized, and the scopes each side wrote agree on as many of the innermost levels as the shorter side spelled out (a +// declaration inside `namespace mcpp::build {` matches a definition qualified `mcpp::build::f`, one qualified only `f` +// inside a reopened `namespace mcpp::build { ... }`, and also one qualified only `f` with no enclosing namespace written +// at all -- lexically it cannot tell that last one from a same-named `f` in a different, unrelated scope). Parameters that +// differ only because one side used a type alias the other spelled out are never equal here, so they never match: this is +// a lexical comparison, not a resolution of what the alias names. +bool same_function(const DeclaredFunction& declaration, const FunctionDefinition& definition); + +// `units_to_search`'s ranking, refined by whether each unit's text (as `read` returns it; nullopt -- unreadable -- ranks a +// unit as not defining it, never as an error) holds a definition of `name`: those units come first, in path order; the +// rest keep the relative order `units` was given in (pass `units_to_search`'s own result to keep its stem/directory +// ranking for them -- this helper does not have the interface path that ranking needs). At most `limit` paths. +std::vector units_defining(std::string_view name, std::span units, + const std::function(const std::string&)>& read, + std::size_t limit); + } // namespace mcppls::engine::clangd diff --git a/src/engine/clangd/process.cpp b/src/engine/clangd/process.cpp index e19f010..9760dde 100644 --- a/src/engine/clangd/process.cpp +++ b/src/engine/clangd/process.cpp @@ -223,6 +223,13 @@ FailureKind failure_kind(const ModuleFailure& failure) { return FailureKind::other; } +FailureAction failure_action(FailureKind kind, const FailureContext& context) { + if (kind == FailureKind::unresolved && context.providerPlanned && !context.providerRead) return FailureAction::ignore; + const bool kitHelps { kind == FailureKind::compile || (kind == FailureKind::unresolved && !context.providerPlanned) }; + if (context.standardLibrary && kitHelps && !context.alreadyOnKit) return FailureAction::use_kit; + return FailureAction::record; +} + std::string parse_clangd_version(std::string_view output) { for (auto line : base::split_lines(output)) { const std::size_t marker { line.find("clangd version ") }; diff --git a/src/engine/clangd/process.cppm b/src/engine/clangd/process.cppm index 436938f..e974ac1 100644 --- a/src/engine/clangd/process.cppm +++ b/src/engine/clangd/process.cppm @@ -127,6 +127,19 @@ bool loader_failure(std::string_view line); // built. `compile`: the unit was found and did not compile; its importers get errors, not a hang (S3). enum class FailureKind { unresolved, compile, other }; FailureKind failure_kind(const ModuleFailure& failure); + +// What a module failure clangd reported means for the session (plan 2026-09-27 Q1-1, Q1-4). +// ignore the unit joined an engine database this clangd has not read yet: the report is about the old one +// use_kit the standard library's own unit failed to compile, or the plan has no unit for it: the kit replaces it +// record everything else: the failure is recorded where it is, as before +enum class FailureAction { ignore, use_kit, record }; +struct FailureContext { + bool standardLibrary { false }; // the module is std or std.compat, or the unit that failed is std's + bool providerPlanned { false }; // the plan gives the module a unit of the project or the toolchain (not a stand-in) + bool providerRead { true }; // this clangd has read the engine database in which that unit joined + bool alreadyOnKit { false }; // the standard library is already the kit's +}; +FailureAction failure_action(FailureKind kind, const FailureContext& context); // "clangd version 23.1.0 (https://github.com/llvm/llvm-project ea7d852a70e8...)" -> "23.1.0" std::string parse_clangd_version(std::string_view output); diff --git a/src/engine/clangd/workarounds.cpp b/src/engine/clangd/workarounds.cpp index 337612a..73ee9dc 100644 --- a/src/engine/clangd/workarounds.cpp +++ b/src/engine/clangd/workarounds.cpp @@ -7,7 +7,7 @@ namespace mcppls::engine::clangd { namespace { -constexpr std::array REGISTRY { { +constexpr std::array REGISTRY { { { .id = TRAILING_DOT_MODULE_NAME, .title = "a module name ending in '.' at the end of its line spins clangd forever; clangd is given the line with ';' after the dot", @@ -85,6 +85,17 @@ constexpr std::array REGISTRY { { .canary = "", .premise = "the module is provided by a unit of the engine database, and the import is in the buffer but not on disk", }, + { + .id = BACKGROUND_INDEX_WITHOUT_MODULES, + .title = "clangd's background index compiles a module unit without building its modules, so a definition in an implementation unit is indexed apart from its declaration or not at all; the server builds implementation units through clangd's foreground", + .fixedIn = "", + .upstream = "unfiled; the symptoms of clangd/clangd#2569 (references and rename inside modules)", + .evidence = ".agents/docs/2026-09-27-qt-demo-navigation-discovery-plan.md §2.3 (BackgroundIndex::index has no ModulesBuilder); conformance fixture mcpp-partition-definition", + .added = "0.0.6", + .removeWhen = "clangd's background index builds the modules a unit imports before indexing it", + .canary = "", + .premise = "a unit clangd has built in the foreground keeps its symbols in clangd's index after it is closed", + }, } }; // "23.1.0" -> {23, 1, 0}; anything else -> nullopt. diff --git a/src/engine/clangd/workarounds.cppm b/src/engine/clangd/workarounds.cppm index bedf49e..478a65d 100644 --- a/src/engine/clangd/workarounds.cppm +++ b/src/engine/clangd/workarounds.cppm @@ -35,6 +35,7 @@ inline constexpr std::string_view MODULE_HINTS { "WA-CLANGD-004" }; inline constexpr std::string_view MSVC_STL_ALIGNED_ALLOCATION { "WA-CLANGD-005" }; inline constexpr std::string_view DIRECTIVE_SEMICOLON_POSITION { "WA-CLANGD-006" }; inline constexpr std::string_view UNSAVED_IMPORT_NOT_FOUND { "WA-CLANGD-007" }; +inline constexpr std::string_view BACKGROUND_INDEX_WITHOUT_MODULES { "WA-CLANGD-008" }; std::span workarounds(); const Workaround* find_workaround(std::string_view id); diff --git a/src/engine/engine.cppm b/src/engine/engine.cppm index e2eadb5..09e3c2c 100644 --- a/src/engine/engine.cppm +++ b/src/engine/engine.cppm @@ -37,6 +37,7 @@ struct EngineTraits { bool hangsOnUnresolvedImports { false }; // units whose imports cannot resolve stay out of its database bool needsModulePreparation { false }; // the server prepares modules in parallel for it bool needsModuleHints { false }; // its database names the unit of each module + bool indexesModuleUnitsWithoutModules { false }; // its background index cannot see a module unit's imports; implementation units are built in the foreground bool msvcStlNeedsNoAlignedAllocation { false }; // MSVC STL contexts turn aligned allocation off bool hangsOnTrailingDotModuleName { false }; // `import a.` at the end of a line spins it; it is given `import a.;` bool misplacesDirectiveSemicolon { false }; // a directive missing its `;` is reported on the next line; moved back diff --git a/src/orchestrator/report.cpp b/src/orchestrator/report.cpp index 5abe078..4c693c5 100644 --- a/src/orchestrator/report.cpp +++ b/src/orchestrator/report.cpp @@ -37,7 +37,7 @@ std::string utc_now(std::string_view format) { } // namespace Json make_report(Json roots, Json client, std::string_view engine, const engine::PayloadPaths& payload, bool payloadCorrupt, - std::chrono::steady_clock::duration uptime) { + std::chrono::steady_clock::duration uptime, Json settings) { return Json { { "generatedAt", utc_now("{:%FT%TZ}") }, { "server", Json { { "name", "mcppls" }, { "version", std::string { base::VERSION } }, { "platform", std::string { mcppls::os::PLATFORM } }, @@ -48,6 +48,7 @@ Json make_report(Json roots, Json client, std::string_view engine, const engine: { "payload", Json { { "directory", payload.directory }, { "clangd", payload.clangd }, { "clangdVersion", payload.clangdVersion }, { "kit", payload.kit }, { "kitNotice", payload.kitNotice }, { "platform", payload.platform }, { "corrupt", payloadCorrupt } } }, { "roots", std::move(roots) }, + { "settings", std::move(settings) }, { "logTail", log::recent(300) }, }; } diff --git a/src/orchestrator/report.cppm b/src/orchestrator/report.cppm index e1b172b..5527445 100644 --- a/src/orchestrator/report.cppm +++ b/src/orchestrator/report.cppm @@ -9,9 +9,10 @@ import mcppls.engine.payload; export namespace mcppls::orchestrator { // The report around the roots' own (Workspace::report): when, which server and client, which payload, -// and the latest lines of the log. +// the latest lines of the log, and `settings` (config settings §9 T1: every resolved value, its +// origin, and the problems every layer applied so far ran into -- `config::settings::Settings::to_json`). nlohmann::json make_report(nlohmann::json roots, nlohmann::json client, std::string_view engine, const engine::PayloadPaths& payload, - bool payloadCorrupt, std::chrono::steady_clock::duration uptime); + bool payloadCorrupt, std::chrono::steady_clock::duration uptime, nlohmann::json settings); // Opens `/logs/-