From 7ed74bf65dea263509dc9677ff04eceb9ef84607 Mon Sep 17 00:00:00 2001 From: wellwei Date: Fri, 31 Jul 2026 18:59:35 +0800 Subject: [PATCH 001/119] chore: initialize mcpp VS Code extension --- .gitignore | 5 + .vscodeignore | 12 + CHANGELOG.md | 40 + LICENSE | 201 ++ README.md | 200 ++ package-lock.json | 3852 +++++++++++++++++++++++++ package.json | 119 + src/analysis.ts | 403 +++ src/discovery.ts | 76 + src/extension.ts | 829 ++++++ src/process.ts | 110 + src/workflow.ts | 222 ++ syntaxes/mcpp-modules.tmLanguage.json | 72 + test/analysis.test.ts | 302 ++ test/artifacts.test.ts | 66 + test/discovery.test.ts | 50 + test/process.test.ts | 81 + test/workflow.test.ts | 380 +++ tsconfig.json | 16 + 19 files changed, 7036 insertions(+) create mode 100644 .gitignore create mode 100644 .vscodeignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/analysis.ts create mode 100644 src/discovery.ts create mode 100644 src/extension.ts create mode 100644 src/process.ts create mode 100644 src/workflow.ts create mode 100644 syntaxes/mcpp-modules.tmLanguage.json create mode 100644 test/analysis.test.ts create mode 100644 test/artifacts.test.ts create mode 100644 test/discovery.test.ts create mode 100644 test/process.test.ts create mode 100644 test/workflow.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46f3a90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.vsix +.DS_Store +.vscode-test/ diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..19d5237 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,12 @@ +.vscode/** +.github/** +.gitignore +docs/** +src/** +test/** +tsconfig.json +package-lock.json +*.tsbuildinfo +dist/**/*.map +dist/test/** +node_modules/** diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9389bb1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# 更新日志 + +## 0.1.5 + +- 打开已有编译数据库的 LLVM mcpp 工程时自动执行 `clangd --check`,状态栏无需点击即可显示“模块可用”或“模块不可用”。 +- 编译数据库创建或变化后自动重新检查模块支持;工程内合并重复事件,共享 clangd 操作全局串行,只允许当前活动工程接管窗口级配置,并且只接受最新检查结果。 +- 修改 `mcpp.clangd.path` 或模块支持模式后自动复查,并为直接 clangd 检查增加 60 秒超时。 +- 收紧 Restricted Mode:未受信任工作区不执行 CDB 编译器、mcpp 或 clangd,也不修改 clangd 配置;授予信任后自动协调。 +- 补充独立安装 mcpp 的配置说明;`mcpp.path`、CDB 中的编译器与 `mcpp.clangd.path` 相互独立,`clang-tidy` 和 `clang-format` 不是当前扩展的运行依赖。 + +## 0.1.4 + +- 缺少编译数据库时,状态栏改为直接执行“刷新编译数据库”;CDB 创建或变化后会自动重新配置并重启 clangd,不再要求重载窗口。 +- 增加 `mcpp.path` 设置,允许 macOS GUI 环境显式指定 mcpp 可执行文件。 +- `mcpp build` 失败后仍会检查现有 CDB;IDE 配置成功与构建失败分别提示,不再误报 clangd 已刷新。 +- Restricted Mode 下不会执行 `mcpp build`、依赖安装或 `build.mcpp`。 +- 在 README 中记录无需正式构建即可获得模块语义能力的长期核心接口、CDB 合同和插件演进计划。 + +## 0.1.3 + +- 修复 C++ 模块语法未实际注入 `source.cpp` 的问题,`import`、模块名和模块分区现在会使用扩展提供的 TextMate scope。 +- 增加扩展清单回归测试,防止语法注入注册字段再次写错。 + +## 0.1.2 + +- 在扩展激活和刷新编译数据库时自动配置匹配的 clangd,避免官方 clangd 扩展启动到 xlings shim。 +- 模块语法高亮支持未输入分号的编辑中间态,并补充 `.mpp`、`.ccm` 文件关联。 +- 避免重载窗口期间异步检查向已关闭的输出频道写入。 + +## 0.1.1 + +- 修复 `clangd.arguments` 中 `${workspaceFolder}` 未解析导致 clangd 忽略编译数据库的问题。 +- 从 mcpp 的 `.mcpp` 工具链路径自动查找用户目录下 `.xlings` 中匹配版本的 clangd。 + +## 0.1.0 + +- 为 LLVM 后端的 mcpp 工程配置官方 clangd 扩展。 +- 通过直接执行 `clangd --check` 诊断 PCM 和工具链不匹配。 +- 将 GCC、MSVC 模块工程降级为语法高亮,并显示明确的能力提示。 +- 增加覆盖声明、导入、模块名和分区的 C++ 模块注入语法。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b4cab6 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# mcpp VS Code 扩展 + +本扩展把 mcpp 工程接入官方 +`llvm-vs-code-extensions.vscode-clangd` 扩展。它不实现第二套语言服务器, +也不会改写 `compile_commands.json` 或模块文件。 + +## 首版范围 + +- LLVM/Clang mcpp 工程:配置匹配的 clangd,并直接检查 mcpp 编译数据库和 + PCM 文件。扩展激活时会自动把官方 clangd 扩展切换到匹配的真实可执行文件, + 不会继续使用未激活版本的 xlings shim。 +- GCC 和 MSVC mcpp 工程:保留 C++ 模块语法高亮,并明确提示 `.gcm`、 + `.ifc` 不能由 clangd 消费。 +- `.cppm`、`.ixx`、`.mpp`、`.ccm` 关联到内置 `cpp` 语言。注入语法规则覆盖 `module`、 + `export module`、`import`、`export import`、模块名和分区。 + +clangd 的 C++20 模块支持仍处于实验阶段。语法颜色正常不代表语义诊断可用。 +clangd、mcpp 编译器、libc++ 和 PCM/BMI 应来自同一个 LLVM 构建;只匹配版本号 +并不足够。 + +## 使用条件 + +1. VS Code 1.90 或更高版本。 +2. 官方 `llvm-vs-code-extensions.vscode-clangd` 扩展。它是功能依赖,VS Code + 会随本扩展一起安装。 +3. mcpp,以及包含 `mcpp.toml` 的 mcpp 工程。mcpp 可以由 xlings 安装,也可以使用 + 官方独立安装脚本;扩展不依赖 mcpp 的安装来源。 +4. 要获得模块语义诊断,需要 LLVM mcpp 工具链和匹配的 clangd。扩展会优先使用 + `mcpp.clangd.path`,否则依次查找编译器同目录、同一工具包根目录中的 + `xim-x-llvm-tools//bin`、从 `.mcpp` 工具链映射到用户目录下的 + `.xlings/data/xpkgs/xim-x-llvm-tools//bin`,最后查找 `PATH`。 + +mcpp 会把 `compile_commands.json` 写到工程根目录。扩展只读取它,不会修改它。 + +## 当前版本行为 + +1. 工程已有可用的 `compile_commands.json` 时,扩展激活后会分析其中的 LLVM + 编译器,选择匹配的 clangd,并应用工作区配置;随后自动执行一次 `clangd --check`, + 状态栏直接显示“模块可用”或“模块不可用”,不需要先点击状态栏按钮。 +2. 工程缺少编译数据库时,状态栏显示“缺少模块 CDB”;在受信任工作区点击该状态 + 会执行 **mcpp: 刷新编译数据库**。未受信任工作区只保留模块语法高亮,不会执行 + CDB 中的编译器、mcpp 或 clangd,也不会接管 clangd 配置。扩展在受信任工作区中 + 也不会仅因打开工程而静默构建。 +3. `compile_commands.json` 创建或变化后,扩展会自动重新分析、重启 clangd 并再次 + 检查模块支持,不再要求手动点击检查按钮或 Reload Window。检查结果按工程缓存, + 工程内的重复事件会合并,所有共享 clangd 操作全局串行。多根工作区仅由当前活动 + mcpp 工程接管窗口级 clangd 配置;后台工程只失效缓存,切换过去时再自动配置, + 因此迟到检查不会覆盖当前工程状态。 +4. 修改 `mcpp.clangd.path` 或 `mcpp.modulesSupport` 后,扩展会自动重新配置和检查。 + 单次 `clangd --check` 最长等待 60 秒,避免异常进程让激活流程一直停住。 +5. `mcpp build` 即使因普通源码错误返回非零,只要工程中已经出现可用 CDB, + 扩展仍会配置 clangd,并明确提示“构建失败,但 IDE 数据已就绪”。 + +## 命令 + +- **mcpp: 配置 clangd**:手动重新应用自动配置。命令会查找最近的工程,保留已有 + `clangd.arguments`,将其中的 + `${workspaceFolder}`/`${workspaceRoot}`解析为 mcpp 工程根目录,添加精确的 mcpp + 编译器 `--query-driver`,并应用模块支持策略。如果 cpptools IntelliSense 已启用, + 会先询问是否只关闭它的 IntelliSense 引擎。 +- **mcpp: 刷新编译数据库**:在最近的工程中显式执行 `mcpp build`,随后无论构建 + 是否成功都会重新读取 CDB;只要 CDB 和 clangd 可用,就会刷新配置并重启 + clangd。扩展激活时不会自动执行构建。 +- **mcpp: 检查模块支持**:立即重新执行 clangd 直接检查,报告检查成功、模块文件 + 缺失、语言模式错误或 PCM/LLVM 不匹配。扩展激活和 CDB 变化时已经会自动检查; + 此命令用于主动复查。完整输出位于 `mcpp` 输出频道。 + +## 设置 + +```json +{ + "mcpp.path": "/path/to/mcpp", + "mcpp.clangd.path": "/path/to/matching/clangd", + "mcpp.modulesSupport": "auto", + "mcpp.configureCppTools": true +} +``` + +`mcpp.path` 只用于“刷新编译数据库”命令执行 `mcpp build`,用于解决 macOS GUI +启动的 VS Code 没有继承终端 `PATH` 的情况。留空时扩展直接执行 `mcpp`;也可以设置 +独立安装的绝对路径,例如 `/opt/homebrew/bin/mcpp`。工程实际使用的编译器始终从 +`compile_commands.json` 读取,与 `mcpp.path` 相互独立。 + +`mcpp.clangd.path` 只指定语言服务器。独立安装的 mcpp LLVM 工具链即使只包含 +`clang`/`clang++`,也仍可配合其他位置的 clangd 使用;关键是 clangd、编译器和 +PCM/BMI 来自同一 LLVM revision。可以单独安装 xlings 的 `xim-x-llvm-tools` 并让 +扩展自动映射,也可以显式填写其他匹配的 clangd。当前版本不调用 `clang-tidy` 或 +`clang-format`,缺少这两个工具不会影响模块诊断、悬停、补全和跳转。 + +`mcpp.modulesSupport` 可设为 `auto`、`on` 或 `off`。`auto` 检测到 mcpp CDB 已经 +包含显式 `-fmodule-file`/`-fprebuilt-module-path` 时,会直接使用这些 PCM;否则 +clangd 21 及更高版本会请求 `--experimental-modules-support`。`on` 强制启用 clangd +实验模块构建,`off` 始终禁用它。已有的 clangd 参数会保留;扩展只替换自己管理的 +`--query-driver` 和实验模块参数。 + +## 限制 + +- clangd 模块实现仍处于实验阶段,编译数据库必须包含有效的 mcpp PCM 映射。单独 + 添加开关不能生成缺失的 BMI。 +- GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。本版本不会为这两类工程额外构建 + LLVM 分析副本。 +- 语法规则只负责词法着色。红色诊断来自语言服务器,需要兼容的 LLVM 工具链。 +- 扩展不会静默执行无缓存构建,也不会自动下载工具链。 +- 未受信任工作区不执行任何由工程文件或设置选择的外部工具;信任工作区后会自动开始 + 配置和模块检查。 + +## 长期完整计划 + +### 理想最终效果 + +用户打开一个受信任的 mcpp 工程后,不需要先执行 `mcpp build`,也不需要 Reload +Window。扩展应自动读取 `mcpp.toml` 对应的工程模型,准备依赖和工具链,生成专供 +IDE 使用的完整模块图、BMI/PCM 和 `compile_commands.json`,然后启动匹配的 +clangd。即使普通源码暂时存在语法或编译错误,编辑器仍应具备: + +- 对错误模块名和不可见模块给出诊断; +- 对模块导出的声明提供悬停、补全、查找引用和定义跳转; +- 在修改 `mcpp.toml` 后增量刷新依赖和模块图; +- 在没有网络时复用本机缓存和最后一次可用的 IDE 数据。 + +这应当接近 Cargo 与 rust-analyzer 的分工:构建工具提供稳定、可查询的工程模型, +语言服务器消费该模型;VS Code 扩展只负责生命周期、用户授权和状态展示,不在插件 +中重新实现一套 mcpp TOML 解析器或包管理器。 + +### mcpp 核心需要提供的接口 + +mcpp 核心需要先提供带版本的机器可读协议,例如: + +```sh +mcpp metadata --format-version=1 +mcpp configure --ide --json +``` + +`metadata` 应稳定输出工作区成员、目标、源码、模块、依赖、工具链和缓存位置,不执行 +构建。`configure --ide` 负责解析和下载依赖、准备工具链、处理 `build.mcpp` 与生成源、 +扫描完整模块图、预编译语义分析所需的 BMI/PCM,并原子写入 IDE CDB;它不编译普通 +实现源文件,也不链接最终可执行文件或库。命令需要结构化输出进度、诊断、产物路径和 +可取消状态,不能要求插件解析面向人的终端文本。 + +IDE CDB 与模块图至少需要保证: + +- 模块 producer 条目包含确定的 `-fmodule-output=`; +- consumer 条目包含确定的 `-fmodule-file==`,不依赖 clangd 猜测产物; +- 覆盖命名模块、模块分区、`std`、依赖包、工作区成员和测试目标; +- 编译器路径、语言标准、宏、包含目录、工作目录与正式构建一致; +- 先写临时文件再原子替换,失败时保留最后一次可用 CDB,避免 clangd 读到半写入 JSON; +- 协议和路径字段有版本,允许 mcpp 与插件独立升级。 + +### VS Code 扩展需要完成的工作 + +在上述核心接口可用后,扩展按以下阶段演进: + +1. 打开工程或 `mcpp.toml` 变化时,在工作区受信任后调用 IDE 配置命令;涉及下载、 + 执行 `build.mcpp` 或生成文件时,必须遵守 VS Code Workspace Trust。 +2. 显示可取消的进度,支持在线、离线和只使用缓存三种模式;同一工程的连续事件使用 + debounce 与 singleflight 合并,避免并发下载或反复重启 clangd。 +3. 配置失败时继续使用 last-known-good CDB,并在状态栏区分“正在准备”“可用但过期” + “配置失败”和“模块语义可用”。 +4. mcpp 输出新模块图和 CDB 后再切换 clangd;多根工作区按工程根目录隔离状态、进程和 + 取消操作。 +5. 增加协议兼容测试、无网络缓存测试、源码错误但 IDE 配置成功测试,以及真实 VS Code + 中的诊断、悬停、补全和跳转端到端测试。 + +### 技术边界 + +- clangd 要完成跨模块语义分析,所需 BMI/PCM 仍必须由兼容的 LLVM 工具链预编译; + “不执行正式 build”不等于完全不产生模块二进制产物。 +- 第一版完整语义能力只面向 LLVM。GCC `.gcm` 和 MSVC `.ifc` 不能直接交给 clangd, + 在没有独立 LLVM 分析配置前仍只保证模块语法高亮。 +- clangd 负责编辑期语法和语义,不替代链接、代码生成、运行期或完整打包检查;这些错误 + 仍由正式 `mcpp build`/`mcpp test` 报告。 +- 当前 `0.1.5` 只是过渡方案:它能在用户显式构建后自动接管 CDB,但无法仅凭现有插件 + 在无 CDB 时推导完整依赖图。最终体验依赖上述 mcpp 核心协议和 IDE 配置阶段。 + +## 故障排查 + +- mcpp 生成的 PCM 必须由同一 LLVM revision 的 clangd 读取。Homebrew clangd 即使 + 版本号相同,也可能与 mcpp xlings 工具链来自不同 revision。此时应将 + `mcpp.clangd.path` 留空,让扩展自动选择 `.xlings` 中匹配的 clangd,或显式设置 + 对应的 `.xlings/data/xpkgs/xim-x-llvm-tools//bin/clangd`。 +- 独立 mcpp 的 LLVM 编译器通常位于 `.mcpp/registry/.../xim-x-llvm//bin`。 + 扩展会尝试映射到 `.xlings/.../xim-x-llvm-tools//bin/clangd`;如果该工具包 + 未安装,则只需安装匹配的 clangd 或配置 `mcpp.clangd.path`,不需要改用 xlings 版 + mcpp。 +- 扩展会把已有参数中的 `${workspaceFolder}` 和 `${workspaceRoot}` 展开为当前 mcpp + 工程根目录,避免 clangd 将变量名当成字面路径后忽略编译数据库。 +- 安装后应能在 `code --list-extensions` 中看到 `mcpp-community.mcpp-vscode`。仅保留在 + `~/.vscode/extensions` 目录、但没有登记到当前 VS Code profile 时,扩展不会激活, + 语法注入和 clangd 自动配置也都不会发生。 + +## 开发 + +```sh +npm install +npm test +npm run compile +``` + +生成的 `dist/` 是扩展运行时目录。请在 Extension Development Host 中运行上述 +扩展,验证 VS Code API 集成。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b7169ac --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3852 @@ +{ + "name": "mcpp-vscode", + "version": "0.1.5", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcpp-vscode", + "version": "0.1.5", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^24.0.0", + "@types/vscode": "^1.90.0", + "@vscode/vsce": "^3.9.2", + "typescript": "^5.9.0" + }, + "engines": { + "vscode": "^1.90.0" + } + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.17.3", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.17.3.tgz", + "integrity": "sha512-qMabD7Xrm/UgRhs+/IVyCTZRjUl8Qb+uGsRrCkaKNWsiTwRaeyStJbChE7/ySNTNYtiWo7khfF7vUDd0wGMnLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.11.3" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.11.3", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.11.3.tgz", + "integrity": "sha512-VeXOW+t3Rdd9XGX6lVyIg3DhtjMR1JD8ARKcsnGbJFUWwAmF3sHL7GwZc/ZjEUfHESResAonETRYCuG06OBT7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.4.3.tgz", + "integrity": "sha512-tumCMmzrRhKmTbYQg/7OlfbrIKcKaf8Ed0Fw3suUpRT3owFYljznVgxcfHe8RycQXY9uyROGiLD1GjhpF45AwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.11.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", + "chalk": "^4.1.2", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.7.1" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.8.tgz", + "integrity": "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^13.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^10.2.2", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e6aafb7 --- /dev/null +++ b/package.json @@ -0,0 +1,119 @@ +{ + "name": "mcpp-vscode", + "displayName": "mcpp", + "description": "mcpp 与 C++ 模块的 VS Code 集成", + "version": "0.1.5", + "publisher": "mcpp-community", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/mcpp-community/mcpp-vscode.git" + }, + "homepage": "https://github.com/mcpp-community/mcpp-vscode#readme", + "bugs": { + "url": "https://github.com/mcpp-community/mcpp-vscode/issues" + }, + "engines": { + "vscode": "^1.90.0" + }, + "extensionDependencies": [ + "llvm-vs-code-extensions.vscode-clangd" + ], + "activationEvents": [ + "workspaceContains:mcpp.toml", + "onLanguage:cpp", + "onCommand:mcpp.configureClangd", + "onCommand:mcpp.refreshCompilationDatabase", + "onCommand:mcpp.checkModuleSupport" + ], + "categories": [ + "Programming Languages", + "Other" + ], + "capabilities": { + "untrustedWorkspaces": { + "supported": "limited", + "description": "未受信任工作区仅启用模块语法高亮,不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。" + } + }, + "main": "./dist/src/extension.js", + "contributes": { + "commands": [ + { + "command": "mcpp.configureClangd", + "title": "mcpp: 配置 clangd" + }, + { + "command": "mcpp.refreshCompilationDatabase", + "title": "mcpp: 刷新编译数据库" + }, + { + "command": "mcpp.checkModuleSupport", + "title": "mcpp: 检查模块支持" + } + ], + "configuration": { + "title": "mcpp 设置", + "properties": { + "mcpp.path": { + "type": "string", + "default": "", + "scope": "resource", + "description": "仅供“刷新编译数据库”执行 mcpp build 使用;留空时从 VS Code 进程的 PATH 查找 mcpp。" + }, + "mcpp.clangd.path": { + "type": "string", + "default": "", + "scope": "resource", + "description": "与 CDB 中 LLVM 编译器来自同一 revision 的 clangd 可执行文件路径;不要求与 mcpp 安装在同一位置。" + }, + "mcpp.modulesSupport": { + "type": "string", + "enum": [ + "auto", + "on", + "off" + ], + "default": "auto", + "scope": "resource", + "description": "控制 clangd 的 C++ 模块实验支持参数。" + }, + "mcpp.configureCppTools": { + "type": "boolean", + "default": true, + "scope": "resource", + "description": "配置 clangd 时,是否询问关闭 Microsoft C/C++ IntelliSense。" + } + } + }, + "configurationDefaults": { + "files.associations": { + "*.ccm": "cpp", + "*.cppm": "cpp", + "*.ixx": "cpp", + "*.mpp": "cpp" + } + }, + "grammars": [ + { + "scopeName": "source.cpp.mcpp-modules", + "injectTo": [ + "source.cpp" + ], + "path": "./syntaxes/mcpp-modules.tmLanguage.json" + } + ] + }, + "scripts": { + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "compile": "tsc -p tsconfig.json", + "test": "npm run clean && npm run compile && node --test dist/test/*.test.js", + "package": "npm run clean && npm run compile && vsce package" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "@types/vscode": "^1.90.0", + "@vscode/vsce": "^3.9.2", + "typescript": "^5.9.0" + } +} diff --git a/src/analysis.ts b/src/analysis.ts new file mode 100644 index 0000000..48e1356 --- /dev/null +++ b/src/analysis.ts @@ -0,0 +1,403 @@ +import path from "node:path"; + +export type CompilerKind = "llvm" | "gcc" | "msvc" | "unknown"; +export type ModuleCapability = "full" | "syntax-only" | "unavailable"; +export type ModulesSupportMode = "auto" | "on" | "off"; +export type CheckResult = + | "ready" + | "pcm-mismatch" + | "module-unavailable" + | "wrong-language-mode" + | "check-failed"; + +export interface CompilationDatabaseAnalysis { + kind: CompilerKind; + capability: ModuleCapability; + compilerPath?: string; + sourceFile?: string; + directory?: string; + arguments?: string[]; + hasPrebuiltModules?: boolean; + reason: string; +} + +export interface ToolIdentity { + major: number; + minor: number; + patch: number; + revision?: string; +} + +export interface ToolIdentityComparison { + compatible: boolean; + reason: string; +} + +export interface ClangdArgumentOptions { + compilerPath: string; + modulesSupport: ModulesSupportMode; + clangdIdentity?: ToolIdentity; + platform: NodeJS.Platform; + hasPrebuiltModules?: boolean; + workspaceFolder?: string; +} + +export interface ClangdConfigurationPlan { + path: string; + arguments: string[]; + changed: boolean; +} + +interface CompilationCommand { + directory?: unknown; + file?: unknown; + arguments?: unknown; + command?: unknown; +} + +function unavailable(reason: string): CompilationDatabaseAnalysis { + return { + kind: "unknown", + capability: "unavailable", + reason, + }; +} + +function splitCommand(command: string): string[] { + const result: string[] = []; + let token = ""; + let quote: "'" | '"' | undefined; + + for (let index = 0; index < command.length; index += 1) { + const character = command[index]; + + if (quote !== "'" && character === "\\" && index + 1 < command.length) { + const next = command[index + 1]; + if (next === "\\" || next === "'" || next === '"' || /\s/.test(next)) { + token += next; + index += 1; + continue; + } + token += character; + continue; + } + + if (character === "'" || character === '"') { + if (quote === character) { + quote = undefined; + } else if (quote === undefined) { + quote = character; + } else { + token += character; + } + continue; + } + + if (/\s/.test(character) && quote === undefined) { + if (token.length > 0) { + result.push(token); + token = ""; + } + continue; + } + + token += character; + } + + if (token.length > 0) { + result.push(token); + } + + return result; +} + +function commandArguments(command: CompilationCommand): string[] | undefined { + if ( + Array.isArray(command.arguments) + && command.arguments.length > 0 + && command.arguments.every((argument) => typeof argument === "string") + ) { + return command.arguments; + } + + if (typeof command.command === "string") { + const argumentsFromCommand = splitCommand(command.command); + return argumentsFromCommand.length > 0 ? argumentsFromCommand : undefined; + } + + return undefined; +} + +function compilerKind(compilerPath: string): CompilerKind { + const executable = path.win32.basename(compilerPath).toLowerCase(); + const name = executable.endsWith(".exe") ? executable.slice(0, -4) : executable; + + if (name === "clang" || name === "clang++" || name === "clang-cl") { + return "llvm"; + } + if (name === "gcc" || name === "g++" || name === "c++") { + return "gcc"; + } + if (name === "cl") { + return "msvc"; + } + return "unknown"; +} + +export function analyzeCompilationDatabase(contents: string): CompilationDatabaseAnalysis { + let parsed: unknown; + try { + parsed = JSON.parse(contents); + } catch { + return unavailable("compile_commands.json 不是有效的 JSON"); + } + + if (!Array.isArray(parsed) || parsed.length === 0) { + return unavailable("compile_commands.json 至少需要包含一条编译命令"); + } + + const candidates: CompilationDatabaseAnalysis[] = []; + + for (const value of parsed) { + if (value === null || typeof value !== "object") { + continue; + } + + const command = value as CompilationCommand; + const args = commandArguments(command); + if (args === undefined) { + continue; + } + + const kind = compilerKind(args[0]); + if (kind === "unknown") { + continue; + } + + const hasPrebuiltModules = args.some((argument) => ( + argument.startsWith("-fmodule-file=") + || argument.startsWith("-fprebuilt-module-path=") + )); + candidates.push({ + kind, + capability: kind === "llvm" ? "full" : "syntax-only", + compilerPath: args[0], + sourceFile: typeof command.file === "string" ? command.file : undefined, + directory: typeof command.directory === "string" ? command.directory : undefined, + arguments: args, + hasPrebuiltModules, + reason: kind === "llvm" + ? "Clang 编译命令可以由 clangd 使用" + : `${kind.toUpperCase()} 模块产物不能由 clangd 使用`, + }); + } + + if (candidates.length === 0) { + return unavailable("compile_commands.json 不包含受支持的编译器命令"); + } + + const score = (candidate: CompilationDatabaseAnalysis): number => { + const sourceFile = candidate.sourceFile ?? ""; + const moduleInterface = /\.(?:cppm|ixx|mpp|ccm)$/i.test(sourceFile); + const inProject = candidate.directory !== undefined && isWithinDirectory(candidate.directory, sourceFile); + return (inProject ? 200 : 0) + + (moduleInterface ? 100 : 0) + + (candidate.hasPrebuiltModules ? 10 : 0); + }; + + return candidates.reduce((best, candidate) => (score(candidate) > score(best) ? candidate : best)); +} + +function isWithinDirectory(directory: string, file: string): boolean { + const windows = /^[A-Za-z]:[\\/]/.test(directory) || directory.includes("\\") + || /^[A-Za-z]:[\\/]/.test(file) || file.includes("\\"); + const pathApi = windows ? path.win32 : path.posix; + const relative = pathApi.relative(pathApi.resolve(directory), pathApi.resolve(file)); + return relative === "" + || (relative !== ".." && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative)); +} + +function removeManagedArguments(arguments_: readonly string[]): string[] { + const result: string[] = []; + + for (let index = 0; index < arguments_.length; index += 1) { + const argument = arguments_[index]; + if (argument === "--experimental-modules-support") { + continue; + } + if (argument === "--query-driver") { + index += 1; + continue; + } + if (argument.startsWith("--query-driver=")) { + continue; + } + result.push(argument); + } + + return result; +} + +function expandWorkspaceVariables(argument: string, workspaceFolder?: string): string { + if (workspaceFolder === undefined) { + return argument; + } + + return argument.replace(/\$\{workspace(?:Folder|Root)\}/g, () => workspaceFolder); +} + +function shouldEnableExperimentalModules(options: ClangdArgumentOptions): boolean { + if (options.modulesSupport === "on") { + return true; + } + if (options.modulesSupport === "off") { + return false; + } + if (options.hasPrebuiltModules) { + return false; + } + + const identity = options.clangdIdentity; + if (identity === undefined || identity.major < 21) { + return false; + } + + return !( + options.platform === "win32" + && identity.major === 20 + && identity.minor === 1 + && identity.patch === 7 + ); +} + +export function buildClangdArguments( + existingArguments: readonly string[], + options: ClangdArgumentOptions, +): string[] { + const result = removeManagedArguments(existingArguments) + .map((argument) => expandWorkspaceVariables(argument, options.workspaceFolder)); + result.push(`--query-driver=${options.compilerPath}`); + + if (shouldEnableExperimentalModules(options)) { + result.push("--experimental-modules-support"); + } + + return result; +} + +export function buildClangdConfigurationPlan( + currentPath: string, + currentArguments: readonly string[], + resolvedPath: string, + options: ClangdArgumentOptions, +): ClangdConfigurationPlan { + const arguments_ = buildClangdArguments(currentArguments, options); + return { + path: resolvedPath, + arguments: arguments_, + changed: currentPath !== resolvedPath + || arguments_.length !== currentArguments.length + || arguments_.some((argument, index) => argument !== currentArguments[index]), + }; +} + +export function parseToolIdentity(versionOutput: string): ToolIdentity | undefined { + const version = versionOutput.match(/\b(?:clangd|clang)(?:\s+version)?\s+(\d+)\.(\d+)(?:\.(\d+))?/i) + ?? versionOutput.match(/\b(\d+)\.(\d+)(?:\.(\d+))?\b/); + if (version === null) { + return undefined; + } + + const revision = versionOutput.match(/\b[0-9a-f]{7,40}\b/gi)?.at(-1); + return { + major: Number(version[1]), + minor: Number(version[2]), + patch: Number(version[3] ?? 0), + ...(revision === undefined ? {} : { revision: revision.toLowerCase() }), + }; +} + +export function compareToolIdentities( + compiler: ToolIdentity | undefined, + clangd: ToolIdentity | undefined, +): ToolIdentityComparison { + if (compiler === undefined || clangd === undefined) { + return { + compatible: false, + reason: "无法确定两套 LLVM 工具的身份", + }; + } + + if ( + compiler.major !== clangd.major + || compiler.minor !== clangd.minor + || compiler.patch !== clangd.patch + ) { + return { + compatible: false, + reason: "编译器与 clangd 的 LLVM 版本不同", + }; + } + + if (compiler.revision === undefined || clangd.revision === undefined) { + return { + compatible: false, + reason: "LLVM 版本相同,但无法获得精确的 revision", + }; + } + + if ( + !compiler.revision.startsWith(clangd.revision) + && !clangd.revision.startsWith(compiler.revision) + ) { + return { + compatible: false, + reason: "编译器与 clangd 的 LLVM revision 不同", + }; + } + + return { + compatible: true, + reason: "LLVM 版本和 revision 均匹配", + }; +} + +export function classifyCheckResult(exitCode: number, output: string): CheckResult { + if (exitCode === 0) { + return "ready"; + } + + const normalized = output.toLowerCase(); + if ( + normalized.includes("ast_file_different_branch") + || normalized.includes("different branch") + || normalized.includes("pch file uses an older pch format") + ) { + return "pcm-mismatch"; + } + if ( + normalized.includes("don't get the module unit") + || normalized.includes("failed to build module") + || normalized.includes("module file not found") + || /module ['\"].+['\"] not found/.test(normalized) + ) { + return "module-unavailable"; + } + if ( + normalized.includes("unknown type name 'import'") + || normalized.includes('unknown type name "import"') + || normalized.includes("expected unqualified-id") && normalized.includes("import") + ) { + return "wrong-language-mode"; + } + if (normalized.includes("all checks completed")) { + const diagnostics = output + .split(/\r?\n/) + .filter((line) => /^\s*e\[/i.test(line)); + if (diagnostics.length === 0 || diagnostics.every((line) => /\btweak:/i.test(line))) { + return "ready"; + } + } + + return "check-failed"; +} diff --git a/src/discovery.ts b/src/discovery.ts new file mode 100644 index 0000000..837bc9b --- /dev/null +++ b/src/discovery.ts @@ -0,0 +1,76 @@ +import { + existsSync, + statSync, +} from "node:fs"; +import path from "node:path"; + +export interface McppProjectDiscovery { + root: string; + manifestPath: string; + compilationDatabasePath: string; +} + +function isWindowsPath(value: string): boolean { + return /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\"); +} + +function unique(values: string[]): string[] { + return values.filter((value, index) => values.indexOf(value) === index); +} + +export function findNearestMcppProject(startPath: string): McppProjectDiscovery | undefined { + let current = path.resolve(startPath); + + try { + if (statSync(current).isFile()) { + current = path.dirname(current); + } + } catch { + // 新创建的 VS Code 工作区路径可能尚不存在,此时按目录处理。 + } + + while (true) { + const manifestPath = path.join(current, "mcpp.toml"); + if (existsSync(manifestPath)) { + return { + root: current, + manifestPath, + compilationDatabasePath: path.join(current, "compile_commands.json"), + }; + } + + const parent = path.dirname(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +export function deriveClangdCandidates(compilerPath: string): string[] { + const windows = isWindowsPath(compilerPath); + const pathApi = windows ? path.win32 : path.posix; + const compilerName = pathApi.basename(compilerPath); + const extension = /\.exe$/i.test(compilerName) ? ".exe" : ""; + const sibling = pathApi.join(pathApi.dirname(compilerPath), `clangd${extension}`); + const xlingsMatch = compilerPath.match( + /^(.*?)([\\/])xim-x-llvm[\\/](\d+(?:\.\d+){1,3})[\\/]bin[\\/]clang(?:\+\+|-cl)(\.exe)?$/i, + ); + + const xlingsCandidate = xlingsMatch === null + ? undefined + : `${xlingsMatch[1]}${xlingsMatch[2]}xim-x-llvm-tools${xlingsMatch[2]}${xlingsMatch[3]}${xlingsMatch[2]}bin${xlingsMatch[2]}clangd${xlingsMatch[4] ?? ""}`; + const userXlingsMatch = compilerPath.match( + /^(.*?)([\\/])\.mcpp[\\/]registry[\\/]data[\\/]xpkgs[\\/]xim-x-llvm[\\/](\d+(?:\.\d+){1,3})[\\/]bin[\\/]clang(?:\+\+|-cl)(\.exe)?$/i, + ); + const userXlingsCandidate = userXlingsMatch === null + ? undefined + : `${userXlingsMatch[1]}${userXlingsMatch[2]}.xlings${userXlingsMatch[2]}data${userXlingsMatch[2]}xpkgs${userXlingsMatch[2]}xim-x-llvm-tools${userXlingsMatch[2]}${userXlingsMatch[3]}${userXlingsMatch[2]}bin${userXlingsMatch[2]}clangd${userXlingsMatch[4] ?? ""}`; + + return unique([ + sibling, + ...(xlingsCandidate === undefined ? [] : [xlingsCandidate]), + ...(userXlingsCandidate === undefined ? [] : [userXlingsCandidate]), + "clangd", + ]); +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..b2610bb --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,829 @@ +import { existsSync, readFileSync } from "node:fs"; +import process from "node:process"; + +import * as vscode from "vscode"; + +import { + analyzeCompilationDatabase, + buildClangdArguments, + buildClangdConfigurationPlan, + compareToolIdentities, + type CheckResult, + type CompilationDatabaseAnalysis, + type ModulesSupportMode, + type ToolIdentityComparison, +} from "./analysis"; +import { + deriveClangdCandidates, + findNearestMcppProject, + type McppProjectDiscovery, +} from "./discovery"; +import { + runClangdCheck, + runMcppBuild, + runToolVersion, + type ToolVersionResult, +} from "./process"; +import { + configurationReadyAfterRestart, + configurationAffectsModuleSupport, + createKeyedSingleFlightReconciler, + createLatestOperationTracker, + createSerialExecutor, + describeRefreshOutcome, + moduleSupportState, + registerCompilationDatabaseReconciliation, + shouldRestartClangd, + shouldCheckModuleSupport, + shouldRenderProjectStatus, + shouldUseWorkspaceClangd, + statusCommandForCapability, + workspaceAllowsToolExecution, + type ModuleSupportState, +} from "./workflow"; + +const COMMAND_CONFIGURE = "mcpp.configureClangd"; +const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; +const COMMAND_CHECK = "mcpp.checkModuleSupport"; + +interface ProjectContext { + project: McppProjectDiscovery; + analysis: CompilationDatabaseAnalysis; +} + +interface ClangdResolution { + path: string; + version: ToolVersionResult; + comparison: ToolIdentityComparison; +} + +interface ProjectReconciliation { + context: ProjectContext | undefined; + databaseFound: boolean; + configured: boolean; +} + +interface ModuleStatus { + state: ModuleSupportState; + message: string; +} + +type ConfigureMode = "automatic" | "interactive"; + +const moduleStatusByProject = new Map(); +const moduleCheckOperations = createLatestOperationTracker(); + +function findCurrentProject(): McppProjectDiscovery | undefined { + const activePath = vscode.window.activeTextEditor?.document.uri.scheme === "file" + ? vscode.window.activeTextEditor.document.uri.fsPath + : undefined; + const searchPaths = [ + ...(activePath === undefined ? [] : [activePath]), + ...(vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath) ?? []), + ]; + + for (const searchPath of searchPaths) { + const project = findNearestMcppProject(searchPath); + if (project !== undefined) { + return project; + } + } + return undefined; +} + +function loadProjectContext(project: McppProjectDiscovery | undefined = findCurrentProject()): ProjectContext | undefined { + if (project === undefined) { + return undefined; + } + + if (!existsSync(project.compilationDatabasePath)) { + return { + project, + analysis: { + kind: "unknown", + capability: "unavailable", + reason: `找不到编译数据库:${project.compilationDatabasePath}`, + }, + }; + } + + try { + return { + project, + analysis: analyzeCompilationDatabase(readFileSync(project.compilationDatabasePath, "utf8")), + }; + } catch (error) { + return { + project, + analysis: { + kind: "unknown", + capability: "unavailable", + reason: error instanceof Error ? error.message : String(error), + }, + }; + } +} + +function configurationTarget(uri: vscode.Uri): vscode.ConfigurationTarget { + return vscode.workspace.getWorkspaceFolder(uri) === undefined + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.WorkspaceFolder; +} + +function projectConfiguration(project: McppProjectDiscovery): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration("mcpp", vscode.Uri.file(project.root)); +} + +function officialClangdConfiguration(project: McppProjectDiscovery): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration("clangd", vscode.Uri.file(project.root)); +} + +function appendProcessOutput( + output: vscode.OutputChannel, + title: string, + executable: string, + args: string[], + result: { exitCode: number; stdout: string; stderr: string }, +): void { + try { + output.appendLine(`\n[${new Date().toISOString()}] ${title}`); + output.appendLine(`$ ${executable} ${args.join(" ")}`); + if (result.stdout.length > 0) { + output.appendLine(result.stdout.trimEnd()); + } + if (result.stderr.length > 0) { + output.appendLine(result.stderr.trimEnd()); + } + output.appendLine(`[exit ${result.exitCode}]`); + } catch { + // 重载窗口时输出频道可能先于异步检查关闭。 + } +} + +function appendOutputLine(output: vscode.OutputChannel, value: string): void { + try { + output.appendLine(value); + } catch { + // 重载窗口时输出频道可能先于异步操作关闭。 + } +} + +async function resolveClangd(context: ProjectContext): Promise { + const compilerPath = context.analysis.compilerPath; + if (compilerPath === undefined) { + return undefined; + } + + const compilerVersion = await runToolVersion(compilerPath); + const configuredPath = projectConfiguration(context.project).get("clangd.path", "").trim(); + const candidates = configuredPath.length > 0 + ? [configuredPath] + : deriveClangdCandidates(compilerPath); + let fallback: ClangdResolution | undefined; + + for (const candidate of candidates) { + if (candidate !== "clangd" && !existsSync(candidate)) { + continue; + } + + const version = await runToolVersion(candidate); + if (version.exitCode !== 0 || version.identity === undefined) { + continue; + } + + const comparison = compareToolIdentities(compilerVersion.identity, version.identity); + const resolution = { path: candidate, version, comparison }; + if (comparison.compatible) { + return resolution; + } + fallback ??= resolution; + } + + return fallback; +} + +async function maybeDisableCppTools(project: McppProjectDiscovery): Promise { + if (vscode.extensions.getExtension("ms-vscode.cpptools") === undefined) { + return; + } + if (!projectConfiguration(project).get("configureCppTools", true)) { + return; + } + + const uri = vscode.Uri.file(project.root); + const configuration = vscode.workspace.getConfiguration("C_Cpp", uri); + if (configuration.get("intelliSenseEngine") === "disabled") { + return; + } + + const disable = "关闭 IntelliSense"; + const choice = await vscode.window.showWarningMessage( + "clangd 和 Microsoft C/C++ IntelliSense 都可能报告诊断。是否只关闭当前工作区的 cpptools IntelliSense?", + disable, + "保留 IntelliSense", + ); + if (choice === disable) { + await configuration.update( + "intelliSenseEngine", + "disabled", + configurationTarget(uri), + ); + } +} + +async function restartClangd(): Promise { + if (vscode.extensions.getExtension("llvm-vs-code-extensions.vscode-clangd") === undefined) { + return false; + } + try { + await vscode.commands.executeCommand("clangd.restart"); + return true; + } catch { + // clangd 扩展可能尚未激活,但配置仍然已经写入。 + return false; + } +} + +async function configureClangd( + context: ProjectContext, + status: vscode.StatusBarItem, + output: vscode.OutputChannel, + mode: ConfigureMode, + forceRestart: boolean = false, +): Promise { + const interactive = mode === "interactive"; + if (context.analysis.capability === "syntax-only") { + if (interactive) { + await vscode.window.showWarningMessage( + `${context.analysis.kind.toUpperCase()} 模块产物无法由 clangd 读取。语法高亮仍然可用,但模块语义诊断需要 LLVM mcpp 工具链。`, + ); + } + updateStatusBar(status, context); + return false; + } + if (context.analysis.capability !== "full" || context.analysis.compilerPath === undefined) { + if (interactive) { + await vscode.window.showWarningMessage( + `${context.analysis.reason} 请先运行“mcpp: 刷新编译数据库”。`, + ); + } + return false; + } + if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { + const message = "当前工作区未受信任,mcpp 不会执行 CDB 中的编译器或 clangd,也不会接管 clangd 配置。"; + if (interactive) { + await vscode.window.showWarningMessage(message); + } else { + appendOutputLine(output, `[自动配置] ${message}`); + } + return false; + } + + const clangd = await resolveClangd(context); + if (clangd === undefined) { + const message = "没有找到可用的 clangd。请安装与 mcpp LLVM 编译器来自同一 revision 的 clangd,或设置 mcpp.clangd.path;clangd 可以来自 xlings llvm-tools,也可以独立安装。"; + if (interactive) { + await vscode.window.showErrorMessage(message); + } else { + appendOutputLine(output, `[自动配置] ${message}`); + } + return false; + } + if (!clangd.comparison.compatible && !interactive) { + appendOutputLine( + output, + `[自动配置] 跳过不匹配的 clangd ${clangd.path}:${clangd.comparison.reason}。`, + ); + return false; + } + + const clangdConfiguration = officialClangdConfiguration(context.project); + const modulesSupport = projectConfiguration(context.project) + .get("modulesSupport", "auto"); + const plan = buildClangdConfigurationPlan( + clangdConfiguration.get("path", "clangd"), + clangdConfiguration.get("arguments", []), + clangd.path, + { + compilerPath: context.analysis.compilerPath, + modulesSupport, + clangdIdentity: clangd.version.identity, + platform: process.platform, + hasPrebuiltModules: context.analysis.hasPrebuiltModules, + workspaceFolder: context.project.root, + }, + ); + + if (plan.changed) { + // clangd.arguments 不是资源域设置,必须与 clangd.path 一起写到工作区层级。 + await clangdConfiguration.update("path", plan.path, vscode.ConfigurationTarget.Workspace); + await clangdConfiguration.update("arguments", plan.arguments, vscode.ConfigurationTarget.Workspace); + } + if (interactive) { + await maybeDisableCppTools(context.project); + } + const restartRequired = shouldRestartClangd(plan.changed, interactive, forceRestart); + const restartSucceeded = restartRequired ? await restartClangd() : false; + if (!configurationReadyAfterRestart(restartRequired, restartSucceeded)) { + const message = "clangd 配置已写入,但无法重启语言服务器。请查看 mcpp 输出频道,或手动执行 clangd 重启命令。"; + if (interactive) { + await vscode.window.showErrorMessage(message); + } else { + appendOutputLine(output, `[自动配置] ${message}`); + } + return false; + } + updateStatusBar(status, context); + + if (!clangd.comparison.compatible) { + await vscode.window.showWarningMessage( + `clangd 已配置,但 LLVM 身份与 mcpp 编译器不匹配:${clangd.comparison.reason}。`, + ); + } else if (interactive) { + await vscode.window.showInformationMessage("mcpp 已为当前工作区配置匹配的 clangd。"); + } else if (plan.changed) { + appendOutputLine(output, `[自动配置] clangd.path = ${plan.path}`); + } + + return true; +} + +function renderModuleStatus(status: vscode.StatusBarItem, moduleStatus: ModuleStatus): void { + status.command = COMMAND_CHECK; + status.show(); + status.text = moduleStatus.state === "available" + ? "$(pass) mcpp: 模块可用" + : "$(warning) mcpp: 模块不可用"; + status.tooltip = moduleStatus.message; +} + +function invalidateModuleStatus(projectRoot: string): void { + moduleCheckOperations.invalidate(projectRoot); + moduleStatusByProject.delete(projectRoot); +} + +function storeModuleStatus( + status: vscode.StatusBarItem, + context: ProjectContext, + moduleStatus: ModuleStatus, + checkToken?: number, +): boolean { + if ( + checkToken !== undefined + && !moduleCheckOperations.isCurrent(context.project.root, checkToken) + ) { + return false; + } + moduleStatusByProject.set(context.project.root, moduleStatus); + if (shouldRenderProjectStatus(findCurrentProject()?.root, context.project.root)) { + renderModuleStatus(status, moduleStatus); + } + return true; +} + +function updateStatusBar(status: vscode.StatusBarItem, context?: ProjectContext): void { + const currentProjectRoot = findCurrentProject()?.root; + if (context === undefined) { + if (currentProjectRoot === undefined) { + status.hide(); + } + return; + } + + if (context.analysis.capability !== "full") { + invalidateModuleStatus(context.project.root); + } + if (!shouldRenderProjectStatus(currentProjectRoot, context.project.root)) { + return; + } + + status.command = statusCommandForCapability(context.analysis.capability); + status.show(); + if (context.analysis.capability === "full") { + const moduleStatus = moduleStatusByProject.get(context.project.root); + if (moduleStatus !== undefined) { + renderModuleStatus(status, moduleStatus); + return; + } + status.text = "$(symbol-interface) mcpp: LLVM 模块"; + status.tooltip = "正在等待 clangd 检查模块编译命令和 PCM。"; + return; + } + if (context.analysis.capability === "syntax-only") { + status.text = `$(info) mcpp: 仅${context.analysis.kind.toUpperCase()}语法`; + status.tooltip = `${context.analysis.reason} 模块语法高亮仍然可用。`; + return; + } + + status.text = "$(warning) mcpp: 缺少模块 CDB"; + status.tooltip = context.analysis.reason; +} + +function checkResultMessage(classification: CheckResult): string { + switch (classification) { + case "ready": + return "clangd 已加载 mcpp 编译命令和模块产物。"; + case "pcm-mismatch": + return "clangd 与 mcpp 编译器或 PCM 来自不同的 LLVM 构建,请选择同一 LLVM revision 的 clangd。"; + case "module-unavailable": + return "clangd 无法加载所需模块产物,请运行“mcpp: 刷新编译数据库”,并检查 CDB 中的 PCM 路径。"; + case "wrong-language-mode": + return "clangd 没有为当前文件读取 C++20 或更高版本的编译命令。"; + default: + return "clangd 模块检查失败,完整诊断请查看 mcpp 输出频道。"; + } +} + +async function runModuleSupportCheck( + context: ProjectContext, + status: vscode.StatusBarItem, + output: vscode.OutputChannel, + mode: ConfigureMode, +): Promise { + const interactive = mode === "interactive"; + if (context.analysis.capability === "syntax-only") { + const message = `${context.analysis.kind.toUpperCase()} 模块产物不能由 clangd 消费,模块语法高亮仍然可用。`; + updateStatusBar(status, context); + if (interactive) { + await vscode.window.showWarningMessage(message); + } + return undefined; + } + if ( + context.analysis.capability !== "full" + || context.analysis.compilerPath === undefined + || context.analysis.sourceFile === undefined + ) { + invalidateModuleStatus(context.project.root); + const message = context.analysis.capability === "full" + ? "compile_commands.json 没有可供 clangd 检查的源文件。" + : context.analysis.reason; + if (context.analysis.capability === "full") { + storeModuleStatus(status, context, { state: "unavailable", message }); + } + if (interactive) { + await vscode.window.showWarningMessage(message); + } + return context.analysis.capability === "full" + ? { state: "unavailable", message } + : undefined; + } + + if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { + invalidateModuleStatus(context.project.root); + const message = "当前工作区未受信任,不会执行 clangd 模块检查。信任工作区后扩展会自动重新检查。"; + const moduleStatus = { state: "unavailable", message } as const; + storeModuleStatus(status, context, moduleStatus); + if (interactive) { + await vscode.window.showWarningMessage(message); + } else { + appendOutputLine(output, `[自动检查] ${message}`); + } + return moduleStatus; + } + + const checkToken = moduleCheckOperations.begin(context.project.root); + moduleStatusByProject.delete(context.project.root); + if (shouldRenderProjectStatus(findCurrentProject()?.root, context.project.root)) { + status.command = COMMAND_CHECK; + status.text = "$(sync~spin) mcpp: 正在检查模块"; + status.tooltip = "正在使用 clangd 检查模块编译命令和 PCM。"; + status.show(); + } + + const clangd = await resolveClangd(context); + if (clangd === undefined) { + const message = "当前 LLVM 工具链没有找到可用的 clangd。请设置 mcpp.clangd.path。"; + const moduleStatus = { state: "unavailable", message } as const; + if (!storeModuleStatus(status, context, moduleStatus, checkToken)) { + return undefined; + } + if (interactive) { + await vscode.window.showErrorMessage(message); + } else { + appendOutputLine(output, `[自动检查] ${message}`); + } + return moduleStatus; + } + + const arguments_ = buildClangdArguments( + officialClangdConfiguration(context.project).get("arguments", []), + { + compilerPath: context.analysis.compilerPath, + modulesSupport: projectConfiguration(context.project) + .get("modulesSupport", "auto"), + clangdIdentity: clangd.version.identity, + platform: process.platform, + hasPrebuiltModules: context.analysis.hasPrebuiltModules, + workspaceFolder: context.project.root, + }, + ); + const result = await runClangdCheck( + clangd.path, + context.analysis.sourceFile, + context.project.root, + arguments_, + ); + appendProcessOutput( + output, + interactive ? "检查模块支持" : "自动检查模块支持", + clangd.path, + [`--check=${context.analysis.sourceFile}`, ...arguments_], + result, + ); + if (interactive) { + output.show(true); + } + + const message = checkResultMessage(result.classification); + const moduleStatus = { + state: moduleSupportState(result.classification), + message, + }; + if (!storeModuleStatus(status, context, moduleStatus, checkToken)) { + return undefined; + } + if (interactive) { + if (moduleStatus.state === "available") { + await vscode.window.showInformationMessage(message); + } else { + await vscode.window.showErrorMessage(message); + } + } else { + appendOutputLine(output, `[自动检查] ${message}`); + } + return moduleStatus; +} + +async function updateModuleSupportForContext( + context: ProjectContext, + configured: boolean, + status: vscode.StatusBarItem, + output: vscode.OutputChannel, +): Promise { + if (shouldCheckModuleSupport( + context.analysis.capability, + configured, + context.analysis.sourceFile, + )) { + const moduleStatus = await runModuleSupportCheck(context, status, output, "automatic"); + return moduleStatus?.state === "available"; + } + + if (context.analysis.capability === "full") { + invalidateModuleStatus(context.project.root); + const message = configured + ? "compile_commands.json 没有可供 clangd 检查的源文件。" + : "clangd 未完成配置,模块语义当前不可用。详情请查看 mcpp 输出频道。"; + storeModuleStatus(status, context, { state: "unavailable", message }); + } + return false; +} + +export async function activate(extensionContext: vscode.ExtensionContext): Promise { + moduleStatusByProject.clear(); + moduleCheckOperations.clear(); + const output = vscode.window.createOutputChannel("mcpp"); + const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); + + const refreshStatus = (): void => updateStatusBar(status, loadProjectContext()); + const runGuarded = (operation: () => Promise): (() => Promise) => async () => { + try { + await operation(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `发生未预期错误:${message}`); + void vscode.window.showErrorMessage(`mcpp:${message}`); + } + }; + const manifestWatcher = vscode.workspace.createFileSystemWatcher("**/mcpp.toml"); + const compilationDatabaseWatcher = vscode.workspace.createFileSystemWatcher("**/compile_commands.json"); + const executeWithWorkspaceClangd = createSerialExecutor(); + const reconcileProjectContext = async ( + project: McppProjectDiscovery | undefined, + forceRestart: boolean, + ): Promise => { + const context = loadProjectContext(project); + updateStatusBar(status, context); + if (context === undefined) { + return { + context, + databaseFound: false, + configured: false, + }; + } + + const configured = await configureClangd( + context, + status, + output, + "automatic", + forceRestart, + ); + await updateModuleSupportForContext(context, configured, status, output); + return { + context, + databaseFound: existsSync(context.project.compilationDatabasePath), + configured, + }; + }; + const reconcileProjectByRoot = createKeyedSingleFlightReconciler( + (projectRoot: string, forceRestart) => executeWithWorkspaceClangd( + async () => { + const project = findNearestMcppProject(projectRoot); + if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, projectRoot)) { + const context = loadProjectContext(project); + return { + context, + databaseFound: context !== undefined + && existsSync(context.project.compilationDatabasePath), + configured: false, + }; + } + return reconcileProjectContext(project, forceRestart); + }, + ), + ); + const reconcileProject = ( + project: McppProjectDiscovery, + forceRestart: boolean = false, + ): Promise => { + invalidateModuleStatus(project.root); + return reconcileProjectByRoot(project.root, forceRestart); + }; + const requestAutomaticReconciliation = ( + compilationDatabase: vscode.Uri, + forceRestart: boolean, + ): void => { + const project = findNearestMcppProject(compilationDatabase.fsPath); + if (project === undefined) { + return; + } + invalidateModuleStatus(project.root); + if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { + return; + } + void reconcileProject(project, forceRestart).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[自动配置] ${message}`); + }); + }; + const requestCurrentProjectReconciliation = (forceRestart: boolean): void => { + const project = findCurrentProject(); + if (project === undefined) { + refreshStatus(); + return; + } + void reconcileProject(project, forceRestart).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[自动配置] ${message}`); + }); + }; + const configurationWatcher = vscode.workspace.onDidChangeConfiguration((event) => { + const project = findCurrentProject(); + if (project === undefined) { + return; + } + const uri = vscode.Uri.file(project.root); + if (configurationAffectsModuleSupport( + (section) => event.affectsConfiguration(section, uri), + )) { + requestCurrentProjectReconciliation(true); + } + }); + const trustWatcher = vscode.workspace.onDidGrantWorkspaceTrust(() => { + requestCurrentProjectReconciliation(true); + }); + + extensionContext.subscriptions.push( + output, + status, + vscode.commands.registerCommand(COMMAND_CONFIGURE, runGuarded(async () => { + const project = findCurrentProject(); + if (project === undefined) { + await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); + return; + } + await executeWithWorkspaceClangd(async () => { + if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { + return; + } + const context = loadProjectContext(project); + if (context === undefined) { + return; + } + const configured = await configureClangd(context, status, output, "interactive"); + await updateModuleSupportForContext(context, configured, status, output); + }); + })), + vscode.commands.registerCommand(COMMAND_REFRESH, runGuarded(async () => { + const context = loadProjectContext(); + if (context === undefined) { + await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); + return; + } + if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { + await vscode.window.showWarningMessage( + "当前工作区未受信任,mcpp 刷新可能执行 build.mcpp 和依赖安装。请先信任工作区后重试。", + ); + return; + } + + output.show(true); + const configuredMcppPath = projectConfiguration(context.project).get("path", "").trim(); + const mcppExecutable = configuredMcppPath.length > 0 ? configuredMcppPath : "mcpp"; + const result = await runMcppBuild(context.project.root, mcppExecutable); + appendProcessOutput(output, "刷新编译数据库", mcppExecutable, ["build"], result); + + if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, context.project.root)) { + invalidateModuleStatus(context.project.root); + const message = result.exitCode === 0 + ? "mcpp 构建完成;切回该工程后将自动配置 clangd。" + : "mcpp 构建失败;切回该工程后仍会自动检查现有编译数据库。"; + if (result.exitCode === 0) { + await vscode.window.showInformationMessage(message); + } else { + await vscode.window.showWarningMessage(message); + } + return; + } + const reconciled = await reconcileProject(context.project, true); + if (reconciled.context?.analysis.capability === "syntax-only") { + const buildMessage = result.exitCode === 0 ? "mcpp 构建完成" : "mcpp 构建失败"; + await vscode.window.showWarningMessage( + `${buildMessage};${reconciled.context.analysis.reason}。模块语法高亮仍然可用。`, + ); + return; + } + + const outcome = describeRefreshOutcome( + result.exitCode, + reconciled.databaseFound, + reconciled.configured, + ); + if (outcome.level === "information") { + await vscode.window.showInformationMessage(outcome.message); + } else if (outcome.level === "warning") { + await vscode.window.showWarningMessage(outcome.message); + } else { + await vscode.window.showErrorMessage(outcome.message); + } + })), + vscode.commands.registerCommand(COMMAND_CHECK, runGuarded(async () => { + const project = findCurrentProject(); + if (project === undefined) { + await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); + return; + } + await executeWithWorkspaceClangd(async () => { + if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { + return; + } + const context = loadProjectContext(project); + if (context !== undefined) { + await runModuleSupportCheck(context, status, output, "interactive"); + } + }); + })), + configurationWatcher, + trustWatcher, + vscode.window.onDidChangeActiveTextEditor(() => { + refreshStatus(); + requestCurrentProjectReconciliation(true); + }), + vscode.workspace.onDidChangeWorkspaceFolders(() => { + refreshStatus(); + requestCurrentProjectReconciliation(true); + }), + manifestWatcher, + compilationDatabaseWatcher, + ); + + extensionContext.subscriptions.push( + manifestWatcher.onDidCreate(refreshStatus), + manifestWatcher.onDidChange(refreshStatus), + manifestWatcher.onDidDelete(refreshStatus), + ...registerCompilationDatabaseReconciliation( + compilationDatabaseWatcher, + requestAutomaticReconciliation, + ), + ); + + try { + const project = findCurrentProject(); + if (project === undefined) { + await reconcileProjectContext(undefined, false); + } else { + await reconcileProject(project, false); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[自动配置] ${message}`); + } +} + +export function deactivate(): void { + moduleStatusByProject.clear(); + moduleCheckOperations.clear(); + // VS Code 会释放 activate() 注册的所有订阅。 +} diff --git a/src/process.ts b/src/process.ts new file mode 100644 index 0000000..c772b7a --- /dev/null +++ b/src/process.ts @@ -0,0 +1,110 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +import { + classifyCheckResult, + parseToolIdentity, + type CheckResult, + type ToolIdentity, +} from "./analysis"; + +const execFileAsync = promisify(execFile); + +export interface ProcessResult { + exitCode: number; + stdout: string; + stderr: string; +} + +export interface ProcessRunOptions { + timeoutMs?: number; +} + +export type ProcessRunner = ( + executable: string, + args: string[], + cwd?: string, + options?: ProcessRunOptions, +) => Promise; + +export interface ToolVersionResult extends ProcessResult { + identity: ToolIdentity | undefined; +} + +export interface ClangdCheckResult extends ProcessResult { + output: string; + classification: CheckResult; +} + +export async function runProcess( + executable: string, + args: string[], + cwd?: string, + options: ProcessRunOptions = {}, +): Promise { + try { + const result = await execFileAsync(executable, args, { + cwd, + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + timeout: options.timeoutMs, + }); + return { + exitCode: 0, + stdout: result.stdout, + stderr: result.stderr, + }; + } catch (error) { + const processError = error as NodeJS.ErrnoException & { + stdout?: string; + stderr?: string; + code?: number | string; + }; + return { + exitCode: typeof processError.code === "number" ? processError.code : 1, + stdout: processError.stdout ?? "", + stderr: processError.stderr ?? (typeof processError.message === "string" ? processError.message : ""), + }; + } +} + +export async function runToolVersion( + executable: string, + versionArguments: string[] = ["--version"], + runner: ProcessRunner = runProcess, +): Promise { + const result = await runner(executable, versionArguments); + return { + ...result, + identity: parseToolIdentity(`${result.stdout}\n${result.stderr}`), + }; +} + +export async function runClangdCheck( + clangdPath: string, + sourceFile: string, + cwd: string, + clangdArguments: string[] = [], + runner: ProcessRunner = runProcess, +): Promise { + const result = await runner( + clangdPath, + [`--check=${sourceFile}`, ...clangdArguments], + cwd, + { timeoutMs: 60_000 }, + ); + const output = `${result.stdout}\n${result.stderr}`; + return { + ...result, + output, + classification: classifyCheckResult(result.exitCode, output), + }; +} + +export function runMcppBuild( + cwd: string, + executable: string = "mcpp", + runner: ProcessRunner = runProcess, +): Promise { + return runner(executable, ["build"], cwd); +} diff --git a/src/workflow.ts b/src/workflow.ts new file mode 100644 index 0000000..ccd2fd1 --- /dev/null +++ b/src/workflow.ts @@ -0,0 +1,222 @@ +import type { CheckResult, ModuleCapability } from "./analysis"; + +export type RefreshOutcomeLevel = "information" | "warning" | "error"; +export type ModuleSupportState = "available" | "unavailable"; + +export interface RefreshOutcome { + level: RefreshOutcomeLevel; + message: string; +} + +export interface CompilationDatabaseWatcher { + onDidCreate(listener: (event: E) => void): T; + onDidChange(listener: (event: E) => void): T; + onDidDelete(listener: (event: E) => void): T; +} + +export function registerCompilationDatabaseReconciliation( + watcher: CompilationDatabaseWatcher, + requestReconciliation: (event: E, forceRestart: boolean) => void, +): T[] { + return [ + watcher.onDidCreate((event) => requestReconciliation(event, true)), + watcher.onDidChange((event) => requestReconciliation(event, true)), + watcher.onDidDelete((event) => requestReconciliation(event, false)), + ]; +} + +export function shouldRestartClangd( + configurationChanged: boolean, + interactive: boolean, + forceRestart: boolean, +): boolean { + return configurationChanged || interactive || forceRestart; +} + +export function configurationReadyAfterRestart( + restartRequired: boolean, + restartSucceeded: boolean, +): boolean { + return !restartRequired || restartSucceeded; +} + +export function createSingleFlightReconciler( + operation: (forceRestart: boolean) => Promise, +): (forceRestart?: boolean) => Promise { + let active: Promise | undefined; + let requested = false; + let pendingForceRestart = false; + + return (forceRestart = false): Promise => { + requested = true; + pendingForceRestart ||= forceRestart; + if (active !== undefined) { + return active; + } + + active = (async () => { + let result: T | undefined; + let completed = false; + do { + const currentForceRestart = pendingForceRestart; + requested = false; + pendingForceRestart = false; + try { + result = await operation(currentForceRestart); + completed = true; + } catch (error) { + if (!requested) { + throw error; + } + } + } while (requested); + if (!completed) { + throw new Error("重协调没有产生结果"); + } + return result as T; + })().finally(() => { + active = undefined; + requested = false; + pendingForceRestart = false; + }); + return active as Promise; + }; +} + +export function createKeyedSingleFlightReconciler( + operation: (key: K, forceRestart: boolean) => Promise, +): (key: K, forceRestart?: boolean) => Promise { + const reconcilerByKey = new Map Promise>(); + + return (key: K, forceRestart = false): Promise => { + let reconciler = reconcilerByKey.get(key); + if (reconciler === undefined) { + reconciler = createSingleFlightReconciler( + (pendingForceRestart) => operation(key, pendingForceRestart), + ); + reconcilerByKey.set(key, reconciler); + } + return reconciler(forceRestart); + }; +} + +export function createSerialExecutor(): (operation: () => Promise) => Promise { + let tail: Promise = Promise.resolve(); + + return (operation: () => Promise): Promise => { + const result = tail.then(operation, operation); + tail = result.then( + () => undefined, + () => undefined, + ); + return result; + }; +} + +export interface LatestOperationTracker { + begin(key: K): number; + invalidate(key: K): void; + isCurrent(key: K, token: number): boolean; + clear(): void; +} + +export function createLatestOperationTracker(): LatestOperationTracker { + const latestTokenByKey = new Map(); + let nextToken = 0; + + return { + begin(key: K): number { + nextToken += 1; + latestTokenByKey.set(key, nextToken); + return nextToken; + }, + invalidate(key: K): void { + latestTokenByKey.delete(key); + }, + isCurrent(key: K, token: number): boolean { + return latestTokenByKey.get(key) === token; + }, + clear(): void { + latestTokenByKey.clear(); + }, + }; +} + +export function shouldRenderProjectStatus( + currentProjectRoot: string | undefined, + statusProjectRoot: string, +): boolean { + return currentProjectRoot === statusProjectRoot; +} + +export function configurationAffectsModuleSupport( + affectsConfiguration: (section: string) => boolean, +): boolean { + return ["mcpp.clangd.path", "mcpp.modulesSupport"].some(affectsConfiguration); +} + +export function workspaceAllowsToolExecution(trusted: boolean): boolean { + return trusted; +} + +export function shouldUseWorkspaceClangd( + currentProjectRoot: string | undefined, + projectRoot: string, +): boolean { + return currentProjectRoot === projectRoot; +} + +export function describeRefreshOutcome( + buildExitCode: number, + databaseFound: boolean, + configured: boolean, +): RefreshOutcome { + if (configured) { + return buildExitCode === 0 + ? { + level: "information", + message: "mcpp 构建完成,clangd 配置已刷新。", + } + : { + level: "warning", + message: "mcpp 构建失败,但检测到可用的编译数据库,clangd 配置已刷新。请继续修复源码构建错误。", + }; + } + + if (!databaseFound) { + return buildExitCode === 0 + ? { + level: "error", + message: "mcpp 构建完成,但没有生成 compile_commands.json。", + } + : { + level: "error", + message: "mcpp 构建失败,且没有生成 compile_commands.json。请查看 mcpp 输出频道。", + }; + } + + return { + level: "error", + message: buildExitCode === 0 + ? "已检测到 compile_commands.json,但 clangd 配置未完成。请查看 mcpp 输出频道。" + : "mcpp 构建失败;已检测到 compile_commands.json,但 clangd 配置未完成。请查看 mcpp 输出频道。", + }; +} + +export function statusCommandForCapability(capability: ModuleCapability): string { + return capability === "unavailable" + ? "mcpp.refreshCompilationDatabase" + : "mcpp.checkModuleSupport"; +} + +export function shouldCheckModuleSupport( + capability: ModuleCapability, + configured: boolean, + sourceFile?: string, +): boolean { + return capability === "full" && configured && sourceFile !== undefined && sourceFile.length > 0; +} + +export function moduleSupportState(classification: CheckResult): ModuleSupportState { + return classification === "ready" ? "available" : "unavailable"; +} diff --git a/syntaxes/mcpp-modules.tmLanguage.json b/syntaxes/mcpp-modules.tmLanguage.json new file mode 100644 index 0000000..39bd45d --- /dev/null +++ b/syntaxes/mcpp-modules.tmLanguage.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "mcpp C++ Modules", + "scopeName": "source.cpp.mcpp-modules", + "injectionSelector": "L:source.cpp", + "patterns": [ + { + "include": "#module-declaration" + }, + { + "include": "#import-declaration" + } + ], + "repository": { + "module-declaration": { + "match": "^\\s*(?:(export)\\s+)?(module)\\b\\s*(?:([A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*(:)\\s*([A-Za-z_][A-Za-z0-9_]*))?|(:)\\s*([A-Za-z_][A-Za-z0-9_]*))?(?:\\s*;)?", + "captures": { + "1": { + "name": "keyword.control.export.cpp" + }, + "2": { + "name": "keyword.control.module.cpp" + }, + "3": { + "name": "entity.name.namespace.module.cpp" + }, + "4": { + "name": "punctuation.separator.module-partition.cpp" + }, + "5": { + "name": "entity.name.namespace.module.partition.cpp" + }, + "6": { + "name": "punctuation.separator.module-partition.cpp" + }, + "7": { + "name": "entity.name.namespace.module.partition.cpp" + } + } + }, + "import-declaration": { + "match": "^\\s*(?:(export)\\s+)?(import)\\b\\s*(?:(:)([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*(?::[A-Za-z_][A-Za-z0-9_]*)?)|(<[^>]+>)|(\\\"[^\\\"]+\\\"))(?:\\s*;)?", + "captures": { + "1": { + "name": "keyword.control.export.cpp" + }, + "2": { + "name": "keyword.control.import.cpp" + }, + "3": { + "name": "punctuation.separator.module-partition.cpp" + }, + "4": { + "name": "entity.name.namespace.module.partition.cpp" + }, + "5": { + "name": "entity.name.namespace.module.cpp" + }, + "6": { + "name": "string.quoted.other.header.cpp" + }, + "7": { + "name": "string.quoted.double.header.cpp" + } + } + }, + "module-name": { + "match": "\\b[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*(?::[A-Za-z_][A-Za-z0-9_]*)?\\b", + "name": "entity.name.namespace.module.cpp" + } + } +} diff --git a/test/analysis.test.ts b/test/analysis.test.ts new file mode 100644 index 0000000..b8653be --- /dev/null +++ b/test/analysis.test.ts @@ -0,0 +1,302 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + analyzeCompilationDatabase, + buildClangdArguments, + classifyCheckResult, + compareToolIdentities, + parseToolIdentity, +} from "../src/analysis"; +import * as analysisModule from "../src/analysis"; + +function cdb(compiler: string, flags: string[]): string { + return JSON.stringify([ + { + directory: "/work/app", + file: "/work/app/src/main.cpp", + arguments: [compiler, "-std=c++23", ...flags, "-c", "/work/app/src/main.cpp"], + }, + ]); +} + +test("classifies an mcpp LLVM CDB as full module support", () => { + const result = analyzeCompilationDatabase(cdb("/tools/xim-x-llvm/22.1.8/bin/clang++", [ + "-fmodule-file=std=/work/app/target/pcm.cache/std.pcm", + "-fprebuilt-module-path=/work/app/target/pcm.cache", + ])); + + assert.equal(result.kind, "llvm"); + assert.equal(result.capability, "full"); + assert.equal(result.compilerPath, "/tools/xim-x-llvm/22.1.8/bin/clang++"); + assert.equal(result.sourceFile, "/work/app/src/main.cpp"); + assert.equal(result.hasPrebuiltModules, true); +}); + +test("prefers a project module interface over an earlier external command", () => { + const result = analyzeCompilationDatabase(JSON.stringify([ + { + directory: "/work/app", + file: "/deps/gtest.cc", + arguments: ["/tools/clang++", "-std=c++23", "-c", "/deps/gtest.cc"], + }, + { + directory: "/work/app", + file: "/work/app/src/demo.cppm", + arguments: [ + "/tools/clang++", + "-std=c++23", + "-fmodule-file=std=/work/app/target/pcm.cache/std.pcm", + "-c", + "/work/app/src/demo.cppm", + ], + }, + ])); + + assert.equal(result.sourceFile, "/work/app/src/demo.cppm"); + assert.equal(result.hasPrebuiltModules, true); +}); + +test("prefers a project source over an external module interface", () => { + const result = analyzeCompilationDatabase(JSON.stringify([ + { + directory: "/work/app", + file: "/deps/fmt_module.cppm", + arguments: [ + "/tools/clang++", + "-std=c++23", + "-fmodule-file=std=/work/app/target/pcm.cache/std.pcm", + "-c", + "/deps/fmt_module.cppm", + ], + }, + { + directory: "/work/app", + file: "/work/app/src/main.cpp", + arguments: [ + "/tools/clang++", + "-std=c++23", + "-fmodule-file=std=/work/app/target/pcm.cache/std.pcm", + "-c", + "/work/app/src/main.cpp", + ], + }, + ])); + + assert.equal(result.sourceFile, "/work/app/src/main.cpp"); +}); + +test("keeps Windows separators when reading a command-form CDB entry", () => { + const result = analyzeCompilationDatabase(JSON.stringify([ + { + directory: "C:\\work\\app", + file: "C:\\work\\app\\main.cpp", + command: "C:\\LLVM\\bin\\clang++.exe -std=c++23 -c C:\\work\\app\\main.cpp", + }, + ])); + + assert.equal(result.kind, "llvm"); + assert.equal(result.compilerPath, "C:\\LLVM\\bin\\clang++.exe"); +}); + +test("degrades GCC module databases to syntax-only", () => { + const result = analyzeCompilationDatabase(cdb("/tools/g++", ["-fmodules"])); + + assert.equal(result.kind, "gcc"); + assert.equal(result.capability, "syntax-only"); +}); + +test("degrades MSVC module databases to syntax-only", () => { + const result = analyzeCompilationDatabase(cdb("C:\\LLVM\\cl.exe", [ + "/reference", + "std=C:\\work\\ifc.cache\\std.ifc", + ])); + + assert.equal(result.kind, "msvc"); + assert.equal(result.capability, "syntax-only"); +}); + +test("reports malformed compilation databases without throwing", () => { + const result = analyzeCompilationDatabase("not-json"); + + assert.equal(result.kind, "unknown"); + assert.equal(result.capability, "unavailable"); + assert.match(result.reason, /JSON/i); +}); + +test("auto module arguments enable experimental support for clangd 22", () => { + const args = buildClangdArguments( + ["--background-index", "--query-driver=/old/compiler"], + { + compilerPath: "/tools/clang++", + modulesSupport: "auto", + clangdIdentity: { major: 22, minor: 1, patch: 8 }, + platform: "darwin", + }, + ); + + assert.deepEqual(args, [ + "--background-index", + "--query-driver=/tools/clang++", + "--experimental-modules-support", + ]); +}); + +test("auto module arguments avoid the known Windows clangd 20 crash", () => { + const args = buildClangdArguments([], { + compilerPath: "C:\\tools\\clang++.exe", + modulesSupport: "auto", + clangdIdentity: { major: 20, minor: 1, patch: 7 }, + platform: "win32", + }); + + assert.deepEqual(args, ["--query-driver=C:\\tools\\clang++.exe"]); +}); + +test("auto consumes explicit mcpp PCMs without invoking clangd's module builder", () => { + const args = buildClangdArguments([], { + compilerPath: "/tools/clang++", + modulesSupport: "auto", + clangdIdentity: { major: 22, minor: 1, patch: 8 }, + platform: "darwin", + hasPrebuiltModules: true, + }); + + assert.deepEqual(args, ["--query-driver=/tools/clang++"]); +}); + +test("expands workspace folder variables before invoking clangd", () => { + const options = { + compilerPath: "/tools/clang++", + modulesSupport: "off" as const, + platform: "darwin" as const, + workspaceFolder: "/work/app", + }; + const args = buildClangdArguments([ + "--compile-commands-dir=${workspaceFolder}", + "--background-index", + ], options); + + assert.deepEqual(args, [ + "--compile-commands-dir=/work/app", + "--background-index", + "--query-driver=/tools/clang++", + ]); +}); + +test("off removes an existing experimental module flag", () => { + const args = buildClangdArguments(["--experimental-modules-support"], { + compilerPath: "/tools/clang++", + modulesSupport: "off", + clangdIdentity: { major: 22, minor: 1, patch: 8 }, + platform: "linux", + }); + + assert.deepEqual(args, ["--query-driver=/tools/clang++"]); +}); + +test("plans replacing the default clangd shim with the matched executable", () => { + const planner = (analysisModule as typeof analysisModule & { + buildClangdConfigurationPlan?: ( + currentPath: string, + currentArguments: string[], + resolvedPath: string, + options: { + compilerPath: string; + modulesSupport: "auto"; + clangdIdentity: { major: number; minor: number; patch: number }; + platform: NodeJS.Platform; + hasPrebuiltModules: boolean; + workspaceFolder: string; + }, + ) => { path: string; arguments: string[]; changed: boolean }; + }).buildClangdConfigurationPlan; + + assert.equal(typeof planner, "function"); + const plan = planner?.( + "clangd", + ["--compile-commands-dir=${workspaceFolder}", "--background-index"], + "/tools/xim-x-llvm-tools/22.1.8/bin/clangd", + { + compilerPath: "/tools/xim-x-llvm/22.1.8/bin/clang++", + modulesSupport: "auto", + clangdIdentity: { major: 22, minor: 1, patch: 8 }, + platform: "darwin", + hasPrebuiltModules: true, + workspaceFolder: "/work/app", + }, + ); + + assert.deepEqual(plan, { + path: "/tools/xim-x-llvm-tools/22.1.8/bin/clangd", + arguments: [ + "--compile-commands-dir=/work/app", + "--background-index", + "--query-driver=/tools/xim-x-llvm/22.1.8/bin/clang++", + ], + changed: true, + }); +}); + +test("parses LLVM version and revision identities", () => { + const identity = parseToolIdentity( + "clangd version 22.1.8 (https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174dcb5aa28b59d1)", + ); + + assert.deepEqual(identity, { + major: 22, + minor: 1, + patch: 8, + revision: "ca7933e47d3a3451d81e72ac174dcb5aa28b59d1", + }); +}); + +test("rejects matching semantic versions from different LLVM revisions", () => { + const result = compareToolIdentities( + { major: 22, minor: 1, patch: 8, revision: "aaaa" }, + { major: 22, minor: 1, patch: 8, revision: "bbbb" }, + ); + + assert.equal(result.compatible, false); + assert.match(result.reason, /revision/i); +}); + +test("does not claim exact identity when an LLVM revision is unavailable", () => { + const result = compareToolIdentities( + { major: 22, minor: 1, patch: 8 }, + { major: 22, minor: 1, patch: 8 }, + ); + + assert.equal(result.compatible, false); + assert.match(result.reason, /revision/i); +}); + +test("classifies clangd check failures into actionable states", () => { + assert.equal( + classifyCheckResult(1, "error: ast_file_different_branch"), + "pcm-mismatch", + ); + assert.equal( + classifyCheckResult(1, "Failed to build module std; Don't get the module unit"), + "module-unavailable", + ); + assert.equal( + classifyCheckResult(3, "unknown type name 'import'"), + "wrong-language-mode", + ); + assert.equal( + classifyCheckResult(0, "All checks completed, 0 errors"), + "ready", + ); + assert.equal( + classifyCheckResult(3, "tweak: ExtractFunction ==> FAIL\nAll checks completed, 4 errors"), + "ready", + ); + assert.equal( + classifyCheckResult( + 3, + "E[12:00:00] [undeclared_var_use] Line 4: use of undeclared identifier 'missing'\nAll checks completed, 1 errors", + ), + "check-failed", + ); +}); diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts new file mode 100644 index 0000000..7e2f8f0 --- /dev/null +++ b/test/artifacts.test.ts @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +interface PackageManifest { + extensionDependencies?: string[]; + activationEvents?: string[]; + capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string } }; + contributes?: { + commands?: Array<{ command: string }>; + configuration?: { properties?: Record }; + configurationDefaults?: Record; + grammars?: Array<{ scopeName: string; injectTo?: string[]; path: string }>; + }; +} + +const root = path.resolve(process.cwd()); + +test("declares the official clangd dependency and mcpp commands", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); + assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); + assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); + assert.equal( + manifest.capabilities?.untrustedWorkspaces?.description, + "未受信任工作区仅启用模块语法高亮,不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。", + ); + assert.deepEqual( + manifest.contributes?.commands?.map((command) => command.command), + ["mcpp.configureClangd", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport"], + ); + assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.path"]); + assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.modulesSupport"]); + assert.deepEqual(manifest.contributes?.configurationDefaults?.["files.associations"], { + "*.ccm": "cpp", + "*.cppm": "cpp", + "*.ixx": "cpp", + "*.mpp": "cpp", + }); +}); + +test("ships an injection grammar with module-specific scopes", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + const grammar = manifest.contributes?.grammars?.find((item) => item.scopeName === "source.cpp.mcpp-modules"); + assert.deepEqual(grammar, { + scopeName: "source.cpp.mcpp-modules", + injectTo: ["source.cpp"], + path: "./syntaxes/mcpp-modules.tmLanguage.json", + }); + + const grammarText = readFileSync(path.join(root, "syntaxes/mcpp-modules.tmLanguage.json"), "utf8"); + const grammarDocument = JSON.parse(grammarText) as { repository?: Record }; + for (const key of ["module-declaration", "import-declaration", "module-name"]) { + assert.ok(grammarDocument.repository?.[key], `missing grammar rule: ${key}`); + } + const importRule = grammarDocument.repository?.["import-declaration"] as { match?: string }; + assert.ok(importRule.match?.includes("(?:\\.[A-Za-z_][A-Za-z0-9_]*)*")); + assert.ok(importRule.match?.includes("(?:\\s*;)?")); + assert.ok(!importRule.match?.includes("(?=;)")); + + const javascriptPattern = (importRule.match ?? "").replace(/^\(\?x\)/, ""); + const importPattern = new RegExp(javascriptPattern); + assert.match("import xxx", importPattern); + assert.match("export import foo.bar;", importPattern); +}); diff --git a/test/discovery.test.ts b/test/discovery.test.ts new file mode 100644 index 0000000..6a91d01 --- /dev/null +++ b/test/discovery.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { deriveClangdCandidates, findNearestMcppProject } from "../src/discovery"; + +test("finds the nearest mcpp manifest and root compilation database", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-discovery-")); + try { + const sourceDirectory = path.join(root, "src", "nested"); + mkdirSync(sourceDirectory, { recursive: true }); + writeFileSync(path.join(root, "mcpp.toml"), "[package]\nname = 'demo'\n"); + writeFileSync(path.join(root, "compile_commands.json"), "[]\n"); + + assert.deepEqual(findNearestMcppProject(sourceDirectory), { + root, + manifestPath: path.join(root, "mcpp.toml"), + compilationDatabasePath: path.join(root, "compile_commands.json"), + }); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("derives sibling, xlings and PATH clangd candidates", () => { + assert.deepEqual( + deriveClangdCandidates("/tools/xim-x-llvm/22.1.8/bin/clang++"), + [ + "/tools/xim-x-llvm/22.1.8/bin/clangd", + "/tools/xim-x-llvm-tools/22.1.8/bin/clangd", + "clangd", + ], + ); +}); + +test("derives the user xlings tools path from an mcpp registry compiler", () => { + assert.deepEqual( + deriveClangdCandidates( + "/Users/demo/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + ), + [ + "/Users/demo/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clangd", + "/Users/demo/.mcpp/registry/data/xpkgs/xim-x-llvm-tools/22.1.8/bin/clangd", + "/Users/demo/.xlings/data/xpkgs/xim-x-llvm-tools/22.1.8/bin/clangd", + "clangd", + ], + ); +}); diff --git a/test/process.test.ts b/test/process.test.ts new file mode 100644 index 0000000..dc2d61d --- /dev/null +++ b/test/process.test.ts @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import process from "node:process"; +import test from "node:test"; + +import { + runClangdCheck, + runMcppBuild, + runProcess, + runToolVersion, +} from "../src/process"; + +test("captures output and exit status from a real child process", async () => { + const result = await runProcess(process.execPath, ["-e", "process.stdout.write('ok')"]); + + assert.equal(result.exitCode, 0); + assert.equal(result.stdout, "ok"); + assert.equal(result.stderr, ""); +}); + +test("parses a tool identity from the version command", async () => { + const result = await runToolVersion(process.execPath, [ + "-e", + "process.stdout.write('clangd version 22.1.8 (git ca7933e47d3a3451d81e72ac174dcb5aa28b59d1)')", + ]); + + assert.deepEqual(result.identity, { + major: 22, + minor: 1, + patch: 8, + revision: "ca7933e47d3a3451d81e72ac174dcb5aa28b59d1", + }); +}); + +test("classifies clangd check output at the process boundary", async () => { + const calls: Array<{ executable: string; arguments: string[]; cwd?: string }> = []; + const timeouts: Array = []; + const result = await runClangdCheck( + "/tools/clangd", + "/work/app/src/main.cpp", + "/work/app", + ["--background-index"], + async ( + executable: string, + args: string[], + cwd: string | undefined, + options?: { timeoutMs?: number }, + ) => { + calls.push({ executable, arguments: args, cwd }); + timeouts.push(options?.timeoutMs); + return { + exitCode: 1, + stdout: "", + stderr: "error: ast_file_different_branch", + }; + }, + ); + + assert.deepEqual(calls, [{ + executable: "/tools/clangd", + arguments: ["--check=/work/app/src/main.cpp", "--background-index"], + cwd: "/work/app", + }]); + assert.deepEqual(timeouts, [60_000]); + assert.equal(result.classification, "pcm-mismatch"); +}); + +test("uses the configured mcpp executable for builds", async () => { + const calls: Array<{ executable: string; arguments: string[]; cwd?: string }> = []; + const runner = async (executable: string, args: string[], cwd?: string) => { + calls.push({ executable, arguments: args, cwd }); + return { exitCode: 0, stdout: "", stderr: "" }; + }; + + await Reflect.apply(runMcppBuild, undefined, ["/work/app", "/tools/mcpp", runner]); + + assert.deepEqual(calls, [{ + executable: "/tools/mcpp", + arguments: ["build"], + cwd: "/work/app", + }]); +}); diff --git a/test/workflow.test.ts b/test/workflow.test.ts new file mode 100644 index 0000000..d665774 --- /dev/null +++ b/test/workflow.test.ts @@ -0,0 +1,380 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import * as workflow from "../src/workflow"; +import { + createSingleFlightReconciler, + describeRefreshOutcome, + statusCommandForCapability, +} from "../src/workflow"; + +test("selects refresh when the compilation database is unavailable", () => { + assert.equal(statusCommandForCapability("unavailable"), "mcpp.refreshCompilationDatabase"); + assert.equal(statusCommandForCapability("full"), "mcpp.checkModuleSupport"); + assert.equal(statusCommandForCapability("syntax-only"), "mcpp.checkModuleSupport"); +}); + +test("keeps IDE configuration when a source build fails after producing a database", () => { + assert.deepEqual(describeRefreshOutcome(1, true, true), { + level: "warning", + message: "mcpp 构建失败,但检测到可用的编译数据库,clangd 配置已刷新。请继续修复源码构建错误。", + }); + assert.deepEqual(describeRefreshOutcome(0, true, true), { + level: "information", + message: "mcpp 构建完成,clangd 配置已刷新。", + }); + assert.equal(describeRefreshOutcome(0, true, false).level, "error"); + assert.equal(describeRefreshOutcome(1, false, false).level, "error"); +}); + +test("serializes reconciliation and merges pending restart requests", async () => { + let releaseFirst: (() => void) | undefined; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + let active = 0; + let maxConcurrent = 0; + const restartRequests: boolean[] = []; + const reconcile = createSingleFlightReconciler(async (forceRestart) => { + restartRequests.push(forceRestart); + active += 1; + maxConcurrent = Math.max(maxConcurrent, active); + try { + if (restartRequests.length === 1) { + await firstGate; + } + return restartRequests.length; + } finally { + active -= 1; + } + }); + + const first = reconcile(false); + const second = reconcile(true); + const third = reconcile(true); + assert.deepEqual(restartRequests, [false]); + assert.ok(releaseFirst); + releaseFirst(); + + assert.deepEqual(await Promise.all([first, second, third]), [2, 2, 2]); + assert.deepEqual(restartRequests, [false, true]); + assert.equal(maxConcurrent, 1); +}); + +test("coalesces reconciliation requests independently for each project", async () => { + const createKeyedReconciler = ( + workflow as typeof workflow & { + createKeyedSingleFlightReconciler?: ( + operation: (key: K, forceRestart: boolean) => Promise, + ) => (key: K, forceRestart?: boolean) => Promise; + } + ).createKeyedSingleFlightReconciler; + assert.equal(typeof createKeyedReconciler, "function"); + assert.ok(createKeyedReconciler); + + let releaseFirstProject: (() => void) | undefined; + const firstProjectGate = new Promise((resolve) => { + releaseFirstProject = resolve; + }); + const calls: string[] = []; + const reconcile = createKeyedReconciler(async (project: string, forceRestart: boolean) => { + calls.push(`${project}:${forceRestart}`); + if (project === "/work/a" && calls.filter((call) => call.startsWith("/work/a")).length === 1) { + await firstProjectGate; + } + return `${project}:${forceRestart}`; + }); + + const firstA = reconcile("/work/a", false); + const secondA = reconcile("/work/a", true); + const firstB = reconcile("/work/b", true); + + assert.equal(await firstB, "/work/b:true"); + assert.deepEqual(calls, ["/work/a:false", "/work/b:true"]); + assert.ok(releaseFirstProject); + releaseFirstProject(); + assert.deepEqual(await Promise.all([firstA, secondA]), ["/work/a:true", "/work/a:true"]); + assert.deepEqual(calls, ["/work/a:false", "/work/b:true", "/work/a:true"]); +}); + +test("serializes operations that share the workspace clangd configuration", async () => { + const createSerialExecutor = ( + workflow as typeof workflow & { + createSerialExecutor?: () => (operation: () => Promise) => Promise; + } + ).createSerialExecutor; + assert.equal(typeof createSerialExecutor, "function"); + assert.ok(createSerialExecutor); + + let releaseFirst: (() => void) | undefined; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + const calls: string[] = []; + const execute = createSerialExecutor(); + const first = execute(async () => { + calls.push("a:start"); + await firstGate; + calls.push("a:end"); + return "a"; + }); + const second = execute(async () => { + calls.push("b:start"); + calls.push("b:end"); + return "b"; + }); + + await Promise.resolve(); + assert.deepEqual(calls, ["a:start"]); + assert.ok(releaseFirst); + releaseFirst(); + assert.deepEqual(await Promise.all([first, second]), ["a", "b"]); + assert.deepEqual(calls, ["a:start", "a:end", "b:start", "b:end"]); +}); + +test("retries a queued reconciliation after a transient failure", async () => { + let releaseFirst: (() => void) | undefined; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + const restartRequests: boolean[] = []; + const reconcile = createSingleFlightReconciler(async (forceRestart) => { + restartRequests.push(forceRestart); + if (restartRequests.length === 1) { + await firstGate; + throw new Error("temporary CDB read failure"); + } + return 2; + }); + + const first = reconcile(false); + const second = reconcile(true); + assert.ok(releaseFirst); + releaseFirst(); + + assert.deepEqual(await Promise.allSettled([first, second]), [ + { status: "fulfilled", value: 2 }, + { status: "fulfilled", value: 2 }, + ]); + assert.deepEqual(restartRequests, [false, true]); +}); + +test("forces a clangd restart when the compilation database changes", () => { + const shouldRestart = ( + workflow as typeof workflow & { + shouldRestartClangd?: ( + configurationChanged: boolean, + interactive: boolean, + forceRestart: boolean, + ) => boolean; + } + ).shouldRestartClangd; + assert.equal(typeof shouldRestart, "function"); + assert.ok(shouldRestart); + + assert.equal(shouldRestart(false, false, true), true); + assert.equal(shouldRestart(false, false, false), false); + assert.equal(shouldRestart(true, false, false), true); + assert.equal(shouldRestart(false, true, false), true); +}); + +test("does not call clangd configuration ready when a required restart fails", () => { + const configurationReady = ( + workflow as typeof workflow & { + configurationReadyAfterRestart?: ( + restartRequired: boolean, + restartSucceeded: boolean, + ) => boolean; + } + ).configurationReadyAfterRestart; + assert.equal(typeof configurationReady, "function"); + assert.ok(configurationReady); + + assert.equal(configurationReady(false, false), true); + assert.equal(configurationReady(true, true), true); + assert.equal(configurationReady(true, false), false); +}); + +test("maps compilation database file events to reconciliation requests", () => { + type EventName = "create" | "change" | "delete"; + const registerEvents = ( + workflow as typeof workflow & { + registerCompilationDatabaseReconciliation?: ( + watcher: { + onDidCreate(listener: (event: E) => void): T; + onDidChange(listener: (event: E) => void): T; + onDidDelete(listener: (event: E) => void): T; + }, + requestReconciliation: (event: E, forceRestart: boolean) => void, + ) => T[]; + } + ).registerCompilationDatabaseReconciliation; + assert.equal(typeof registerEvents, "function"); + assert.ok(registerEvents); + + const listeners: Partial void>> = {}; + const watcher = { + onDidCreate(listener: (event: string) => void): EventName { + listeners.create = listener; + return "create"; + }, + onDidChange(listener: (event: string) => void): EventName { + listeners.change = listener; + return "change"; + }, + onDidDelete(listener: (event: string) => void): EventName { + listeners.delete = listener; + return "delete"; + }, + }; + const requests: Array<{ event: string; forceRestart: boolean }> = []; + + assert.deepEqual(registerEvents(watcher, (event: string, forceRestart: boolean) => { + requests.push({ event, forceRestart }); + }), [ + "create", + "change", + "delete", + ]); + listeners.create?.("/work/a/compile_commands.json"); + listeners.change?.("/work/b/compile_commands.json"); + listeners.delete?.("/work/c/compile_commands.json"); + + assert.deepEqual(requests, [ + { event: "/work/a/compile_commands.json", forceRestart: true }, + { event: "/work/b/compile_commands.json", forceRestart: true }, + { event: "/work/c/compile_commands.json", forceRestart: false }, + ]); +}); + +test("accepts module status only from the latest check for the current project", () => { + const createTracker = ( + workflow as typeof workflow & { + createLatestOperationTracker?: () => { + begin(key: K): number; + invalidate(key: K): void; + isCurrent(key: K, token: number): boolean; + clear(): void; + }; + } + ).createLatestOperationTracker; + const shouldRender = ( + workflow as typeof workflow & { + shouldRenderProjectStatus?: ( + currentProjectRoot: string | undefined, + statusProjectRoot: string, + ) => boolean; + } + ).shouldRenderProjectStatus; + assert.equal(typeof createTracker, "function"); + assert.equal(typeof shouldRender, "function"); + assert.ok(createTracker); + assert.ok(shouldRender); + + const tracker = createTracker(); + const first = tracker.begin("/work/a"); + const second = tracker.begin("/work/a"); + const other = tracker.begin("/work/b"); + assert.equal(tracker.isCurrent("/work/a", first), false); + assert.equal(tracker.isCurrent("/work/a", second), true); + assert.equal(tracker.isCurrent("/work/b", other), true); + + tracker.invalidate("/work/a"); + assert.equal(tracker.isCurrent("/work/a", second), false); + tracker.clear(); + assert.equal(tracker.isCurrent("/work/b", other), false); + + assert.equal(shouldRender("/work/a", "/work/a"), true); + assert.equal(shouldRender("/work/b", "/work/a"), false); + assert.equal(shouldRender(undefined, "/work/a"), false); +}); + +test("reconciles when a module-related mcpp setting changes", () => { + const affectsModuleConfiguration = ( + workflow as typeof workflow & { + configurationAffectsModuleSupport?: ( + affectsConfiguration: (section: string) => boolean, + ) => boolean; + } + ).configurationAffectsModuleSupport; + assert.equal(typeof affectsModuleConfiguration, "function"); + assert.ok(affectsModuleConfiguration); + + assert.equal( + affectsModuleConfiguration((section) => section === "mcpp.clangd.path"), + true, + ); + assert.equal( + affectsModuleConfiguration((section) => section === "mcpp.modulesSupport"), + true, + ); + assert.equal( + affectsModuleConfiguration((section) => section === "mcpp.path"), + false, + ); + assert.equal(affectsModuleConfiguration(() => false), false); +}); + +test("only executes workspace-selected tools in trusted workspaces", () => { + const allowsToolExecution = ( + workflow as typeof workflow & { + workspaceAllowsToolExecution?: (trusted: boolean) => boolean; + } + ).workspaceAllowsToolExecution; + assert.equal(typeof allowsToolExecution, "function"); + assert.ok(allowsToolExecution); + + assert.equal(allowsToolExecution(true), true); + assert.equal(allowsToolExecution(false), false); +}); + +test("only lets the current project own the shared workspace clangd", () => { + const shouldUseWorkspaceClangd = ( + workflow as typeof workflow & { + shouldUseWorkspaceClangd?: ( + currentProjectRoot: string | undefined, + projectRoot: string, + ) => boolean; + } + ).shouldUseWorkspaceClangd; + assert.equal(typeof shouldUseWorkspaceClangd, "function"); + assert.ok(shouldUseWorkspaceClangd); + + assert.equal(shouldUseWorkspaceClangd("/work/a", "/work/a"), true); + assert.equal(shouldUseWorkspaceClangd("/work/b", "/work/a"), false); + assert.equal(shouldUseWorkspaceClangd(undefined, "/work/a"), false); +}); + +test("only runs automatic module checks for configured LLVM source commands", () => { + const shouldCheck = ( + workflow as typeof workflow & { + shouldCheckModuleSupport?: ( + capability: "full" | "syntax-only" | "unavailable", + configured: boolean, + sourceFile?: string, + ) => boolean; + } + ).shouldCheckModuleSupport; + assert.equal(typeof shouldCheck, "function"); + assert.ok(shouldCheck); + + assert.equal(shouldCheck("full", true, "/work/src/main.cpp"), true); + assert.equal(shouldCheck("full", false, "/work/src/main.cpp"), false); + assert.equal(shouldCheck("full", true), false); + assert.equal(shouldCheck("syntax-only", true, "/work/src/main.cpp"), false); + assert.equal(shouldCheck("unavailable", true, "/work/src/main.cpp"), false); +}); + +test("maps clangd check classifications to module availability", () => { + const moduleSupportState = ( + workflow as typeof workflow & { + moduleSupportState?: (classification: string) => "available" | "unavailable"; + } + ).moduleSupportState; + assert.equal(typeof moduleSupportState, "function"); + assert.ok(moduleSupportState); + + assert.equal(moduleSupportState("ready"), "available"); + assert.equal(moduleSupportState("module-unavailable"), "unavailable"); + assert.equal(moduleSupportState("wrong-language-mode"), "unavailable"); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f28e625 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "lib": ["ES2022"], + "rootDir": ".", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} From 9bbd656c91373407dd2c5f2135f0550d851580a4 Mon Sep 17 00:00:00 2001 From: wellwei Date: Fri, 31 Jul 2026 18:59:42 +0800 Subject: [PATCH 002/119] docs: add mcpp CLI integration design --- .../2026-07-31-mcpp-cli-integration-design.md | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md diff --git a/docs/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md b/docs/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md new file mode 100644 index 0000000..ef3ed76 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md @@ -0,0 +1,230 @@ +# mcpp CLI 基础功能集成设计 + +## 背景 + +当前扩展已经能在 mcpp 工程中配置 clangd、监听编译数据库并检查 C++ 模块支持, +但日常的 mcpp 操作仍需在终端手动执行。本版本在不改变现有 clangd 集成边界的前提下, +增加工具链管理和常用项目命令。 + +本设计对应扩展 `0.2.0`。所有 README、CHANGELOG、代码注释、命令标题、提示和错误信息 +均使用中文。 + +## 本版目标 + +本版提供以下能力: + +- 从状态栏快捷菜单或命令面板执行 `mcpp build`、`mcpp run`、`mcpp test` 和 + `mcpp clean`; +- 查看已安装和可安装的工具链; +- 安装主机工具链; +- 通过 `mcpp toolchain default` 选择全局默认工具链; +- 在构建类任务结束后复用现有流程刷新 CDB 和 clangd; +- 对长任务提供实时终端输出、交互输入和取消能力。 + +## 非目标 + +本版不包含: + +- 修改当前项目 `mcpp.toml` 中的 `[toolchain]`; +- 依赖搜索、添加、删除或更新; +- 选择构建 profile、工作区成员、target triple、feature 或 capability; +- 为 `mcpp run` 或 `mcpp test` 设置目标、过滤条件或运行参数; +- 清理全局 BMI 缓存; +- 自动下载工具链、自动切换全局默认工具链或自动执行构建; +- 新增侧边栏视图。 + +## 用户界面 + +### 状态栏入口 + +新增独立的 `$(tools) mcpp` 状态栏项,仅在当前活动编辑器属于 mcpp 工程时显示。 +点击后打开 mcpp 快捷菜单。现有模块状态栏项继续只表示 clangd 模块能力,二者不合并。 + +快捷菜单按以下顺序组织: + +1. 项目:构建、运行、测试、清理; +2. 工具链:查看工具链、安装工具链、选择全局默认工具链; +3. IDE:配置 clangd、刷新编译数据库、检查模块支持。 + +每项同时注册独立命令,允许用户直接从 VS Code 命令面板调用。 + +### 工具链操作 + +“查看工具链”执行 `mcpp toolchain list`,并用 Quick Pick 展示: + +- 已安装的工具链和检测到的系统工具链; +- 当前有效工具链与全局默认项; +- mcpp 输出中声明的可安装工具链; +- 当前项目是否通过 `mcpp.toml` 覆盖全局默认值。 + +“选择全局默认工具链”只列出已安装项和检测到的系统工具链。用户确认后执行: + +```text +mcpp toolchain default @ +``` + +该命令只修改 mcpp 的全局配置 `~/.mcpp/config.toml`。若当前项目包含项目级 +`[toolchain]`,插件明确说明该配置优先于全局默认值。 + +“安装工具链”优先列出 `mcpp toolchain list` 的可安装项,同时提供输入其他 +`family@version` 的入口。确认后执行主机工具链安装: + +```text +mcpp toolchain install @ +``` + +本版不传递 `--target`。安装成功后重新读取工具链列表,并询问是否进入“选择全局默认 +工具链”;由用户从包含准确安装版本的列表中选择,不根据部分版本输入猜测最终版本。 + +### 项目命令 + +项目命令使用当前活动编辑器所属的 mcpp 工程根目录作为工作目录: + +```text +mcpp build +mcpp run +mcpp test +mcpp clean +``` + +构建、运行和测试直接使用 mcpp 默认行为。清理前明确确认将删除当前工程的 +`target/`,且不使用 `--bmi-cache`。 + +## 执行模型 + +### 可执行文件解析 + +所有新增命令复用资源级设置 `mcpp.path`: + +- 设置非空时使用该值; +- 设置为空时执行 `mcpp`,由 VS Code 进程环境的 `PATH` 查找; +- 扩展不假设 mcpp 来自 xlings,也不根据 clangd 或 CDB 编译器反推 mcpp 路径。 + +设置说明改为“供扩展执行全部 mcpp CLI 命令使用”。 + +### 长任务 + +构建、运行、测试、清理和工具链安装通过 VS Code Task 的 `ProcessExecution` 启动: + +- 可执行文件和参数分别传递,不拼接 shell 字符串; +- 工作目录为目标 mcpp 工程根目录; +- 使用可见的专用终端实时输出; +- 用户可以向 `mcpp run` 启动的程序输入内容; +- 用户可以通过 VS Code 的终止任务功能取消任务; +- 插件等待任务结束事件,以区分成功、失败和取消。 + +### 短命令 + +工具链列表和设置全局默认值使用现有 `execFile` 风格的短时进程调用,便于读取退出码、 +标准输出和标准错误。所有参数仍以数组形式传递。 + +### 构建后协调 + +`build`、`run` 和 `test` 都可能生成或更新编译数据库。任务结束后,无论退出码是否为 +零,扩展都复用现有重协调入口: + +1. 重新发现当前工程及 `compile_commands.json`; +2. 从 CDB 分析实际编译器; +3. 必要时更新并重启 clangd; +4. 重新执行模块支持检查。 + +普通源码错误不阻止插件消费已经成功生成的 CDB。 + +`clean` 成功或失败后都使当前模块检查结果失效,并重新检查现有 CDB,避免已经删除的 +PCM 仍被显示为可用。 + +设置全局默认工具链后不自动构建,也不直接声称当前项目已经切换成功。插件显示结果并 +提供“立即构建”操作;只有用户选择该操作后才生成新的 CDB 并刷新 clangd。 + +## 工具链列表解析 + +mcpp 当前没有为 `toolchain list` 提供 JSON 输出。本版只解析以下稳定语义: + +- `Toolchains:` 区块中的 family、version、当前有效标记和已安装状态; +- `System:` 区块中检测到的系统工具链,例如 Windows 上的 MSVC; +- `Available toolchains` 区块中的 family 与 version; +- “effective toolchain from project mcpp.toml”提示及其中报告的全局默认值。 + +没有项目级覆盖时,星号项就是全局默认工具链;存在项目级覆盖时,星号项表示当前项目 +有效工具链,全局默认值必须读取覆盖提示,二者不得混淆。 + +解析器不依赖列宽,并忽略不认识的辅助文本。解析结果必须保留原始输出。若无法得到任何 +已安装项,插件不猜测工具链名称,而是把原始输出写入 `mcpp` 输出频道并报告兼容性错误。 + +## 安全边界 + +- 所有 mcpp 命令都要求工作区已受信任,包括工具链列表; +- 原因是 `mcpp.path` 可以来自工作区设置,不能在 Restricted Mode 中执行其指定程序; +- 工具链安装和全局默认值修改在执行前显示准确目标并要求确认; +- 清理操作在执行前显示准确工程路径并要求确认; +- 扩展不会因激活、打开工程、修改配置或切换编辑器而自动执行上述 CLI 操作; +- 外部进程始终使用参数数组,不通过 shell 解释用户输入。 + +## 多根工作区与并发 + +- 项目命令只作用于当前活动编辑器所属的 mcpp 工程; +- 后台工程不会因为前台命令而构建、清理或重新配置 clangd; +- 同一工程一次只允许运行一个由扩展启动的项目任务; +- 再次执行命令时聚焦已有任务,不并发写入同一构建目录; +- 工具链安装和全局默认值修改共享全局互斥状态; +- 取消任务不显示成功提示,也不把缺失退出码解释为成功。 + +扩展只能协调自己启动的任务,不阻止用户在外部终端直接运行 mcpp。 + +## 组件边界 + +实现按以下职责拆分: + +- CLI 纯逻辑模块:命令参数、工具链数据类型、列表解析和结果分类; +- 任务纯逻辑模块:任务标识、并发状态和任务结束后的动作决策; +- VS Code 适配层:Quick Pick、确认框、Task、状态栏和命令注册; +- 现有 CDB/clangd 工作流:继续负责工程发现、配置、重启和模块检查。 + +纯逻辑模块不导入 `vscode`,使其可以继续使用当前 Node.js 测试运行器验证。 + +## 错误处理 + +- 进程启动失败时显示可执行文件和失败原因; +- 长任务的完整输出保留在 VS Code 任务终端,退出码和摘要写入 `mcpp` 输出频道; +- 短命令的标准输出、标准错误和退出码写入 `mcpp` 输出频道; +- 用户提示使用简短中文,并引导查看输出频道; +- 工具链输出解析失败时显示原始输出,不返回伪造的空列表; +- 任务取消、命令失败和 clangd 重协调失败分别报告; +- clangd 重协调失败不改写 mcpp 任务本身的退出结果。 + +## 测试与交付 + +在现有测试基础上增加: + +- 工具链已安装项、系统项、有效项、全局默认项、可安装项和项目级覆盖提示解析; +- 工具链未知辅助文本及解析失败回退; +- build、run、test、clean、install、default 的参数数组; +- 同工程并发限制、全局工具链互斥和取消结果; +- 工具链切换后不自动构建; +- 构建类任务完成后请求 CDB/clangd 重协调; +- Restricted Mode 和多根工作区边界; +- `package.json` 命令、激活事件、版本与中文文案。 + +交付前执行: + +```text +npm test +npm run compile +npm run package +``` + +随后生成 `mcpp-community.mcpp-vscode@0.2.0` VSIX,并覆盖安装到当前 VS Code。 +真实完成哪些 Extension Host 图形操作会单独记录;未执行的 UI 端到端验证不得写成已通过。 + +## 后续计划 + +后续版本采用侧边栏视图,按工程展示: + +- 当前工程及工作区成员; +- 实际生效的工具链、全局默认值和项目级覆盖; +- 构建 profile、target、feature、capability 和运行参数; +- 最近任务、运行状态、取消与重新执行; +- 依赖搜索、添加、删除和更新; +- CDB、clangd 和模块语义状态。 + +侧边栏只是交互层升级,仍复用本版建立的 CLI、任务和协调边界。 From 8dca63f4f27b534a2c5e495028120afe613d0a73 Mon Sep 17 00:00:00 2001 From: wellwei Date: Fri, 31 Jul 2026 19:12:34 +0800 Subject: [PATCH 003/119] docs: add mcpp CLI implementation plan --- .../plans/2026-07-31-mcpp-cli-integration.md | 345 ++++++++++++++++++ .../2026-07-31-mcpp-cli-integration-design.md | 7 +- 2 files changed, 349 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-31-mcpp-cli-integration.md diff --git a/docs/superpowers/plans/2026-07-31-mcpp-cli-integration.md b/docs/superpowers/plans/2026-07-31-mcpp-cli-integration.md new file mode 100644 index 0000000..74d8702 --- /dev/null +++ b/docs/superpowers/plans/2026-07-31-mcpp-cli-integration.md @@ -0,0 +1,345 @@ +# mcpp CLI 基础功能实施计划 + +> 面向执行代理:按任务逐项执行;每个任务先写失败测试,再写最小实现,并在任务边界提交一次。用户已选择在新建的 mcpp-vscode Git 仓库中直接执行本计划。 + +目标:在保留现有 clangd/CDB 自动协调的前提下,增加状态栏快捷菜单、工具链管理以及 build/run/test/clean 基础命令。 + +架构:src/cli.ts 负责工具链输出解析和 mcpp 参数;src/tasks.ts 负责任务计划、退出结果和并发状态;src/commands.ts 负责无 vscode 依赖的命令模型;src/cliController.ts 负责 VS Code Task、Quick Pick、确认框和状态栏;src/extension.ts 只接入控制器并复用现有 clangd 重协调。长任务使用 ProcessExecution,短命令继续使用参数数组的 execFile。 + +技术栈:TypeScript 5.9、Node.js node:test、VS Code Extension API 1.90、@vscode/vsce。 + +--- + +## 文件地图 + +新增:src/cli.ts、src/tasks.ts、src/commands.ts、src/cliController.ts,以及对应的 test/cli.test.ts、test/tasks.test.ts、test/commands.test.ts。 + +修改:src/extension.ts、src/process.ts(如需通用短命令入口)、src/workflow.ts(只在需要复用纯判定时)、package.json、package-lock.json、test/artifacts.test.ts、README.md、CHANGELOG.md。 + +保留:此前未跟踪的 docs/superpowers 旧设计和计划文件,不删除、不纳入本次提交。 + +## Task 1:工具链 CLI 模型与解析器 + +文件:创建 src/cli.ts、test/cli.test.ts。 + +### 1.1 红灯测试 + +先在 test/cli.test.ts 写固定输出测试,不访问本机工具链目录: + +~~~ts +import assert from "node:assert/strict"; +import test from "node:test"; +import { mcppCommandArguments, normalizeToolchainSpec, parseToolchainList } from "../src/cli"; + +const plainOutput = [ + "Toolchains:", + " * llvm 22.1.8 (default)", + " gcc 16.1.0", + "", + "Targets:", + " * aarch64-macos host llvm 22.1.8 installed", + "", + "Available toolchains:", + " llvm 20.1.7", + "", +].join("\n"); + +test("解析已安装、默认和可安装项", () => { + const inventory = parseToolchainList(plainOutput); + assert.equal(inventory.recognized, true); + assert.equal(inventory.projectOverridesGlobal, false); + assert.equal(inventory.globalDefaultSpec, "llvm@22.1.8"); + assert.equal(inventory.effective?.spec, "llvm@22.1.8"); + assert.deepEqual(inventory.installed.map((item) => item.spec), ["llvm@22.1.8", "gcc@16.1.0"]); + assert.deepEqual(inventory.available.map((item) => item.spec), ["llvm@20.1.7"]); +}); + +test("项目覆盖时分开记录有效项和全局默认", () => { + const output = [ + "Toolchains:", + " * llvm 22.1.8 (default)", + " gcc 16.1.0", + " (* = effective toolchain from project mcpp.toml [toolchain]; global default is 'gcc@16.1.0')", + ].join("\n"); + const inventory = parseToolchainList(output); + assert.equal(inventory.projectOverridesGlobal, true); + assert.equal(inventory.effective?.spec, "llvm@22.1.8"); + assert.equal(inventory.globalDefaultSpec, "gcc@16.1.0"); +}); + +test("支持系统 MSVC 和合法的空安装状态", () => { + const system = parseToolchainList([ + "System:", + " * msvc 17.10 C:\\\\Program Files\\\\MSVC\\\\cl.exe", + ].join("\n")); + assert.equal(system.effective?.spec, "msvc"); + assert.equal(system.installed[0]?.source, "system"); + + const empty = parseToolchainList("(no toolchains installed - run mcpp build to auto-install the default)\\n"); + assert.equal(empty.recognized, true); + assert.deepEqual(empty.installed, []); +}); + +test("未知输出不伪造工具链", () => { + const inventory = parseToolchainList("unexpected output\\n"); + assert.equal(inventory.recognized, false); + assert.deepEqual(inventory.installed, []); + assert.equal(inventory.rawOutput, "unexpected output\\n"); +}); + +test("规范化输入并构造参数数组", () => { + assert.equal(normalizeToolchainSpec(" llvm@20 "), "llvm@20"); + assert.equal(normalizeToolchainSpec("gcc 16.1.0"), "gcc@16.1.0"); + assert.equal(normalizeToolchainSpec("gcc; rm -rf /"), undefined); + assert.deepEqual(mcppCommandArguments("toolchain", "default", "llvm@22.1.8"), ["toolchain", "default", "llvm@22.1.8"]); + assert.deepEqual(mcppCommandArguments("build"), ["build"]); +}); +~~~ + +运行 npm test。预期 TypeScript 报告 src/cli.ts 不存在;确认是新功能缺失导致的红灯。 + +### 1.2 最小实现 + +src/cli.ts 提供以下无 vscode 类型: + +~~~ts +export type ToolchainSource = "managed" | "system"; +export interface ToolchainItem { + family: string; + version: string; + spec: string; + source: ToolchainSource; + effective: boolean; +} +export interface ToolchainInventory { + installed: ToolchainItem[]; + available: ToolchainItem[]; + effective: ToolchainItem | undefined; + globalDefaultSpec: string | undefined; + projectOverridesGlobal: boolean; + recognized: boolean; + rawOutput: string; +} +export function mcppCommandArguments(...args: string[]): string[]; +export function normalizeToolchainSpec(input: string): string | undefined; +export function parseToolchainList(output: string): ToolchainInventory; +~~~ + +按 Toolchains:、System:、Available toolchains 区块识别行,不依赖列宽。管理工具链规范为 family@version,系统 MSVC 规范为 msvc。星号表示当前有效项;出现 global default is '...' 时使用其中的全局值,否则有效项就是全局值。no toolchains installed 是已识别的空状态。结果始终保留 rawOutput。 + +运行 npm test,预期新增 CLI 测试和原有 41 项全部通过。 + +提交:git add src/cli.ts test/cli.test.ts && git commit -m "feat: parse mcpp toolchain inventory" + +## Task 2:任务计划、退出状态和并发登记 + +文件:创建 src/tasks.ts、test/tasks.test.ts。 + +### 2.1 红灯测试 + +测试基础命令、退出状态和双层锁: + +~~~ts +import assert from "node:assert/strict"; +import test from "node:test"; +import { McppOperationRegistry, classifyTaskExit, projectTaskPlan, shouldReconcileAfterTask } from "../src/tasks"; + +test("基础任务不带高级参数", () => { + assert.deepEqual(projectTaskPlan("build"), { kind: "build", title: "mcpp: 构建", args: ["build"] }); + assert.deepEqual(projectTaskPlan("run"), { kind: "run", title: "mcpp: 运行", args: ["run"] }); + assert.deepEqual(projectTaskPlan("test"), { kind: "test", title: "mcpp: 测试", args: ["test"] }); + assert.deepEqual(projectTaskPlan("clean"), { kind: "clean", title: "mcpp: 清理", args: ["clean"] }); +}); + +test("四种项目任务都需要重协调", () => { + for (const kind of ["build", "run", "test", "clean"] as const) { + assert.equal(shouldReconcileAfterTask(kind), true); + } +}); + +test("退出码区分成功、失败和取消", () => { + assert.deepEqual(classifyTaskExit(0), { state: "succeeded", exitCode: 0 }); + assert.deepEqual(classifyTaskExit(2), { state: "failed", exitCode: 2 }); + assert.deepEqual(classifyTaskExit(undefined), { state: "cancelled" }); +}); + +test("同一项目拒绝重复任务,全局锁独立", () => { + const registry = new McppOperationRegistry(); + const first = {}; + const second = {}; + assert.equal(registry.beginProject("/work/app", first), undefined); + assert.equal(registry.beginProject("/work/app", second), first); + assert.equal(registry.beginProject("/work/lib", second), undefined); + assert.equal(registry.beginGlobal(first), undefined); + assert.equal(registry.beginGlobal(second), first); + registry.finishProject("/work/app", first); + registry.finishGlobal(first); + assert.equal(registry.beginProject("/work/app", second), undefined); + assert.equal(registry.beginGlobal(second), undefined); +}); +~~~ + +运行 npm test,预期 src/tasks.ts 缺失。 + +### 2.2 最小实现和验证 + +src/tasks.ts 提供: + +~~~ts +export type ProjectTaskKind = "build" | "run" | "test" | "clean"; +export type TaskState = "succeeded" | "failed" | "cancelled"; +export interface ProjectTaskPlan { kind: ProjectTaskKind; title: string; args: string[]; } +export interface TaskCompletion { state: TaskState; exitCode?: number; } +export function projectTaskPlan(kind: ProjectTaskKind): ProjectTaskPlan; +export function shouldReconcileAfterTask(kind: ProjectTaskKind): boolean; +export function classifyTaskExit(exitCode: number | undefined): TaskCompletion; +export class McppOperationRegistry { /* begin/finish project and global */ } +~~~ + +finish 只有 token 相同才清除;undefined 退出码不得归类为成功。运行 npm test,确认新测试和原有测试全部通过。 + +提交:git add src/tasks.ts test/tasks.test.ts && git commit -m "feat: model mcpp task lifecycle" + +## Task 3:命令模型和清单声明 + +文件:创建 src/commands.ts、test/commands.test.ts;修改 package.json、test/artifacts.test.ts。 + +### 3.1 红灯测试 + +要求命令模型包含: + +~~~ts +import assert from "node:assert/strict"; +import test from "node:test"; +import { CLI_COMMANDS, quickMenuItems } from "../src/commands"; + +test("CLI 命令覆盖项目、工具链和 IDE", () => { + assert.deepEqual(Object.values(CLI_COMMANDS), [ + "mcpp.showMenu", "mcpp.build", "mcpp.run", "mcpp.test", "mcpp.clean", + "mcpp.showToolchains", "mcpp.installToolchain", "mcpp.selectDefaultToolchain", + ]); + assert.deepEqual(quickMenuItems.map((item) => item.command), [ + "mcpp.build", "mcpp.run", "mcpp.test", "mcpp.clean", + "mcpp.showToolchains", "mcpp.installToolchain", "mcpp.selectDefaultToolchain", + "mcpp.configureClangd", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport", + ]); + assert.ok(quickMenuItems.every((item) => item.label.length > 0)); +}); +~~~ + +扩展清单断言完整命令清单、版本 0.2.0、全部新增命令的 activationEvents,以及 mcpp.path 描述包含“全部 mcpp CLI 命令”。先运行 npm test 确认失败。 + +### 3.2 最小实现和验证 + +src/commands.ts 导出不可变命令 ID、group、label、command,不导入 vscode。package.json 增加 mcpp.showMenu、mcpp.build、mcpp.run、mcpp.test、mcpp.clean、mcpp.showToolchains、mcpp.installToolchain、mcpp.selectDefaultToolchain 的中文标题和激活事件,版本改为 0.2.0,mcpp.path 改为“供扩展执行全部 mcpp CLI 命令使用”。 + +运行 npm test,确认命令模型、清单和原有测试通过。 + +提交:git add src/commands.ts test/commands.test.ts package.json test/artifacts.test.ts && git commit -m "feat: declare mcpp CLI commands" + +## Task 4:VS Code Task 适配层 + +文件:创建 src/cliController.ts;修改 src/extension.ts。 + +### 4.1 先写边界测试 + +在 test/tasks.test.ts 增加一个纯判定:取消结果不调用重协调成功路径;成功、失败结果都允许刷新 CDB。先运行 npm test,确认新增断言在控制器未接入时失败。 + +### 4.2 实现任务启动 + +cliController.ts 创建长任务: + +~~~ts +const task = new vscode.Task( + { type: "mcpp", command: plan.kind, projectRoot }, + vscode.workspace.getWorkspaceFolder(vscode.Uri.file(projectRoot)) ?? vscode.TaskScope.Workspace, + plan.title, + "mcpp", + new vscode.ProcessExecution(executable, plan.args, { cwd: projectRoot }), +); +task.presentationOptions = { + reveal: vscode.TaskRevealKind.Always, + panel: vscode.TaskPanelKind.Dedicated, + focus: true, + clear: true, + showReuseMessage: false, +}; +~~~ + +在 executeTask 前监听 onDidEndTaskProcess,使用任务执行对象和 classifyTaskExit 解析结果。完整输出留在任务终端;mcpp 输出频道记录命令、退出状态和摘要。 + +用 McppOperationRegistry.beginProject 登记项目任务;重复时提示已有任务并提供 workbench.action.tasks.showTasks,不假装聚焦某个终端。finally 中用 token 解锁。工具链安装占用全局 token;默认值短命令也占用全局 token。 + +运行 npm run compile && npm test,预期编译成功、纯测试通过;不声称 Extension Host 图形 UI 已验证。 + +## Task 5:状态栏、项目命令和 clangd 协调 + +文件:修改 src/cliController.ts、src/extension.ts、src/process.ts(仅必要时)、test/artifacts.test.ts。 + +### 5.1 接入状态栏菜单 + +控制器创建第二个状态栏项,文本 $(tools) mcpp,只在 findCurrentProject() 有结果时显示;现有模块状态栏不改。点击使用 showQuickPick 展示 quickMenuItems,并按命令 ID 执行命令。监听活动编辑器、工作区文件夹和 mcpp.toml 变化刷新可见性。 + +### 5.2 接入项目命令 + +统一流程:解析当前工程和 mcpp.path;未信任或无 mcpp.toml 时只显示中文提示;clean 使用 modal 确认 target/;使用 projectTaskPlan 创建 Task;结束后调用扩展提供的 afterProjectTask(project, kind, completion): Promise;四种项目任务均调用 reconcileProject(project, true),但只有当前活动工程接管窗口 clangd;取消不显示成功。 + +把旧 mcpp.refreshCompilationDatabase 的直接 runMcppBuild 替换为同一套 build 控制器,保留原 ID 和标题,避免隐藏 execFile 与 Task 并发写同一 target/。构建结果继续复用 describeRefreshOutcome。 + +运行 npm test,预期全部通过。 + +提交:git add src/cliController.ts src/extension.ts src/process.ts test/tasks.test.ts test/artifacts.test.ts && git commit -m "feat: run mcpp project tasks from VS Code" + +## Task 6:工具链查看、安装和全局默认 + +文件:修改 src/cliController.ts、src/cli.ts(仅测试揭示缺口时)、test/cli.test.ts、README.md、CHANGELOG.md。 + +### 6.1 红灯测试 + +增加非零列表结果、解析失败回退原始输出、安装后重新列表、项目覆盖文案的测试;先运行 npm test 确认失败。 + +### 6.2 实现短命令与 Quick Pick + +短命令调用 runProcess(executable, ["toolchain", "list"], projectRoot),并用 appendProcessOutput 写入 mcpp 输出频道。非零直接报错;recognized 为 false 时保留原始输出并报告兼容性错误。Quick Pick 分隔当前有效、已安装、系统和可安装项;无已安装项显示安装提示。 + +设置默认值只允许已安装/系统项,modal 确认后执行 mcppCommandArguments("toolchain", "default", item.spec),占用全局 token。成功提示全局配置已更新;项目覆盖时同时提示覆盖关系,并提供“立即构建”按钮,不自动构建。 + +安装先列出 available,附加“输入其他 family@version”,用 normalizeToolchainSpec 校验;确认后 Task 执行 mcppCommandArguments("toolchain", "install", spec),不传 --target。成功后重新读取列表,再询问是否进入选择全局默认;不从部分版本输入猜测最终版本。 + +### 6.3 文档和验证 + +README 增加快捷菜单、四个项目命令、工具链全局作用域、项目覆盖、任务终端、Workspace Trust、独立 mcpp/clangd 路径和高级参数限制。CHANGELOG 增加 0.2.0,并明确侧边栏是后续版本。运行 npm test,确认所有测试通过且新增用户提示为中文。 + +提交:git add src/cliController.ts src/cli.ts test/cli.test.ts README.md CHANGELOG.md && git commit -m "feat: add mcpp toolchain controls" + +## Task 7:打包、安装和最终验证 + +### 7.1 完整验证 + +运行: + +~~~sh +npm test +npm run compile +npm run package +~~~ + +预期:测试 0 失败、编译退出码为 0,生成 mcpp-vscode-0.2.0.vsix。 + +检查包: + +~~~sh +unzip -l mcpp-vscode-0.2.0.vsix | rg "package.json|dist/src/cli|dist/src/cliController|dist/src/tasks|README.md|CHANGELOG.md" +~~~ + +包中必须包含新模块和中文文档,不包含 node_modules、dist/test 或 map 测试产物。 + +### 7.2 安装与烟测 + +运行 code --install-extension mcpp-vscode-0.2.0.vsix --force,再用 code --list-extensions --show-versions 检查登记版本;若 code 命令不可用,只报告 VSIX 已生成。 + +在受信任 mcpp 工程中从命令面板执行“mcpp: 查看工具链”,确认输出频道记录 toolchain list;检查“选择全局默认工具链”的确认和取消路径,不在未明确确认时修改用户全局默认。 + +### 7.3 提交验证状态 + +运行 git status --short --branch 和 git log --oneline --decorate -8。保留用户已有未跟踪 docs/superpowers 文件;最终报告提交、测试数量、VSIX 路径、安装结果和未完成的真实 Extension Host UI 验证。 diff --git a/docs/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md b/docs/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md index ef3ed76..b04de74 100644 --- a/docs/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md +++ b/docs/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md @@ -148,8 +148,9 @@ mcpp 当前没有为 `toolchain list` 提供 JSON 输出。本版只解析以下 没有项目级覆盖时,星号项就是全局默认工具链;存在项目级覆盖时,星号项表示当前项目 有效工具链,全局默认值必须读取覆盖提示,二者不得混淆。 -解析器不依赖列宽,并忽略不认识的辅助文本。解析结果必须保留原始输出。若无法得到任何 -已安装项,插件不猜测工具链名称,而是把原始输出写入 `mcpp` 输出频道并报告兼容性错误。 +解析器不依赖列宽,并忽略不认识的辅助文本。解析结果必须保留原始输出。mcpp 明确输出 +“no toolchains installed”时,空的已安装列表是合法结果;只有既未识别已知区块、也未识别 +该空状态时,插件才把原始输出写入 `mcpp` 输出频道并报告兼容性错误。 ## 安全边界 @@ -165,7 +166,7 @@ mcpp 当前没有为 `toolchain list` 提供 JSON 输出。本版只解析以下 - 项目命令只作用于当前活动编辑器所属的 mcpp 工程; - 后台工程不会因为前台命令而构建、清理或重新配置 clangd; - 同一工程一次只允许运行一个由扩展启动的项目任务; -- 再次执行命令时聚焦已有任务,不并发写入同一构建目录; +- 再次执行命令时不并发写入同一构建目录,并提供“显示正在运行的任务”操作; - 工具链安装和全局默认值修改共享全局互斥状态; - 取消任务不显示成功提示,也不把缺失退出码解释为成功。 From aa556296f12862228d7211827d05a72f1894f9bc Mon Sep 17 00:00:00 2001 From: wellwei Date: Fri, 31 Jul 2026 19:21:15 +0800 Subject: [PATCH 004/119] feat: parse mcpp toolchain inventory --- src/cli.ts | 239 +++++++++++++++++++++++++++++++++++++++++++++++ test/cli.test.ts | 91 ++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/cli.ts create mode 100644 test/cli.test.ts diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..a8fa94f --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,239 @@ +export type ToolchainSource = "managed" | "system"; + +export interface ToolchainItem { + family: string; + version: string; + spec: string; + source: ToolchainSource; + effective: boolean; +} + +export interface ToolchainInventory { + installed: ToolchainItem[]; + available: ToolchainItem[]; + effective: ToolchainItem | undefined; + globalDefaultSpec: string | undefined; + projectOverridesGlobal: boolean; + recognized: boolean; + rawOutput: string; +} + +type Section = "managed" | "system" | "available" | undefined; + +const ANSI_ESCAPE = /\u001b\[[0-?]*[ -/]*[@-~]/g; +const FAMILY_PATTERN = "[A-Za-z][A-Za-z0-9+_.-]*"; +const VERSION_PATTERN = "[0-9][A-Za-z0-9+_.-]*"; +const AUXILIARY_WORDS = new Set([ + "available", + "default", + "global", + "host", + "installed", + "no", + "none", + "run", + "system", + "target", + "targets", + "toolchain", + "toolchains", +]); + +export function mcppCommandArguments(...args: string[]): string[] { + return [...args]; +} + +export function normalizeToolchainSpec(input: string): string | undefined { + let value = input.trim(); + if (value.length >= 2) { + const first = value[0]; + const last = value[value.length - 1]; + if ((first === "'" && last === "'") || (first === '"' && last === '"')) { + value = value.slice(1, -1).trim(); + } + } + + if (/^msvc$/i.test(value)) { + return "msvc"; + } + + const atMatch = value.match( + new RegExp(`^(${FAMILY_PATTERN})@(${VERSION_PATTERN})$`), + ); + if (atMatch !== null) { + if (atMatch[1].toLowerCase() === "msvc") { + return "msvc"; + } + return `${atMatch[1].toLowerCase()}@${atMatch[2]}`; + } + + const spacedMatch = value.match( + new RegExp(`^(${FAMILY_PATTERN})\\s+(${VERSION_PATTERN})$`), + ); + if (spacedMatch !== null) { + if (spacedMatch[1].toLowerCase() === "msvc") { + return "msvc"; + } + return `${spacedMatch[1].toLowerCase()}@${spacedMatch[2]}`; + } + + return undefined; +} + +function sectionForHeader(line: string): Section | "header" | false { + if (/^\s*Toolchains\s*:\s*$/i.test(line)) { + return "managed"; + } + if (/^\s*System(?:\s+toolchains?)?\s*:\s*$/i.test(line)) { + return "system"; + } + if (/^\s*Available\s+toolchains?\s*:\s*$/i.test(line)) { + return "available"; + } + if (/^\s*[A-Za-z][A-Za-z0-9 _-]*\s*:\s*$/i.test(line)) { + return "header"; + } + return false; +} + +function parseToolchainRow( + line: string, + source: ToolchainSource, +): { family: string; version: string; effective: boolean; spec: string } | undefined { + let value = line.trim(); + let effective = false; + if (value.startsWith("*")) { + effective = true; + value = value.slice(1).trimStart(); + } + if (value.startsWith("-")) { + value = value.slice(1).trimStart(); + } + if (value.startsWith("(") || value.length === 0) { + return undefined; + } + + const tokens = value.split(/\s+/); + const firstToken = tokens[0]; + if (firstToken === undefined || AUXILIARY_WORDS.has(firstToken.toLowerCase())) { + return undefined; + } + + let family: string; + let version: string; + let spec: string | undefined; + if (firstToken.includes("@")) { + spec = normalizeToolchainSpec(firstToken); + if (spec === undefined) { + return undefined; + } + const atIndex = firstToken.indexOf("@"); + family = firstToken.slice(0, atIndex).toLowerCase(); + version = firstToken.slice(atIndex + 1); + } else { + const secondToken = tokens[1]; + if (secondToken === undefined || !new RegExp(`^${VERSION_PATTERN}$`).test(secondToken)) { + return undefined; + } + family = firstToken.toLowerCase(); + version = secondToken; + spec = normalizeToolchainSpec(`${family} ${version}`); + } + if (spec === undefined) { + return undefined; + } + + if (source === "system" && family === "msvc") { + spec = "msvc"; + } + return { family, version, spec, effective }; +} + +function parseGlobalDefault(line: string): string | undefined { + const match = line.match(/\bglobal\s+default\s+is\s+['"]([^'"]+)['"]/i); + if (match !== null) { + return normalizeToolchainSpec(match[1]); + } + const unquoted = line.match( + new RegExp(`\\bglobal\\s+default\\s+is\\s+(${FAMILY_PATTERN}(?:@${VERSION_PATTERN}|\\s+${VERSION_PATTERN}|))\\b`, "i"), + ); + return unquoted === null ? undefined : normalizeToolchainSpec(unquoted[1]); +} + +export function parseToolchainList(output: string): ToolchainInventory { + const installed: ToolchainItem[] = []; + const available: ToolchainItem[] = []; + let effective: ToolchainItem | undefined; + let section: Section; + let recognized = false; + let projectMarker = false; + let explicitGlobalDefault: string | undefined; + + for (const rawLine of output.split(/\r?\n/)) { + const line = rawLine.replace(ANSI_ESCAPE, ""); + const lowerLine = line.toLowerCase(); + if (lowerLine.includes("no toolchains installed")) { + recognized = true; + } + if (/effective\s+toolchain\s+from\s+project\s+mcpp\.toml\s+\[toolchain\]/i.test(line)) { + projectMarker = true; + } + const parsedGlobalDefault = parseGlobalDefault(line); + if (parsedGlobalDefault !== undefined) { + explicitGlobalDefault = parsedGlobalDefault; + } + + const header = sectionForHeader(line); + if (header !== false) { + if (header === "header") { + section = undefined; + } else { + recognized = true; + section = header; + } + continue; + } + + if (section === undefined) { + continue; + } + const parsed = parseToolchainRow(line, section === "system" ? "system" : "managed"); + if (parsed === undefined) { + continue; + } + const item: ToolchainItem = { + family: parsed.family, + version: parsed.version, + spec: parsed.spec, + source: section === "system" ? "system" : "managed", + effective: parsed.effective, + }; + if (section === "available") { + available.push(item); + } else { + installed.push(item); + if (item.effective && effective === undefined) { + effective = item; + } + } + } + + const globalDefaultSpec = recognized + ? explicitGlobalDefault ?? effective?.spec + : undefined; + const projectOverridesGlobal = recognized + && (projectMarker + || (explicitGlobalDefault !== undefined + && effective !== undefined + && explicitGlobalDefault !== effective.spec)); + + return { + installed, + available, + effective, + globalDefaultSpec, + projectOverridesGlobal, + recognized, + rawOutput: output, + }; +} diff --git a/test/cli.test.ts b/test/cli.test.ts new file mode 100644 index 0000000..be48a14 --- /dev/null +++ b/test/cli.test.ts @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + mcppCommandArguments, + normalizeToolchainSpec, + parseToolchainList, +} from "../src/cli"; + +const plainOutput = [ + "Toolchains:", + " * llvm 22.1.8 (default)", + " gcc 16.1.0", + "", + "Targets:", + " * aarch64-macos host llvm 22.1.8 installed", + "", + "Available toolchains:", + " llvm 20.1.7", + "", +].join("\n"); + +test("解析已安装、默认和可安装项", () => { + const inventory = parseToolchainList(plainOutput); + assert.equal(inventory.recognized, true); + assert.equal(inventory.projectOverridesGlobal, false); + assert.equal(inventory.globalDefaultSpec, "llvm@22.1.8"); + assert.equal(inventory.effective?.spec, "llvm@22.1.8"); + assert.deepEqual( + inventory.installed.map((item) => item.spec), + ["llvm@22.1.8", "gcc@16.1.0"], + ); + assert.deepEqual( + inventory.available.map((item) => item.spec), + ["llvm@20.1.7"], + ); +}); + +test("项目覆盖时分开记录有效项和全局默认", () => { + const output = [ + "Toolchains:", + " * llvm 22.1.8 (default)", + " gcc 16.1.0", + " (* = effective toolchain from project mcpp.toml [toolchain]; global default is 'gcc@16.1.0')", + ].join("\n"); + const inventory = parseToolchainList(output); + assert.equal(inventory.projectOverridesGlobal, true); + assert.equal(inventory.effective?.spec, "llvm@22.1.8"); + assert.equal(inventory.globalDefaultSpec, "gcc@16.1.0"); +}); + +test("支持系统 MSVC 和合法的空安装状态", () => { + const system = parseToolchainList([ + "System:", + " * msvc 17.10 C:\\\\Program Files\\\\MSVC\\\\cl.exe", + ].join("\n")); + assert.equal(system.effective?.spec, "msvc"); + assert.equal(system.installed[0]?.source, "system"); + + const empty = parseToolchainList( + "(no toolchains installed - run mcpp build to auto-install the default)\n", + ); + assert.equal(empty.recognized, true); + assert.deepEqual(empty.installed, []); +}); + +test("未知输出不伪造工具链", () => { + const inventory = parseToolchainList("unexpected output\n"); + assert.equal(inventory.recognized, false); + assert.deepEqual(inventory.installed, []); + assert.equal(inventory.rawOutput, "unexpected output\n"); + + const unknownSection = parseToolchainList([ + "Metadata:", + " global default is 'gcc@16.1.0'", + ].join("\n")); + assert.equal(unknownSection.recognized, false); + assert.equal(unknownSection.globalDefaultSpec, undefined); +}); + +test("规范化输入并构造参数数组", () => { + assert.equal(normalizeToolchainSpec(" llvm@20 "), "llvm@20"); + assert.equal(normalizeToolchainSpec("gcc 16.1.0"), "gcc@16.1.0"); + assert.equal(normalizeToolchainSpec("MSVC 17.10"), "msvc"); + assert.equal(normalizeToolchainSpec("gcc; rm -rf /"), undefined); + assert.deepEqual( + mcppCommandArguments("toolchain", "default", "llvm@22.1.8"), + ["toolchain", "default", "llvm@22.1.8"], + ); + assert.deepEqual(mcppCommandArguments("build"), ["build"]); +}); From 81f3e5aa3557391842fcbb2fc0e88b9833742f80 Mon Sep 17 00:00:00 2001 From: wellwei Date: Fri, 31 Jul 2026 22:01:50 +0800 Subject: [PATCH 005/119] feat: integrate mcpp cli and toolchain controls --- CHANGELOG.md | 15 + README.md | 73 ++++- package-lock.json | 4 +- package.json | 46 ++- src/cli.ts | 258 +++++++++++++--- src/cliController.ts | 678 +++++++++++++++++++++++++++++++++++++++++ src/commands.ts | 30 ++ src/extension.ts | 123 ++++---- src/tasks.ts | 84 +++++ test/artifacts.test.ts | 57 +++- test/cli.test.ts | 130 +++++++- test/commands.test.ts | 34 +++ test/tasks.test.ts | 74 +++++ 13 files changed, 1490 insertions(+), 116 deletions(-) create mode 100644 src/cliController.ts create mode 100644 src/commands.ts create mode 100644 src/tasks.ts create mode 100644 test/commands.test.ts create mode 100644 test/tasks.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9389bb1..f822828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # 更新日志 +## 0.2.0 + +- 新增 `$(tools) mcpp` 状态栏快捷菜单和命令面板命令:构建、运行、测试、清理 target、 + 查看工具链、安装工具链、选择全局默认工具链。 +- 项目任务使用 VS Code 的无 shell `ProcessExecution`,显示实时任务终端;同一工程任务 + 互斥,工具链安装/默认值操作共享全局锁,取消不会误报成功。 +- 项目任务结束后复用 CDB/clangd 自动协调;`mcpp.refreshCompilationDatabase` 与构建 + 命令共用同一任务路径,避免并发写入工程构建目录。 +- 按 mcpp 官方工具链契约解析 `Toolchains:`、`System:`、`Targets:` 和可安装列表: + toolchain 与 target 分轴处理,首版 UI 不提供结构化 `--target` 选择器。 +- 全局默认选择过滤普通 target-only payload,保留 mcpp 在 Windows 上将 host GCC 映射到 + MinGW payload 的官方规则,并明确设置 host 默认会清空 `default_target`; + 安装入口把 mcpp 兼容 spec 原样传给 CLI,MSVC 走系统检测,隐含 target 的别名走对应 payload。 +- 工具链安装和全局默认修改均需要用户确认;Restricted Mode 不执行 mcpp 或其他外部工具。 + ## 0.1.5 - 打开已有编译数据库的 LLVM mcpp 工程时自动执行 `clangd --check`,状态栏无需点击即可显示“模块可用”或“模块不可用”。 diff --git a/README.md b/README.md index 3b4cab6..02b6111 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,14 @@ mcpp 会把 `compile_commands.json` 写到工程根目录。扩展只读取它 单次 `clangd --check` 最长等待 60 秒,避免异常进程让激活流程一直停住。 5. `mcpp build` 即使因普通源码错误返回非零,只要工程中已经出现可用 CDB, 扩展仍会配置 clangd,并明确提示“构建失败,但 IDE 数据已就绪”。 +6. 版本 `0.2.0` 增加独立的 `$(tools) mcpp` 状态栏入口。它可以在当前工程中执行 + `mcpp build`、`run`、`test`、`clean`,任务终端会实时显示完整输出;任务结束后会 + 重新协调 CDB 和 clangd。`clean` 只确认并清理当前工程的 `target/`,不会删除全局 + BMI 缓存。 +7. 工具链菜单可以查看 mcpp 的 `Toolchains:`、`System:`、`Targets:` 和 + `Available toolchains:` 区块,安装可用工具链,或从已安装/系统项中选择全局默认。 + 安装和修改默认值都需要用户确认;扩展激活、打开工程或修改配置不会自动下载、安装 + 或切换工具链。 ## 命令 @@ -64,6 +72,30 @@ mcpp 会把 `compile_commands.json` 写到工程根目录。扩展只读取它 - **mcpp: 检查模块支持**:立即重新执行 clangd 直接检查,报告检查成功、模块文件 缺失、语言模式错误或 PCM/LLVM 不匹配。扩展激活和 CDB 变化时已经会自动检查; 此命令用于主动复查。完整输出位于 `mcpp` 输出频道。 +- **mcpp: 打开快捷菜单**:从状态栏打开项目、工具链和 IDE 操作。 +- **mcpp: 构建 / 运行 / 测试 / 清理 target**:分别执行对应的 mcpp CLI 子命令。 + 同一工程同时只允许一个由扩展启动的任务;取消任务不会被当成成功,也不会触发成功 + 提示。构建、运行和测试的工作目录固定为当前工程根目录。 +- **mcpp: 查看工具链**:执行 `mcpp toolchain list`,保留原始输出并在 Quick Pick 中 + 展示当前有效工具链、全局默认、系统工具链、target 状态和可安装版本。 +- **mcpp: 安装工具链**:从 mcpp 列出的可用项或兼容 spec 中选择,执行 + `mcpp toolchain install `。规范的 `gcc@V` / `llvm@V` 按 host target 安装; + `gcc@V-musl`、`mingw@V`、`-gcc@V` 等显式兼容写法原样交给 mcpp 规范化并安装 + 对应 target。输入 `msvc` 时,同一命令只检测系统 Visual Studio 或显示官方安装指引, + 不会由 mcpp 下载、升级或删除 MSVC。本版本暂不提供结构化 `--target` 选择器。 +- **mcpp: 选择全局默认工具链**:从当前主机可作为默认值的已安装项或检测到的系统项中 + 选择,执行 `mcpp toolchain default `。普通 target-only payload 不会混入列表; + Windows 上省略 target 的 GCC 按 mcpp 官方规则映射到已安装的 MinGW payload。按 mcpp 的官方 + 语义,这会把全局默认对设为“所选工具链 + host target”,并清空已有的 + `default_target`。它只修改 mcpp 当前全局配置;配置文件通常位于 `~/.mcpp/config.toml`, + 但 `MCPP_HOME` 和独立安装位置可能改变实际路径。当前项目的成员配置、 + 工作区配置、`[target.]` 和命令行参数仍按 mcpp 官方优先级覆盖全局默认;扩展不 + 自动构建,用户可在确认后选择立即构建。 + +当 mcpp 工程根位于 VS Code 工作区文件夹的子目录时,它可能是 mcpp 工作区成员,也可能 +只是嵌套的独立工程,扩展不会仅凭路径关系猜测其身份。“查看工具链”按当前工程根执行 +`mcpp toolchain list` 并标明这是当前目录视图;若存在父级 mcpp 工作区继承,实际生效 +工具链以 mcpp 构建解析为准。 ## 设置 @@ -76,8 +108,8 @@ mcpp 会把 `compile_commands.json` 写到工程根目录。扩展只读取它 } ``` -`mcpp.path` 只用于“刷新编译数据库”命令执行 `mcpp build`,用于解决 macOS GUI -启动的 VS Code 没有继承终端 `PATH` 的情况。留空时扩展直接执行 `mcpp`;也可以设置 +`mcpp.path` 供上述全部 mcpp CLI 命令使用,用于解决 macOS GUI 启动的 VS Code 没有 +继承终端 `PATH` 的情况。留空时扩展直接执行 `mcpp`;也可以设置 独立安装的绝对路径,例如 `/opt/homebrew/bin/mcpp`。工程实际使用的编译器始终从 `compile_commands.json` 读取,与 `mcpp.path` 相互独立。 @@ -93,14 +125,42 @@ clangd 21 及更高版本会请求 `--experimental-modules-support`。`on` 强 实验模块构建,`off` 始终禁用它。已有的 clangd 参数会保留;扩展只替换自己管理的 `--query-driver` 和实验模块参数。 +## mcpp 工具链契约 + +插件按 mcpp `origin/main`(核对提交 `0547324`)的工具链模型设计,不能把工具链列表硬编码成某几个主机版本: + +- `toolchain` 是 `family@version`(`gcc`、`llvm`、`msvc`),`target` 是独立的 + `arch-os[-env]` triple。`musl`、MinGW 和 cross 是 target 变体或构建关系,不是新的 family。 +- `mcpp toolchain default` 保存工具链和 target 的默认对;省略 `--target` 表示 host, + 全局配置分别使用 `default` 与 `default_target`。 +- toolchain 轴按“成员/项目 `[toolchain]` > 继承的工作区根 `[toolchain]` > 全局默认”解析; + target 轴按“命令行 `--target` > 项目/工作区 `[build].target` > 全局 `default_target` > host” + 解析。`[target.]` 可为特定 target 指定工具链;`--static` 只覆盖链接方式。 +- 工具链安装和首次构建支持部分版本并可自动安装;`MCPP_NO_AUTO_INSTALL=1` 或离线模式 + 会禁止网络安装。插件不因 mcpp 来自 xlings 还是独立安装而改变这套解析。 +- MSVC 是 Windows 系统工具链:mcpp 负责检测和构建,不负责安装、升级或删除;稳定身份是 + `msvc@system`,`msvc@` 是版本 pin-verify。0.0.90 起 native `cl.exe` 后端支持 + `import std`、命名模块、`.ifc`、增量构建和运行。 +- `musl-gcc`、`gcc@-musl`、`mingw@`、`mingw-gcc@`、 + `mingw-cross-gcc@`、`-gcc@` 和 `clang@` 等旧拼写仍由 + mcpp 兼容;其中除 `clang@` 外的这些变体会隐含 target。插件通过参数数组把 + 用户选择原样交给 mcpp,不自行重写其规范化结果。 + +当前 0.2.0 在消费编译数据库并配置 clangd 的基础上增加了 mcpp CLI 菜单;首版 UI +只读展示 target,不提供结构化 target 选择。用户仍可输入 mcpp 兼容 spec 安装 target +payload;target 专用默认值设置继续使用带 `--target` 的 mcpp CLI。 +`Available toolchains:` 是 mcpp 按 family 汇总多个 host 可读取 payload 索引后的版本集合, +并不承诺每个版本都存在 host payload;插件选择其中一项时仍按 host target 调用 mcpp, +最终可安装性由 mcpp 自身判断并原样报告。 + ## 限制 - clangd 模块实现仍处于实验阶段,编译数据库必须包含有效的 mcpp PCM 映射。单独 添加开关不能生成缺失的 BMI。 -- GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。本版本不会为这两类工程额外构建 - LLVM 分析副本。 +- GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它们,但 clangd + 不能直接消费;本版本不会为这两类工程额外构建 LLVM 分析副本。 - 语法规则只负责词法着色。红色诊断来自语言服务器,需要兼容的 LLVM 工具链。 -- 扩展不会静默执行无缓存构建,也不会自动下载工具链。 +- 扩展不会静默执行无缓存构建,也不会在未确认时自动下载、安装或切换工具链。 - 未受信任工作区不执行任何由工程文件或设置选择的外部工具;信任工作区后会自动开始 配置和模块检查。 @@ -169,7 +229,8 @@ IDE CDB 与模块图至少需要保证: 在没有独立 LLVM 分析配置前仍只保证模块语法高亮。 - clangd 负责编辑期语法和语义,不替代链接、代码生成、运行期或完整打包检查;这些错误 仍由正式 `mcpp build`/`mcpp test` 报告。 -- 当前 `0.1.5` 只是过渡方案:它能在用户显式构建后自动接管 CDB,但无法仅凭现有插件 +- 当前 `0.2.0` 仍是过渡方案:它能在用户显式构建后自动接管 CDB,并提供常用 CLI 操作, + 但无法仅凭现有插件 在无 CDB 时推导完整依赖图。最终体验依赖上述 mcpp 核心协议和 IDE 配置阶段。 ## 故障排查 diff --git a/package-lock.json b/package-lock.json index b7169ac..585a1f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.1.5", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.1.5", + "version": "0.2.0", "license": "Apache-2.0", "devDependencies": { "@types/node": "^24.0.0", diff --git a/package.json b/package.json index e6aafb7..8c43321 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.1.5", + "version": "0.2.0", "publisher": "mcpp-community", "license": "Apache-2.0", "repository": { @@ -24,7 +24,15 @@ "onLanguage:cpp", "onCommand:mcpp.configureClangd", "onCommand:mcpp.refreshCompilationDatabase", - "onCommand:mcpp.checkModuleSupport" + "onCommand:mcpp.checkModuleSupport", + "onCommand:mcpp.showMenu", + "onCommand:mcpp.build", + "onCommand:mcpp.run", + "onCommand:mcpp.test", + "onCommand:mcpp.clean", + "onCommand:mcpp.showToolchains", + "onCommand:mcpp.installToolchain", + "onCommand:mcpp.selectDefaultToolchain" ], "categories": [ "Programming Languages", @@ -39,6 +47,38 @@ "main": "./dist/src/extension.js", "contributes": { "commands": [ + { + "command": "mcpp.showMenu", + "title": "mcpp: 打开快捷菜单" + }, + { + "command": "mcpp.build", + "title": "mcpp: 构建" + }, + { + "command": "mcpp.run", + "title": "mcpp: 运行" + }, + { + "command": "mcpp.test", + "title": "mcpp: 测试" + }, + { + "command": "mcpp.clean", + "title": "mcpp: 清理 target" + }, + { + "command": "mcpp.showToolchains", + "title": "mcpp: 查看工具链" + }, + { + "command": "mcpp.installToolchain", + "title": "mcpp: 安装工具链" + }, + { + "command": "mcpp.selectDefaultToolchain", + "title": "mcpp: 选择全局默认工具链" + }, { "command": "mcpp.configureClangd", "title": "mcpp: 配置 clangd" @@ -59,7 +99,7 @@ "type": "string", "default": "", "scope": "resource", - "description": "仅供“刷新编译数据库”执行 mcpp build 使用;留空时从 VS Code 进程的 PATH 查找 mcpp。" + "description": "供扩展执行全部 mcpp CLI 命令使用;留空时从 VS Code 进程的 PATH 查找 mcpp。" }, "mcpp.clangd.path": { "type": "string", diff --git a/src/cli.ts b/src/cli.ts index a8fa94f..fb928fa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,5 @@ export type ToolchainSource = "managed" | "system"; +export type ToolchainStatus = "installed" | "available" | "planned"; export interface ToolchainItem { family: string; @@ -8,21 +9,32 @@ export interface ToolchainItem { effective: boolean; } +export interface TargetItem { + target: string; + note: string; + toolchainSpec: string | undefined; + status: ToolchainStatus; + effective: boolean; +} + export interface ToolchainInventory { installed: ToolchainItem[]; available: ToolchainItem[]; + targets: TargetItem[]; effective: ToolchainItem | undefined; + effectiveTarget: string | undefined; globalDefaultSpec: string | undefined; projectOverridesGlobal: boolean; recognized: boolean; rawOutput: string; } -type Section = "managed" | "system" | "available" | undefined; +type Section = "managed" | "system" | "available" | "targets" | undefined; const ANSI_ESCAPE = /\u001b\[[0-?]*[ -/]*[@-~]/g; const FAMILY_PATTERN = "[A-Za-z][A-Za-z0-9+_.-]*"; -const VERSION_PATTERN = "[0-9][A-Za-z0-9+_.-]*"; +const VERSION_PATTERN = "[0-9][A-Za-z0-9+_.:-]*"; +const SPEC_VERSION_PATTERN = `(?:${VERSION_PATTERN}|system)`; const AUXILIARY_WORDS = new Set([ "available", "default", @@ -58,28 +70,100 @@ export function normalizeToolchainSpec(input: string): string | undefined { } const atMatch = value.match( - new RegExp(`^(${FAMILY_PATTERN})@(${VERSION_PATTERN})$`), + new RegExp(`^(${FAMILY_PATTERN})@(${SPEC_VERSION_PATTERN})$`, "i"), ); if (atMatch !== null) { - if (atMatch[1].toLowerCase() === "msvc") { - return "msvc"; + const family = atMatch[1].toLowerCase(); + const version = atMatch[2]; + if (version.toLowerCase() === "system" && family !== "msvc") { + return undefined; } - return `${atMatch[1].toLowerCase()}@${atMatch[2]}`; + return `${family}@${version}`; } const spacedMatch = value.match( new RegExp(`^(${FAMILY_PATTERN})\\s+(${VERSION_PATTERN})$`), ); if (spacedMatch !== null) { - if (spacedMatch[1].toLowerCase() === "msvc") { - return "msvc"; - } return `${spacedMatch[1].toLowerCase()}@${spacedMatch[2]}`; } return undefined; } +export function isMsvcToolchainSpec(input: string): boolean { + const normalized = normalizeToolchainSpec(input); + return normalized === "msvc" || normalized?.startsWith("msvc@") === true; +} + +export type ToolchainInstallKind = "managed-host" | "managed-target" | "system-detect"; + +/** + * 返回 mcpp 旧兼容拼写隐含的 target 轴;undefined 表示没有隐含 target。 + * 首版插件不传 --target,因此这类输入交给 mcpp CLI 处理。 + */ +export type ToolchainSpecTargetHint = "musl" | "windows-gnu" | "target"; + +export function toolchainSpecTargetHint(input: string): ToolchainSpecTargetHint | undefined { + const normalized = normalizeToolchainSpec(input)?.toLowerCase(); + if (normalized === undefined) { + return undefined; + } + if ( + normalized.startsWith("musl-gcc@") + || (normalized.startsWith("gcc@") && normalized.endsWith("-musl")) + ) { + return "musl"; + } + const atIndex = normalized.indexOf("@"); + const compiler = atIndex === -1 ? normalized : normalized.slice(0, atIndex); + if (compiler.includes("mingw")) { + return "windows-gnu"; + } + // mcpp 还接受带 triple 的旧编译器写法,例如 + // `aarch64-linux-musl-gcc@16`。即使环境不是上面列出的 host-only 别名, + // 这种写法仍然携带 target 轴。 + if (compiler.endsWith("-gcc")) { + return compiler.includes("-musl-") ? "musl" : "target"; + } + return undefined; +} + +export function toolchainInstallKind(input: string): ToolchainInstallKind { + if (isMsvcToolchainSpec(input)) { + return "system-detect"; + } + return toolchainSpecTargetHint(input) === undefined ? "managed-host" : "managed-target"; +} + +export function hostDefaultToolchains(inventory: ToolchainInventory): ToolchainItem[] { + const hostSpecs = new Set( + inventory.targets + .filter((target) => target.status === "installed" && /\bhost\b/i.test(target.note)) + .map((target) => target.toolchainSpec) + .filter((spec): spec is string => spec !== undefined), + ); + const windowsHost = inventory.targets.some((target) => + target.status !== "planned" + && target.target.endsWith("-windows-msvc") + && /\bhost\b/i.test(target.note), + ); + const windowsGccSpecs = new Set( + inventory.targets + .filter((target) => target.status === "installed" && target.target.endsWith("-windows-gnu")) + .map((target) => target.toolchainSpec) + .filter((spec): spec is string => spec !== undefined), + ); + if (inventory.targets.length === 0) { + return inventory.installed; + } + return inventory.installed.filter((toolchain) => + toolchain.source === "system" + || hostSpecs.has(toolchain.spec) + // mcpp 在 Windows 上将省略 target 的 GCC 映射到 MinGW payload。 + || (windowsHost && toolchain.family === "gcc" && windowsGccSpecs.has(toolchain.spec))); +} + function sectionForHeader(line: string): Section | "header" | false { if (/^\s*Toolchains\s*:\s*$/i.test(line)) { return "managed"; @@ -87,7 +171,10 @@ function sectionForHeader(line: string): Section | "header" | false { if (/^\s*System(?:\s+toolchains?)?\s*:\s*$/i.test(line)) { return "system"; } - if (/^\s*Available\s+toolchains?\s*:\s*$/i.test(line)) { + if (/^\s*Targets\s*:\s*$/i.test(line)) { + return "targets"; + } + if (/^\s*Available\s+toolchains?\b.*:\s*$/i.test(line)) { return "available"; } if (/^\s*[A-Za-z][A-Za-z0-9 _-]*\s*:\s*$/i.test(line)) { @@ -99,7 +186,7 @@ function sectionForHeader(line: string): Section | "header" | false { function parseToolchainRow( line: string, source: ToolchainSource, -): { family: string; version: string; effective: boolean; spec: string } | undefined { +): Array<{ family: string; version: string; effective: boolean; spec: string }> | undefined { let value = line.trim(); let effective = false; if (value.startsWith("*")) { @@ -120,54 +207,123 @@ function parseToolchainRow( } let family: string; - let version: string; - let spec: string | undefined; + let versions: string[]; if (firstToken.includes("@")) { - spec = normalizeToolchainSpec(firstToken); + const spec = normalizeToolchainSpec(firstToken); if (spec === undefined) { return undefined; } const atIndex = firstToken.indexOf("@"); family = firstToken.slice(0, atIndex).toLowerCase(); - version = firstToken.slice(atIndex + 1); + versions = [firstToken.slice(atIndex + 1)]; } else { const secondToken = tokens[1]; if (secondToken === undefined || !new RegExp(`^${VERSION_PATTERN}$`).test(secondToken)) { return undefined; } family = firstToken.toLowerCase(); - version = secondToken; - spec = normalizeToolchainSpec(`${family} ${version}`); + versions = [secondToken]; + for (let index = 2; index + 1 < tokens.length; index += 2) { + if (tokens[index] !== "/" || !new RegExp(`^${VERSION_PATTERN}$`).test(tokens[index + 1])) { + break; + } + versions.push(tokens[index + 1]); + } + } + + return versions.flatMap((version) => { + const spec = source === "system" && family === "msvc" + ? "msvc" + : normalizeToolchainSpec(`${family} ${version}`); + return spec === undefined ? [] : [{ family, version, spec, effective }]; + }); +} + +function parseTargetRow(line: string): TargetItem | undefined { + let value = line.trim(); + let effective = false; + if (value.startsWith("*")) { + effective = true; + value = value.slice(1).trimStart(); } - if (spec === undefined) { + if (value.startsWith("-")) { + value = value.slice(1).trimStart(); + } + + const tokens = value.split(/\s+/); + if (tokens.length < 3 || tokens[0]?.toUpperCase() === "TARGET") { + return undefined; + } + const statusToken = tokens.at(-1)?.toLowerCase(); + if (statusToken !== "installed" && statusToken !== "available" && statusToken !== "planned") { return undefined; } - if (source === "system" && family === "msvc") { - spec = "msvc"; + const target = tokens[0]; + const columns = tokens.slice(1, -1); + if (target === undefined || columns.length === 0) { + return undefined; } - return { family, version, spec, effective }; + + const lastColumn = columns.at(-1); + let noteColumns: string[]; + let toolchainSpec: string | undefined; + if (lastColumn === "\u2014" || lastColumn === "-") { + noteColumns = columns.slice(0, -1); + } else if (columns.length >= 2) { + const family = columns.at(-2); + const version = columns.at(-1); + if (family === undefined || version === undefined) { + return undefined; + } + toolchainSpec = normalizeToolchainSpec(family + " " + version); + if (toolchainSpec === undefined) { + return undefined; + } + noteColumns = columns.slice(0, -2); + } else { + return undefined; + } + + return { + target, + note: noteColumns.join(" "), + toolchainSpec, + status: statusToken, + effective, + }; } -function parseGlobalDefault(line: string): string | undefined { +interface ParsedGlobalDefault { + value: string | undefined; +} + +function parseGlobalDefault(line: string): ParsedGlobalDefault | undefined { const match = line.match(/\bglobal\s+default\s+is\s+['"]([^'"]+)['"]/i); if (match !== null) { - return normalizeToolchainSpec(match[1]); + return { value: normalizeToolchainSpec(match[1]) }; + } + const none = line.match(/\bglobal\s+default\s+is\s+['"]?['"]?/i); + if (none !== null) { + return { value: undefined }; } const unquoted = line.match( - new RegExp(`\\bglobal\\s+default\\s+is\\s+(${FAMILY_PATTERN}(?:@${VERSION_PATTERN}|\\s+${VERSION_PATTERN}|))\\b`, "i"), + new RegExp(`\\bglobal\\s+default\\s+is\\s+(${FAMILY_PATTERN}(?:@${SPEC_VERSION_PATTERN}|\\s+${VERSION_PATTERN}|))\\b`, "i"), ); - return unquoted === null ? undefined : normalizeToolchainSpec(unquoted[1]); + return unquoted === null ? undefined : { value: normalizeToolchainSpec(unquoted[1]) }; } export function parseToolchainList(output: string): ToolchainInventory { const installed: ToolchainItem[] = []; const available: ToolchainItem[] = []; + const targets: TargetItem[] = []; let effective: ToolchainItem | undefined; + let effectiveTarget: string | undefined; let section: Section; let recognized = false; let projectMarker = false; let explicitGlobalDefault: string | undefined; + let explicitGlobalDefaultSeen = false; for (const rawLine of output.split(/\r?\n/)) { const line = rawLine.replace(ANSI_ESCAPE, ""); @@ -180,7 +336,8 @@ export function parseToolchainList(output: string): ToolchainInventory { } const parsedGlobalDefault = parseGlobalDefault(line); if (parsedGlobalDefault !== undefined) { - explicitGlobalDefault = parsedGlobalDefault; + explicitGlobalDefaultSeen = true; + explicitGlobalDefault = parsedGlobalDefault.value; } const header = sectionForHeader(line); @@ -197,40 +354,55 @@ export function parseToolchainList(output: string): ToolchainInventory { if (section === undefined) { continue; } - const parsed = parseToolchainRow(line, section === "system" ? "system" : "managed"); - if (parsed === undefined) { + if (section === "targets") { + const target = parseTargetRow(line); + if (target === undefined) { + continue; + } + targets.push(target); + if (target.effective && effectiveTarget === undefined) { + effectiveTarget = target.target; + } + continue; + } + const parsedRows = parseToolchainRow(line, section === "system" ? "system" : "managed"); + if (parsedRows === undefined) { continue; } - const item: ToolchainItem = { - family: parsed.family, - version: parsed.version, - spec: parsed.spec, - source: section === "system" ? "system" : "managed", - effective: parsed.effective, - }; - if (section === "available") { - available.push(item); - } else { - installed.push(item); - if (item.effective && effective === undefined) { - effective = item; + for (const parsed of parsedRows) { + const item: ToolchainItem = { + family: parsed.family, + version: parsed.version, + spec: parsed.spec, + source: section === "system" ? "system" : "managed", + effective: parsed.effective, + }; + if (section === "available") { + available.push(item); + } else { + installed.push(item); + if (item.effective && effective === undefined) { + effective = item; + } } } } const globalDefaultSpec = recognized - ? explicitGlobalDefault ?? effective?.spec + ? explicitGlobalDefaultSeen ? explicitGlobalDefault : effective?.spec : undefined; const projectOverridesGlobal = recognized && (projectMarker - || (explicitGlobalDefault !== undefined + || (explicitGlobalDefaultSeen && effective !== undefined && explicitGlobalDefault !== effective.spec)); return { installed, available, + targets, effective, + effectiveTarget, globalDefaultSpec, projectOverridesGlobal, recognized, diff --git a/src/cliController.ts b/src/cliController.ts new file mode 100644 index 0000000..71d324d --- /dev/null +++ b/src/cliController.ts @@ -0,0 +1,678 @@ +import process from "node:process"; + +import * as vscode from "vscode"; + +import { + hostDefaultToolchains, + mcppCommandArguments, + normalizeToolchainSpec, + parseToolchainList, + toolchainInstallKind, + type ToolchainInventory, + type ToolchainItem, +} from "./cli"; +import type { McppProjectDiscovery } from "./discovery"; +import { runProcess } from "./process"; +import { + McppOperationRegistry, + classifyTaskExit, + projectTaskPlan, + shouldReconcileAfterTask, + type ProjectTaskKind, + type TaskCompletion, +} from "./tasks"; +import { CLI_COMMANDS, quickMenuItems } from "./commands"; + +export interface McppCliControllerOptions { + output: vscode.OutputChannel; + currentProject: () => McppProjectDiscovery | undefined; + afterProjectTask: ( + project: McppProjectDiscovery, + kind: ProjectTaskKind, + completion: TaskCompletion, + ) => Promise; + isTrusted: () => boolean; +} + +interface ToolchainPickItem extends vscode.QuickPickItem { + spec?: string; + toolchain?: ToolchainItem; + customInput?: boolean; +} + +type OperationToken = object; + +const INSTALL_CUSTOM_LABEL = "$(edit) 输入其他兼容工具链 spec…"; +const CONFIRM_INSTALL = "安装"; +const CONFIRM_DETECT = "检测"; +const CONFIRM_DEFAULT = "设为全局默认"; +const CONFIRM_CLEAN = "清理 target"; +const SHOW_TASKS = "显示正在运行的任务"; + +function taskScope(root: string): vscode.WorkspaceFolder | vscode.TaskScope { + return vscode.workspace.getWorkspaceFolder(vscode.Uri.file(root)) + ?? vscode.TaskScope.Workspace; +} + +function workingDirectory(project: McppProjectDiscovery | undefined): string { + if (project !== undefined) { + return project.root; + } + return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd(); +} + +function commandLine(executable: string, args: string[]): string { + return [executable, ...args].join(" "); +} + +export class McppCliController { + private readonly status: vscode.StatusBarItem; + + private readonly operations = new McppOperationRegistry(); + + public constructor(private readonly options: McppCliControllerOptions) { + this.status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 40); + this.status.command = CLI_COMMANDS.showMenu; + this.status.text = "$(tools) mcpp"; + this.status.tooltip = "打开 mcpp 项目和工具链快捷菜单"; + } + + public register(): vscode.Disposable[] { + const disposables: vscode.Disposable[] = [ + this.status, + vscode.commands.registerCommand(CLI_COMMANDS.showMenu, this.guarded(() => this.showMenu())), + vscode.commands.registerCommand(CLI_COMMANDS.build, this.guarded(() => this.runProjectTask("build"))), + vscode.commands.registerCommand(CLI_COMMANDS.run, this.guarded(() => this.runProjectTask("run"))), + vscode.commands.registerCommand(CLI_COMMANDS.test, this.guarded(() => this.runProjectTask("test"))), + vscode.commands.registerCommand(CLI_COMMANDS.clean, this.guarded(() => this.runProjectTask("clean"))), + vscode.commands.registerCommand(CLI_COMMANDS.showToolchains, this.guarded(() => this.showToolchains())), + vscode.commands.registerCommand(CLI_COMMANDS.installToolchain, this.guarded(() => this.installToolchain())), + vscode.commands.registerCommand(CLI_COMMANDS.selectDefaultToolchain, this.guarded(() => this.selectDefaultToolchain())), + vscode.window.onDidChangeActiveTextEditor(() => this.refreshStatus()), + vscode.workspace.onDidChangeWorkspaceFolders(() => this.refreshStatus()), + ]; + this.refreshStatus(); + return disposables; + } + + public refreshStatus(): void { + if (this.options.currentProject() === undefined) { + this.status.hide(); + return; + } + this.status.show(); + } + + public async runProjectTask(kind: ProjectTaskKind): Promise { + const project = this.requireProject(); + if (project === undefined || !this.requireTrusted()) { + return; + } + + if (kind === "clean") { + const choice = await vscode.window.showWarningMessage( + `将删除当前工程的 target 目录:${project.root}/target`, + { modal: true, detail: "此操作不会清理全局 BMI 缓存。" }, + CONFIRM_CLEAN, + ); + if (choice !== CONFIRM_CLEAN) { + return; + } + } + + const token: OperationToken = {}; + const active = this.operations.beginProject(project.root, token); + if (active !== undefined) { + const choice = await vscode.window.showWarningMessage( + `已有 mcpp 操作正在运行,暂不启动 ${kind}。`, + SHOW_TASKS, + ); + if (choice === SHOW_TASKS) { + await vscode.commands.executeCommand("workbench.action.tasks.showTasks"); + } + return; + } + + try { + const plan = projectTaskPlan(kind); + const completion = await this.executeTask( + project.root, + this.mcppExecutable(project), + plan.title, + plan.args, + ); + this.appendTaskCompletion(project.root, plan.title, plan.args, completion); + if (shouldReconcileAfterTask(kind, completion)) { + await this.options.afterProjectTask(project, kind, completion); + } + } finally { + this.operations.finishProject(project.root, token); + } + } + + public async showToolchains(): Promise { + if (!this.requireTrusted()) { + return; + } + const project = this.options.currentProject(); + const inventory = await this.readToolchainInventory(project); + if (inventory === undefined) { + return; + } + + const items = this.inventoryItems(inventory, false, project); + if (items.length === 0) { + await vscode.window.showInformationMessage( + "mcpp 没有列出可用工具链;请查看 mcpp 输出频道中的原始结果。", + ); + return; + } + await vscode.window.showQuickPick(items, { + title: "mcpp 工具链与 target", + placeHolder: "只读查看 mcpp 当前解析结果", + matchOnDescription: true, + matchOnDetail: true, + }); + } + + public async installToolchain(): Promise { + if (!this.requireTrusted()) { + return; + } + const project = this.options.currentProject(); + const inventory = await this.readToolchainInventory(project); + const spec = await this.pickInstallSpec(inventory); + if (spec === undefined) { + return; + } + const installKind = toolchainInstallKind(spec); + const confirmLabel = installKind === "system-detect" ? CONFIRM_DETECT : CONFIRM_INSTALL; + const confirmation = installKind === "system-detect" + ? `将调用 mcpp 检测系统 MSVC(${spec})。` + : installKind === "managed-target" + ? `将把携带 target 语义的兼容 spec ${spec} 交给 mcpp 安装。` + : `将安装 mcpp 工具链 ${spec}(不指定 target,使用 host target)。`; + const detail = installKind === "system-detect" + ? "mcpp 不会下载或安装 MSVC;它会检测 Visual Studio,并在缺失时给出官方安装指引。" + : installKind === "managed-target" + ? "mcpp 会规范化兼容写法,并可能下载对应 target 的较大工具链包。" + : "安装可能下载较大的工具链包,并修改 mcpp 全局缓存。"; + + const choice = await vscode.window.showWarningMessage( + confirmation, + { modal: true, detail }, + confirmLabel, + ); + if (choice !== confirmLabel) { + return; + } + + const token: OperationToken = {}; + const active = this.operations.beginGlobal(token); + if (active !== undefined) { + const duplicateChoice = await vscode.window.showWarningMessage( + "已有 mcpp 操作正在运行。", + SHOW_TASKS, + ); + if (duplicateChoice === SHOW_TASKS) { + await vscode.commands.executeCommand("workbench.action.tasks.showTasks"); + } + return; + } + + const taskTitle = installKind === "system-detect" + ? `mcpp: 检测系统 MSVC(${spec})` + : `mcpp: 安装工具链 ${spec}`; + try { + const installArgs = mcppCommandArguments("toolchain", "install", spec); + const completion = await this.executeTask( + workingDirectory(project), + this.mcppExecutable(project), + taskTitle, + installArgs, + ); + this.appendTaskCompletion( + workingDirectory(project), + taskTitle, + installArgs, + completion, + ); + if (completion.state !== "succeeded") { + return; + } + } finally { + this.operations.finishGlobal(token); + } + + if (installKind === "managed-target") { + await vscode.window.showInformationMessage( + `mcpp 已完成 ${spec}。首版插件不修改 target 默认;如需设为默认,请使用带 --target 的 mcpp CLI。`, + ); + return; + } + + const refreshed = await this.readToolchainInventory(project); + const defaultChoice = await vscode.window.showInformationMessage( + installKind === "system-detect" + ? "MSVC 检测完成。是否从最新列表中选择全局默认?" + : `工具链 ${spec} 安装完成。是否从最新列表中选择全局默认?`, + "选择全局默认", + ); + if (defaultChoice === "选择全局默认" && refreshed !== undefined) { + await this.selectDefaultToolchainFromInventory(project, refreshed); + } + } + + public async selectDefaultToolchain(): Promise { + if (!this.requireTrusted()) { + return; + } + const project = this.options.currentProject(); + const inventory = await this.readToolchainInventory(project); + if (inventory !== undefined) { + await this.selectDefaultToolchainFromInventory(project, inventory); + } + } + + private async selectDefaultToolchainFromInventory( + project: McppProjectDiscovery | undefined, + inventory: ToolchainInventory, + ): Promise { + const installed = hostDefaultToolchains(inventory); + if (installed.length === 0) { + await vscode.window.showWarningMessage( + "没有可用于 host target 的已安装工具链,不能在此处设置全局默认。target 专用工具链请使用 mcpp CLI;Windows MSVC 请先安装 Visual Studio。", + ); + return; + } + + const items: ToolchainPickItem[] = installed.map((toolchain) => ({ + label: `${toolchain.effective ? "$(check) " : ""}${toolchain.spec}`, + description: toolchain.source === "system" ? "系统工具链(mcpp 只检测)" : "mcpp 管理的工具链", + detail: toolchain.effective ? "当前工程有效工具链" : undefined, + spec: toolchain.spec, + toolchain, + })); + const picked = await vscode.window.showQuickPick(items, { + title: "选择 mcpp 全局默认工具链", + placeHolder: this.isNestedWorkspaceProject(project) + ? "当前工程根位于 VS Code 文件夹子目录;实际构建解析以 mcpp 为准" + : inventory.projectOverridesGlobal + ? "项目当前覆盖全局默认;这里只修改 mcpp 当前全局配置" + : "选择后只修改 mcpp 全局配置,不会自动构建工程", + matchOnDescription: true, + matchOnDetail: true, + }); + if (picked?.spec === undefined) { + return; + } + + const choice = await vscode.window.showWarningMessage( + `将把全局默认对设为 ${picked.spec} + host target。当前项目的 mcpp.toml/target 配置仍可能覆盖它。`, + { modal: true, detail: "mcpp 会同时清空全局 default_target;配置文件位置由当前 mcpp 安装及 MCPP_HOME 决定。" }, + CONFIRM_DEFAULT, + ); + if (choice !== CONFIRM_DEFAULT) { + return; + } + + const token: OperationToken = {}; + const active = this.operations.beginGlobal(token); + if (active !== undefined) { + await vscode.window.showWarningMessage("已有 mcpp 操作正在运行。", SHOW_TASKS); + return; + } + + try { + const args = mcppCommandArguments("toolchain", "default", picked.spec); + const result = await runProcess( + this.mcppExecutable(project), + args, + workingDirectory(project), + ); + this.appendShortCommand("设置全局默认工具链", this.mcppExecutable(project), args, result); + if (result.exitCode !== 0) { + await vscode.window.showErrorMessage( + `设置全局默认工具链失败(退出码 ${result.exitCode})。请查看 mcpp 输出频道。`, + ); + return; + } + + } finally { + this.operations.finishGlobal(token); + } + + const buildChoice = await vscode.window.showInformationMessage( + `mcpp 全局默认已更新为 ${picked.spec} + host target。当前项目不会自动切换,下一次构建时才会解析新的默认值。`, + ...(project === undefined ? [] : ["立即构建"]), + ); + if (buildChoice === "立即构建") { + await this.runProjectTask("build"); + } + } + + private async pickInstallSpec(inventory: ToolchainInventory | undefined): Promise { + const items: ToolchainPickItem[] = []; + for (const toolchain of inventory?.available ?? []) { + items.push({ + label: toolchain.spec, + description: "mcpp 按 family 聚合的可用版本;本操作按 host target 安装", + spec: toolchain.spec, + }); + } + items.push({ + label: INSTALL_CUSTOM_LABEL, + detail: "支持 family@version、部分版本和 mcpp 兼容旧拼写", + customInput: true, + }); + + const picked = await vscode.window.showQuickPick(items, { + title: "安装 mcpp 工具链", + placeHolder: "不选择 target;target 专用安装请使用 mcpp CLI", + matchOnDescription: true, + matchOnDetail: true, + }); + if (picked === undefined) { + return undefined; + } + if (!picked.customInput) { + return picked.spec; + } + + const input = await vscode.window.showInputBox({ + title: "输入工具链 spec", + prompt: "例如 gcc@16、llvm@20.1.7、msvc、mingw@16;兼容写法由 mcpp 规范化", + placeHolder: "gcc@16", + validateInput: (value) => { + const normalized = normalizeToolchainSpec(value); + if (normalized === undefined) { + return "请输入 family@version、family version 或 mcpp 支持的兼容 spec"; + } + return undefined; + }, + }); + return input === undefined ? undefined : normalizeToolchainSpec(input); + } + + private inventoryItems( + inventory: ToolchainInventory, + includeActions: boolean, + project: McppProjectDiscovery | undefined, + ): ToolchainPickItem[] { + const items: ToolchainPickItem[] = []; + const nestedView = this.isNestedWorkspaceProject(project); + if (inventory.effective !== undefined) { + items.push({ + label: `$(check) ${nestedView ? "mcpp list 当前目录工具链" : "当前有效工具链"}:${inventory.effective.spec}`, + description: nestedView + ? "当前目录视图;实际生效值以 mcpp build 解析为准" + : inventory.projectOverridesGlobal ? "来自当前项目 mcpp.toml,覆盖全局默认" : "来自全局默认", + detail: inventory.effectiveTarget === undefined + ? "有效 target:host(mcpp 未显示显式 target)" + : `有效 target:${inventory.effectiveTarget}`, + }); + } + if (inventory.globalDefaultSpec !== undefined) { + items.push({ + label: `全局默认:${inventory.globalDefaultSpec}`, + description: nestedView + ? "当前目录视图;项目或父级配置的覆盖以实际构建为准" + : inventory.projectOverridesGlobal ? "当前项目可能没有使用此值" : "mcpp 全局配置", + }); + } else if (inventory.recognized) { + items.push({ + label: "全局默认:", + description: "mcpp 尚未设置全局默认工具链", + }); + } + for (const toolchain of inventory.installed) { + items.push({ + label: `${toolchain.effective ? "$(check) " : ""}${toolchain.spec}`, + description: toolchain.source === "system" ? "System:系统工具链,仅检测" : "已安装", + detail: toolchain.effective + ? nestedView ? "mcpp list 当前目录有效项;实际构建解析可能受父级 mcpp 工作区影响" : "当前有效项" + : undefined, + spec: toolchain.spec, + toolchain, + }); + } + for (const target of inventory.targets) { + items.push({ + label: `${target.effective ? "$(check) " : ""}target ${target.target}`, + description: `${target.status}${target.toolchainSpec === undefined ? "" : ` · 约定 ${target.toolchainSpec}`}`, + detail: target.note.length === 0 ? "target 轴只读展示;选择 target 请使用 mcpp CLI" : target.note, + }); + } + for (const toolchain of inventory.available) { + items.push({ + label: `可安装:${toolchain.spec}`, + description: "mcpp 按 family 聚合的索引版本;未承诺 host payload", + spec: toolchain.spec, + }); + } + if (includeActions) { + items.push({ + label: "$(cloud-download) 安装工具链…", + detail: "回到工具链安装流程", + customInput: true, + }); + } + return items; + } + + private async showMenu(): Promise { + const items = quickMenuItems.map((item) => ({ + label: item.label, + description: item.group === "project" + ? "当前 mcpp 工程" + : item.group === "toolchain" ? "mcpp 工具链管理" : "clangd 与编译数据库", + command: item.command, + })); + const picked = await vscode.window.showQuickPick(items, { + title: "mcpp 快捷菜单", + placeHolder: "选择项目、工具链或 IDE 操作", + matchOnDescription: true, + }); + if (picked !== undefined) { + await vscode.commands.executeCommand(picked.command); + } + } + + private guarded(operation: () => Promise): () => Promise { + return async () => { + try { + await operation(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + try { + this.options.output.appendLine(`mcpp CLI 操作失败:${message}`); + } catch { + // 输出频道可能已经在窗口重载时释放。 + } + await vscode.window.showErrorMessage(`mcpp:${message}`); + } + }; + } + + private requireProject(): McppProjectDiscovery | undefined { + const project = this.options.currentProject(); + if (project === undefined) { + void vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。请在 mcpp 工程中执行此命令。"); + } + return project; + } + + private requireTrusted(): boolean { + if (this.options.isTrusted()) { + return true; + } + void vscode.window.showWarningMessage( + "当前工作区未受信任。mcpp 命令可能执行工作区设置指定的外部程序,请先信任工作区。", + ); + return false; + } + + private mcppExecutable(project: McppProjectDiscovery | undefined): string { + const uri = project === undefined + ? vscode.workspace.workspaceFolders?.[0]?.uri + : vscode.Uri.file(project.root); + const configured = vscode.workspace.getConfiguration("mcpp", uri).get("path", "").trim(); + return configured.length === 0 ? "mcpp" : configured; + } + + private isNestedWorkspaceProject(project: McppProjectDiscovery | undefined): boolean { + if (project === undefined) { + return false; + } + const folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(project.root)); + return folder !== undefined && folder.uri.fsPath !== project.root; + } + + private async readToolchainInventory( + project: McppProjectDiscovery | undefined, + ): Promise { + const executable = this.mcppExecutable(project); + const args = mcppCommandArguments("toolchain", "list"); + const result = await runProcess(executable, args, workingDirectory(project)); + this.appendShortCommand("查看工具链", executable, args, result); + if (result.exitCode !== 0) { + await vscode.window.showErrorMessage( + `mcpp toolchain list 失败(退出码 ${result.exitCode})。请查看 mcpp 输出频道。`, + ); + return undefined; + } + + const inventory = parseToolchainList(`${result.stdout}${result.stderr.length > 0 ? `\n${result.stderr}` : ""}`); + if (!inventory.recognized) { + await vscode.window.showErrorMessage( + "无法识别当前 mcpp toolchain list 输出;原始输出已保留在 mcpp 输出频道,请检查 mcpp 版本。", + ); + return undefined; + } + return inventory; + } + + private async executeTask( + root: string, + executable: string, + title: string, + args: string[], + ): Promise { + const task = new vscode.Task( + { type: "mcpp", command: args[0] ?? "mcpp", projectRoot: root }, + taskScope(root), + title, + "mcpp", + new vscode.ProcessExecution(executable, args, { cwd: root }), + ); + task.presentationOptions = { + reveal: vscode.TaskRevealKind.Always, + panel: vscode.TaskPanelKind.Dedicated, + focus: true, + clear: true, + showReuseMessage: false, + }; + + let execution: vscode.TaskExecution | undefined; + let earlyCompletion: TaskCompletion | undefined; + let settled = false; + let processEndSubscription: vscode.Disposable | undefined; + let taskEndSubscription: vscode.Disposable | undefined; + const disposeListeners = (): void => { + processEndSubscription?.dispose(); + taskEndSubscription?.dispose(); + }; + const finish = (completion: TaskCompletion): void => { + if (settled) { + return; + } + settled = true; + disposeListeners(); + resolveCompletion?.(completion); + }; + let resolveCompletion: ((completion: TaskCompletion) => void) | undefined; + const completion = new Promise((resolve) => { + resolveCompletion = resolve; + processEndSubscription = vscode.tasks.onDidEndTaskProcess((event) => { + if (event.execution.task !== task) { + return; + } + const classified = classifyTaskExit(event.exitCode); + if (execution === undefined) { + earlyCompletion ??= classified; + return; + } + finish(classified); + }); + taskEndSubscription = vscode.tasks.onDidEndTask((event) => { + if (event.execution.task !== task) { + return; + } + const classified = classifyTaskExit(undefined); + if (execution === undefined) { + earlyCompletion ??= classified; + return; + } + finish(classified); + }); + }); + + try { + execution = await vscode.tasks.executeTask(task); + } catch (error) { + disposeListeners(); + throw error; + } + if (earlyCompletion !== undefined) { + finish(earlyCompletion); + } + return completion; + } + + private appendTaskCompletion( + root: string, + title: string, + args: string[], + completion: TaskCompletion, + ): void { + const suffix = completion.state === "succeeded" + ? `退出码 ${completion.exitCode ?? 0}` + : completion.state === "cancelled" + ? "已取消" + : `失败,退出码 ${completion.exitCode ?? "未知"}`; + try { + this.options.output.appendLine(`\n[${new Date().toISOString()}] ${title}`); + this.options.output.appendLine(`工作目录:${root}`); + this.options.output.appendLine(`任务参数:${args.join(" ")}`); + this.options.output.appendLine(`结果:${suffix}`); + } catch { + // 窗口重载时输出频道可能早于任务事件被释放。 + } + if (completion.state === "failed") { + void vscode.window.showErrorMessage(`${title}失败(退出码 ${completion.exitCode ?? "未知"})。请查看任务终端。`); + } else if (completion.state === "cancelled") { + void vscode.window.showWarningMessage(`${title}已取消。`); + } + } + + private appendShortCommand( + title: string, + executable: string, + args: string[], + result: { exitCode: number; stdout: string; stderr: string }, + ): void { + try { + this.options.output.appendLine(`\n[${new Date().toISOString()}] ${title}`); + this.options.output.appendLine(`$ ${commandLine(executable, args)}`); + if (result.stdout.length > 0) { + this.options.output.appendLine(result.stdout.trimEnd()); + } + if (result.stderr.length > 0) { + this.options.output.appendLine(result.stderr.trimEnd()); + } + this.options.output.appendLine(`[exit ${result.exitCode}]`); + } catch { + // 窗口重载时输出频道可能早于短命令结束被释放。 + } + } +} diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..9a46aaf --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,30 @@ +export const CLI_COMMANDS = { + showMenu: "mcpp.showMenu", + build: "mcpp.build", + run: "mcpp.run", + test: "mcpp.test", + clean: "mcpp.clean", + showToolchains: "mcpp.showToolchains", + installToolchain: "mcpp.installToolchain", + selectDefaultToolchain: "mcpp.selectDefaultToolchain", +} as const; + +export interface QuickMenuItem { + label: string; + command: string; + group: "project" | "toolchain" | "ide"; +} + +export const quickMenuItems: readonly QuickMenuItem[] = [ + { label: "$(gear) 构建", command: CLI_COMMANDS.build, group: "project" }, + { label: "$(play) 运行", command: CLI_COMMANDS.run, group: "project" }, + { label: "$(beaker) 测试", command: CLI_COMMANDS.test, group: "project" }, + { label: "$(trash) 清理 target", command: CLI_COMMANDS.clean, group: "project" }, + { label: "$(list-unordered) 查看工具链", command: CLI_COMMANDS.showToolchains, group: "toolchain" }, + { label: "$(cloud-download) 安装工具链", command: CLI_COMMANDS.installToolchain, group: "toolchain" }, + { label: "$(settings-gear) 选择全局默认工具链", command: CLI_COMMANDS.selectDefaultToolchain, group: "toolchain" }, + { label: "$(symbol-interface) 配置 clangd", command: "mcpp.configureClangd", group: "ide" }, + { label: "$(database) 刷新编译数据库", command: "mcpp.refreshCompilationDatabase", group: "ide" }, + { label: "$(check) 检查模块支持", command: "mcpp.checkModuleSupport", group: "ide" }, +]; + diff --git a/src/extension.ts b/src/extension.ts index b2610bb..99de45f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,12 +18,8 @@ import { findNearestMcppProject, type McppProjectDiscovery, } from "./discovery"; -import { - runClangdCheck, - runMcppBuild, - runToolVersion, - type ToolVersionResult, -} from "./process"; +import { McppCliController } from "./cliController"; +import { runClangdCheck, runToolVersion, type ToolVersionResult } from "./process"; import { configurationReadyAfterRestart, configurationAffectsModuleSupport, @@ -695,9 +691,57 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi requestCurrentProjectReconciliation(true); }); + const afterProjectTask = async ( + project: McppProjectDiscovery, + kind: "build" | "run" | "test" | "clean", + completion: { state: "succeeded" | "failed" | "cancelled"; exitCode?: number }, + ): Promise => { + if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { + invalidateModuleStatus(project.root); + refreshStatus(); + return; + } + + const reconciled = await reconcileProject(project, true); + if (kind === "build") { + if (reconciled.context?.analysis.capability === "syntax-only") { + const buildMessage = completion.state === "succeeded" ? "mcpp 构建完成" : "mcpp 构建失败"; + await vscode.window.showWarningMessage( + `${buildMessage};${reconciled.context.analysis.reason}。模块语法高亮仍然可用。`, + ); + return; + } + const outcome = describeRefreshOutcome( + completion.exitCode ?? 1, + reconciled.databaseFound, + reconciled.configured, + ); + if (outcome.level === "information") { + await vscode.window.showInformationMessage(outcome.message); + } else if (outcome.level === "warning") { + await vscode.window.showWarningMessage(outcome.message); + } else { + await vscode.window.showErrorMessage(outcome.message); + } + return; + } + + if (completion.state === "succeeded") { + const label = kind === "run" ? "运行" : kind === "test" ? "测试" : "清理"; + await vscode.window.showInformationMessage(`mcpp ${label}完成;clangd/CDB 状态已重新检查。`); + } + }; + const cliController = new McppCliController({ + output, + currentProject: findCurrentProject, + afterProjectTask, + isTrusted: () => vscode.workspace.isTrusted, + }); + extensionContext.subscriptions.push( output, status, + ...cliController.register(), vscode.commands.registerCommand(COMMAND_CONFIGURE, runGuarded(async () => { const project = findCurrentProject(); if (project === undefined) { @@ -717,57 +761,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi }); })), vscode.commands.registerCommand(COMMAND_REFRESH, runGuarded(async () => { - const context = loadProjectContext(); - if (context === undefined) { - await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); - return; - } - if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { - await vscode.window.showWarningMessage( - "当前工作区未受信任,mcpp 刷新可能执行 build.mcpp 和依赖安装。请先信任工作区后重试。", - ); - return; - } - - output.show(true); - const configuredMcppPath = projectConfiguration(context.project).get("path", "").trim(); - const mcppExecutable = configuredMcppPath.length > 0 ? configuredMcppPath : "mcpp"; - const result = await runMcppBuild(context.project.root, mcppExecutable); - appendProcessOutput(output, "刷新编译数据库", mcppExecutable, ["build"], result); - - if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, context.project.root)) { - invalidateModuleStatus(context.project.root); - const message = result.exitCode === 0 - ? "mcpp 构建完成;切回该工程后将自动配置 clangd。" - : "mcpp 构建失败;切回该工程后仍会自动检查现有编译数据库。"; - if (result.exitCode === 0) { - await vscode.window.showInformationMessage(message); - } else { - await vscode.window.showWarningMessage(message); - } - return; - } - const reconciled = await reconcileProject(context.project, true); - if (reconciled.context?.analysis.capability === "syntax-only") { - const buildMessage = result.exitCode === 0 ? "mcpp 构建完成" : "mcpp 构建失败"; - await vscode.window.showWarningMessage( - `${buildMessage};${reconciled.context.analysis.reason}。模块语法高亮仍然可用。`, - ); - return; - } - - const outcome = describeRefreshOutcome( - result.exitCode, - reconciled.databaseFound, - reconciled.configured, - ); - if (outcome.level === "information") { - await vscode.window.showInformationMessage(outcome.message); - } else if (outcome.level === "warning") { - await vscode.window.showWarningMessage(outcome.message); - } else { - await vscode.window.showErrorMessage(outcome.message); - } + await cliController.runProjectTask("build"); })), vscode.commands.registerCommand(COMMAND_CHECK, runGuarded(async () => { const project = findCurrentProject(); @@ -800,9 +794,18 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi ); extensionContext.subscriptions.push( - manifestWatcher.onDidCreate(refreshStatus), - manifestWatcher.onDidChange(refreshStatus), - manifestWatcher.onDidDelete(refreshStatus), + manifestWatcher.onDidCreate(() => { + refreshStatus(); + cliController.refreshStatus(); + }), + manifestWatcher.onDidChange(() => { + refreshStatus(); + cliController.refreshStatus(); + }), + manifestWatcher.onDidDelete(() => { + refreshStatus(); + cliController.refreshStatus(); + }), ...registerCompilationDatabaseReconciliation( compilationDatabaseWatcher, requestAutomaticReconciliation, diff --git a/src/tasks.ts b/src/tasks.ts new file mode 100644 index 0000000..c667e80 --- /dev/null +++ b/src/tasks.ts @@ -0,0 +1,84 @@ +export type ProjectTaskKind = "build" | "run" | "test" | "clean"; +export type TaskState = "succeeded" | "failed" | "cancelled"; + +export interface ProjectTaskPlan { + kind: ProjectTaskKind; + title: string; + args: string[]; +} + +export interface TaskCompletion { + state: TaskState; + exitCode?: number; +} + +const TASK_TITLES: Record = { + build: "mcpp: 构建", + run: "mcpp: 运行", + test: "mcpp: 测试", + clean: "mcpp: 清理", +}; + +export function projectTaskPlan(kind: ProjectTaskKind): ProjectTaskPlan { + return { + kind, + title: TASK_TITLES[kind], + args: [kind], + }; +} + +export function shouldReconcileAfterTask( + _kind: ProjectTaskKind, + completion?: TaskCompletion, +): boolean { + return completion?.state !== "cancelled"; +} + +export function classifyTaskExit(exitCode: number | undefined): TaskCompletion { + if (exitCode === undefined) { + return { state: "cancelled" }; + } + if (exitCode === 0) { + return { state: "succeeded", exitCode }; + } + return { state: "failed", exitCode }; +} + +export class McppOperationRegistry { + private readonly projectTokens = new Map(); + + private globalToken: T | undefined; + + beginProject(projectRoot: string, token: T): T | undefined { + const active = this.projectTokens.get(projectRoot) ?? this.globalToken; + if (active !== undefined) { + return active; + } + this.projectTokens.set(projectRoot, token); + return undefined; + } + + finishProject(projectRoot: string, token: T): void { + if (this.projectTokens.get(projectRoot) === token) { + this.projectTokens.delete(projectRoot); + } + } + + beginGlobal(token: T): T | undefined { + const activeProject = this.projectTokens.values().next().value as T | undefined; + if (this.globalToken !== undefined) { + return this.globalToken; + } + if (activeProject !== undefined) { + return activeProject; + } + this.globalToken = token; + return undefined; + } + + finishGlobal(token: T): void { + if (this.globalToken === token) { + this.globalToken = undefined; + } + } +} diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 7e2f8f0..5db16a4 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import test from "node:test"; interface PackageManifest { + version?: string; extensionDependencies?: string[]; activationEvents?: string[]; capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string } }; @@ -19,6 +20,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + assert.equal(manifest.version, "0.2.0"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); @@ -28,7 +30,19 @@ test("declares the official clangd dependency and mcpp commands", () => { ); assert.deepEqual( manifest.contributes?.commands?.map((command) => command.command), - ["mcpp.configureClangd", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport"], + [ + "mcpp.showMenu", + "mcpp.build", + "mcpp.run", + "mcpp.test", + "mcpp.clean", + "mcpp.showToolchains", + "mcpp.installToolchain", + "mcpp.selectDefaultToolchain", + "mcpp.configureClangd", + "mcpp.refreshCompilationDatabase", + "mcpp.checkModuleSupport", + ], ); assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.path"]); assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.modulesSupport"]); @@ -64,3 +78,44 @@ test("ships an injection grammar with module-specific scopes", () => { assert.match("import xxx", importPattern); assert.match("export import foo.bar;", importPattern); }); + +test("设置全局默认后先释放工具链锁再提供立即构建", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const start = source.indexOf("private async selectDefaultToolchainFromInventory"); + const end = source.indexOf("private async pickInstallSpec", start); + assert.notEqual(start, -1); + assert.notEqual(end, -1); + + const method = source.slice(start, end); + const unlock = method.indexOf("this.operations.finishGlobal(token)"); + const immediateBuild = method.indexOf("const buildChoice"); + assert.ok(unlock >= 0 && unlock < immediateBuild); +}); + +test("安装流程把系统工具链和 target 兼容 spec 交给 mcpp 解析", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const start = source.indexOf("public async installToolchain"); + const end = source.indexOf("public async selectDefaultToolchain", start); + assert.notEqual(start, -1); + assert.notEqual(end, -1); + + const method = source.slice(start, end); + assert.doesNotMatch( + method, + /if \(isMsvcToolchainSpec\(spec\) \|\| toolchainSpecTargetHint\(spec\) !== undefined\)/, + ); + assert.match(method, /toolchainInstallKind\(spec\)/); +}); + +test("README 区分 mcpp 的 MSVC 构建能力和 clangd 的 IFC 限制", () => { + const readme = readFileSync(path.join(root, "README.md"), "utf8"); + assert.match(readme, /0\.0\.90 起 native `cl\.exe` 后端支持/); + assert.match(readme, /mcpp 可以正常构建它们,但 clangd\s+不能直接消费/); + assert.doesNotMatch(readme, /native MSVC\s+构建仍提示/); +}); + +test("嵌套工程提示不猜测它一定是 mcpp 工作区成员", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + assert.doesNotMatch(source, /isWorkspaceMember/); + assert.doesNotMatch(source, /当前是工作区成员/); +}); diff --git a/test/cli.test.ts b/test/cli.test.ts index be48a14..f79907d 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -2,9 +2,13 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + isMsvcToolchainSpec, + hostDefaultToolchains, mcppCommandArguments, normalizeToolchainSpec, parseToolchainList, + toolchainInstallKind, + toolchainSpecTargetHint, } from "../src/cli"; const plainOutput = [ @@ -64,6 +68,59 @@ test("支持系统 MSVC 和合法的空安装状态", () => { assert.deepEqual(empty.installed, []); }); +test("解析 mcpp 实际的可安装标题、斜线版本和 msvc@system", () => { + const inventory = parseToolchainList([ + "Toolchains:", + " * msvc 17.10 (default)", + " (* = effective toolchain from project mcpp.toml [toolchain]; global default is 'msvc@system')", + "", + "Available toolchains (run `mcpp toolchain install `):", + " gcc 15.1.0 / 13.3.0 / 11.5.0 / 9.4.0", + ].join("\n")); + assert.equal(inventory.globalDefaultSpec, "msvc@system"); + assert.deepEqual( + inventory.available.map((item) => item.spec), + ["gcc@15.1.0", "gcc@13.3.0", "gcc@11.5.0", "gcc@9.4.0"], + ); +}); + +test("解析独立的 target 轴和默认 target", () => { + const inventory = parseToolchainList([ + "Toolchains:", + " * gcc 16.1.0 (default)", + "", + "Targets:", + " TARGET NOTE TOOLCHAIN STATUS", + " * x86_64-linux-musl static gcc 16.1.0 installed", + " aarch64-linux-musl static, cross gcc 16.1.0 available", + " riscv64-linux-musl static, cross — planned", + ].join("\n")); + assert.deepEqual( + inventory.targets.map((target) => [ + target.target, + target.toolchainSpec, + target.status, + target.effective, + ]), + [ + ["x86_64-linux-musl", "gcc@16.1.0", "installed", true], + ["aarch64-linux-musl", "gcc@16.1.0", "available", false], + ["riscv64-linux-musl", undefined, "planned", false], + ], + ); + assert.equal(inventory.effectiveTarget, "x86_64-linux-musl"); +}); + +test("全局默认明确为 none 时不回退成有效工具链", () => { + const inventory = parseToolchainList([ + "Toolchains:", + " * llvm 22.1.8 (default)", + " (* = effective toolchain from project mcpp.toml [toolchain]; global default is '')", + ].join("\n")); + assert.equal(inventory.globalDefaultSpec, undefined); + assert.equal(inventory.projectOverridesGlobal, true); +}); + test("未知输出不伪造工具链", () => { const inventory = parseToolchainList("unexpected output\n"); assert.equal(inventory.recognized, false); @@ -81,7 +138,11 @@ test("未知输出不伪造工具链", () => { test("规范化输入并构造参数数组", () => { assert.equal(normalizeToolchainSpec(" llvm@20 "), "llvm@20"); assert.equal(normalizeToolchainSpec("gcc 16.1.0"), "gcc@16.1.0"); - assert.equal(normalizeToolchainSpec("MSVC 17.10"), "msvc"); + assert.equal(normalizeToolchainSpec("MSVC 17.10"), "msvc@17.10"); + assert.equal(normalizeToolchainSpec("gcc@16.1.0-musl"), "gcc@16.1.0-musl"); + assert.equal(normalizeToolchainSpec("musl-gcc 16"), "musl-gcc@16"); + assert.equal(normalizeToolchainSpec("mingw@16.1.0"), "mingw@16.1.0"); + assert.equal(normalizeToolchainSpec("clang@19"), "clang@19"); assert.equal(normalizeToolchainSpec("gcc; rm -rf /"), undefined); assert.deepEqual( mcppCommandArguments("toolchain", "default", "llvm@22.1.8"), @@ -89,3 +150,70 @@ test("规范化输入并构造参数数组", () => { ); assert.deepEqual(mcppCommandArguments("build"), ["build"]); }); + +test("识别系统项和隐含 target 别名", () => { + assert.equal(isMsvcToolchainSpec("msvc"), true); + assert.equal(isMsvcToolchainSpec("msvc@system"), true); + assert.equal(isMsvcToolchainSpec("llvm@20"), false); + assert.equal(toolchainSpecTargetHint("gcc@16-musl"), "musl"); + assert.equal(toolchainSpecTargetHint("musl-gcc@16"), "musl"); + assert.equal(toolchainSpecTargetHint("mingw@16"), "windows-gnu"); + assert.equal(toolchainSpecTargetHint("clang@19"), undefined); +}); + +test("安装流程区分 host、target 和系统检测但都交给 mcpp", () => { + assert.equal(toolchainInstallKind("llvm@20"), "managed-host"); + assert.equal(toolchainInstallKind("clang@19"), "managed-host"); + assert.equal(toolchainInstallKind("mingw@16"), "managed-target"); + assert.equal(toolchainInstallKind("aarch64-linux-musl-gcc@16"), "managed-target"); + assert.equal(toolchainInstallKind("msvc"), "system-detect"); + assert.equal(toolchainInstallKind("msvc@19.44"), "system-detect"); +}); + +test("覆盖 mcpp 兼容层的扩展 target 别名", () => { + assert.equal(toolchainSpecTargetHint("mingw-gcc@16"), "windows-gnu"); + assert.equal(toolchainSpecTargetHint("mingw-cross-gcc@16"), "windows-gnu"); + assert.equal(toolchainSpecTargetHint("x86_64-w64-mingw32-gcc@16"), "windows-gnu"); + assert.equal(toolchainSpecTargetHint("aarch64-linux-musl-gcc@16"), "musl"); + assert.equal(toolchainSpecTargetHint("aarch64-linux-gnu-gcc@16"), "target"); +}); + +test("全局默认选择只暴露 host target 的已安装 payload", () => { + const targetOnly = parseToolchainList([ + "Toolchains:", + " * gcc 16.1.0 (default)", + "", + "Targets:", + " TARGET NOTE TOOLCHAIN STATUS", + " * x86_64-linux-musl static gcc 16.1.0 installed", + ].join("\n")); + assert.deepEqual(hostDefaultToolchains(targetOnly), []); + + const hostAndTarget = parseToolchainList([ + "Toolchains:", + " gcc 16.1.0", + "", + "Targets:", + " TARGET NOTE TOOLCHAIN STATUS", + " x86_64-linux-gnu host gcc 16.1.0 installed", + " x86_64-linux-musl static gcc 16.1.0 installed", + ].join("\n")); + assert.deepEqual(hostDefaultToolchains(hostAndTarget).map((item) => item.spec), ["gcc@16.1.0"]); +}); + +test("Windows 上 GCC 的 host 默认使用已安装的 MinGW payload", () => { + const windowsInventory = parseToolchainList([ + "Toolchains:", + " gcc 16.1.0", + "", + "Targets:", + " TARGET NOTE TOOLCHAIN STATUS", + " x86_64-windows-gnu PE, static gcc 16.1.0 installed", + " x86_64-windows-msvc host, PE — available", + ].join("\n")); + + assert.deepEqual( + hostDefaultToolchains(windowsInventory).map((item) => item.spec), + ["gcc@16.1.0"], + ); +}); diff --git a/test/commands.test.ts b/test/commands.test.ts new file mode 100644 index 0000000..c930d90 --- /dev/null +++ b/test/commands.test.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { CLI_COMMANDS, quickMenuItems } from "../src/commands"; + +test("CLI 命令覆盖项目、工具链和 IDE", () => { + assert.deepEqual(Object.values(CLI_COMMANDS), [ + "mcpp.showMenu", + "mcpp.build", + "mcpp.run", + "mcpp.test", + "mcpp.clean", + "mcpp.showToolchains", + "mcpp.installToolchain", + "mcpp.selectDefaultToolchain", + ]); + assert.deepEqual( + quickMenuItems.map((item) => item.command), + [ + "mcpp.build", + "mcpp.run", + "mcpp.test", + "mcpp.clean", + "mcpp.showToolchains", + "mcpp.installToolchain", + "mcpp.selectDefaultToolchain", + "mcpp.configureClangd", + "mcpp.refreshCompilationDatabase", + "mcpp.checkModuleSupport", + ], + ); + assert.ok(quickMenuItems.every((item) => item.label.length > 0)); +}); + diff --git a/test/tasks.test.ts b/test/tasks.test.ts new file mode 100644 index 0000000..fc5b8e5 --- /dev/null +++ b/test/tasks.test.ts @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + McppOperationRegistry, + classifyTaskExit, + projectTaskPlan, + shouldReconcileAfterTask, +} from "../src/tasks"; + +test("基础项目任务使用固定的 mcpp 参数数组", () => { + assert.deepEqual(projectTaskPlan("build"), { + kind: "build", + title: "mcpp: 构建", + args: ["build"], + }); + assert.deepEqual(projectTaskPlan("run"), { + kind: "run", + title: "mcpp: 运行", + args: ["run"], + }); + assert.deepEqual(projectTaskPlan("test"), { + kind: "test", + title: "mcpp: 测试", + args: ["test"], + }); + assert.deepEqual(projectTaskPlan("clean"), { + kind: "clean", + title: "mcpp: 清理", + args: ["clean"], + }); +}); + +test("构建类项目任务结束后都需要重新协调 IDE", () => { + for (const kind of ["build", "run", "test", "clean"] as const) { + assert.equal(shouldReconcileAfterTask(kind), true); + } +}); + +test("取消的项目任务不进入成功后的 IDE 重协调", () => { + assert.equal(shouldReconcileAfterTask("build", { state: "succeeded", exitCode: 0 }), true); + assert.equal(shouldReconcileAfterTask("build", { state: "failed", exitCode: 2 }), true); + assert.equal(shouldReconcileAfterTask("build", { state: "cancelled" }), false); +}); + +test("退出码区分成功、失败和取消", () => { + assert.deepEqual(classifyTaskExit(0), { state: "succeeded", exitCode: 0 }); + assert.deepEqual(classifyTaskExit(2), { state: "failed", exitCode: 2 }); + assert.deepEqual(classifyTaskExit(undefined), { state: "cancelled" }); +}); + +test("项目任务锁和全局工具链锁互相阻塞且只接受原 token 解锁", () => { + const registry = new McppOperationRegistry(); + const first = {}; + const second = {}; + + assert.equal(registry.beginProject("/work/app", first), undefined); + assert.equal(registry.beginProject("/work/app", second), first); + assert.equal(registry.beginProject("/work/lib", second), undefined); + assert.notEqual(registry.beginGlobal(first), undefined); + registry.finishGlobal(first); + registry.finishProject("/work/lib", second); + registry.finishProject("/work/app", second); + registry.finishProject("/work/app", first); + assert.equal(registry.beginGlobal(second), undefined); + assert.equal(registry.beginProject("/work/lib", first), second); + + registry.finishProject("/work/lib", first); + registry.finishGlobal(first); + registry.finishGlobal(second); + assert.equal(registry.beginProject("/work/app", second), undefined); + registry.finishProject("/work/app", second); + assert.equal(registry.beginGlobal(second), undefined); +}); From 6de32de62f52dc1b6e61d8cf9426d985613e50c3 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 1 Aug 2026 03:34:30 +0800 Subject: [PATCH 006/119] feat: finalize toolchain input and branding --- CHANGELOG.md | 3 ++- README.md | 19 ++++++++++++------ images/logo.png | Bin 0 -> 6719 bytes package.json | 7 ++++--- src/cli.ts | 43 ++++++++++++++++++++++++++--------------- src/cliController.ts | 24 ++++++++++++++--------- test/artifacts.test.ts | 24 +++++++++++++++++++++++ test/cli.test.ts | 16 +++++++++++++++ 8 files changed, 101 insertions(+), 35 deletions(-) create mode 100644 images/logo.png diff --git a/CHANGELOG.md b/CHANGELOG.md index f822828..316bf6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ toolchain 与 target 分轴处理,首版 UI 不提供结构化 `--target` 选择器。 - 全局默认选择过滤普通 target-only payload,保留 mcpp 在 Windows 上将 host GCC 映射到 MinGW payload 的官方规则,并明确设置 host 默认会清空 `default_target`; - 安装入口把 mcpp 兼容 spec 原样传给 CLI,MSVC 走系统检测,隐含 target 的别名走对应 payload。 + 安装入口接受省略版本和 namespace,把兼容 spec 安全规范化为单个参数后交给 mcpp 最终解析; + MSVC 走系统检测,已知 target 别名走对应 payload,泛化 triple 由 mcpp 校验。 - 工具链安装和全局默认修改均需要用户确认;Restricted Mode 不执行 mcpp 或其他外部工具。 ## 0.1.5 diff --git a/README.md b/README.md index 02b6111..271c3d9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ mcpp 标志 +

+ # mcpp VS Code 扩展 本扩展把 mcpp 工程接入官方 @@ -79,10 +83,12 @@ mcpp 会把 `compile_commands.json` 写到工程根目录。扩展只读取它 - **mcpp: 查看工具链**:执行 `mcpp toolchain list`,保留原始输出并在 Quick Pick 中 展示当前有效工具链、全局默认、系统工具链、target 状态和可安装版本。 - **mcpp: 安装工具链**:从 mcpp 列出的可用项或兼容 spec 中选择,执行 - `mcpp toolchain install `。规范的 `gcc@V` / `llvm@V` 按 host target 安装; - `gcc@V-musl`、`mingw@V`、`-gcc@V` 等显式兼容写法原样交给 mcpp 规范化并安装 - 对应 target。输入 `msvc` 时,同一命令只检测系统 Visual Studio 或显示官方安装指引, - 不会由 mcpp 下载、升级或删除 MSVC。本版本暂不提供结构化 `--target` 选择器。 + `mcpp toolchain install `。`gcc`、`llvm` 等省略版本的 family 会由 mcpp 选择最高 + 可用版本;`xim:gcc@16` 等 namespace 输入也可直接使用。插件只把输入安全规范化为单个 + 参数,family、别名和 triple 的合法性最终由 mcpp 解析。`gcc@V-musl`、`mingw@V` 等 + 兼容写法会携带 target;泛化的 `-gcc@V` 可能携带 target,仍由 mcpp 校验 triple。 + 输入 `msvc` 时,同一命令只检测系统 Visual Studio 或显示官方安装指引,不会由 mcpp + 下载、升级或删除 MSVC。本版本暂不提供结构化 `--target` 选择器。 - **mcpp: 选择全局默认工具链**:从当前主机可作为默认值的已安装项或检测到的系统项中 选择,执行 `mcpp toolchain default `。普通 target-only payload 不会混入列表; Windows 上省略 target 的 GCC 按 mcpp 官方规则映射到已安装的 MinGW payload。按 mcpp 的官方 @@ -143,8 +149,9 @@ clangd 21 及更高版本会请求 `--experimental-modules-support`。`on` 强 `import std`、命名模块、`.ifc`、增量构建和运行。 - `musl-gcc`、`gcc@-musl`、`mingw@`、`mingw-gcc@`、 `mingw-cross-gcc@`、`-gcc@` 和 `clang@` 等旧拼写仍由 - mcpp 兼容;其中除 `clang@` 外的这些变体会隐含 target。插件通过参数数组把 - 用户选择原样交给 mcpp,不自行重写其规范化结果。 + mcpp 兼容;其中除 `clang@` 外的已知变体会隐含 target,泛化 triple 写法的 + 合法性由 mcpp 判断。插件通过参数数组把用户输入安全规范化为单个参数后交给 mcpp, + 最终由 mcpp 解析,不自行复制其 family、alias 或 triple 规则。 当前 0.2.0 在消费编译数据库并配置 clangd 的基础上增加了 mcpp CLI 菜单;首版 UI 只读展示 target,不提供结构化 target 选择。用户仍可输入 mcpp 兼容 spec 安装 target diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4f6692c5c8420884b5ea8433f2f699c5a6f18dee GIT binary patch literal 6719 zcmds6^-~lMut&N};sEJ7LZlIpP`cxYqg%SW>j;k$ln{Y0b>sm@2uH)g0fHc%(jXnu zA@F>E#(O{P?#yRqXLe_Hc0arOL04Oagpi&P0|SFZO;u6fk*`y@DV_?wZ zswuuT2+BLmCkQf}X}w;R$(Q8*F8M_@iaZ)32#u*A=C##ROu}C+S0YRUG2YG8xg1Qp znq|sfTh>9ZnoV7anthabDw?1`Uf~jX3|4GmLHh!oGLfUk>#m=y= zo>}aqmZ-``R*av)@&PjuQ?;0iKxxq>uGyuilc_)PV0b&6x42@fRp;3#E>wbK{_Ebc zD&`_soW#=N#m?m&>CN^aBESl>O)zL(co11@?$N&G^8Da4>hVr4%nAz8goF6zX-}B& z+``CnfI7n1?QC2Gk~Nc>XQ*)_=eLZKdpI1qe?C5a zpOZy}#D6stL3@SSIhSrXwa&O;-WvpsU?YH4fq(41maMlF458gqNJZ zn{sRuy5#^5o{_)A!9hG6CN^eti5Zfk&M~&H=cqM6I}gJdVz|I$Y3_l{m2aLeyY38o z+xhIBb0jG%x3EOk%DjJz-fg@a(l+8fJrz6cefYR9ST5;@{2}D5Va#n2PKGx1YEfGH z)S5hjG}GJ1m|f)ltw<}82sg+6{wJPJ%br_h$H?$~YRsryJb%l??~g@!*{kcFo7Zmp z2#A>TXnroD`7YKNY@9r<`&xvcpun{W?X94z@!PRy0FPzap-IUQIF=Q1KzI#L07|_b z$oDwh^kkC9dTnWAQS06A`*TL{mtBQYb=FhhE#2wgm+`rvIB!_!)5(b&yW^TW7n9qt zuG!&< ze#YfvXNO*nq4Ryu=?@*H2k}K(OQQWf4#`&n5h(%mK_c-sARlw%zh3gW13GYHpbWYa)KO;jYOAue*6hHREy-K%J$IVbz1DVwSnfJk0%0G#og=Y|$zOM&BZKUazuS?Kv|%&u z|2;WYggFjtbr%x>hiDXI+m#e%>gqq{G#re00ly5$Pk zq>N53n0sdI{QZ$3qIUq&pFg?Znv1EUuYa@ZRqW8WsgKlfRk-p#cVbkiwDckvafqYB zO7LzZ@{_Ib92A^5y~}419+SONuF)Yl^4B_%p?duoS5|&=xA~IczbtIaDG*hkazR5f8zl$u8 z;fODyIabTN*BP{T zot3|Nq=YKq25*RC3fSz)Mjww-v+FKfE0;M3@QNln%-g%JWG2Q*U)Pdmd7!7CcReD@ z&NTIL%L*=Vvk{tq3N``L?EsB(Iv3SJgWZBZ?EM+N4epHX#;zV1bX|9dva@7Rdv;!c z7Xcr;8VqCOKqk62uUhBy&df3d)gA;k;FAVVsEbGo;~Y&emVla-=3?+5Ly zImG>D1+)_(pGUGS09p2KjZfuakzgCJW)5XXdsU%hU}o%E+gyGcM9~>96FFfBW?w6e zB!maQ!K=pezY$*7YECNi$%7W%JHcA*l!-y`J%9um47}47m=d5WonGd{q%~^FXmn}Q zfDm5Z_vk9mExP*aj$s*7KH%%PG$6Vpl*1FK?Yc~jOD$h2^ofOIv5bHIf@-M#>1oY}WP(-IVnvD+ zDYWhKny%JAkO*X?dy!8rO_HX!ECp}#B_NoLa|6^gy|5lK=mux?U zQ8oHYIz-H;%!RJLnlnhhZ*0}AbS(U7>hyO^7D<{vW00Jbh&t^dV|v!q=mAsjmdymS zND4wt97)p~>*BbY8O4GcI9ap&R?#ao0>3Y-mYAlAq80CLGS7k293>a^4!qTkU>Gai zDufhX(t2WgN3kbsU@}WRxbI%HOxX1OZr;)?%wJJGuk;!X?DUUk5K(~-)*1&gJ)J4$ zNe|XVRCYm_b1Cy3+fBqtepk6uU{f%GiSk+Jebr(15(-o&9&^2}d@g5*#aOLw!~9X!*hsE-iPB*LjSvGU9~Hq-kTH12R-u zBh9dalkwZkl_j<#bX^JQghjG$N`Bf0m(-QddvBCd>KM_upI6`J@(Z zRp6+o-Ii1r0ZQG!=Lp~UV|R5l2r5*OE2Wo4nt9v7VQT1^I>#=vn{^ve{7Y6>P$QQV zy*4aI?n}w5bir)=l?4tnW}ySO?#nJb^yB@Wvc=s#d?I=|HK_|>Fu!9MIX&=Jlqe;rR?(vh>tppz4^bd?GsS@3rP)gMRr z?PdC42rJ^iXrLgEWS&IbQyyPnw*qC*QxVxx_oMp>El*NkhD`Lxi#`WxVePu{1$?T} z;PafV*9lBnMW6T;HU~{9V%;z6_>@FvUcos*>1+Xh*fu5VANRn}1M5(_AJa^3JAOpd zLyA?kN{QAlX=&tlbz5R5w8_xmx`uhbDl6+Tw!iB}IM;7cRD~S+48SHPb)}OdelA#SjKR;7sp$6x$ zlp3?vxy+B&CY5u)hW?V-j4t@Ergh4XgZZO^UPKQNaSC z<(MP6w)vJazha$+bnZ!mFtDU#fRI7RP#^$E_tB5=Afn0PzFw--w+-3RMbez*0*oxh zRo~j=@WY;QFZDqY&QX_OAGT~;IflVF{X}<>Nvuk!ccIG*zof(6w$sa?vs&H9w_<)S zX`~0A0sBy+6;I~EygZek^sWX{F64vVP6RyUvB2JNSL>w`-mZKZ_;Q_oVJXvlpec>b+Bv2p691 z<~}Ed)l4v+9<)KqX^fN@5A&6u_;vFyXmsQ73emAWJub|4&t5j`ZxL7R-KvJ9d;f$m}V0%h4D{Rk_k z>b+Yrhn2>w7)hD#D|hXTcNQ@nSZPl~gGIx|1#9c?jD6FraM50SBXfesM0-}KZz}hS z#o3BN^Q2CC{bD+CzC<>XrjY(4hjF)gg~|%b_g$10nMA@nMfX2@><(=HnC(wA8yy)* zNb!Fpu%`z;QaQksCg3|)#_-mVzL@`1E?Uk~9AutCs`ud*qvO#n<3>{;Ur$eFwKcoV zIKbb!{|`XH4DP4v+fjf>6$rl=&0=UuOw@;+89G zIF@xJjb?J>vLzI<*cFBYYtht?cU+H@5*gu7q=Lg*&`7{HWfTlUU`CZ{pR%up`7(D&;dsxVC$ugFtjvp`zx#{TVV$u%fL4 zCJO1V<5>PqG0p1~G)ibBxv=&FYEsa4Is9A3lLxWVj8}!S<9}NfZgCEg(JFI&`!Z6U zu29~Y6qm*=l19d*h=fH-rSc!4v%=UKOy@cNoz63+pKO2JC7VU6N*mauoT7Y9&&(LB zVgz7XeLdtfYfDqS4pjV)>x(75lXVnJ#bDR{m`J(EJ+#3cS5VoiB z1U-H_kYB+%*DmSgWCx*`=_xnf=Fz%7l3tOQz?o>a|GBZcKa^TYJR{-y>}xTnNHXSw z?I-$5IyLamU--r@1OUG_9Nr(Q^~tdhF1Th1IAz1D!xUjNjQ{*SQ=6q^h^!h?-XTYl zG?OLS2jDOUp|zy?82vo0Yl@F;2oEMQIm32!`aTyC^EshtA4;!20IWNJy5=M`IZD+} zJMXC@^mlBdZ zH0J3n3t|2qX+vN=`#eLraH;4*=!CqOw`~zufWe=AwI(YB`s2m$f z+w%f~V;z=8JA;*_WoFkw@|3YKD+Yqt>NCd0OC+2d`GSG8^1oVQ{%jKCRzQ9_6{DZ@ zwgj{O{Li`U`N3cE&VN+j|N4?j(4rdlu};Em<2if!topz8VsC+fLzdi`H%-(K&{|v~ zOg%Q1eK~8mR+F!M_X*!XA|69f#asMvZ1`96wo;82C~rCZNN4i+28&Chf9-8RKm8Y6 zvxs8J(1~@Ipmosmwz$m1!UzYg&(^VvI3HE*=+KC)7o)d_0OMS{zhMF*a+3zExa4op z0`Kf!o=lxx*#EiTEM%_5SaW)LgUQ=EUiZ_O06nWUI3}l`w_M3L(QuTp*W6JZGTj4P<(J=iL$}M6%kwR2XPjc{7`@W@2Gb zT>NZkEFf(x-Z8y6_dj+v6ogtekOmj4mFe|Gllqr3ZD0_oKgN5RR_DOS-p^Ci6RY%6 z-z0*30(UPYfJN!R*Hyl~34~yA^3od8wkG0}f5%cp#;4 zgvSda*OOV@i8eN*XNqLFI&nL50*XIVSfV?Xo*ZFjR@qW(4H|7HWF;)BVteW(dzXpl zWJc+(Q|A7?i9N2ld_2`)F@5;8OjBPH6|HuBVqMi`A9v`F8ZI_Ieg?el?ny7%*9V!q zS*L6pI}$xpF40P>i#TbXOxI~{&DY@WXuIL!Skf|s8}I!za@nYY9s^Gikn z>6L^SD@%2<1YuSKCpx4b$eKLEYn+c_6Bkr)u_k7UOa|vWb?@da-;K34oN4%md{WhJ zF<0GVS83BgYsm_PE!UTcT?ZWAefs?I1^IwYC)YN|eq07Zmr@y{r?)EUy$GN$6kkQB z#aTSEK#5A~x@)=V_X@-zMlgmW=Lttwnhzc&Es*`KTZ8h=d0^0u%v5M53#oCRSc!pt zxw#aTw&+kiL|6Hu<*s{azSS}FX&e{&8<`w7%z=mZ{15Y8VfE18zjC}M=YAS~)Xa12 zt$f(m-PI8?HXlEwW6xE&R}nT^&v=lN91g=&fqr*id+L8WRVQ7h0^XbvzF3}qoKcV6 z8THnSOjsIWkrN4Uj3~vWdDII}5x)6@Gd*<^ls~>P7YS>}n3<%CTCkoo&!joZA^p8VyU{k=2V(>adjqa0GUh0pz0 zv@%Oo&%b+J7U|CPo_={?Gi}20Wktmt6KhE`W6|^uJqxz`{lJ{1~ zpzBU5yyyj{62yTf!q=d`L(Js}o=MAPiLNRa(qnM$Cu$uw2Jt zV@4km5(KfJ*sP=RlcI3g_%Ue`5%}Q$Z@_va76!)CQ;#(P%vP^v_upd(LrqCrvG$#H G%>MvTxQ|o- literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 8c43321..9ef9b40 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,14 @@ "version": "0.2.0", "publisher": "mcpp-community", "license": "Apache-2.0", + "icon": "images/logo.png", "repository": { "type": "git", - "url": "https://github.com/mcpp-community/mcpp-vscode.git" + "url": "https://github.com/wellwei/mcpp-vscode.git" }, - "homepage": "https://github.com/mcpp-community/mcpp-vscode#readme", + "homepage": "https://github.com/wellwei/mcpp-vscode#readme", "bugs": { - "url": "https://github.com/mcpp-community/mcpp-vscode/issues" + "url": "https://github.com/wellwei/mcpp-vscode/issues" }, "engines": { "vscode": "^1.90.0" diff --git a/src/cli.ts b/src/cli.ts index fb928fa..fe43a18 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -32,9 +32,11 @@ export interface ToolchainInventory { type Section = "managed" | "system" | "available" | "targets" | undefined; const ANSI_ESCAPE = /\u001b\[[0-?]*[ -/]*[@-~]/g; -const FAMILY_PATTERN = "[A-Za-z][A-Za-z0-9+_.-]*"; +const NAME_PATTERN = "[A-Za-z][A-Za-z0-9+_.-]*"; +const FAMILY_PATTERN = `(?:${NAME_PATTERN}:)?${NAME_PATTERN}`; const VERSION_PATTERN = "[0-9][A-Za-z0-9+_.:-]*"; const SPEC_VERSION_PATTERN = `(?:${VERSION_PATTERN}|system)`; +const INPUT_VERSION_PATTERN = "[A-Za-z0-9][A-Za-z0-9+_.:-]*"; const AUXILIARY_WORDS = new Set([ "available", "default", @@ -55,6 +57,11 @@ export function mcppCommandArguments(...args: string[]): string[] { return [...args]; } +function unqualifiedFamily(value: string): string { + const colon = value.indexOf(":"); + return (colon === -1 ? value : value.slice(colon + 1)).toLowerCase(); +} + export function normalizeToolchainSpec(input: string): string | undefined { let value = input.trim(); if (value.length >= 2) { @@ -70,22 +77,26 @@ export function normalizeToolchainSpec(input: string): string | undefined { } const atMatch = value.match( - new RegExp(`^(${FAMILY_PATTERN})@(${SPEC_VERSION_PATTERN})$`, "i"), + new RegExp(`^(${FAMILY_PATTERN})@(${INPUT_VERSION_PATTERN})$`), ); if (atMatch !== null) { const family = atMatch[1].toLowerCase(); const version = atMatch[2]; - if (version.toLowerCase() === "system" && family !== "msvc") { - return undefined; - } return `${family}@${version}`; } const spacedMatch = value.match( - new RegExp(`^(${FAMILY_PATTERN})\\s+(${VERSION_PATTERN})$`), + new RegExp(`^(${FAMILY_PATTERN})\\s+(${INPUT_VERSION_PATTERN})$`), ); if (spacedMatch !== null) { - return `${spacedMatch[1].toLowerCase()}@${spacedMatch[2]}`; + const family = spacedMatch[1].toLowerCase(); + const version = spacedMatch[2]; + return `${family}@${version}`; + } + + const bareMatch = value.match(new RegExp(`^(${FAMILY_PATTERN})$`)); + if (bareMatch !== null) { + return bareMatch[1].toLowerCase(); } return undefined; @@ -93,14 +104,15 @@ export function normalizeToolchainSpec(input: string): string | undefined { export function isMsvcToolchainSpec(input: string): boolean { const normalized = normalizeToolchainSpec(input); - return normalized === "msvc" || normalized?.startsWith("msvc@") === true; + const compiler = normalized?.split("@", 1)[0]; + return compiler !== undefined && unqualifiedFamily(compiler) === "msvc"; } export type ToolchainInstallKind = "managed-host" | "managed-target" | "system-detect"; /** - * 返回 mcpp 旧兼容拼写隐含的 target 轴;undefined 表示没有隐含 target。 - * 首版插件不传 --target,因此这类输入交给 mcpp CLI 处理。 + * 返回兼容拼写可能隐含的 target 提示;undefined 表示没有对应提示。 + * 结果只用于 UI 分流,输入和 triple 的合法性最终由 mcpp CLI 判断。 */ export type ToolchainSpecTargetHint = "musl" | "windows-gnu" | "target"; @@ -109,20 +121,19 @@ export function toolchainSpecTargetHint(input: string): ToolchainSpecTargetHint if (normalized === undefined) { return undefined; } + const qualifiedCompiler = normalized.split("@", 1)[0]; + const compiler = unqualifiedFamily(qualifiedCompiler); if ( - normalized.startsWith("musl-gcc@") - || (normalized.startsWith("gcc@") && normalized.endsWith("-musl")) + compiler === "musl-gcc" + || (compiler === "gcc" && normalized.endsWith("-musl")) ) { return "musl"; } - const atIndex = normalized.indexOf("@"); - const compiler = atIndex === -1 ? normalized : normalized.slice(0, atIndex); if (compiler.includes("mingw")) { return "windows-gnu"; } // mcpp 还接受带 triple 的旧编译器写法,例如 - // `aarch64-linux-musl-gcc@16`。即使环境不是上面列出的 host-only 别名, - // 这种写法仍然携带 target 轴。 + // `aarch64-linux-musl-gcc@16`。这里只识别其外形,triple 是否有效由 mcpp 判断。 if (compiler.endsWith("-gcc")) { return compiler.includes("-musl-") ? "musl" : "target"; } diff --git a/src/cliController.ts b/src/cliController.ts index 71d324d..981a924 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -8,6 +8,7 @@ import { normalizeToolchainSpec, parseToolchainList, toolchainInstallKind, + toolchainSpecTargetHint, type ToolchainInventory, type ToolchainItem, } from "./cli"; @@ -186,17 +187,22 @@ export class McppCliController { return; } const installKind = toolchainInstallKind(spec); + const targetHint = toolchainSpecTargetHint(spec); const confirmLabel = installKind === "system-detect" ? CONFIRM_DETECT : CONFIRM_INSTALL; const confirmation = installKind === "system-detect" ? `将调用 mcpp 检测系统 MSVC(${spec})。` - : installKind === "managed-target" - ? `将把携带 target 语义的兼容 spec ${spec} 交给 mcpp 安装。` - : `将安装 mcpp 工具链 ${spec}(不指定 target,使用 host target)。`; + : targetHint === "target" + ? `将把可能携带 target 语义的兼容 spec ${spec} 交给 mcpp 安装,最终由 mcpp 校验。` + : installKind === "managed-target" + ? `将把携带 target 语义的兼容 spec ${spec} 交给 mcpp 安装。` + : `将安装 mcpp 工具链 ${spec}(不指定 target,使用 host target)。`; const detail = installKind === "system-detect" ? "mcpp 不会下载或安装 MSVC;它会检测 Visual Studio,并在缺失时给出官方安装指引。" - : installKind === "managed-target" - ? "mcpp 会规范化兼容写法,并可能下载对应 target 的较大工具链包。" - : "安装可能下载较大的工具链包,并修改 mcpp 全局缓存。"; + : targetHint === "target" + ? "mcpp 会判断编译器前缀是否为有效 triple;有效时可能下载对应 target 的较大工具链包。" + : installKind === "managed-target" + ? "mcpp 会规范化兼容写法,并可能下载对应 target 的较大工具链包。" + : "安装可能下载较大的工具链包,并修改 mcpp 全局缓存。"; const choice = await vscode.window.showWarningMessage( confirmation, @@ -362,7 +368,7 @@ export class McppCliController { } items.push({ label: INSTALL_CUSTOM_LABEL, - detail: "支持 family@version、部分版本和 mcpp 兼容旧拼写", + detail: "支持 family、family@version、namespace、部分版本和 mcpp 兼容旧拼写", customInput: true, }); @@ -381,12 +387,12 @@ export class McppCliController { const input = await vscode.window.showInputBox({ title: "输入工具链 spec", - prompt: "例如 gcc@16、llvm@20.1.7、msvc、mingw@16;兼容写法由 mcpp 规范化", + prompt: "例如 gcc、llvm@20.1.7、xim:gcc@16、msvc、mingw;兼容写法由 mcpp 规范化", placeHolder: "gcc@16", validateInput: (value) => { const normalized = normalizeToolchainSpec(value); if (normalized === undefined) { - return "请输入 family@version、family version 或 mcpp 支持的兼容 spec"; + return "请输入 family、family@version、family version、namespace 或 mcpp 兼容 spec"; } return undefined; }, diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 5db16a4..71bb86e 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -5,6 +5,10 @@ import test from "node:test"; interface PackageManifest { version?: string; + icon?: string; + repository?: { url?: string }; + homepage?: string; + bugs?: { url?: string }; extensionDependencies?: string[]; activationEvents?: string[]; capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string } }; @@ -107,6 +111,26 @@ test("安装流程把系统工具链和 target 兼容 spec 交给 mcpp 解析", assert.match(method, /toolchainInstallKind\(spec\)/); }); +test("泛化 triple 工具链由 mcpp 最终校验", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const start = source.indexOf("public async installToolchain"); + const end = source.indexOf("public async selectDefaultToolchain", start); + const method = source.slice(start, end); + + assert.match(method, /可能携带 target 语义.*最终由 mcpp 校验/s); +}); + +test("声明 GitHub 仓库和扩展图标", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + assert.equal(manifest.icon, "images/logo.png"); + assert.equal(manifest.repository?.url, "https://github.com/wellwei/mcpp-vscode.git"); + assert.equal(manifest.homepage, "https://github.com/wellwei/mcpp-vscode#readme"); + assert.equal(manifest.bugs?.url, "https://github.com/wellwei/mcpp-vscode/issues"); + + const icon = readFileSync(path.join(root, manifest.icon)); + assert.deepEqual([...icon.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); +}); + test("README 区分 mcpp 的 MSVC 构建能力和 clangd 的 IFC 限制", () => { const readme = readFileSync(path.join(root, "README.md"), "utf8"); assert.match(readme, /0\.0\.90 起 native `cl\.exe` 后端支持/); diff --git a/test/cli.test.ts b/test/cli.test.ts index f79907d..aaa95ad 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -151,6 +151,22 @@ test("规范化输入并构造参数数组", () => { assert.deepEqual(mcppCommandArguments("build"), ["build"]); }); +test("保留 mcpp 支持的省略版本和 namespace 输入", () => { + const inputs = [ + "gcc", + "llvm", + "clang", + "musl-gcc", + "mingw", + "xim:gcc@16", + ]; + assert.deepEqual(inputs.map(normalizeToolchainSpec), inputs); + assert.equal(normalizeToolchainSpec("xim:gcc 16"), "xim:gcc@16"); + assert.equal(normalizeToolchainSpec("xim:gcc@system"), "xim:gcc@system"); + assert.equal(normalizeToolchainSpec("future@preview"), "future@preview"); + assert.equal(isMsvcToolchainSpec("xim:msvc@system"), true); +}); + test("识别系统项和隐含 target 别名", () => { assert.equal(isMsvcToolchainSpec("msvc"), true); assert.equal(isMsvcToolchainSpec("msvc@system"), true); From 96ab2ed3fec5a4819ddfe60670792c506310eefb Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 1 Aug 2026 10:56:09 +0800 Subject: [PATCH 007/119] docs: design tag release workflow --- .../specs/2026-08-01-tag-release-design.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-01-tag-release-design.md diff --git a/docs/superpowers/specs/2026-08-01-tag-release-design.md b/docs/superpowers/specs/2026-08-01-tag-release-design.md new file mode 100644 index 0000000..77c1481 --- /dev/null +++ b/docs/superpowers/specs/2026-08-01-tag-release-design.md @@ -0,0 +1,35 @@ +# mcpp VS Code tag Release 设计 + +## 目标 + +为 `mcpp-vscode` 增加基于 GitHub tag 的自动 Release。维护者推送与扩展版本一致的 +`v` tag 后,仓库自动验证、测试、打包并发布可安装的 VSIX,避免手工打包和 +上传造成版本漂移。 + +## 范围 + +- 触发方式:推送匹配 `v*` 的 tag,例如 `v0.2.1`。 +- 发布平台:GitHub Releases。 +- Release 资产:`mcpp-vscode-.vsix` 和对应的 SHA-256 校验文件。 +- 不包含 VS Marketplace 发布;Marketplace 使用独立凭据和审批流程,留待后续设计。 + +## 工作流 + +`.github/workflows/release.yml` 使用 GitHub 托管的 Ubuntu runner 和仓库内置 +`GITHUB_TOKEN`,执行以下步骤: + +1. 检出 tag 对应的提交并安装仓库锁定的 npm 依赖。 +2. 读取 `package.json` 版本,要求 tag 精确等于 `v`。 +3. 执行完整 `npm test`,再执行 `npm run package` 生成 VSIX。 +4. 为 VSIX 生成 SHA-256 校验文件。 +5. 若 tag 尚无 Release,则创建 Release 并生成 GitHub 自动说明;若 Release 已存在, + 则覆盖上传同名资产,支持失败后重新运行工作流。 + +工作流声明 `contents: write`,只使用必要的发布权限。版本不匹配、测试失败或打包 +失败时工作流直接失败,不创建或更新 Release 资产。 + +## 验证 + +扩展测试增加工作流契约测试,检查 tag 触发器、版本校验、测试、打包、校验文件和 +`gh release` 发布步骤仍存在。发布前本地仍运行完整测试、VSIX 打包和 `unzip -t`; +真实 GitHub Release 由推送 tag 后的 Actions 运行结果验证。 From c7a3a3b44a76e47e7b7bcd55f0b8c1b868d8eeeb Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 1 Aug 2026 11:36:32 +0800 Subject: [PATCH 008/119] docs: plan tag release implementation --- .../2026-08-01-tag-release-implementation.md | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-01-tag-release-implementation.md diff --git a/docs/superpowers/plans/2026-08-01-tag-release-implementation.md b/docs/superpowers/plans/2026-08-01-tag-release-implementation.md new file mode 100644 index 0000000..5d43cc0 --- /dev/null +++ b/docs/superpowers/plans/2026-08-01-tag-release-implementation.md @@ -0,0 +1,181 @@ +# Tag Release 工作流实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 推送与扩展版本匹配的 Git tag 后,自动验证、打包并在 GitHub Releases 发布 VSIX 和 SHA-256 校验文件。 + +**Architecture:** 新增独立的 tag 触发 GitHub Actions 工作流。工作流读取 `package.json` 的版本并拒绝不匹配的 tag,在测试和打包成功后使用 `gh release` 创建或更新 Release;仓库测试通过检查工作流文本中的关键契约,避免发布链路被静默删改。 + +**Tech Stack:** GitHub Actions、Node.js/npm、VSCE、GitHub CLI、Node 内置测试框架。 + +--- + +### Task 1: 定义 tag Release 工作流契约 + +**Files:** +- Modify: `test/artifacts.test.ts` +- Create: `.github/workflows/release.yml` + +- [x] **Step 1: 写入失败的发布工作流契约测试** + +在 `test/artifacts.test.ts` 末尾添加: + +```ts +test("tag release 工作流校验版本并发布 VSIX", () => { + const workflow = readFileSync(path.join(root, ".github/workflows/release.yml"), "utf8"); + assert.match(workflow, /push:\s*\n\s+tags:\s*\n\s+- "v\*"/); + assert.match(workflow, /contents: write/); + assert.match(workflow, /npm ci/); + assert.match(workflow, /npm test/); + assert.match(workflow, /npm run package/); + assert.match(workflow, /GITHUB_REF_NAME.*v\$\{PACKAGE_VERSION\}/s); + assert.match(workflow, /sha256sum/); + assert.match(workflow, /gh release create/); + assert.match(workflow, /gh release upload.*--clobber/s); +}); +``` + +- [x] **Step 2: 运行测试确认失败** + +运行: + +```bash +npm test -- --test-name-pattern='tag release 工作流校验版本并发布 VSIX' +``` + +预期:失败,原因是 `.github/workflows/release.yml` 尚不存在。 + +- [x] **Step 3: 创建最小发布工作流** + +创建 `.github/workflows/release.yml`,内容如下: + +```yaml +name: 发布 VSIX + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: 检出 tag + uses: actions/checkout@v4 + + - name: 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: 安装依赖 + run: npm ci + + - name: 校验 tag 与扩展版本 + shell: bash + run: | + PACKAGE_VERSION="$(node -p 'require("./package.json").version')" + if [[ "$GITHUB_REF_NAME" != "v${PACKAGE_VERSION}" ]]; then + echo "tag $GITHUB_REF_NAME 与 package.json 版本 $PACKAGE_VERSION 不一致。" + exit 1 + fi + echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> "$GITHUB_ENV" + + - name: 运行测试 + run: npm test + + - name: 打包 VSIX + run: npm run package + + - name: 生成 SHA-256 校验文件 + shell: bash + run: sha256sum "mcpp-vscode-${PACKAGE_VERSION}.vsix" > "mcpp-vscode-${PACKAGE_VERSION}.vsix.sha256" + + - name: 创建或更新 GitHub Release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + VSIX="mcpp-vscode-${PACKAGE_VERSION}.vsix" + CHECKSUM="${VSIX}.sha256" + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + gh release upload "$GITHUB_REF_NAME" "$VSIX" "$CHECKSUM" --clobber + else + gh release create "$GITHUB_REF_NAME" "$VSIX" "$CHECKSUM" \ + --title "mcpp-vscode ${PACKAGE_VERSION}" \ + --generate-notes + fi +``` + +- [x] **Step 4: 运行目标测试确认转绿** + +运行: + +```bash +npm test -- --test-name-pattern='tag release 工作流校验版本并发布 VSIX' +``` + +预期:测试通过,工作流具备 tag 触发、版本校验、测试、打包、校验文件与可重跑发布步骤。 + +### Task 2: 验证并提交当前修复与发布链路 + +**Files:** +- Modify: `CHANGELOG.md` +- Modify: `README.md` +- Modify: `package.json` +- Modify: `package-lock.json` +- Modify: `src/cliController.ts` +- Modify: `test/artifacts.test.ts` +- Create: `.github/workflows/release.yml` + +- [x] **Step 1: 运行完整测试** + +运行: + +```bash +npm test +``` + +预期:所有测试通过,包括项目锁释放和 tag release 工作流契约测试。 + +- [x] **Step 2: 打包并检查 VSIX** + +运行: + +```bash +npm run package +unzip -t mcpp-vscode-0.2.1.vsix +unzip -p mcpp-vscode-0.2.1.vsix extension/package.json +``` + +预期:打包成功,压缩包无错误,清单版本为 `0.2.1` 且包含 `images/logo.png`。 + +- [ ] **Step 3: 检查差异与暂存范围** + +运行: + +```bash +git diff --check +git add CHANGELOG.md README.md package.json package-lock.json src/cliController.ts test/artifacts.test.ts .github/workflows/release.yml +git diff --cached --check +git diff --cached --stat +``` + +预期:仅暂存 CLI 锁修复、`0.2.1` 发布记录、工作流和对应测试;不暂存现有 +`docs/superpowers` 用户改动。 + +- [ ] **Step 4: 提交实现** + +运行: + +```bash +git commit -m "fix: release project lock before IDE reconciliation" +``` + +预期:提交作者为 `wellwei `,发布工作流等待维护者推送 +`v0.2.1` tag 后在 GitHub Actions 中进行真实验证。 From 42de803b9c132b7d49cf326dfe6920946947f6a9 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 1 Aug 2026 11:44:01 +0800 Subject: [PATCH 009/119] fix: release CLI lock and automate tag releases --- .github/workflows/release.yml | 60 +++++++++++++++++++ CHANGELOG.md | 7 +++ README.md | 18 +++++- .../2026-08-01-tag-release-implementation.md | 4 +- package-lock.json | 4 +- package.json | 2 +- src/cliController.ts | 10 ++-- test/artifacts.test.ts | 28 ++++++++- 8 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..825307a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +name: 发布 VSIX + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: 检出 tag + uses: actions/checkout@v4 + + - name: 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: 安装依赖 + run: npm ci + + - name: 校验 tag 与扩展版本 + shell: bash + run: | + PACKAGE_VERSION="$(node -p 'require("./package.json").version')" + if [[ "$GITHUB_REF_NAME" != "v${PACKAGE_VERSION}" ]]; then + echo "tag $GITHUB_REF_NAME 与 package.json 版本 $PACKAGE_VERSION 不一致。" + exit 1 + fi + echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> "$GITHUB_ENV" + + - name: 运行测试 + run: npm test + + - name: 打包 VSIX + run: npm run package + + - name: 生成 SHA-256 校验文件 + shell: bash + run: sha256sum "mcpp-vscode-${PACKAGE_VERSION}.vsix" > "mcpp-vscode-${PACKAGE_VERSION}.vsix.sha256" + + - name: 创建或更新 GitHub Release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + VSIX="mcpp-vscode-${PACKAGE_VERSION}.vsix" + CHECKSUM="${VSIX}.sha256" + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + gh release upload "$GITHUB_REF_NAME" "$VSIX" "$CHECKSUM" --clobber + else + gh release create "$GITHUB_REF_NAME" "$VSIX" "$CHECKSUM" \ + --title "mcpp-vscode ${PACKAGE_VERSION}" \ + --generate-notes + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 316bf6a..f29253c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +## 0.2.1 + +- 修复项目任务已经结束后,clangd/CDB 重协调仍占用项目锁,导致下一条 CLI 命令误报 + “已有 mcpp 操作正在运行”的问题。 +- 增加 GitHub tag Release 工作流:校验 tag 与扩展版本,运行测试和打包,并发布 VSIX + 及其 SHA-256 校验文件。 + ## 0.2.0 - 新增 `$(tools) mcpp` 状态栏快捷菜单和命令面板命令:构建、运行、测试、清理 target、 diff --git a/README.md b/README.md index 271c3d9..fc0be95 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ clangd 21 及更高版本会请求 `--experimental-modules-support`。`on` 强 合法性由 mcpp 判断。插件通过参数数组把用户输入安全规范化为单个参数后交给 mcpp, 最终由 mcpp 解析,不自行复制其 family、alias 或 triple 规则。 -当前 0.2.0 在消费编译数据库并配置 clangd 的基础上增加了 mcpp CLI 菜单;首版 UI +当前 0.2.1 在消费编译数据库并配置 clangd 的基础上增加了 mcpp CLI 菜单;首版 UI 只读展示 target,不提供结构化 target 选择。用户仍可输入 mcpp 兼容 spec 安装 target payload;target 专用默认值设置继续使用带 `--target` 的 mcpp CLI。 `Available toolchains:` 是 mcpp 按 family 汇总多个 host 可读取 payload 索引后的版本集合, @@ -236,7 +236,7 @@ IDE CDB 与模块图至少需要保证: 在没有独立 LLVM 分析配置前仍只保证模块语法高亮。 - clangd 负责编辑期语法和语义,不替代链接、代码生成、运行期或完整打包检查;这些错误 仍由正式 `mcpp build`/`mcpp test` 报告。 -- 当前 `0.2.0` 仍是过渡方案:它能在用户显式构建后自动接管 CDB,并提供常用 CLI 操作, +- 当前 `0.2.1` 仍是过渡方案:它能在用户显式构建后自动接管 CDB,并提供常用 CLI 操作, 但无法仅凭现有插件 在无 CDB 时推导完整依赖图。最终体验依赖上述 mcpp 核心协议和 IDE 配置阶段。 @@ -266,3 +266,17 @@ npm run compile 生成的 `dist/` 是扩展运行时目录。请在 Extension Development Host 中运行上述 扩展,验证 VS Code API 集成。 + +## 发布 + +先更新 `package.json`、`package-lock.json` 和更新日志中的版本并提交到主分支,然后 +推送与扩展版本完全一致的 tag: + +```sh +git tag -a v0.2.1 -m "mcpp-vscode 0.2.1" +git push origin v0.2.1 +``` + +`.github/workflows/release.yml` 会自动执行完整测试、打包 VSIX、生成 SHA-256 校验文件, +并创建或更新对应的 GitHub Release。tag 与 `package.json` 版本不一致时发布会失败; +该工作流不发布 VS Marketplace。 diff --git a/docs/superpowers/plans/2026-08-01-tag-release-implementation.md b/docs/superpowers/plans/2026-08-01-tag-release-implementation.md index 5d43cc0..599f274 100644 --- a/docs/superpowers/plans/2026-08-01-tag-release-implementation.md +++ b/docs/superpowers/plans/2026-08-01-tag-release-implementation.md @@ -155,7 +155,7 @@ unzip -p mcpp-vscode-0.2.1.vsix extension/package.json 预期:打包成功,压缩包无错误,清单版本为 `0.2.1` 且包含 `images/logo.png`。 -- [ ] **Step 3: 检查差异与暂存范围** +- [x] **Step 3: 检查差异与暂存范围** 运行: @@ -169,7 +169,7 @@ git diff --cached --stat 预期:仅暂存 CLI 锁修复、`0.2.1` 发布记录、工作流和对应测试;不暂存现有 `docs/superpowers` 用户改动。 -- [ ] **Step 4: 提交实现** +- [x] **Step 4: 提交实现** 运行: diff --git a/package-lock.json b/package-lock.json index 585a1f5..b4291bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.2.0", + "version": "0.2.1", "license": "Apache-2.0", "devDependencies": { "@types/node": "^24.0.0", diff --git a/package.json b/package.json index 9ef9b40..f805eec 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.0", + "version": "0.2.1", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", diff --git a/src/cliController.ts b/src/cliController.ts index 981a924..c17e955 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -134,21 +134,23 @@ export class McppCliController { return; } + let completion: TaskCompletion | undefined; try { const plan = projectTaskPlan(kind); - const completion = await this.executeTask( + completion = await this.executeTask( project.root, this.mcppExecutable(project), plan.title, plan.args, ); this.appendTaskCompletion(project.root, plan.title, plan.args, completion); - if (shouldReconcileAfterTask(kind, completion)) { - await this.options.afterProjectTask(project, kind, completion); - } } finally { this.operations.finishProject(project.root, token); } + + if (completion !== undefined && shouldReconcileAfterTask(kind, completion)) { + await this.options.afterProjectTask(project, kind, completion); + } } public async showToolchains(): Promise { diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 71bb86e..06e170e 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -24,7 +24,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.0"); + assert.equal(manifest.version, "0.2.1"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); @@ -96,6 +96,19 @@ test("设置全局默认后先释放工具链锁再提供立即构建", () => { assert.ok(unlock >= 0 && unlock < immediateBuild); }); +test("项目任务结束后先释放项目锁再重新协调 IDE", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const start = source.indexOf("public async runProjectTask"); + const end = source.indexOf("public async showToolchains", start); + assert.notEqual(start, -1); + assert.notEqual(end, -1); + + const method = source.slice(start, end); + const unlock = method.indexOf("this.operations.finishProject(project.root, token)"); + const reconcile = method.indexOf("this.options.afterProjectTask(project, kind, completion)"); + assert.ok(unlock >= 0 && unlock < reconcile); +}); + test("安装流程把系统工具链和 target 兼容 spec 交给 mcpp 解析", () => { const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); const start = source.indexOf("public async installToolchain"); @@ -143,3 +156,16 @@ test("嵌套工程提示不猜测它一定是 mcpp 工作区成员", () => { assert.doesNotMatch(source, /isWorkspaceMember/); assert.doesNotMatch(source, /当前是工作区成员/); }); + +test("tag release 工作流校验版本并发布 VSIX", () => { + const workflow = readFileSync(path.join(root, ".github/workflows/release.yml"), "utf8"); + assert.match(workflow, /push:\s*\n\s+tags:\s*\n\s+- "v\*"/); + assert.match(workflow, /contents: write/); + assert.match(workflow, /npm ci/); + assert.match(workflow, /npm test/); + assert.match(workflow, /npm run package/); + assert.match(workflow, /GITHUB_REF_NAME.*v\$\{PACKAGE_VERSION\}/s); + assert.match(workflow, /sha256sum/); + assert.match(workflow, /gh release create/); + assert.match(workflow, /gh release upload.*--clobber/s); +}); From b8d8bc1a44d141b47cc5f9aaec2cf75b398fae33 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 1 Aug 2026 17:03:34 +0800 Subject: [PATCH 010/119] docs: clarify extension capabilities and roadmap --- README.md | 557 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 325 insertions(+), 232 deletions(-) diff --git a/README.md b/README.md index fc0be95..b8f8e89 100644 --- a/README.md +++ b/README.md @@ -4,104 +4,148 @@ # mcpp VS Code 扩展 -本扩展把 mcpp 工程接入官方 -`llvm-vs-code-extensions.vscode-clangd` 扩展。它不实现第二套语言服务器, -也不会改写 `compile_commands.json` 或模块文件。 +把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -## 首版范围 +当前版本为 `0.2.1`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 -- LLVM/Clang mcpp 工程:配置匹配的 clangd,并直接检查 mcpp 编译数据库和 - PCM 文件。扩展激活时会自动把官方 clangd 扩展切换到匹配的真实可执行文件, - 不会继续使用未激活版本的 xlings shim。 -- GCC 和 MSVC mcpp 工程:保留 C++ 模块语法高亮,并明确提示 `.gcm`、 - `.ifc` 不能由 clangd 消费。 -- `.cppm`、`.ixx`、`.mpp`、`.ccm` 关联到内置 `cpp` 语言。注入语法规则覆盖 `module`、 - `export module`、`import`、`export import`、模块名和分区。 +> 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 +> 模块语法高亮、mcpp 命令和工具链管理,但插件不为其提供模块诊断、悬停或跳转。 -clangd 的 C++20 模块支持仍处于实验阶段。语法颜色正常不代表语义诊断可用。 -clangd、mcpp 编译器、libc++ 和 PCM/BMI 应来自同一个 LLVM 构建;只匹配版本号 -并不足够。 +## 能力矩阵 + +| 能力 | LLVM/Clang | GCC | MSVC | +| --- | --- | --- | --- | +| 识别 mcpp 工程 | 支持 | 支持 | 支持 | +| `.cppm`、`.ixx`、`.mpp`、`.ccm` 文件关联 | 支持 | 支持 | 支持 | +| `module`、`export module`、`import` 语法高亮 | 支持 | 支持 | 支持 | +| 构建、运行、测试、清理命令 | 支持 | 支持 | 支持 | +| 查看、安装或选择 mcpp 工具链 | 支持 | 支持 | 系统检测与选择 | +| 自动配置语言服务 | clangd | 不支持 | 不支持 | +| 消费模块产物 | `.pcm` | 不读取 `.gcm` | 不读取 `.ifc` | +| 模块诊断、悬停、补全和跳转 | 实验性支持 | 不支持 | 不支持 | + +这里的“GCC/MSVC 不支持”只表示插件当前没有对应的模块语义后端,不表示 mcpp +不能使用这些工具链构建项目。mcpp 可以生成 GCC `.gcm` 和 MSVC `.ifc`,但 clangd +不能直接消费这些编译器专用格式。 + +## 安装 + +从 [GitHub Releases](https://github.com/wellwei/mcpp-vscode/releases) 下载对应版本的 +VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: + +```sh +code --install-extension /path/to/mcpp-vscode-0.2.1.vsix +``` + +安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 +`llvm-vs-code-extensions.vscode-clangd`。当前发布流程不会自动发布到 VS Marketplace。 ## 使用条件 -1. VS Code 1.90 或更高版本。 -2. 官方 `llvm-vs-code-extensions.vscode-clangd` 扩展。它是功能依赖,VS Code - 会随本扩展一起安装。 -3. mcpp,以及包含 `mcpp.toml` 的 mcpp 工程。mcpp 可以由 xlings 安装,也可以使用 - 官方独立安装脚本;扩展不依赖 mcpp 的安装来源。 -4. 要获得模块语义诊断,需要 LLVM mcpp 工具链和匹配的 clangd。扩展会优先使用 - `mcpp.clangd.path`,否则依次查找编译器同目录、同一工具包根目录中的 - `xim-x-llvm-tools//bin`、从 `.mcpp` 工具链映射到用户目录下的 - `.xlings/data/xpkgs/xim-x-llvm-tools//bin`,最后查找 `PATH`。 - -mcpp 会把 `compile_commands.json` 写到工程根目录。扩展只读取它,不会修改它。 - -## 当前版本行为 - -1. 工程已有可用的 `compile_commands.json` 时,扩展激活后会分析其中的 LLVM - 编译器,选择匹配的 clangd,并应用工作区配置;随后自动执行一次 `clangd --check`, - 状态栏直接显示“模块可用”或“模块不可用”,不需要先点击状态栏按钮。 -2. 工程缺少编译数据库时,状态栏显示“缺少模块 CDB”;在受信任工作区点击该状态 - 会执行 **mcpp: 刷新编译数据库**。未受信任工作区只保留模块语法高亮,不会执行 - CDB 中的编译器、mcpp 或 clangd,也不会接管 clangd 配置。扩展在受信任工作区中 - 也不会仅因打开工程而静默构建。 -3. `compile_commands.json` 创建或变化后,扩展会自动重新分析、重启 clangd 并再次 - 检查模块支持,不再要求手动点击检查按钮或 Reload Window。检查结果按工程缓存, - 工程内的重复事件会合并,所有共享 clangd 操作全局串行。多根工作区仅由当前活动 - mcpp 工程接管窗口级 clangd 配置;后台工程只失效缓存,切换过去时再自动配置, - 因此迟到检查不会覆盖当前工程状态。 -4. 修改 `mcpp.clangd.path` 或 `mcpp.modulesSupport` 后,扩展会自动重新配置和检查。 - 单次 `clangd --check` 最长等待 60 秒,避免异常进程让激活流程一直停住。 -5. `mcpp build` 即使因普通源码错误返回非零,只要工程中已经出现可用 CDB, - 扩展仍会配置 clangd,并明确提示“构建失败,但 IDE 数据已就绪”。 -6. 版本 `0.2.0` 增加独立的 `$(tools) mcpp` 状态栏入口。它可以在当前工程中执行 - `mcpp build`、`run`、`test`、`clean`,任务终端会实时显示完整输出;任务结束后会 - 重新协调 CDB 和 clangd。`clean` 只确认并清理当前工程的 `target/`,不会删除全局 - BMI 缓存。 -7. 工具链菜单可以查看 mcpp 的 `Toolchains:`、`System:`、`Targets:` 和 - `Available toolchains:` 区块,安装可用工具链,或从已安装/系统项中选择全局默认。 - 安装和修改默认值都需要用户确认;扩展激活、打开工程或修改配置不会自动下载、安装 - 或切换工具链。 +- VS Code 1.90 或更高版本。 +- 包含 `mcpp.toml` 的 mcpp 工程。 +- 可执行的 mcpp。它可以由 xlings、官方独立安装脚本或其他受支持方式安装。 +- 官方 `llvm-vs-code-extensions.vscode-clangd` 扩展。它是本扩展的功能依赖。 +- 要启用模块语义,需要 LLVM mcpp 工具链、工程编译数据库,以及与该工具链来自 + 同一 LLVM revision 的 clangd。 + +安装来源彼此独立:mcpp、构建编译器和 clangd 不必位于同一个目录,也不要求都由 +xlings 安装。 + +## 快速开始 + +1. 使用 LLVM 工具链构建一次项目,使 mcpp 在工程根目录生成 + `compile_commands.json` 和所需 PCM。 +2. 使用 VS Code 打开包含 `mcpp.toml` 的目录。 +3. 扩展会自动分析编译数据库、选择匹配的 clangd、写入工作区配置并检查模块支持。 +4. 查看状态栏中的 `mcpp: 模块可用`、`mcpp: 模块不可用` 或 + `mcpp: 缺少模块 CDB` 状态。 + +工程还没有编译数据库时,可以点击“缺少模块 CDB”,或执行 +**mcpp: 刷新编译数据库**。这个命令当前实际执行一次 `mcpp build`,并在任务结束后 +重新协调 CDB 和 clangd。 + +## 已实现功能 + +### 工程发现与自动激活 + +- 工作区包含 `mcpp.toml`,或者打开 C++ 文件时激活扩展。 +- 从活动文件或工作区目录查找最近的 mcpp 工程。 +- 监听 `mcpp.toml`、`compile_commands.json`、活动编辑器、工作区和相关设置变化。 +- 打开已有有效 CDB 的 LLVM 工程后自动配置并检查,不需要先点击状态栏或 Reload + Window。 + +扩展激活本身不会静默运行 `mcpp build`,也不会自动下载、安装或切换工具链。 + +### C++ 模块语法高亮 + +- 将 `.cppm`、`.ixx`、`.mpp` 和 `.ccm` 关联到 VS Code 内置 `cpp` 语言。 +- 向 `source.cpp` 注入模块语法规则。 +- 覆盖 `module`、`export module`、`import`、`export import`、模块名和模块分区。 +- 支持尚未输入分号的编辑中间态。 + +语法高亮只负责词法着色。错误模块名、不可见声明等红色诊断来自语言服务器,不能由 +TextMate 语法规则提供。 + +### LLVM 与 clangd 集成 + +扩展读取 mcpp 生成的 `compile_commands.json`,然后: + +1. 识别其中的 LLVM 编译器及待检查源文件。 +2. 从显式设置、编译器目录、匹配的 xlings `llvm-tools` 目录和 `PATH` 中查找 clangd。 +3. 执行编译器和 clangd 的版本命令,比较 LLVM 版本与 revision。 +4. 保留用户已有的 `clangd.arguments`,展开 `${workspaceFolder}` 和 + `${workspaceRoot}`,并添加精确的 `--query-driver`。 +5. 根据 CDB 中是否已有显式 PCM 映射以及 `mcpp.modulesSupport` 设置,决定是否增加 + clangd 实验模块参数。 +6. 必要时重启官方 clangd 扩展,并执行一次最长 60 秒的 `clangd --check`。 + +检查可以区分模块可用、PCM 不匹配、模块产物不可用、语言模式错误和一般检查失败。 +完整命令及输出会写入 `mcpp` 输出频道。 + +### 自动协调与多根工作区 + +- CDB 创建或变化后自动重新分析、配置、重启和检查;删除后更新为缺少 CDB 状态。 +- 同一工程的重复事件通过 singleflight 合并,共享的 clangd 操作全局串行。 +- 只接受当前工程最新一次模块检查的结果,迟到结果不会覆盖新状态。 +- 多根工作区中只有活动 mcpp 工程接管窗口级 clangd 配置;后台工程只失效缓存,切换 + 过去时再协调。 +- 修改 `mcpp.clangd.path` 或 `mcpp.modulesSupport` 后自动重新配置和检查。 +- 构建即使因普通源码错误退出,只要留下可用 CDB,扩展仍会尝试恢复 IDE 状态,并把 + “构建失败”和“IDE 数据可用”分别报告。 + +### mcpp CLI 与工具链管理 + +状态栏中的 `$(tools) mcpp` 菜单提供: + +- 在当前工程根目录执行 `mcpp build`、`run`、`test` 和 `clean`。 +- 在 VS Code 任务终端中实时显示完整输出。 +- 同一工程同时只运行一个由扩展启动的项目任务。 +- 项目任务结束后先释放任务锁,再重新协调 CDB 和 clangd。 +- 清理当前工程的 `target/`;不会删除 mcpp 全局 BMI 缓存。 +- 读取并展示 `mcpp toolchain list` 的当前有效工具链、全局默认、系统工具链、target + 状态和可安装版本。 +- 安装 mcpp 支持的工具链 spec,或从已安装/系统项中选择全局默认工具链。 + +安装和修改默认值都需要确认。取消操作不会被当成成功,工具链安装和默认值修改共享 +全局互斥锁。 ## 命令 -- **mcpp: 配置 clangd**:手动重新应用自动配置。命令会查找最近的工程,保留已有 - `clangd.arguments`,将其中的 - `${workspaceFolder}`/`${workspaceRoot}`解析为 mcpp 工程根目录,添加精确的 mcpp - 编译器 `--query-driver`,并应用模块支持策略。如果 cpptools IntelliSense 已启用, - 会先询问是否只关闭它的 IntelliSense 引擎。 -- **mcpp: 刷新编译数据库**:在最近的工程中显式执行 `mcpp build`,随后无论构建 - 是否成功都会重新读取 CDB;只要 CDB 和 clangd 可用,就会刷新配置并重启 - clangd。扩展激活时不会自动执行构建。 -- **mcpp: 检查模块支持**:立即重新执行 clangd 直接检查,报告检查成功、模块文件 - 缺失、语言模式错误或 PCM/LLVM 不匹配。扩展激活和 CDB 变化时已经会自动检查; - 此命令用于主动复查。完整输出位于 `mcpp` 输出频道。 -- **mcpp: 打开快捷菜单**:从状态栏打开项目、工具链和 IDE 操作。 -- **mcpp: 构建 / 运行 / 测试 / 清理 target**:分别执行对应的 mcpp CLI 子命令。 - 同一工程同时只允许一个由扩展启动的任务;取消任务不会被当成成功,也不会触发成功 - 提示。构建、运行和测试的工作目录固定为当前工程根目录。 -- **mcpp: 查看工具链**:执行 `mcpp toolchain list`,保留原始输出并在 Quick Pick 中 - 展示当前有效工具链、全局默认、系统工具链、target 状态和可安装版本。 -- **mcpp: 安装工具链**:从 mcpp 列出的可用项或兼容 spec 中选择,执行 - `mcpp toolchain install `。`gcc`、`llvm` 等省略版本的 family 会由 mcpp 选择最高 - 可用版本;`xim:gcc@16` 等 namespace 输入也可直接使用。插件只把输入安全规范化为单个 - 参数,family、别名和 triple 的合法性最终由 mcpp 解析。`gcc@V-musl`、`mingw@V` 等 - 兼容写法会携带 target;泛化的 `-gcc@V` 可能携带 target,仍由 mcpp 校验 triple。 - 输入 `msvc` 时,同一命令只检测系统 Visual Studio 或显示官方安装指引,不会由 mcpp - 下载、升级或删除 MSVC。本版本暂不提供结构化 `--target` 选择器。 -- **mcpp: 选择全局默认工具链**:从当前主机可作为默认值的已安装项或检测到的系统项中 - 选择,执行 `mcpp toolchain default `。普通 target-only payload 不会混入列表; - Windows 上省略 target 的 GCC 按 mcpp 官方规则映射到已安装的 MinGW payload。按 mcpp 的官方 - 语义,这会把全局默认对设为“所选工具链 + host target”,并清空已有的 - `default_target`。它只修改 mcpp 当前全局配置;配置文件通常位于 `~/.mcpp/config.toml`, - 但 `MCPP_HOME` 和独立安装位置可能改变实际路径。当前项目的成员配置、 - 工作区配置、`[target.]` 和命令行参数仍按 mcpp 官方优先级覆盖全局默认;扩展不 - 自动构建,用户可在确认后选择立即构建。 - -当 mcpp 工程根位于 VS Code 工作区文件夹的子目录时,它可能是 mcpp 工作区成员,也可能 -只是嵌套的独立工程,扩展不会仅凭路径关系猜测其身份。“查看工具链”按当前工程根执行 -`mcpp toolchain list` 并标明这是当前目录视图;若存在父级 mcpp 工作区继承,实际生效 -工具链以 mcpp 构建解析为准。 +| 命令 | 行为 | +| --- | --- | +| **mcpp: 打开快捷菜单** | 打开项目、工具链和 IDE 操作入口 | +| **mcpp: 构建** | 在当前工程执行 `mcpp build` | +| **mcpp: 运行** | 在当前工程执行 `mcpp run` | +| **mcpp: 测试** | 在当前工程执行 `mcpp test` | +| **mcpp: 清理 target** | 确认后执行 `mcpp clean` | +| **mcpp: 查看工具链** | 只读展示 `mcpp toolchain list` 的解析结果和原始输出 | +| **mcpp: 安装工具链** | 确认后执行 `mcpp toolchain install ` | +| **mcpp: 选择全局默认工具链** | 确认后执行 `mcpp toolchain default ` | +| **mcpp: 配置 clangd** | 手动重新应用当前 LLVM 工程的 clangd 配置 | +| **mcpp: 刷新编译数据库** | 执行 `mcpp build`,随后重新读取 CDB 并协调 clangd | +| **mcpp: 检查模块支持** | 立即执行 clangd 直接检查并刷新模块状态 | ## 设置 @@ -114,147 +158,193 @@ mcpp 会把 `compile_commands.json` 写到工程根目录。扩展只读取它 } ``` -`mcpp.path` 供上述全部 mcpp CLI 命令使用,用于解决 macOS GUI 启动的 VS Code 没有 -继承终端 `PATH` 的情况。留空时扩展直接执行 `mcpp`;也可以设置 -独立安装的绝对路径,例如 `/opt/homebrew/bin/mcpp`。工程实际使用的编译器始终从 -`compile_commands.json` 读取,与 `mcpp.path` 相互独立。 - -`mcpp.clangd.path` 只指定语言服务器。独立安装的 mcpp LLVM 工具链即使只包含 -`clang`/`clang++`,也仍可配合其他位置的 clangd 使用;关键是 clangd、编译器和 -PCM/BMI 来自同一 LLVM revision。可以单独安装 xlings 的 `xim-x-llvm-tools` 并让 -扩展自动映射,也可以显式填写其他匹配的 clangd。当前版本不调用 `clang-tidy` 或 -`clang-format`,缺少这两个工具不会影响模块诊断、悬停、补全和跳转。 - -`mcpp.modulesSupport` 可设为 `auto`、`on` 或 `off`。`auto` 检测到 mcpp CDB 已经 -包含显式 `-fmodule-file`/`-fprebuilt-module-path` 时,会直接使用这些 PCM;否则 -clangd 21 及更高版本会请求 `--experimental-modules-support`。`on` 强制启用 clangd -实验模块构建,`off` 始终禁用它。已有的 clangd 参数会保留;扩展只替换自己管理的 -`--query-driver` 和实验模块参数。 - -## mcpp 工具链契约 - -插件按 mcpp `origin/main`(核对提交 `0547324`)的工具链模型设计,不能把工具链列表硬编码成某几个主机版本: - -- `toolchain` 是 `family@version`(`gcc`、`llvm`、`msvc`),`target` 是独立的 - `arch-os[-env]` triple。`musl`、MinGW 和 cross 是 target 变体或构建关系,不是新的 family。 -- `mcpp toolchain default` 保存工具链和 target 的默认对;省略 `--target` 表示 host, - 全局配置分别使用 `default` 与 `default_target`。 -- toolchain 轴按“成员/项目 `[toolchain]` > 继承的工作区根 `[toolchain]` > 全局默认”解析; - target 轴按“命令行 `--target` > 项目/工作区 `[build].target` > 全局 `default_target` > host” - 解析。`[target.]` 可为特定 target 指定工具链;`--static` 只覆盖链接方式。 -- 工具链安装和首次构建支持部分版本并可自动安装;`MCPP_NO_AUTO_INSTALL=1` 或离线模式 - 会禁止网络安装。插件不因 mcpp 来自 xlings 还是独立安装而改变这套解析。 -- MSVC 是 Windows 系统工具链:mcpp 负责检测和构建,不负责安装、升级或删除;稳定身份是 - `msvc@system`,`msvc@` 是版本 pin-verify。0.0.90 起 native `cl.exe` 后端支持 - `import std`、命名模块、`.ifc`、增量构建和运行。 -- `musl-gcc`、`gcc@-musl`、`mingw@`、`mingw-gcc@`、 - `mingw-cross-gcc@`、`-gcc@` 和 `clang@` 等旧拼写仍由 - mcpp 兼容;其中除 `clang@` 外的已知变体会隐含 target,泛化 triple 写法的 - 合法性由 mcpp 判断。插件通过参数数组把用户输入安全规范化为单个参数后交给 mcpp, - 最终由 mcpp 解析,不自行复制其 family、alias 或 triple 规则。 - -当前 0.2.1 在消费编译数据库并配置 clangd 的基础上增加了 mcpp CLI 菜单;首版 UI -只读展示 target,不提供结构化 target 选择。用户仍可输入 mcpp 兼容 spec 安装 target -payload;target 专用默认值设置继续使用带 `--target` 的 mcpp CLI。 -`Available toolchains:` 是 mcpp 按 family 汇总多个 host 可读取 payload 索引后的版本集合, -并不承诺每个版本都存在 host payload;插件选择其中一项时仍按 host target 调用 mcpp, -最终可安装性由 mcpp 自身判断并原样报告。 - -## 限制 - -- clangd 模块实现仍处于实验阶段,编译数据库必须包含有效的 mcpp PCM 映射。单独 - 添加开关不能生成缺失的 BMI。 -- GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它们,但 clangd - 不能直接消费;本版本不会为这两类工程额外构建 LLVM 分析副本。 -- 语法规则只负责词法着色。红色诊断来自语言服务器,需要兼容的 LLVM 工具链。 -- 扩展不会静默执行无缓存构建,也不会在未确认时自动下载、安装或切换工具链。 -- 未受信任工作区不执行任何由工程文件或设置选择的外部工具;信任工作区后会自动开始 - 配置和模块检查。 - -## 长期完整计划 - -### 理想最终效果 - -用户打开一个受信任的 mcpp 工程后,不需要先执行 `mcpp build`,也不需要 Reload -Window。扩展应自动读取 `mcpp.toml` 对应的工程模型,准备依赖和工具链,生成专供 -IDE 使用的完整模块图、BMI/PCM 和 `compile_commands.json`,然后启动匹配的 -clangd。即使普通源码暂时存在语法或编译错误,编辑器仍应具备: - -- 对错误模块名和不可见模块给出诊断; -- 对模块导出的声明提供悬停、补全、查找引用和定义跳转; -- 在修改 `mcpp.toml` 后增量刷新依赖和模块图; -- 在没有网络时复用本机缓存和最后一次可用的 IDE 数据。 - -这应当接近 Cargo 与 rust-analyzer 的分工:构建工具提供稳定、可查询的工程模型, -语言服务器消费该模型;VS Code 扩展只负责生命周期、用户授权和状态展示,不在插件 -中重新实现一套 mcpp TOML 解析器或包管理器。 - -### mcpp 核心需要提供的接口 - -mcpp 核心需要先提供带版本的机器可读协议,例如: +| 设置 | 默认值 | 说明 | +| --- | --- | --- | +| `mcpp.path` | 空 | mcpp 可执行文件;空值表示从 VS Code 进程的 `PATH` 查找 | +| `mcpp.clangd.path` | 空 | 与 CDB 中 LLVM 编译器匹配的 clangd;空值表示自动发现 | +| `mcpp.modulesSupport` | `auto` | `auto`、`on` 或 `off`,控制 clangd 实验模块参数 | +| `mcpp.configureCppTools` | `true` | 手动配置 clangd 时,是否询问关闭当前工作区的 cpptools IntelliSense | + +`mcpp.path` 只影响插件执行 mcpp CLI 命令。工程实际编译器来自 +`compile_commands.json`,`mcpp.clangd.path` 只指定语言服务器,三者相互独立。 + +`mcpp.modulesSupport=auto` 检测到 CDB 已含 `-fmodule-file` 或 +`-fprebuilt-module-path` 时,直接使用 mcpp 生成的 PCM;否则只为 clangd 21 及以上 +请求实验模块构建。`on` 强制请求该参数,`off` 始终不请求。这个设置不能生成缺失的 +PCM。 + +## 工具链管理边界 + +- mcpp 的规范工具链 family 是 `gcc`、`llvm` 和 `msvc`;target 是独立的 + `arch-os[-env]` 维度。 +- 安装入口接受 family、`family@version`、namespace、部分版本及 mcpp 兼容旧拼写, + 例如 `gcc`、`llvm@20.1.7`、`xim:gcc@16`、`msvc` 和 `mingw`。插件只做单参数 + 规范化,最终合法性和具体版本由 mcpp 判断。 +- 当前 UI 只读展示 target,不提供结构化 `--target` 选择器。target 专用默认值继续 + 使用 mcpp CLI 设置。 +- 选择全局默认值会修改 mcpp 的全局配置;项目或工作区的 `[toolchain]`、 + `[build].target` 和命令行参数仍可按 mcpp 规则覆盖它。 +- MSVC 是 Windows 系统工具链。mcpp 负责检测和构建,不负责下载、升级或删除 Visual + Studio;缺失时只显示官方安装指引。 +- 0.0.90 起 native `cl.exe` 后端支持 `import std`、命名模块、`.ifc`、增量构建和 + 运行。这是 mcpp 的构建能力,不代表当前插件或 clangd 能读取 `.ifc`。 +- 当 mcpp 工程位于 VS Code 工作区目录的子目录时,插件不会仅凭路径关系判断它一定是 + mcpp 工作区成员;实际工具链以 mcpp 在该目录中的解析结果为准。 -```sh -mcpp metadata --format-version=1 -mcpp configure --ide --json -``` +## 当前限制与责任边界 + +### 模块语义仅支持 LLVM + +GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它们,但 clangd +不能直接消费;当前版本没有 GCC 原生模块语言服务器、cpptools 模块后端或 LLVM 影子 +分析副本。因此 GCC/MSVC 工程只保证模块语法高亮和 mcpp 操作。 + +### 首次语义配置仍需要构建数据 + +当前插件只读取 `compile_commands.json`,不解析 `mcpp.toml`,也不自行推导依赖和 +模块图。工程没有 CDB 和 PCM 时,必须先显式执行构建或“刷新编译数据库”。插件不会 +因为打开工程就自动构建,也不会自行生成、改写或修复 mcpp 没有输出的 CDB。 + +### clangd 模块支持仍是实验能力 + +- CDB 必须包含正确语言标准、编译器路径和有效 PCM 映射。 +- clangd、mcpp LLVM 编译器、C++ 标准库和 PCM 应来自同一个 LLVM 构建;只匹配主版本 + 或版本号并不足够。 +- 语法颜色正常不代表语义诊断可用,`clangd --check` 成功也不替代完整构建和测试。 +- clangd 负责编辑期诊断、悬停、补全和跳转;链接、代码生成、运行期和打包错误仍由 + `mcpp build`、`run` 或 `test` 报告。 +- 当前扩展不调用 `clang-tidy` 或 `clang-format`,缺少它们不会影响本扩展的模块检查。 -`metadata` 应稳定输出工作区成员、目标、源码、模块、依赖、工具链和缓存位置,不执行 -构建。`configure --ide` 负责解析和下载依赖、准备工具链、处理 `build.mcpp` 与生成源、 -扫描完整模块图、预编译语义分析所需的 BMI/PCM,并原子写入 IDE CDB;它不编译普通 -实现源文件,也不链接最终可执行文件或库。命令需要结构化输出进度、诊断、产物路径和 -可取消状态,不能要求插件解析面向人的终端文本。 - -IDE CDB 与模块图至少需要保证: - -- 模块 producer 条目包含确定的 `-fmodule-output=`; -- consumer 条目包含确定的 `-fmodule-file==`,不依赖 clangd 猜测产物; -- 覆盖命名模块、模块分区、`std`、依赖包、工作区成员和测试目标; -- 编译器路径、语言标准、宏、包含目录、工作目录与正式构建一致; -- 先写临时文件再原子替换,失败时保留最后一次可用 CDB,避免 clangd 读到半写入 JSON; -- 协议和路径字段有版本,允许 mcpp 与插件独立升级。 - -### VS Code 扩展需要完成的工作 - -在上述核心接口可用后,扩展按以下阶段演进: - -1. 打开工程或 `mcpp.toml` 变化时,在工作区受信任后调用 IDE 配置命令;涉及下载、 - 执行 `build.mcpp` 或生成文件时,必须遵守 VS Code Workspace Trust。 -2. 显示可取消的进度,支持在线、离线和只使用缓存三种模式;同一工程的连续事件使用 - debounce 与 singleflight 合并,避免并发下载或反复重启 clangd。 -3. 配置失败时继续使用 last-known-good CDB,并在状态栏区分“正在准备”“可用但过期” - “配置失败”和“模块语义可用”。 -4. mcpp 输出新模块图和 CDB 后再切换 clangd;多根工作区按工程根目录隔离状态、进程和 - 取消操作。 -5. 增加协议兼容测试、无网络缓存测试、源码错误但 IDE 配置成功测试,以及真实 VS Code - 中的诊断、悬停、补全和跳转端到端测试。 - -### 技术边界 - -- clangd 要完成跨模块语义分析,所需 BMI/PCM 仍必须由兼容的 LLVM 工具链预编译; - “不执行正式 build”不等于完全不产生模块二进制产物。 -- 第一版完整语义能力只面向 LLVM。GCC `.gcm` 和 MSVC `.ifc` 不能直接交给 clangd, - 在没有独立 LLVM 分析配置前仍只保证模块语法高亮。 -- clangd 负责编辑期语法和语义,不替代链接、代码生成、运行期或完整打包检查;这些错误 - 仍由正式 `mcpp build`/`mcpp test` 报告。 -- 当前 `0.2.1` 仍是过渡方案:它能在用户显式构建后自动接管 CDB,并提供常用 CLI 操作, - 但无法仅凭现有插件 - 在无 CDB 时推导完整依赖图。最终体验依赖上述 mcpp 核心协议和 IDE 配置阶段。 +### 工作区信任与外部操作 + +未受信任工作区只启用文件关联和语法高亮,不执行 CDB 中的编译器、mcpp、clangd 或 +工程指定程序,也不接管 clangd 配置。授予信任后,扩展会自动重新协调当前工程;仍不 +会在没有用户操作时下载工具链或发起构建。 + +## 当前版本能达到的效果 + +在 LLVM 工程已经具有有效 CDB、完整 PCM 映射和匹配 clangd 的前提下,当前版本可以: + +- 打开工程后自动恢复 clangd 配置和模块状态; +- 由 clangd 对错误导入、不可用模块及跨模块声明提供诊断; +- 由 clangd 提供悬停、补全、定义跳转、引用查找和后台索引; +- 在重新构建或 CDB 变化后自动刷新,不要求 Reload Window; +- 通过 VS Code 任务完成常用 mcpp 项目和工具链操作。 + +这些能力受 clangd 实验模块实现和 PCM 兼容性约束。工程没有 CDB 时,当前版本只能 +提供语法高亮、mcpp 命令和引导操作,不能仅凭插件恢复完整模块语义。 + +## 理想最终效果 + +理想目标仍聚焦 LLVM 模块语义。用户打开一个受信任的 mcpp 工程后,不需要先执行 +完整的 `mcpp build`,也不需要 Reload Window: + +1. mcpp 解析工作区、目标、依赖、工具链、生成源和完整模块图。 +2. 按用户授权准备缺失依赖和工具链,并生成专供 IDE 使用的 CDB 和 PCM。 +3. 插件显示可取消进度,成功后原子切换到新 IDE 数据并启动匹配 clangd。 +4. 即使普通实现源码暂时无法编译或链接,编辑器仍能对错误模块名、不可见模块和导入 + 关系给出精确诊断。 +5. 模块导出的声明可以稳定悬停、补全、跳转定义、查找引用和重命名。 +6. 修改源码或 `mcpp.toml` 后增量刷新;离线时复用本机缓存和最后一次可用数据。 +7. 多根工作区中的工程拥有独立状态、进度、取消操作和 last-known-good 数据。 + +这里的“不需要完整构建”是指不编译普通对象文件、不链接最终程序或库。clangd 要进行 +跨模块语义分析,仍然必须由兼容 LLVM 工具链生成 IDE 专用 PCM,因此不等于完全不运行 +编译器。 + +## 达到理想效果所需支持 + +下面按完成程度区分必要工作。接口名称只是设计示例,不是当前 mcpp 已提供的命令。 + +| 支持项 | 当前程度 | 理想完成标准 | 优先级 | +| --- | --- | --- | --- | +| 机器可读工程模型 | 插件只能读取 CDB,mcpp 主要输出面向人的文本 | mcpp 输出带版本 schema 的成员、目标、源码、模块、依赖、工具链、缓存与诊断 | 必需 | +| IDE 配置阶段 | 必须通过正式构建间接生成 CDB | 提供类似 `mcpp resolve --message-format=json --emit-cdb` 或 `mcpp configure --ide` 的可取消命令 | 必需 | +| IDE 专用模块产物 | PCM 是正式构建的副产物 | 只构建 `std`、依赖和项目模块 PCM,不编译普通对象或链接,并按工具链身份隔离缓存 | 必需 | +| 完整 CDB 与模块图 | LLVM CDB 已有部分显式 PCM 参数 | producer 和 consumer 都使用确定的绝对 PCM 路径,覆盖分区、依赖包、成员和测试目标 | 必需 | +| 结构化诊断 | 未知导入等信息主要来自构建或 clangd 文本 | mcpp 输出文件、行、列、严重级别和稳定错误码,插件可精确定位配置期错误 | 必需 | +| 原子快照与回退 | CDB 变化后直接重新读取 | 新数据完整后原子切换,失败时保留 last-known-good CDB、PCM 和状态 | 必需 | +| 插件生命周期 | 已有监听、singleflight、串行化和工作区信任 | 增加进度、取消、在线/离线/仅缓存模式、过期状态和按工程隔离的配置任务 | 必需 | +| 增量更新 | CDB 变化后整体重新协调 | 只重扫受影响模块并复用兼容 PCM,`mcpp.toml` 变化时增量解析依赖 | 增强 | +| 端到端验证 | 主要是单元测试和直接 `clangd --check` | 在真实 VS Code、LLVM 和示例工程中验证诊断、悬停、补全、跳转、离线及多根行为 | 发布门槛 | +| GCC/MSVC 模块语义 | 仅语法高亮 | 等待成熟原生语言服务,或另行设计 LLVM 影子分析;不属于当前路线 | 暂缓 | + +推荐的职责边界接近 Cargo 与 rust-analyzer:mcpp 提供稳定、可查询的工程模型和 IDE +产物,clangd 消费 CDB 与 PCM,VS Code 扩展只负责授权、生命周期、状态和错误展示。 +插件不应重新实现一套 `mcpp.toml` 解析器、依赖解析器或包管理器。 + +## 路线图 + +### 阶段 1:当前版本 + +- 使用用户显式构建后产生的 LLVM CDB 和 PCM。 +- 自动配置 clangd、检查模块、监听变化并提供 CLI/工具链菜单。 +- GCC/MSVC 保持语法高亮和 mcpp 构建操作,不增加语义后端。 + +### 阶段 2:mcpp IDE 协议 + +- 在 mcpp 核心增加带版本的机器可读工程模型和 IDE 配置命令。 +- 生成完整模块图、结构化诊断、IDE CDB 和专用 PCM。 +- 明确定义工作区成员、配置身份、缓存路径、进度和取消协议。 + +### 阶段 3:打开即用的 LLVM 体验 + +- 插件在工作区信任后自动调用 IDE 配置阶段,而不是完整构建。 +- 支持离线缓存、last-known-good、增量刷新、过期状态和多根隔离。 +- 以真实 VS Code 端到端测试作为模块诊断和跳转能力的发布门槛。 + +### 未来重新评估 GCC/MSVC + +当前不适配 GCC/MSVC 模块语义。只有满足以下条件之一时再单独设计: + +- 出现能够稳定消费 `.gcm` 或 `.ifc`,并提供诊断、索引、悬停和跳转的语言服务; +- mcpp 能生成与正式 GCC/MSVC 构建隔离的 LLVM IDE 分析副本,并明确接受两套编译器 + 在宏、扩展和诊断上的差异。 + +重新评估不会改变当前原则:正式构建结果以项目选择的 mcpp 工具链为准,IDE 分析不能 +代替 `mcpp build` 和 `mcpp test`。 ## 故障排查 -- mcpp 生成的 PCM 必须由同一 LLVM revision 的 clangd 读取。Homebrew clangd 即使 - 版本号相同,也可能与 mcpp xlings 工具链来自不同 revision。此时应将 - `mcpp.clangd.path` 留空,让扩展自动选择 `.xlings` 中匹配的 clangd,或显式设置 - 对应的 `.xlings/data/xpkgs/xim-x-llvm-tools//bin/clangd`。 -- 独立 mcpp 的 LLVM 编译器通常位于 `.mcpp/registry/.../xim-x-llvm//bin`。 - 扩展会尝试映射到 `.xlings/.../xim-x-llvm-tools//bin/clangd`;如果该工具包 - 未安装,则只需安装匹配的 clangd 或配置 `mcpp.clangd.path`,不需要改用 xlings 版 - mcpp。 -- 扩展会把已有参数中的 `${workspaceFolder}` 和 `${workspaceRoot}` 展开为当前 mcpp - 工程根目录,避免 clangd 将变量名当成字面路径后忽略编译数据库。 -- 安装后应能在 `code --list-extensions` 中看到 `mcpp-community.mcpp-vscode`。仅保留在 - `~/.vscode/extensions` 目录、但没有登记到当前 VS Code profile 时,扩展不会激活, - 语法注入和 clangd 自动配置也都不会发生。 +### 扩展没有激活 + +- 确认工作区包含 `mcpp.toml`,或已经打开一个识别为 `cpp` 的文件。 +- 执行 `code --list-extensions`,确认当前 VS Code profile 中存在 + `mcpp-community.mcpp-vscode`。 +- 只有扩展目录但没有登记到当前 profile 时,语法注入和自动配置都不会发生。 + +### 找不到 mcpp + +macOS 从图形界面启动 VS Code 时可能没有继承终端 `PATH`。将 `mcpp.path` 设置为实际 +可执行文件,例如 `/opt/homebrew/bin/mcpp`。这不会改变工程编译器或 clangd 路径。 + +### 缺少模块 CDB + +当前版本不会在打开工程时静默构建。执行 **mcpp: 刷新编译数据库**,或在终端运行 +`mcpp build`。无论构建成功还是失败,只要工程根目录仍没有 +`compile_commands.json`,插件都会保持“缺少模块 CDB”状态;请查看 mcpp 输出和任务 +终端,确认当前构建是否实际生成了 CDB。 + +### clangd 未安装或无法匹配 + +- `mcpp.clangd.path` 可以指向任意安装来源的真实 clangd,不要求与 mcpp 同目录。 +- 自动发现会尝试编译器同目录、匹配的 xlings `xim-x-llvm-tools//bin` 和 + `PATH`。 +- 独立 mcpp 的 LLVM 编译器可以配合单独安装的 xlings llvm-tools 或其他匹配 clangd。 +- Homebrew clangd 即使版本号相同,也可能与 mcpp 工具链来自不同 revision。 + +### 模块不可用或跳转失败 + +- 查看 `mcpp` 输出频道中的 `clangd --check` 完整输出。 +- 确认 CDB 使用 C++20 或更高标准,并包含当前文件的编译命令。 +- 确认 PCM 路径存在,且 clangd、编译器、标准库和 PCM 来自同一 LLVM 构建。 +- 修改工具链或 CDB 后等待自动协调,必要时执行 **mcpp: 检查模块支持**。 + +### 出现两套诊断 + +clangd 和 Microsoft C/C++ IntelliSense 可能同时报告诊断。执行 +**mcpp: 配置 clangd** 时,扩展可以询问是否仅在当前工作区关闭 cpptools +IntelliSense;也可以将 `mcpp.configureCppTools` 设为 `false` 保留它。 ## 开发 @@ -264,19 +354,22 @@ npm test npm run compile ``` -生成的 `dist/` 是扩展运行时目录。请在 Extension Development Host 中运行上述 -扩展,验证 VS Code API 集成。 +生成的 `dist/` 是扩展运行时目录。请在 Extension Development Host 中验证真实 VS Code +API、状态栏、任务和 clangd 集成。 ## 发布 -先更新 `package.json`、`package-lock.json` 和更新日志中的版本并提交到主分支,然后 -推送与扩展版本完全一致的 tag: +先更新 `package.json`、`package-lock.json` 和更新日志中的版本并提交,再推送与扩展 +版本完全一致的 tag: ```sh git tag -a v0.2.1 -m "mcpp-vscode 0.2.1" git push origin v0.2.1 ``` -`.github/workflows/release.yml` 会自动执行完整测试、打包 VSIX、生成 SHA-256 校验文件, -并创建或更新对应的 GitHub Release。tag 与 `package.json` 版本不一致时发布会失败; -该工作流不发布 VS Marketplace。 +`.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, +并创建或更新对应的 GitHub Release。工作流当前不会发布到 VS Marketplace。 + +项目地址:[wellwei/mcpp-vscode](https://github.com/wellwei/mcpp-vscode) + +问题反馈:[GitHub Issues](https://github.com/wellwei/mcpp-vscode/issues) From d5248df828b51862927d235f8acb2b7b6736df03 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 2 Aug 2026 18:41:14 +0800 Subject: [PATCH 011/119] feat: add one-click llvm-tools install and auto-configure wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - detect ast_file_version_too_new/old as pcm-mismatch - new src/llvmTools.ts: xlings detection, version extraction, path derivation - autoConfigureModulesWizard: 8-step guided setup (check toolchain → install matching llvm-tools via xlings → configure clangd → rebuild) - status bar '模块不可用' now shows context-aware QuickPick with auto-fix - enhanced pcm-mismatch error message with actionable guidance - McppOperationRegistry.hasActive() and McppCliController.isBusy() - executeXlingsInstallTask() helper for xlings vscode.Task execution version 0.2.2 --- package.json | 9 +- src/analysis.ts | 4 + src/cliController.ts | 13 +- src/commands.ts | 2 + src/extension.ts | 331 ++++++++++++++++++++++++++++++++++++++++- src/llvmTools.ts | 92 ++++++++++++ src/tasks.ts | 4 + src/workflow.ts | 16 +- test/analysis.test.ts | 12 ++ test/artifacts.test.ts | 3 +- test/commands.test.ts | 2 + test/llvmTools.test.ts | 70 +++++++++ test/tasks.test.ts | 18 +++ 13 files changed, 564 insertions(+), 12 deletions(-) create mode 100644 src/llvmTools.ts create mode 100644 test/llvmTools.test.ts diff --git a/package.json b/package.json index f805eec..73c50e4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.1", + "version": "0.2.2", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", @@ -33,7 +33,8 @@ "onCommand:mcpp.clean", "onCommand:mcpp.showToolchains", "onCommand:mcpp.installToolchain", - "onCommand:mcpp.selectDefaultToolchain" + "onCommand:mcpp.selectDefaultToolchain", + "onCommand:mcpp.autoConfigureModules" ], "categories": [ "Programming Languages", @@ -91,6 +92,10 @@ { "command": "mcpp.checkModuleSupport", "title": "mcpp: 检查模块支持" + }, + { + "command": "mcpp.autoConfigureModules", + "title": "mcpp: 一键配置模块代码提示" } ], "configuration": { diff --git a/src/analysis.ts b/src/analysis.ts index 48e1356..6c8035a 100644 --- a/src/analysis.ts +++ b/src/analysis.ts @@ -372,6 +372,10 @@ export function classifyCheckResult(exitCode: number, output: string): CheckResu normalized.includes("ast_file_different_branch") || normalized.includes("different branch") || normalized.includes("pch file uses an older pch format") + || normalized.includes("ast_file_version_too_new") + || normalized.includes("newer format that cannot be read") + || normalized.includes("ast_file_version_too_old") + || normalized.includes("older format that is no longer supported") ) { return "pcm-mismatch"; } diff --git a/src/cliController.ts b/src/cliController.ts index c17e955..e3453df 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -104,6 +104,10 @@ export class McppCliController { this.status.show(); } + public isBusy(): boolean { + return this.operations.hasActive(); + } + public async runProjectTask(kind: ProjectTaskKind): Promise { const project = this.requireProject(); if (project === undefined || !this.requireTrusted()) { @@ -351,10 +355,13 @@ export class McppCliController { } const buildChoice = await vscode.window.showInformationMessage( - `mcpp 全局默认已更新为 ${picked.spec} + host target。当前项目不会自动切换,下一次构建时才会解析新的默认值。`, - ...(project === undefined ? [] : ["立即构建"]), + `mcpp 全局默认已更新为 ${picked.spec} + host target。建议清理旧工具链产物后重新构建。`, + ...(project === undefined ? [] : ["清理并构建"]), ); - if (buildChoice === "立即构建") { + if (buildChoice === "清理并构建" && project !== undefined) { + const cleanArgs = mcppCommandArguments("clean"); + const cleanResult = await runProcess(this.mcppExecutable(project), cleanArgs, project.root); + this.appendShortCommand("清理旧产物", this.mcppExecutable(project), cleanArgs, cleanResult); await this.runProjectTask("build"); } } diff --git a/src/commands.ts b/src/commands.ts index 9a46aaf..766b3b9 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -7,6 +7,7 @@ export const CLI_COMMANDS = { showToolchains: "mcpp.showToolchains", installToolchain: "mcpp.installToolchain", selectDefaultToolchain: "mcpp.selectDefaultToolchain", + autoConfigureModules: "mcpp.autoConfigureModules", } as const; export interface QuickMenuItem { @@ -26,5 +27,6 @@ export const quickMenuItems: readonly QuickMenuItem[] = [ { label: "$(symbol-interface) 配置 clangd", command: "mcpp.configureClangd", group: "ide" }, { label: "$(database) 刷新编译数据库", command: "mcpp.refreshCompilationDatabase", group: "ide" }, { label: "$(check) 检查模块支持", command: "mcpp.checkModuleSupport", group: "ide" }, + { label: "$(rocket) 一键配置模块代码提示", command: CLI_COMMANDS.autoConfigureModules, group: "ide" }, ]; diff --git a/src/extension.ts b/src/extension.ts index 99de45f..4d492d8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,11 @@ import { findNearestMcppProject, type McppProjectDiscovery, } from "./discovery"; +import { + findXlingsExecutable, + llvmToolsVersionSpec, + xlingsInstallArgs, +} from "./llvmTools"; import { McppCliController } from "./cliController"; import { runClangdCheck, runToolVersion, type ToolVersionResult } from "./process"; import { @@ -37,6 +42,7 @@ import { workspaceAllowsToolExecution, type ModuleSupportState, } from "./workflow"; +import { classifyTaskExit, type TaskCompletion } from "./tasks"; const COMMAND_CONFIGURE = "mcpp.configureClangd"; const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; @@ -68,6 +74,7 @@ type ConfigureMode = "automatic" | "interactive"; const moduleStatusByProject = new Map(); const moduleCheckOperations = createLatestOperationTracker(); +let lastReconciledProjectRoot: string | undefined; function findCurrentProject(): McppProjectDiscovery | undefined { const activePath = vscode.window.activeTextEditor?.document.uri.scheme === "file" @@ -420,7 +427,7 @@ function checkResultMessage(classification: CheckResult): string { case "ready": return "clangd 已加载 mcpp 编译命令和模块产物。"; case "pcm-mismatch": - return "clangd 与 mcpp 编译器或 PCM 来自不同的 LLVM 构建,请选择同一 LLVM revision 的 clangd。"; + return "clangd 与 mcpp 编译器或 PCM 来自不同的 LLVM 构建。建议运行“一键配置模块代码提示”安装匹配的 llvm-tools,或设置 mcpp.clangd.path。"; case "module-unavailable": return "clangd 无法加载所需模块产物,请运行“mcpp: 刷新编译数据库”,并检查 CDB 中的 PCM 路径。"; case "wrong-language-mode": @@ -576,6 +583,254 @@ async function updateModuleSupportForContext( return false; } +async function executeXlingsInstallTask( + xlingsPath: string, + args: string[], + cwd: string, +): Promise { + const task = new vscode.Task( + { type: "mcpp-xlings", command: args[0] ?? "xlings" }, + vscode.TaskScope.Workspace, + "mcpp: 安装 llvm-tools", + "mcpp", + new vscode.ProcessExecution(xlingsPath, args, { cwd }), + ); + task.presentationOptions = { + reveal: vscode.TaskRevealKind.Always, + panel: vscode.TaskPanelKind.Dedicated, + focus: true, + clear: true, + showReuseMessage: false, + }; + + let execution: vscode.TaskExecution | undefined; + let earlyCompletion: TaskCompletion | undefined; + let settled = false; + let processEndSubscription: vscode.Disposable | undefined; + let taskEndSubscription: vscode.Disposable | undefined; + const disposeListeners = (): void => { + processEndSubscription?.dispose(); + taskEndSubscription?.dispose(); + }; + const finish = (completion: TaskCompletion): void => { + if (settled) { + return; + } + settled = true; + disposeListeners(); + resolveCompletion?.(completion); + }; + let resolveCompletion: ((completion: TaskCompletion) => void) | undefined; + + const completion = new Promise((resolve) => { + resolveCompletion = resolve; + processEndSubscription = vscode.tasks.onDidEndTaskProcess((event) => { + if (event.execution.task !== task) { + return; + } + const classified = classifyTaskExit(event.exitCode); + if (execution === undefined) { + earlyCompletion ??= classified; + return; + } + finish(classified); + }); + taskEndSubscription = vscode.tasks.onDidEndTask((event) => { + if (event.execution.task !== task) { + return; + } + const classified = classifyTaskExit(undefined); + if (execution === undefined) { + earlyCompletion ??= classified; + return; + } + finish(classified); + }); + }); + + try { + execution = await vscode.tasks.executeTask(task); + } catch (error) { + disposeListeners(); + throw error; + } + if (earlyCompletion !== undefined) { + finish(earlyCompletion); + } + return completion; +} + +async function autoConfigureModulesWizard( + context: ProjectContext, + status: vscode.StatusBarItem, + output: vscode.OutputChannel, + cliController: McppCliController, +): Promise { + appendOutputLine(output, "[一键配置] 开始一键配置模块代码提示..."); + + // Step 1: Guard checks + if (context.analysis.capability === "unavailable") { + await vscode.window.showErrorMessage(context.analysis.reason); + return; + } + if (context.analysis.capability === "syntax-only") { + const kind = context.analysis.kind.toUpperCase(); + const install = "安装 LLVM 工具链"; + const choice = await vscode.window.showWarningMessage( + `当前工具链是 ${kind},其模块产物不能被 clangd 使用。是否安装 LLVM 工具链?`, + install, + "跳过", + ); + if (choice === install) { + await vscode.commands.executeCommand("mcpp.installToolchain"); + } + return; + } + if (context.analysis.compilerPath === undefined) { + await vscode.window.showErrorMessage("compile_commands.json 中没有可用的编译器路径。"); + return; + } + if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { + await vscode.window.showWarningMessage( + "当前工作区未受信任,不会执行外部程序。请先信任工作区。", + ); + return; + } + + // Step 2: Check current clangd state + const compilerVersion = await runToolVersion(context.analysis.compilerPath); + appendOutputLine( + output, + `[一键配置] 编译器版本:LLVM ${llvmToolsVersionSpec(compilerVersion.identity ?? { major: 0, minor: 0, patch: 0 })}`, + ); + + const clangd = await resolveClangd(context); + if (clangd?.comparison.compatible) { + appendOutputLine(output, "[一键配置] 已找到兼容的 clangd,跳过安装步骤。"); + // Skip to configuration + } else { + if (clangd !== undefined) { + appendOutputLine( + output, + `[一键配置] 未找到兼容的 clangd。原因:${clangd.comparison.reason}`, + ); + } else { + appendOutputLine(output, "[一键配置] 未找到任何可用的 clangd。"); + } + + // Step 3: Check xlings + const xlingsPath = findXlingsExecutable(); + if (xlingsPath === undefined) { + const installXlings = "安装 xlings"; + const setPath = "设置自定义 clangd 路径"; + const choice = await vscode.window.showErrorMessage( + "未找到 xlings,无法自动下载匹配的 clangd。请先安装 xlings(https://xlings.org),\n或手动安装与编译器版本匹配的 llvm-tools,或在设置中指定 clangd 路径。", + installXlings, + setPath, + "跳过", + ); + if (choice === installXlings) { + await vscode.env.openExternal( + vscode.Uri.parse("https://xlings.org"), + ); + } else if (choice === setPath) { + await vscode.commands.executeCommand( + "workbench.action.openSettings", + "mcpp.clangd.path", + ); + } + return; + } + + // Step 4: Offer installation + const version = compilerVersion.identity !== undefined + ? llvmToolsVersionSpec(compilerVersion.identity) + : "latest"; + const install = "$(cloud-download) 安装 llvm-tools(推荐)"; + const setPath = "$(settings-gear) 设置自定义 clangd 路径"; + const choice = await vscode.window.showWarningMessage( + `未找到与 mcpp 编译器匹配的 clangd(LLVM ${version})。是否通过 xlings 安装 llvm-tools?`, + { modal: true, detail: "xlings 将自动下载并安装匹配版本的 clangd、clang-format 和 clang-tidy。" }, + install, + setPath, + "跳过", + ); + if (choice === setPath) { + await vscode.commands.executeCommand( + "workbench.action.openSettings", + "mcpp.clangd.path", + ); + return; + } + if (choice !== install) { + return; + } + + // Check concurrency + if (cliController.isBusy()) { + await vscode.window.showWarningMessage( + "已有 mcpp 操作正在运行,请等待完成后再试。", + ); + return; + } + + // Note: xlings install is not tracked by McppOperationRegistry because it + // operates on xlings-managed paths (not mcpp). The serial executor + // (executeWithWorkspaceClangd) prevents concurrent wizard runs, and the + // isBusy() check catches active mcpp operations before install starts. + + // Execute xlings install + appendOutputLine(output, `[一键配置] 正在通过 xlings 安装 llvm-tools(${xlingsPath})...`); + const installArgs = xlingsInstallArgs( + compilerVersion.identity !== undefined ? version : undefined, + ); + const xlingsResult = await executeXlingsInstallTask( + xlingsPath, + installArgs, + context.project.root, + ); + appendOutputLine( + output, + `[一键配置] xlings install 完成(退出码 ${xlingsResult.exitCode ?? "未知"})`, + ); + if (xlingsResult.state !== "succeeded") { + await vscode.window.showErrorMessage( + `llvm-tools 安装${xlingsResult.state === "cancelled" ? "已取消" : "失败"}。请查看任务终端获取详细信息。`, + ); + return; + } + } + + // Step 5: Re-resolve clangd + const resolvedClangd = await resolveClangd(context); + if (resolvedClangd === undefined || !resolvedClangd.comparison.compatible) { + await vscode.window.showErrorMessage( + "llvm-tools 安装完成,但未找到匹配的 clangd。请检查 xlings 安装日志或手动设置 mcpp.clangd.path。", + ); + return; + } + appendOutputLine(output, `[一键配置] 已找到 clangd:${resolvedClangd.path}`); + + // Step 6: Configure clangd + await configureClangd(context, status, output, "interactive", true); + + // Step 7: Offer rebuild + const rebuild = "$(sync) 刷新编译数据库"; + const done = "$(close) 完成"; + const postChoice = await vscode.window.showInformationMessage( + "clangd 配置完成。建议刷新编译数据库以生成模块 PCM。", + rebuild, + done, + ); + if (postChoice === rebuild) { + await cliController.runProjectTask("build"); + } + + // Step 8: Refresh module status and update status bar + await runModuleSupportCheck(context, status, output, "automatic"); + appendOutputLine(output, `[一键配置] 一键配置完成。clangd:${resolvedClangd.path}`); +} + export async function activate(extensionContext: vscode.ExtensionContext): Promise { moduleStatusByProject.clear(); moduleCheckOperations.clear(); @@ -773,9 +1028,70 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { return; } - const context = loadProjectContext(project); - if (context !== undefined) { - await runModuleSupportCheck(context, status, output, "interactive"); + const ctx = loadProjectContext(project); + if (ctx === undefined) { + return; + } + + // When module support is already known to be unavailable, offer actionable choices + const currentStatus = moduleStatusByProject.get(ctx.project.root); + if (currentStatus?.state === "unavailable" && ctx.analysis.capability === "full") { + interface StatusActionItem extends vscode.QuickPickItem { + action: "auto-configure" | "recheck" | "rebuild"; + } + const choice = await vscode.window.showQuickPick([ + { + label: "$(rocket) 一键配置模块代码提示", + description: "自动安装匹配的 llvm-tools 并配置 clangd", + action: "auto-configure", + }, + { + label: "$(check) 重新检查模块支持", + description: "再次运行 clangd 模块检查", + action: "recheck", + }, + { + label: "$(database) 刷新编译数据库", + description: "重新生成 compile_commands.json", + action: "rebuild", + }, + ], { + title: "mcpp 模块代码提示不可用", + placeHolder: "选择操作以修复模块代码提示", + }); + if (choice === undefined) { + return; + } + switch (choice.action) { + case "auto-configure": + await autoConfigureModulesWizard(ctx, status, output, cliController); + return; + case "recheck": + await runModuleSupportCheck(ctx, status, output, "interactive"); + return; + case "rebuild": + await cliController.runProjectTask("build"); + return; + } + } + + // Original flow: run module check + await runModuleSupportCheck(ctx, status, output, "interactive"); + }); + })), + vscode.commands.registerCommand("mcpp.autoConfigureModules", runGuarded(async () => { + const project = findCurrentProject(); + if (project === undefined) { + await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); + return; + } + await executeWithWorkspaceClangd(async () => { + if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { + return; + } + const ctx = loadProjectContext(project); + if (ctx !== undefined) { + await autoConfigureModulesWizard(ctx, status, output, cliController); } }); })), @@ -783,10 +1099,15 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi trustWatcher, vscode.window.onDidChangeActiveTextEditor(() => { refreshStatus(); - requestCurrentProjectReconciliation(true); + const current = findCurrentProject(); + if (current !== undefined && current.root !== lastReconciledProjectRoot) { + lastReconciledProjectRoot = current.root; + requestCurrentProjectReconciliation(false); + } }), vscode.workspace.onDidChangeWorkspaceFolders(() => { refreshStatus(); + lastReconciledProjectRoot = undefined; requestCurrentProjectReconciliation(true); }), manifestWatcher, diff --git a/src/llvmTools.ts b/src/llvmTools.ts new file mode 100644 index 0000000..cb808eb --- /dev/null +++ b/src/llvmTools.ts @@ -0,0 +1,92 @@ +import { existsSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import process from "node:process"; + +import type { ToolIdentity } from "./analysis"; +import { runProcess, type ProcessResult } from "./process"; + +export function llvmToolsVersionSpec(identity: ToolIdentity): string { + return `${identity.major}.${identity.minor}.${identity.patch}`; +} + +const COMPILER_PATH_VERSION_REGEX = /[\\/]xim-x-llvm[\\/](\d+(?:\.\d+){1,3})[\\/]bin[\\/]clang(?:\+\+|-cl)?(?:\.exe)?$/i; + +export function extractVersionFromCompilerPath(compilerPath: string): string | undefined { + const match = compilerPath.match(COMPILER_PATH_VERSION_REGEX); + return match?.[1]; +} + +function isWindowsPath(value: string): boolean { + return /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\"); +} + +export function deriveInstalledClangdPath( + compilerPath: string, + version: string, +): string[] { + const windows = isWindowsPath(compilerPath); + const pathApi = windows ? path.win32 : path.posix; + const candidateMatch = compilerPath.match( + /^(.*?[\\/])xim-x-llvm[\\/]\d+(?:\.\d+){1,3}[\\/]bin[\\/]clang(?:\+\+|-cl)?(?:\.exe)?$/i, + ); + const basePath = candidateMatch?.[1]; + const extension = /\.exe$/i.test(compilerPath) ? ".exe" : ""; + + const candidates: string[] = []; + if (basePath !== undefined) { + candidates.push( + pathApi.join(basePath, "xim-x-llvm-tools", version, "bin", `clangd${extension}`), + ); + } + + // Also try user xlings registry path if compiler is in mcpp registry + const userMatch = compilerPath.match( + /^(.*?)([\\/])\.mcpp[\\/]registry[\\/]data[\\/]xpkgs[\\/]xim-x-llvm[\\/]\d+(?:\.\d+){1,3}[\\/]bin[\\/]clang(?:\+\+|-cl)?(?:\.exe)?$/i, + ); + if (userMatch !== null) { + candidates.push( + `${userMatch[1]}${userMatch[2]}.xlings${userMatch[2]}data${userMatch[2]}xpkgs${userMatch[2]}xim-x-llvm-tools${userMatch[2]}${version}${userMatch[2]}bin${userMatch[2]}clangd${extension}`, + ); + } + + return candidates; +} + +export function xlingsInstallArgs(version?: string): string[] { + if (version !== undefined) { + return ["install", `xim:llvm-tools@${version}`]; + } + return ["update", "llvm-tools"]; +} + +export function findXlingsExecutable(): string | undefined { + const home = os.homedir(); + const knownPaths = [ + path.join(home, ".xlings", "subos", "current", "bin", "xlings"), + path.join(home, ".xlings", "bin", "xlings"), + ]; + if (process.platform === "win32") { + knownPaths.push( + path.join(home, ".xlings", "subos", "current", "bin", "xlings.exe"), + ); + } + + // Check known install paths first + for (const candidate of knownPaths) { + if (existsSync(candidate)) { + return candidate; + } + } + + // Fall back to PATH — execFile will resolve it + return "xlings"; +} + +export async function runXlingsCommand( + xlingsPath: string, + args: string[], + cwd?: string, +): Promise { + return runProcess(xlingsPath, args, cwd); +} diff --git a/src/tasks.ts b/src/tasks.ts index c667e80..ffa148c 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -81,4 +81,8 @@ export class McppOperationRegistry { this.globalToken = undefined; } } + + public hasActive(): boolean { + return this.projectTokens.size > 0 || this.globalToken !== undefined; + } } diff --git a/src/workflow.ts b/src/workflow.ts index ccd2fd1..2d7d549 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -102,9 +102,23 @@ export function createKeyedSingleFlightReconciler( export function createSerialExecutor(): (operation: () => Promise) => Promise { let tail: Promise = Promise.resolve(); + let depth = 0; return (operation: () => Promise): Promise => { - const result = tail.then(operation, operation); + if (depth > 0) { + return operation(); + } + + const wrapped = async (): Promise => { + depth += 1; + try { + return await operation(); + } finally { + depth -= 1; + } + }; + + const result = tail.then(wrapped, wrapped); tail = result.then( () => undefined, () => undefined, diff --git a/test/analysis.test.ts b/test/analysis.test.ts index b8653be..cd5beae 100644 --- a/test/analysis.test.ts +++ b/test/analysis.test.ts @@ -276,6 +276,18 @@ test("classifies clangd check failures into actionable states", () => { classifyCheckResult(1, "error: ast_file_different_branch"), "pcm-mismatch", ); + assert.equal( + classifyCheckResult(1, "error: module file '.../std.pcm' uses a newer format that cannot be read"), + "pcm-mismatch", + ); + assert.equal( + classifyCheckResult(1, "Module file uses a newer format that cannot be read ast_file_version_too_new"), + "pcm-mismatch", + ); + assert.equal( + classifyCheckResult(1, "error: module file uses an older format that is no longer supported"), + "pcm-mismatch", + ); assert.equal( classifyCheckResult(1, "Failed to build module std; Don't get the module unit"), "module-unavailable", diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 06e170e..dfc0f80 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -24,7 +24,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.1"); + assert.equal(manifest.version, "0.2.2"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); @@ -46,6 +46,7 @@ test("declares the official clangd dependency and mcpp commands", () => { "mcpp.configureClangd", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport", + "mcpp.autoConfigureModules", ], ); assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.path"]); diff --git a/test/commands.test.ts b/test/commands.test.ts index c930d90..28f329b 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -13,6 +13,7 @@ test("CLI 命令覆盖项目、工具链和 IDE", () => { "mcpp.showToolchains", "mcpp.installToolchain", "mcpp.selectDefaultToolchain", + "mcpp.autoConfigureModules", ]); assert.deepEqual( quickMenuItems.map((item) => item.command), @@ -27,6 +28,7 @@ test("CLI 命令覆盖项目、工具链和 IDE", () => { "mcpp.configureClangd", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport", + "mcpp.autoConfigureModules", ], ); assert.ok(quickMenuItems.every((item) => item.label.length > 0)); diff --git a/test/llvmTools.test.ts b/test/llvmTools.test.ts new file mode 100644 index 0000000..b6348f5 --- /dev/null +++ b/test/llvmTools.test.ts @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + llvmToolsVersionSpec, + extractVersionFromCompilerPath, + xlingsInstallArgs, + deriveInstalledClangdPath, + findXlingsExecutable, +} from "../src/llvmTools"; + +test("extracts version string from ToolIdentity", () => { + assert.equal( + llvmToolsVersionSpec({ major: 22, minor: 1, patch: 8, revision: "abc1234" }), + "22.1.8", + ); + assert.equal( + llvmToolsVersionSpec({ major: 20, minor: 1, patch: 7 }), + "20.1.7", + ); +}); + +test("extracts version from mcpp compiler path", () => { + assert.equal( + extractVersionFromCompilerPath("/home/user/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++"), + "22.1.8", + ); + assert.equal( + extractVersionFromCompilerPath("C:\\Users\\test\\.mcpp\\registry\\data\\xpkgs\\xim-x-llvm\\20.1.7\\bin\\clang.exe"), + "20.1.7", + ); +}); + +test("returns undefined for non-xlings compiler paths", () => { + assert.equal(extractVersionFromCompilerPath("/usr/bin/clang++"), undefined); + assert.equal(extractVersionFromCompilerPath("/opt/homebrew/opt/llvm/bin/clang"), undefined); +}); + +test("builds xlings update args without version (auto-detect)", () => { + assert.deepEqual(xlingsInstallArgs(), ["update", "llvm-tools"]); +}); + +test("builds xlings install args with explicit version", () => { + assert.deepEqual(xlingsInstallArgs("22.1.8"), ["install", "xim:llvm-tools@22.1.8"]); +}); + +test("derives clangd path from mcpp registry compiler path", () => { + const paths = deriveInstalledClangdPath( + "/Users/test/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", + "22.1.8", + ); + assert.ok(paths.some((p) => p.includes("xim-x-llvm-tools"))); + assert.ok(paths.some((p) => p.includes("22.1.8"))); + assert.ok(paths.some((p) => p.endsWith("clangd"))); +}); + +test("derives clangd path with .exe extension on Windows", () => { + const paths = deriveInstalledClangdPath( + "C:\\Users\\test\\.mcpp\\registry\\data\\xpkgs\\xim-x-llvm\\20.1.7\\bin\\clang.exe", + "20.1.7", + ); + assert.ok(paths.every((p) => p.endsWith(".exe"))); + assert.ok(paths.some((p) => p.includes(".xlings") && p.includes("clangd.exe"))); +}); + +test("findXlingsExecutable returns a string or undefined", () => { + const result = findXlingsExecutable(); + // Returns string (PATH fallback or known path) or undefined if xlings not found + assert.ok(result === undefined || typeof result === "string"); +}); diff --git a/test/tasks.test.ts b/test/tasks.test.ts index fc5b8e5..9b9f8b2 100644 --- a/test/tasks.test.ts +++ b/test/tasks.test.ts @@ -72,3 +72,21 @@ test("项目任务锁和全局工具链锁互相阻塞且只接受原 token 解 registry.finishProject("/work/app", second); assert.equal(registry.beginGlobal(second), undefined); }); + +test("查询注册表是否有活跃操作", () => { + const registry = new McppOperationRegistry(); + assert.equal(registry.hasActive(), false); + + const token = {}; + assert.equal(registry.beginProject("/a", token), undefined); + assert.equal(registry.hasActive(), true); + + registry.finishProject("/a", token); + assert.equal(registry.hasActive(), false); + + assert.equal(registry.beginGlobal(token), undefined); + assert.equal(registry.hasActive(), true); + + registry.finishGlobal(token); + assert.equal(registry.hasActive(), false); +}); From 9dddad7d6f0eb73b67e0e4dddb8277f50403a8a6 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 2 Aug 2026 19:49:48 +0800 Subject: [PATCH 012/119] =?UTF-8?q?feat:=20=E4=B8=BA=E9=9D=9E=20LLVM=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=93=BE=E9=A1=B9=E7=9B=AE=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E6=B8=85=E6=99=B0=E7=9A=84=E5=BC=95=E5=AF=BC=E8=80=8C=E9=9D=9E?= =?UTF-8?q?=E5=82=AC=E4=BF=83=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wizard 检测到 GCC/MSVC: error 消息说明原因 + 提供安装/选择工具链入口 - 状态栏点击 GCC 项目: QuickPick 提供一键配置/安装/选择默认工具链 - 模块检查 interactive 模式: warning 消息带一键配置按钮形成闭环 --- src/extension.ts | 67 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 4d492d8..864dedc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -23,6 +23,7 @@ import { llvmToolsVersionSpec, xlingsInstallArgs, } from "./llvmTools"; +import { CLI_COMMANDS } from "./commands"; import { McppCliController } from "./cliController"; import { runClangdCheck, runToolVersion, type ToolVersionResult } from "./process"; import { @@ -445,10 +446,18 @@ async function runModuleSupportCheck( ): Promise { const interactive = mode === "interactive"; if (context.analysis.capability === "syntax-only") { - const message = `${context.analysis.kind.toUpperCase()} 模块产物不能由 clangd 消费,模块语法高亮仍然可用。`; + const message = `${context.analysis.kind.toUpperCase()} 模块产物不能由 clangd 消费,模块代码提示不可用。语法高亮仍然可用。`; updateStatusBar(status, context); if (interactive) { - await vscode.window.showWarningMessage(message); + const configure = "一键配置"; + const choice = await vscode.window.showWarningMessage( + `${message}\n\n如需启用模块代码提示,请切换到 LLVM 工具链后重新构建。`, + configure, + "关闭", + ); + if (choice === configure) { + await vscode.commands.executeCommand(CLI_COMMANDS.autoConfigureModules); + } } return undefined; } @@ -676,13 +685,17 @@ async function autoConfigureModulesWizard( if (context.analysis.capability === "syntax-only") { const kind = context.analysis.kind.toUpperCase(); const install = "安装 LLVM 工具链"; - const choice = await vscode.window.showWarningMessage( - `当前工具链是 ${kind},其模块产物不能被 clangd 使用。是否安装 LLVM 工具链?`, + const select = "选择默认工具链"; + const choice = await vscode.window.showErrorMessage( + `当前工具链是 ${kind},其模块产物不能被 clangd 消费,模块代码提示功能不可用。\n\n如需使用模块代码提示,请先切换到 LLVM 工具链后重新构建项目。`, install, - "跳过", + select, + "关闭", ); if (choice === install) { - await vscode.commands.executeCommand("mcpp.installToolchain"); + await vscode.commands.executeCommand(CLI_COMMANDS.installToolchain); + } else if (choice === select) { + await vscode.commands.executeCommand(CLI_COMMANDS.selectDefaultToolchain); } return; } @@ -1075,6 +1088,48 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi } } + // QuickPick for syntax-only (GCC/MSVC) projects — no module code intelligence available + if (ctx.analysis.capability === "syntax-only") { + interface SyntaxOnlyActionItem extends vscode.QuickPickItem { + action: "auto-configure" | "install-toolchain" | "select-default"; + } + const kindLabel = ctx.analysis.kind.toUpperCase(); + const choice = await vscode.window.showQuickPick([ + { + label: "$(rocket) 一键配置模块代码提示", + description: `当前为 ${kindLabel} 工具链,查看如何切换到 LLVM`, + action: "auto-configure", + }, + { + label: "$(cloud-download) 安装 LLVM 工具链", + description: "通过 mcpp 下载并安装 LLVM 工具链", + action: "install-toolchain", + }, + { + label: "$(settings-gear) 选择全局默认工具链", + description: "从已安装的工具链中选择默认", + action: "select-default", + }, + ], { + title: `mcpp: 当前为 ${kindLabel} 工具链,模块代码提示不可用`, + placeHolder: "切换到 LLVM 工具链以启用模块代码提示", + }); + if (choice === undefined) { + return; + } + switch (choice.action) { + case "auto-configure": + await autoConfigureModulesWizard(ctx, status, output, cliController); + return; + case "install-toolchain": + await vscode.commands.executeCommand(CLI_COMMANDS.installToolchain); + return; + case "select-default": + await vscode.commands.executeCommand(CLI_COMMANDS.selectDefaultToolchain); + return; + } + } + // Original flow: run module check await runModuleSupportCheck(ctx, status, output, "interactive"); }); From 5eb82d4aad7c771912233f4e329b5b5237cc1039 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 2 Aug 2026 20:04:14 +0800 Subject: [PATCH 013/119] fix: narrow serial executor reentrancy detection and fix xlings PATH check - createSerialExecutor: use AsyncLocalStorage-scoped flag instead of global depth counter so only same-async-context reentrant calls bypass the queue - findXlingsExecutable: check PATH entries with existsSync before returning 'xlings', so the wizard's 'install xlings' guidance branch is reachable - add 2 tests: concurrent caller queues correctly, reentrant call bypasses --- src/llvmTools.ts | 24 +++++++++++-- src/workflow.ts | 23 ++++++++++--- test/workflow.test.ts | 80 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 7 deletions(-) diff --git a/src/llvmTools.ts b/src/llvmTools.ts index cb808eb..be064f9 100644 --- a/src/llvmTools.ts +++ b/src/llvmTools.ts @@ -79,8 +79,28 @@ export function findXlingsExecutable(): string | undefined { } } - // Fall back to PATH — execFile will resolve it - return "xlings"; + // Fall back to PATH, but only when "xlings" actually resolves there. Always + // returning "xlings" hid the not-installed case, so callers could never show + // the "xlings 未安装" guidance. + return xlingsResolvableOnPath() ? "xlings" : undefined; +} + +function xlingsResolvableOnPath(): boolean { + const names = process.platform === "win32" + ? ["xlings.exe", "xlings.cmd", "xlings.bat"] + : ["xlings"]; + const pathValue = process.env.PATH ?? ""; + for (const dir of pathValue.split(path.delimiter)) { + if (dir.length === 0) { + continue; + } + for (const name of names) { + if (existsSync(path.join(dir, name))) { + return true; + } + } + } + return false; } export async function runXlingsCommand( diff --git a/src/workflow.ts b/src/workflow.ts index 2d7d549..3def1fb 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -1,3 +1,5 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + import type { CheckResult, ModuleCapability } from "./analysis"; export type RefreshOutcomeLevel = "information" | "warning" | "error"; @@ -102,19 +104,30 @@ export function createKeyedSingleFlightReconciler( export function createSerialExecutor(): (operation: () => Promise) => Promise { let tail: Promise = Promise.resolve(); - let depth = 0; + // `running` is scoped to the async context of the in-flight operation via + // AsyncLocalStorage instead of a global counter, so only genuinely reentrant + // calls bypass the queue: + // - a call made from inside an operation's own async chain (e.g. + // wizard → build → reconcile → executor) sees `running` and executes + // immediately — the outer operation is awaiting it, so queuing would hang; + // - an unrelated concurrent caller (a separate command/event that has its + // own async context) does not see it and queues behind the in-flight + // operation, which the previous `depth > 0` check would wrongly let run + // concurrently. + const state = { running: false }; + const storage = new AsyncLocalStorage<{ running: boolean }>(); return (operation: () => Promise): Promise => { - if (depth > 0) { + if (storage.getStore() === state && state.running) { return operation(); } const wrapped = async (): Promise => { - depth += 1; + state.running = true; try { - return await operation(); + return await storage.run(state, operation); } finally { - depth -= 1; + state.running = false; } }; diff --git a/test/workflow.test.ts b/test/workflow.test.ts index d665774..e56019b 100644 --- a/test/workflow.test.ts +++ b/test/workflow.test.ts @@ -3,6 +3,7 @@ import test from "node:test"; import * as workflow from "../src/workflow"; import { + createSerialExecutor, createSingleFlightReconciler, describeRefreshOutcome, statusCommandForCapability, @@ -132,6 +133,85 @@ test("serializes operations that share the workspace clangd configuration", asyn assert.deepEqual(calls, ["a:start", "a:end", "b:start", "b:end"]); }); +test("queues an unrelated concurrent caller behind an in-flight operation", async () => { + let releaseFirst: (() => void) | undefined; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + const calls: string[] = []; + let maxConcurrent = 0; + let active = 0; + const execute = createSerialExecutor(); + + const first = execute(async () => { + active += 1; + maxConcurrent = Math.max(maxConcurrent, active); + calls.push("a:start"); + await firstGate; + calls.push("a:end"); + active -= 1; + return "a"; + }); + + // A second, unrelated invocation arrives while the first is still in flight, + // from a separate async context (a macrotask, like a user command event). + let second: Promise | undefined; + const secondArrived = new Promise((resolve) => { + setTimeout(() => { + second = execute(async () => { + active += 1; + maxConcurrent = Math.max(maxConcurrent, active); + calls.push("b:start"); + calls.push("b:end"); + active -= 1; + return "b"; + }); + resolve(); + }, 10); + }); + await secondArrived; + + assert.deepEqual(calls, ["a:start"]); + assert.ok(releaseFirst); + releaseFirst(); + assert.ok(second); + assert.deepEqual(await Promise.all([first, second]), ["a", "b"]); + assert.deepEqual(calls, ["a:start", "a:end", "b:start", "b:end"]); + assert.equal(maxConcurrent, 1); +}); + +test("runs a reentrant call from inside an operation without queuing", async () => { + const calls: string[] = []; + let releaseInner: (() => void) | undefined; + const innerGate = new Promise((resolve) => { + releaseInner = resolve; + }); + const execute = createSerialExecutor(); + + const outer = execute(async () => { + calls.push("outer:start"); + await Promise.resolve(); + const inner = execute(async () => { + calls.push("inner:start"); + await innerGate; + calls.push("inner:end"); + return "inner"; + }); + calls.push("outer:mid"); + const value = await inner; + calls.push("outer:end"); + return value; + }); + + // The outer operation's continuation (which makes the reentrant call) runs in + // the microtask queue, before the timer below fires. + await new Promise((resolve) => setTimeout(resolve, 0)); + assert.ok(releaseInner); + releaseInner(); + assert.equal(await outer, "inner"); + assert.deepEqual(calls, ["outer:start", "inner:start", "outer:mid", "inner:end", "outer:end"]); +}); + test("retries a queued reconciliation after a transient failure", async () => { let releaseFirst: (() => void) | undefined; const firstGate = new Promise((resolve) => { From 1a881a107117e9978f5435d0de1321fbc29092e1 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 2 Aug 2026 20:11:08 +0800 Subject: [PATCH 014/119] docs: add 0.2.2 changelog and README for auto-configure wizard - changelog: 0.2.2 entry covering one-click auto-configure, PCM mismatch detection, non-LLVM toolchain UX, serial executor fix, xlings detection - README: update version to 0.2.2, add mcpp.autoConfigureModules to command table, add auto-configure to troubleshooting and LLVM integration sections, update roadmap and release tag example --- CHANGELOG.md | 14 ++++++++++++++ README.md | 16 ++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f29253c..603a475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # 更新日志 +## 0.2.2 + +- 新增 **mcpp: 一键配置模块代码提示**:自动检测匹配的 clangd,缺失时通过 xlings 下载 + 对应版本的 llvm-tools,配置 clangd 并刷新模块状态,无需重启 VS Code。 +- 检测 PCM 版本不匹配(`ast_file_version_too_new`、`ast_file_version_too_old`、 + `ast_file_different_branch`),并在诊断信息中引导一键配置。 +- 非 LLVM 工具链(GCC/MSVC)项目提供清晰的引导说明而非催促安装:状态栏点击弹出 + QuickPick,一键配置向导显示原因并提供工具链安装/选择入口。 +- 优化编辑器标签切换性能:相同工程内切换文件不再重复协调 clangd。 +- 序列执行器使用 AsyncLocalStorage 范围标记替代全局深度计数,修复不相关并发调用 + 绕过队列的问题。 +- 完善 xlings 检测:检查 PATH 中是否实际存在 xlings 而非无条件返回路径名,确保 + 未安装时的引导分支可达。 + ## 0.2.1 - 修复项目任务已经结束后,clangd/CDB 重协调仍占用项目锁,导致下一条 CLI 命令误报 diff --git a/README.md b/README.md index b8f8e89..c29732a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -当前版本为 `0.2.1`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +当前版本为 `0.2.2`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 > 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 @@ -35,7 +35,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.2.1.vsix +code --install-extension /path/to/mcpp-vscode-0.2.2.vsix ``` 安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 @@ -104,6 +104,10 @@ TextMate 语法规则提供。 检查可以区分模块可用、PCM 不匹配、模块产物不可用、语言模式错误和一般检查失败。 完整命令及输出会写入 `mcpp` 输出频道。 +当 clangd 与编译器不匹配时,**mcpp: 一键配置模块代码提示** 会通过 xlings 自动下载 +匹配版本的 llvm-tools(含 clangd),完成后配置 clangd 并刷新状态栏,无需 Reload +Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明,不做自动切换。 + ### 自动协调与多根工作区 - CDB 创建或变化后自动重新分析、配置、重启和检查;删除后更新为缺少 CDB 状态。 @@ -146,6 +150,7 @@ TextMate 语法规则提供。 | **mcpp: 配置 clangd** | 手动重新应用当前 LLVM 工程的 clangd 配置 | | **mcpp: 刷新编译数据库** | 执行 `mcpp build`,随后重新读取 CDB 并协调 clangd | | **mcpp: 检查模块支持** | 立即执行 clangd 直接检查并刷新模块状态 | +| **mcpp: 一键配置模块代码提示** | 自动检测匹配 clangd,缺失时通过 xlings 安装 llvm-tools 并配置 | ## 设置 @@ -279,6 +284,7 @@ GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它 - 使用用户显式构建后产生的 LLVM CDB 和 PCM。 - 自动配置 clangd、检查模块、监听变化并提供 CLI/工具链菜单。 +- 一键自动安装匹配的 llvm-tools 并配置模块代码提示。 - GCC/MSVC 保持语法高亮和 mcpp 构建操作,不增加语义后端。 ### 阶段 2:mcpp IDE 协议 @@ -327,6 +333,8 @@ macOS 从图形界面启动 VS Code 时可能没有继承终端 `PATH`。将 `mc ### clangd 未安装或无法匹配 +- 执行 **mcpp: 一键配置模块代码提示**,扩展会自动通过 xlings 安装与当前编译器 + 版本匹配的 llvm-tools(含 clangd)。 - `mcpp.clangd.path` 可以指向任意安装来源的真实 clangd,不要求与 mcpp 同目录。 - 自动发现会尝试编译器同目录、匹配的 xlings `xim-x-llvm-tools//bin` 和 `PATH`。 @@ -363,8 +371,8 @@ API、状态栏、任务和 clangd 集成。 版本完全一致的 tag: ```sh -git tag -a v0.2.1 -m "mcpp-vscode 0.2.1" -git push origin v0.2.1 +git tag -a v0.2.2 -m "mcpp-vscode 0.2.2" +git push origin v0.2.2 ``` `.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, From eb647248df912d96e004b9e16c3d413e3c740f93 Mon Sep 17 00:00:00 2001 From: wellwei Date: Tue, 4 Aug 2026 16:14:11 +0800 Subject: [PATCH 015/119] feat: add build.mcpp syntax highlighting --- CHANGELOG.md | 4 ++++ README.md | 12 ++++++------ package-lock.json | 4 ++-- package.json | 3 ++- test/artifacts.test.ts | 13 ++++++++++++- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603a475..8cc4ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +## 0.2.3 + +- 为精确文件名 `build.mcpp` 增加 C++ 语言关联,复用 VS Code 内置 C++ 高亮和现有模块语法注入;不扩大到所有 `*.mcpp` 文件。 + ## 0.2.2 - 新增 **mcpp: 一键配置模块代码提示**:自动检测匹配的 clangd,缺失时通过 xlings 下载 diff --git a/README.md b/README.md index c29732a..45b99b1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -当前版本为 `0.2.2`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +当前版本为 `0.2.3`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 > 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 @@ -17,7 +17,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 | 能力 | LLVM/Clang | GCC | MSVC | | --- | --- | --- | --- | | 识别 mcpp 工程 | 支持 | 支持 | 支持 | -| `.cppm`、`.ixx`、`.mpp`、`.ccm` 文件关联 | 支持 | 支持 | 支持 | +| `build.mcpp`、`.cppm`、`.ixx`、`.mpp`、`.ccm` 文件关联 | 支持 | 支持 | 支持 | | `module`、`export module`、`import` 语法高亮 | 支持 | 支持 | 支持 | | 构建、运行、测试、清理命令 | 支持 | 支持 | 支持 | | 查看、安装或选择 mcpp 工具链 | 支持 | 支持 | 系统检测与选择 | @@ -35,7 +35,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.2.2.vsix +code --install-extension /path/to/mcpp-vscode-0.2.3.vsix ``` 安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 @@ -80,7 +80,7 @@ xlings 安装。 ### C++ 模块语法高亮 -- 将 `.cppm`、`.ixx`、`.mpp` 和 `.ccm` 关联到 VS Code 内置 `cpp` 语言。 +- 将 `build.mcpp`、`.cppm`、`.ixx`、`.mpp` 和 `.ccm` 关联到 VS Code 内置 `cpp` 语言。 - 向 `source.cpp` 注入模块语法规则。 - 覆盖 `module`、`export module`、`import`、`export import`、模块名和模块分区。 - 支持尚未输入分号的编辑中间态。 @@ -371,8 +371,8 @@ API、状态栏、任务和 clangd 集成。 版本完全一致的 tag: ```sh -git tag -a v0.2.2 -m "mcpp-vscode 0.2.2" -git push origin v0.2.2 +git tag -a v0.2.3 -m "mcpp-vscode 0.2.3" +git push origin v0.2.3 ``` `.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, diff --git a/package-lock.json b/package-lock.json index b4291bd..36029a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.2.1", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.2.1", + "version": "0.2.3", "license": "Apache-2.0", "devDependencies": { "@types/node": "^24.0.0", diff --git a/package.json b/package.json index 73c50e4..4a29e69 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.2", + "version": "0.2.3", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", @@ -134,6 +134,7 @@ }, "configurationDefaults": { "files.associations": { + "build.mcpp": "cpp", "*.ccm": "cpp", "*.cppm": "cpp", "*.ixx": "cpp", diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index dfc0f80..91db1af 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -24,7 +24,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.2"); + assert.equal(manifest.version, "0.2.3"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); @@ -52,6 +52,7 @@ test("declares the official clangd dependency and mcpp commands", () => { assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.path"]); assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.modulesSupport"]); assert.deepEqual(manifest.contributes?.configurationDefaults?.["files.associations"], { + "build.mcpp": "cpp", "*.ccm": "cpp", "*.cppm": "cpp", "*.ixx": "cpp", @@ -59,6 +60,16 @@ test("declares the official clangd dependency and mcpp commands", () => { }); }); +test("associates the build.mcpp file with the C++ language", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + const associations = manifest.contributes?.configurationDefaults?.["files.associations"] as + | Record + | undefined; + + assert.equal(associations?.["build.mcpp"], "cpp"); + assert.equal(associations?.["*.mcpp"], undefined); +}); + test("ships an injection grammar with module-specific scopes", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; const grammar = manifest.contributes?.grammars?.find((item) => item.scopeName === "source.cpp.mcpp-modules"); From 15c5c1cc24ff1992f636f55db38f5e480a6c5676 Mon Sep 17 00:00:00 2001 From: wellwei Date: Tue, 4 Aug 2026 16:55:39 +0800 Subject: [PATCH 016/119] feat: add mcpp.toml syntax highlighting --- CHANGELOG.md | 5 +- README.md | 5 +- package.json | 17 +++++ syntaxes/mcpp-toml.tmLanguage.json | 116 +++++++++++++++++++++++++++++ test/artifacts.test.ts | 47 +++++++++++- 5 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 syntaxes/mcpp-toml.tmLanguage.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc4ac1..ece9c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## 0.2.3 -- 为精确文件名 `build.mcpp` 增加 C++ 语言关联,复用 VS Code 内置 C++ 高亮和现有模块语法注入;不扩大到所有 `*.mcpp` 文件。 +- 为精确文件名 `build.mcpp` 增加 C++ 语言关联,复用 VS Code 内置 C++ 高亮和现有模块 + 语法注入;不扩大到所有 `*.mcpp` 文件。 +- 为精确文件名 `mcpp.toml` 内置独立 TOML 语法高亮,不依赖额外扩展,也不接管普通 + `*.toml` 文件。 ## 0.2.2 diff --git a/README.md b/README.md index 45b99b1..d4402f7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 | 能力 | LLVM/Clang | GCC | MSVC | | --- | --- | --- | --- | | 识别 mcpp 工程 | 支持 | 支持 | 支持 | +| `mcpp.toml` TOML 语法高亮 | 支持 | 支持 | 支持 | | `build.mcpp`、`.cppm`、`.ixx`、`.mpp`、`.ccm` 文件关联 | 支持 | 支持 | 支持 | | `module`、`export module`、`import` 语法高亮 | 支持 | 支持 | 支持 | | 构建、运行、测试、清理命令 | 支持 | 支持 | 支持 | @@ -78,8 +79,10 @@ xlings 安装。 扩展激活本身不会静默运行 `mcpp build`,也不会自动下载、安装或切换工具链。 -### C++ 模块语法高亮 +### 语法高亮 +- 将精确文件名 `mcpp.toml` 识别为独立的 mcpp TOML 语言,内置表、键、字符串、数字、 + 布尔、日期时间和注释高亮。 - 将 `build.mcpp`、`.cppm`、`.ixx`、`.mpp` 和 `.ccm` 关联到 VS Code 内置 `cpp` 语言。 - 向 `source.cpp` 注入模块语法规则。 - 覆盖 `module`、`export module`、`import`、`export import`、模块名和模块分区。 diff --git a/package.json b/package.json index 4a29e69..650abfc 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,18 @@ } } }, + "languages": [ + { + "id": "mcpp-toml", + "aliases": [ + "mcpp TOML", + "mcpp.toml" + ], + "filenames": [ + "mcpp.toml" + ] + } + ], "configurationDefaults": { "files.associations": { "build.mcpp": "cpp", @@ -142,6 +154,11 @@ } }, "grammars": [ + { + "language": "mcpp-toml", + "scopeName": "source.toml.mcpp", + "path": "./syntaxes/mcpp-toml.tmLanguage.json" + }, { "scopeName": "source.cpp.mcpp-modules", "injectTo": [ diff --git a/syntaxes/mcpp-toml.tmLanguage.json b/syntaxes/mcpp-toml.tmLanguage.json new file mode 100644 index 0000000..e0df854 --- /dev/null +++ b/syntaxes/mcpp-toml.tmLanguage.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "mcpp TOML", + "scopeName": "source.toml.mcpp", + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#table" + }, + { + "include": "#key" + }, + { + "include": "#string" + }, + { + "include": "#datetime" + }, + { + "include": "#number" + }, + { + "include": "#boolean" + }, + { + "include": "#punctuation" + } + ], + "repository": { + "comment": { + "match": "#.*$", + "name": "comment.line.number-sign.toml" + }, + "table": { + "match": "^\\s*(\\[\\[?)([^\\]\\r\\n]+?)(\\]\\]?)\\s*(?=#|$)", + "captures": { + "1": { + "name": "punctuation.definition.table.begin.toml" + }, + "2": { + "name": "entity.name.section.toml" + }, + "3": { + "name": "punctuation.definition.table.end.toml" + } + } + }, + "key": { + "match": "^\\s*((?:[A-Za-z0-9_-]+|\"[^\"]+\"|'[^']+')(?:\\s*\\.\\s*(?:[A-Za-z0-9_-]+|\"[^\"]+\"|'[^']+'))*)\\s*(=)", + "captures": { + "1": { + "name": "variable.other.key.toml" + }, + "2": { + "name": "keyword.operator.assignment.toml" + } + } + }, + "string": { + "patterns": [ + { + "begin": "\"\"\"", + "end": "\"\"\"", + "name": "string.quoted.triple.basic.toml", + "patterns": [ + { + "include": "#escape" + } + ] + }, + { + "begin": "'''", + "end": "'''", + "name": "string.quoted.triple.literal.toml" + }, + { + "begin": "\"", + "end": "\"", + "name": "string.quoted.double.toml", + "patterns": [ + { + "include": "#escape" + } + ] + }, + { + "begin": "'", + "end": "'", + "name": "string.quoted.single.toml" + } + ] + }, + "escape": { + "match": "\\\\(?:[btnfr\"\\\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})", + "name": "constant.character.escape.toml" + }, + "datetime": { + "match": "(?; configuration?: { properties?: Record }; configurationDefaults?: Record; - grammars?: Array<{ scopeName: string; injectTo?: string[]; path: string }>; + languages?: Array<{ id: string; aliases?: string[]; filenames?: string[] }>; + grammars?: Array<{ language?: string; scopeName: string; injectTo?: string[]; path: string }>; }; } @@ -70,6 +71,50 @@ test("associates the build.mcpp file with the C++ language", () => { assert.equal(associations?.["*.mcpp"], undefined); }); +test("ships TOML highlighting for the exact mcpp.toml filename", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + const language = manifest.contributes?.languages?.find((item) => item.id === "mcpp-toml"); + assert.deepEqual(language, { + id: "mcpp-toml", + aliases: ["mcpp TOML", "mcpp.toml"], + filenames: ["mcpp.toml"], + }); + + const grammar = manifest.contributes?.grammars?.find((item) => item.language === "mcpp-toml"); + assert.deepEqual(grammar, { + language: "mcpp-toml", + scopeName: "source.toml.mcpp", + path: "./syntaxes/mcpp-toml.tmLanguage.json", + }); + + const grammarText = readFileSync(path.join(root, "syntaxes/mcpp-toml.tmLanguage.json"), "utf8"); + const grammarDocument = JSON.parse(grammarText) as { + repository?: Record; + }; + for (const key of ["comment", "table", "key", "string", "datetime", "number", "boolean", "punctuation"]) { + assert.ok(grammarDocument.repository?.[key], `missing TOML grammar rule: ${key}`); + } + + const tablePattern = new RegExp(grammarDocument.repository?.table.match ?? ""); + assert.match("[package]", tablePattern); + assert.match("[[target.generated]]", tablePattern); + + const keyPattern = new RegExp(grammarDocument.repository?.key.match ?? ""); + assert.match('standard = "c++23"', keyPattern); + assert.match('"quoted.key" = true', keyPattern); + + for (const scope of [ + "comment.line.number-sign.toml", + "entity.name.section.toml", + "variable.other.key.toml", + "string.quoted.double.toml", + "constant.numeric.toml", + "constant.language.boolean.toml", + ]) { + assert.match(grammarText, new RegExp(scope.replaceAll(".", "\\."))); + } +}); + test("ships an injection grammar with module-specific scopes", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; const grammar = manifest.contributes?.grammars?.find((item) => item.scopeName === "source.cpp.mcpp-modules"); From 74bb0aff4cb9e9ac2d6fd5087cc6030822c7779e Mon Sep 17 00:00:00 2001 From: wellwei Date: Wed, 5 Aug 2026 18:45:36 +0800 Subject: [PATCH 017/119] fix: preserve hermetic clangd configuration --- CHANGELOG.md | 10 ++ README.md | 17 ++- ...-08-05-clangd-hermetic-cdb-build-script.md | 140 ++++++++++++++++++ package-lock.json | 4 +- package.json | 20 ++- src/analysis.ts | 46 +++++- src/extension.ts | 2 + .../mcpp-build-language-configuration.json | 25 ++++ syntaxes/mcpp-build.tmLanguage.json | 10 ++ test/analysis.test.ts | 67 +++++++++ test/artifacts.test.ts | 27 +++- 11 files changed, 353 insertions(+), 15 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md create mode 100644 syntaxes/mcpp-build-language-configuration.json create mode 100644 syntaxes/mcpp-build.tmLanguage.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ece9c0b..3975277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # 更新日志 +## 0.2.4 + +- 修复 hermetic mcpp 编译数据库被插件追加 `--query-driver` 后导致的 clangd 标准库和 + 模块误诊断;仅在编译命令未自带完整 sysroot、标准库和 no-default-config 配置时补充 + query driver。 +- 模块检查优先选择项目源码,避免误选 `.mcpp` 依赖缓存或 `target` 生成源码,改善补全、 + 跳转和诊断稳定性。 +- 将 `build.mcpp` 作为独立的语法高亮语言处理,不再让 clangd 把 mcpp 构建脚本当作普通 + C++ 翻译单元。 + ## 0.2.3 - 为精确文件名 `build.mcpp` 增加 C++ 语言关联,复用 VS Code 内置 C++ 高亮和现有模块 diff --git a/README.md b/README.md index d4402f7..801c50d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -当前版本为 `0.2.3`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +当前版本为 `0.2.4`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 > 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 @@ -36,7 +36,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.2.3.vsix +code --install-extension /path/to/mcpp-vscode-0.2.4.vsix ``` 安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 @@ -83,14 +83,20 @@ xlings 安装。 - 将精确文件名 `mcpp.toml` 识别为独立的 mcpp TOML 语言,内置表、键、字符串、数字、 布尔、日期时间和注释高亮。 -- 将 `build.mcpp`、`.cppm`、`.ixx`、`.mpp` 和 `.ccm` 关联到 VS Code 内置 `cpp` 语言。 -- 向 `source.cpp` 注入模块语法规则。 +- 将精确文件名 `build.mcpp` 识别为独立的 mcpp build language,并复用 C++ TextMate + grammar;`.cppm`、`.ixx`、`.mpp` 和 `.ccm` 继续关联到 VS Code 内置 `cpp` 语言。 +- 向 C++ 源文件注入模块语法规则。 - 覆盖 `module`、`export module`、`import`、`export import`、模块名和模块分区。 - 支持尚未输入分号的编辑中间态。 语法高亮只负责词法着色。错误模块名、不可见声明等红色诊断来自语言服务器,不能由 TextMate 语法规则提供。 +`build.mcpp` 是由 mcpp 宿主执行的构建脚本,不是普通 C++ 翻译单元。当前扩展只为它 +提供 C++ 风格的语法高亮和编辑器括号/注释规则,不启动 clangd 语义诊断;因此 +`import mcpp;` 不会再被误报为缺少 C++ 模块。真正的 mcpp API 补全需要 mcpp 核心 +未来生成宿主 helper 的 CDB 和 PCM 映射。 + ### LLVM 与 clangd 集成 扩展读取 mcpp 生成的 `compile_commands.json`,然后: @@ -99,7 +105,8 @@ TextMate 语法规则提供。 2. 从显式设置、编译器目录、匹配的 xlings `llvm-tools` 目录和 `PATH` 中查找 clangd。 3. 执行编译器和 clangd 的版本命令,比较 LLVM 版本与 revision。 4. 保留用户已有的 `clangd.arguments`,展开 `${workspaceFolder}` 和 - `${workspaceRoot}`,并添加精确的 `--query-driver`。 + `${workspaceRoot}`;只有在 CDB 未提供完整 sysroot、标准库和 no-default-config + 参数时,才添加精确的 `--query-driver`。 5. 根据 CDB 中是否已有显式 PCM 映射以及 `mcpp.modulesSupport` 设置,决定是否增加 clangd 实验模块参数。 6. 必要时重启官方 clangd 扩展,并执行一次最长 60 秒的 `clangd --check`。 diff --git a/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md b/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md new file mode 100644 index 0000000..7eb523f --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md @@ -0,0 +1,140 @@ +# Clangd Hermetic CDB and build.mcpp Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove false clangd diagnostics from hermetic mcpp projects, keep module checks on project sources, and prevent clangd from treating `build.mcpp` as an ordinary C++ translation unit. + +**Architecture:** Keep clangd's query-driver discovery for ordinary or cross-toolchain CDBs, but skip it when the selected mcpp command already declares a no-default-config sysroot and standard library. Give `build.mcpp` its own language id backed by the C++ TextMate grammar so syntax highlighting remains while the official clangd extension no longer claims the document. Rank real project sources above `.mcpp` dependency-cache and `target` commands for the extension's direct module check. + +**Tech Stack:** TypeScript 5.9, VS Code extension manifests and TextMate grammars, Node.js built-in test runner, clangd 22.1.8, VSCE. + +--- + +### Task 1: Preserve hermetic compilation commands + +**Files:** +- Modify: `src/analysis.ts` +- Modify: `src/extension.ts` +- Test: `test/analysis.test.ts` + +- [x] **Step 1: Write the failing hermetic CDB argument tests** + +Add tests that pass the selected CDB arguments through `buildClangdArguments`. A command containing `--no-default-config`, `-nostdinc++`, an explicit `-isystem.../include/c++/v1`, and `--sysroot=...` must remove a previously managed `--query-driver`; an ordinary command must still receive the selected compiler as its query driver. + +- [x] **Step 2: Run the analysis test and verify RED** + +Run: `npm run compile && node --test dist/test/analysis.test.js` + +Expected: the hermetic test fails because the current implementation always appends `--query-driver`. + +- [x] **Step 3: Implement the minimum query-driver policy** + +Add `compilationArguments?: readonly string[]` to `ClangdArgumentOptions`. Treat a command as self-contained only when all four hermetic markers are present, remove managed query-driver arguments, and append the selected compiler only for non-self-contained commands. Pass `context.analysis.arguments` from both the workspace configuration path and direct `clangd --check` path in `src/extension.ts`. + +- [x] **Step 4: Run the focused tests and verify GREEN** + +Run: `npm run compile && node --test dist/test/analysis.test.js` + +Expected: all analysis tests pass. + +### Task 2: Select a project-owned module check source + +**Files:** +- Modify: `src/analysis.ts` +- Test: `test/analysis.test.ts` + +- [x] **Step 1: Write the failing dependency-cache ranking test** + +Create a CDB fixture whose first LLVM command points to `/work/app/.mcpp/.../transaction.cpp` with PCM flags and whose later command points to `/work/app/src/main.cpp` with the same flags. Assert that `analyzeCompilationDatabase` selects `/work/app/src/main.cpp`. + +- [x] **Step 2: Run the focused test and verify RED** + +Run: `npm run compile && node --test dist/test/analysis.test.js` + +Expected: the new test selects the `.mcpp` command and fails. + +- [x] **Step 3: Implement project-source ranking** + +Keep the existing in-project and module-interface scoring, but add a higher score for paths below the command directory whose first component is neither `.mcpp` nor `target`. Preserve the existing fallback when a CDB contains only dependency or generated commands. + +- [x] **Step 4: Run the focused tests and verify GREEN** + +Run: `npm run compile && node --test dist/test/analysis.test.js` + +Expected: all analysis tests pass and the selected UChat check source is a member source file. + +### Task 3: Make build.mcpp syntax-only + +**Files:** +- Modify: `package.json` +- Create: `syntaxes/mcpp-build.tmLanguage.json` +- Create: `syntaxes/mcpp-build-language-configuration.json` +- Modify: `test/artifacts.test.ts` +- Modify: `README.md` + +- [x] **Step 1: Write the failing manifest and grammar tests** + +Assert that the manifest declares exact filename language `mcpp-build`, that `files.associations` no longer maps `build.mcpp` to `cpp`, and that the new grammar maps `mcpp-build` to `source.mcpp-build` and includes the external `source.cpp` grammar. Keep the assertion that no broad `*.mcpp` association exists. + +- [x] **Step 2: Run the artifact tests and verify RED** + +Run: `npm run compile && node --test dist/test/artifacts.test.js` + +Expected: the manifest still maps `build.mcpp` to `cpp`, so the new assertions fail. + +- [x] **Step 3: Implement the custom language** + +Register `mcpp-build` with `filenames: ["build.mcpp"]`, a C++-style language configuration, and a TextMate grammar whose root scope is `source.mcpp-build` and whose patterns include `source.cpp`. Remove only the exact `build.mcpp -> cpp` default; keep `.cppm`, `.ixx`, `.mpp`, and `.ccm` associated with `cpp`. Add `onLanguage:mcpp-build` activation. + +- [x] **Step 4: Document the semantic boundary** + +Update README wording so `build.mcpp` is described as C++-style syntax highlighting without clangd diagnostics, while real C++ module files continue to use clangd. Explain that semantic support for `import mcpp;` requires a future mcpp-generated host-helper CDB and PCM mapping. + +- [x] **Step 5: Run the artifact tests and verify GREEN** + +Run: `npm run compile && node --test dist/test/artifacts.test.js` + +Expected: all artifact tests pass. + +### Task 4: Version, full verification, package, install, and tag + +**Files:** +- Modify: `package.json` +- Modify: `package-lock.json` +- Modify: `CHANGELOG.md` + +- [x] **Step 1: Bump the extension version** + +Run: `npm version 0.2.4 --no-git-tag-version` + +Add a `0.2.4` changelog entry covering hermetic query-driver handling, project-source checks, and syntax-only `build.mcpp` handling. + +- [x] **Step 2: Run the full regression suite** + +Run: `npm test` + +Expected: all tests pass with zero failures. + +- [x] **Step 3: Reproduce the original UChat clangd symptom** + +Run matching clangd 22.1.8 against each UChat `RedisMgr.cpp` using its member CDB and the arguments produced by the fixed policy. Confirm there are no `template_instantiate_undefined`, `ovl_no_viable_function_in_call`, SDK type, or module ODR diagnostics. + +- [x] **Step 4: Package and inspect the VSIX** + +Run: `npm run package` + +Expected: `mcpp-vscode-0.2.4.vsix` is created. Inspect its manifest and archive contents to confirm the new language grammar and configuration are included. + +- [x] **Step 5: Commit only the repair files** + +Stage the plan, source, focused tests, manifest, lockfile, grammar, README, and changelog files. Do not stage pre-existing modified or untracked design documents. Commit as `fix: preserve hermetic clangd configuration`. + +- [x] **Step 6: Create the local release tag** + +Create annotated tag `v0.2.4` on the verified repair commit. Do not push it or create a GitHub Release. + +- [x] **Step 7: Install and verify the packaged extension** + +Run: `code --install-extension /Users/cltx/projects/mcpp/mcpp-vscode/mcpp-vscode-0.2.4.vsix --force` + +Then run `code --list-extensions --show-versions` and verify `mcpp-community.mcpp-vscode@0.2.4`. A VS Code window reload is required before the new language registration and clangd arguments take effect. diff --git a/package-lock.json b/package-lock.json index 36029a7..9613a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.2.3", + "version": "0.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.2.3", + "version": "0.2.4", "license": "Apache-2.0", "devDependencies": { "@types/node": "^24.0.0", diff --git a/package.json b/package.json index 650abfc..9f7f151 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.3", + "version": "0.2.4", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", @@ -23,6 +23,7 @@ "activationEvents": [ "workspaceContains:mcpp.toml", "onLanguage:cpp", + "onLanguage:mcpp-build", "onCommand:mcpp.configureClangd", "onCommand:mcpp.refreshCompilationDatabase", "onCommand:mcpp.checkModuleSupport", @@ -133,6 +134,17 @@ } }, "languages": [ + { + "id": "mcpp-build", + "aliases": [ + "mcpp build script", + "build.mcpp" + ], + "filenames": [ + "build.mcpp" + ], + "configuration": "./syntaxes/mcpp-build-language-configuration.json" + }, { "id": "mcpp-toml", "aliases": [ @@ -146,7 +158,6 @@ ], "configurationDefaults": { "files.associations": { - "build.mcpp": "cpp", "*.ccm": "cpp", "*.cppm": "cpp", "*.ixx": "cpp", @@ -154,6 +165,11 @@ } }, "grammars": [ + { + "language": "mcpp-build", + "scopeName": "source.mcpp-build", + "path": "./syntaxes/mcpp-build.tmLanguage.json" + }, { "language": "mcpp-toml", "scopeName": "source.toml.mcpp", diff --git a/src/analysis.ts b/src/analysis.ts index 6c8035a..816fad6 100644 --- a/src/analysis.ts +++ b/src/analysis.ts @@ -35,6 +35,7 @@ export interface ToolIdentityComparison { export interface ClangdArgumentOptions { compilerPath: string; + compilationArguments?: readonly string[]; modulesSupport: ModulesSupportMode; clangdIdentity?: ToolIdentity; platform: NodeJS.Platform; @@ -201,6 +202,7 @@ export function analyzeCompilationDatabase(contents: string): CompilationDatabas const moduleInterface = /\.(?:cppm|ixx|mpp|ccm)$/i.test(sourceFile); const inProject = candidate.directory !== undefined && isWithinDirectory(candidate.directory, sourceFile); return (inProject ? 200 : 0) + + (candidate.directory !== undefined && isProjectSource(candidate.directory, sourceFile) ? 200 : 0) + (moduleInterface ? 100 : 0) + (candidate.hasPrebuiltModules ? 10 : 0); }; @@ -217,6 +219,18 @@ function isWithinDirectory(directory: string, file: string): boolean { || (relative !== ".." && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative)); } +function isProjectSource(directory: string, file: string): boolean { + if (!isWithinDirectory(directory, file)) { + return false; + } + const windows = /^[A-Za-z]:[\\/]/.test(directory) || directory.includes("\\") + || /^[A-Za-z]:[\\/]/.test(file) || file.includes("\\"); + const pathApi = windows ? path.win32 : path.posix; + const relative = pathApi.relative(pathApi.resolve(directory), pathApi.resolve(file)); + const firstComponent = relative.split(pathApi.sep)[0]; + return firstComponent !== ".mcpp" && firstComponent !== "target"; +} + function removeManagedArguments(arguments_: readonly string[]): string[] { const result: string[] = []; @@ -238,6 +252,34 @@ function removeManagedArguments(arguments_: readonly string[]): string[] { return result; } +function hasValueFlag(arguments_: readonly string[], flag: string): boolean { + return arguments_.some((argument, index) => ( + (argument === flag && index + 1 < arguments_.length) + || argument.startsWith(`${flag}=`) + )); +} + +function hasExplicitLibcxxPath(arguments_: readonly string[]): boolean { + return arguments_.some((argument, index) => { + const value = argument === "-isystem" + ? arguments_[index + 1] ?? "" + : argument.startsWith("-isystem") + ? argument.slice("-isystem".length) + : ""; + return /[\\/]include[\\/]c\+\+[\\/]v1(?:[\\/]|$)/.test(value); + }); +} + +function isHermeticClangCommand(arguments_: readonly string[] | undefined): boolean { + if (arguments_ === undefined) { + return false; + } + return arguments_.includes("--no-default-config") + && arguments_.includes("-nostdinc++") + && hasExplicitLibcxxPath(arguments_) + && hasValueFlag(arguments_, "--sysroot"); +} + function expandWorkspaceVariables(argument: string, workspaceFolder?: string): string { if (workspaceFolder === undefined) { return argument; @@ -276,7 +318,9 @@ export function buildClangdArguments( ): string[] { const result = removeManagedArguments(existingArguments) .map((argument) => expandWorkspaceVariables(argument, options.workspaceFolder)); - result.push(`--query-driver=${options.compilerPath}`); + if (!isHermeticClangCommand(options.compilationArguments)) { + result.push(`--query-driver=${options.compilerPath}`); + } if (shouldEnableExperimentalModules(options)) { result.push("--experimental-modules-support"); diff --git a/src/extension.ts b/src/extension.ts index 864dedc..3749f5f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -310,6 +310,7 @@ async function configureClangd( clangd.path, { compilerPath: context.analysis.compilerPath, + compilationArguments: context.analysis.arguments, modulesSupport, clangdIdentity: clangd.version.identity, platform: process.platform, @@ -522,6 +523,7 @@ async function runModuleSupportCheck( officialClangdConfiguration(context.project).get("arguments", []), { compilerPath: context.analysis.compilerPath, + compilationArguments: context.analysis.arguments, modulesSupport: projectConfiguration(context.project) .get("modulesSupport", "auto"), clangdIdentity: clangd.version.identity, diff --git a/syntaxes/mcpp-build-language-configuration.json b/syntaxes/mcpp-build-language-configuration.json new file mode 100644 index 0000000..b49c932 --- /dev/null +++ b/syntaxes/mcpp-build-language-configuration.json @@ -0,0 +1,25 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": ["/*", "*/"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + {"open": "{", "close": "}"}, + {"open": "[", "close": "]"}, + {"open": "(", "close": ")"}, + {"open": "\"", "close": "\""}, + {"open": "'", "close": "'"} + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ] +} diff --git a/syntaxes/mcpp-build.tmLanguage.json b/syntaxes/mcpp-build.tmLanguage.json new file mode 100644 index 0000000..3635680 --- /dev/null +++ b/syntaxes/mcpp-build.tmLanguage.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "mcpp build script", + "scopeName": "source.mcpp-build", + "patterns": [ + { + "include": "source.cpp" + } + ] +} diff --git a/test/analysis.test.ts b/test/analysis.test.ts index cd5beae..c904c05 100644 --- a/test/analysis.test.ts +++ b/test/analysis.test.ts @@ -86,6 +86,35 @@ test("prefers a project source over an external module interface", () => { assert.equal(result.sourceFile, "/work/app/src/main.cpp"); }); +test("prefers a project source over an mcpp dependency cache source", () => { + const result = analyzeCompilationDatabase(JSON.stringify([ + { + directory: "/work/app", + file: "/work/app/.mcpp/packages/redis/src/transaction.cpp", + arguments: [ + "/tools/clang++", + "-std=c++23", + "-fprebuilt-module-path=/work/app/target/pcm.cache", + "-c", + "/work/app/.mcpp/packages/redis/src/transaction.cpp", + ], + }, + { + directory: "/work/app", + file: "/work/app/src/main.cpp", + arguments: [ + "/tools/clang++", + "-std=c++23", + "-fprebuilt-module-path=/work/app/target/pcm.cache", + "-c", + "/work/app/src/main.cpp", + ], + }, + ])); + + assert.equal(result.sourceFile, "/work/app/src/main.cpp"); +}); + test("keeps Windows separators when reading a command-form CDB entry", () => { const result = analyzeCompilationDatabase(JSON.stringify([ { @@ -165,6 +194,44 @@ test("auto consumes explicit mcpp PCMs without invoking clangd's module builder" assert.deepEqual(args, ["--query-driver=/tools/clang++"]); }); +test("hermetic mcpp commands do not use query-driver discovery", () => { + const args = buildClangdArguments( + ["--background-index", "--query-driver=/old/compiler"], + { + compilerPath: "/tools/clang++", + compilationArguments: [ + "/tools/clang++", + "--no-default-config", + "-nostdinc++", + "-isystem/tools/llvm/include/c++/v1", + "--sysroot=/platform/sdk", + "-c", + "/work/app/src/main.cpp", + ], + modulesSupport: "off", + platform: "darwin", + }, + ); + + assert.deepEqual(args, ["--background-index"]); +}); + +test("non-hermetic commands retain query-driver discovery", () => { + const args = buildClangdArguments([], { + compilerPath: "/tools/clang++", + compilationArguments: [ + "/tools/clang++", + "-std=c++23", + "-c", + "/work/app/src/main.cpp", + ], + modulesSupport: "off", + platform: "darwin", + }); + + assert.deepEqual(args, ["--query-driver=/tools/clang++"]); +}); + test("expands workspace folder variables before invoking clangd", () => { const options = { compilerPath: "/tools/clang++", diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 5374e2e..c69696e 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -16,7 +16,7 @@ interface PackageManifest { commands?: Array<{ command: string }>; configuration?: { properties?: Record }; configurationDefaults?: Record; - languages?: Array<{ id: string; aliases?: string[]; filenames?: string[] }>; + languages?: Array<{ id: string; aliases?: string[]; filenames?: string[]; configuration?: string }>; grammars?: Array<{ language?: string; scopeName: string; injectTo?: string[]; path: string }>; }; } @@ -25,9 +25,10 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.3"); + assert.equal(manifest.version, "0.2.4"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); + assert.ok(manifest.activationEvents?.includes("onLanguage:mcpp-build")); assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); assert.equal( manifest.capabilities?.untrustedWorkspaces?.description, @@ -53,7 +54,6 @@ test("declares the official clangd dependency and mcpp commands", () => { assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.path"]); assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.modulesSupport"]); assert.deepEqual(manifest.contributes?.configurationDefaults?.["files.associations"], { - "build.mcpp": "cpp", "*.ccm": "cpp", "*.cppm": "cpp", "*.ixx": "cpp", @@ -61,14 +61,31 @@ test("declares the official clangd dependency and mcpp commands", () => { }); }); -test("associates the build.mcpp file with the C++ language", () => { +test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; const associations = manifest.contributes?.configurationDefaults?.["files.associations"] as | Record | undefined; + const language = manifest.contributes?.languages?.find((item) => item.id === "mcpp-build"); + const grammar = manifest.contributes?.grammars?.find((item) => item.language === "mcpp-build"); - assert.equal(associations?.["build.mcpp"], "cpp"); + assert.equal(associations?.["build.mcpp"], undefined); assert.equal(associations?.["*.mcpp"], undefined); + assert.deepEqual(language, { + id: "mcpp-build", + aliases: ["mcpp build script", "build.mcpp"], + filenames: ["build.mcpp"], + configuration: "./syntaxes/mcpp-build-language-configuration.json", + }); + assert.deepEqual(grammar, { + language: "mcpp-build", + scopeName: "source.mcpp-build", + path: "./syntaxes/mcpp-build.tmLanguage.json", + }); + + const grammarText = readFileSync(path.join(root, "syntaxes/mcpp-build.tmLanguage.json"), "utf8"); + const grammarDocument = JSON.parse(grammarText) as { patterns?: Array<{ include?: string }> }; + assert.ok(grammarDocument.patterns?.some((pattern) => pattern.include === "source.cpp")); }); test("ships TOML highlighting for the exact mcpp.toml filename", () => { From 225c9759250437f8c0de1cc578420032269ef263 Mon Sep 17 00:00:00 2001 From: wellwei Date: Wed, 5 Aug 2026 18:50:55 +0800 Subject: [PATCH 018/119] fix: complete clangd release validation --- CHANGELOG.md | 4 +-- README.md | 8 ++--- ...-08-05-clangd-hermetic-cdb-build-script.md | 7 ++++ package.json | 3 +- src/analysis.ts | 28 +++++++++------ syntaxes/mcpp-modules.tmLanguage.json | 2 +- test/analysis.test.ts | 35 +++++++++++++++++-- test/artifacts.test.ts | 14 ++++++-- 8 files changed, 78 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3975277..4b3344a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ ## 0.2.4 - 修复 hermetic mcpp 编译数据库被插件追加 `--query-driver` 后导致的 clangd 标准库和 - 模块误诊断;仅在编译命令未自带完整 sysroot、标准库和 no-default-config 配置时补充 - query driver。 + 模块误诊断;仅在编译命令未自带 no-default-config 和显式标准库配置时补充 query + driver。 - 模块检查优先选择项目源码,避免误选 `.mcpp` 依赖缓存或 `target` 生成源码,改善补全、 跳转和诊断稳定性。 - 将 `build.mcpp` 作为独立的语法高亮语言处理,不再让 clangd 把 mcpp 构建脚本当作普通 diff --git a/README.md b/README.md index 801c50d..8efa163 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,8 @@ TextMate 语法规则提供。 2. 从显式设置、编译器目录、匹配的 xlings `llvm-tools` 目录和 `PATH` 中查找 clangd。 3. 执行编译器和 clangd 的版本命令,比较 LLVM 版本与 revision。 4. 保留用户已有的 `clangd.arguments`,展开 `${workspaceFolder}` 和 - `${workspaceRoot}`;只有在 CDB 未提供完整 sysroot、标准库和 no-default-config - 参数时,才添加精确的 `--query-driver`。 + `${workspaceRoot}`;只有在 CDB 未提供 no-default-config 和显式标准库配置时,才添加 + 精确的 `--query-driver`。 5. 根据 CDB 中是否已有显式 PCM 映射以及 `mcpp.modulesSupport` 设置,决定是否增加 clangd 实验模块参数。 6. 必要时重启官方 clangd 扩展,并执行一次最长 60 秒的 `clangd --check`。 @@ -381,8 +381,8 @@ API、状态栏、任务和 clangd 集成。 版本完全一致的 tag: ```sh -git tag -a v0.2.3 -m "mcpp-vscode 0.2.3" -git push origin v0.2.3 +git tag -a v0.2.4 -m "mcpp-vscode 0.2.4" +git push origin v0.2.4 ``` `.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, diff --git a/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md b/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md index 7eb523f..991888e 100644 --- a/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md +++ b/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md @@ -138,3 +138,10 @@ Create annotated tag `v0.2.4` on the verified repair commit. Do not push it or c Run: `code --install-extension /Users/cltx/projects/mcpp/mcpp-vscode/mcpp-vscode-0.2.4.vsix --force` Then run `code --list-extensions --show-versions` and verify `mcpp-community.mcpp-vscode@0.2.4`. A VS Code window reload is required before the new language registration and clangd arguments take effect. + +### Review follow-up + +- [x] Extend module grammar injection to `source.mcpp-build` and cover the import capture scope. +- [x] Resolve relative compilation-database source paths from each command directory. +- [x] Treat explicit libc++ and no-default-config commands as hermetic without requiring a sysroot. +- [x] Keep the release instructions synchronized with the extension version. diff --git a/package.json b/package.json index 9f7f151..86b6b0c 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,8 @@ { "scopeName": "source.cpp.mcpp-modules", "injectTo": [ - "source.cpp" + "source.cpp", + "source.mcpp-build" ], "path": "./syntaxes/mcpp-modules.tmLanguage.json" } diff --git a/src/analysis.ts b/src/analysis.ts index 816fad6..921beb3 100644 --- a/src/analysis.ts +++ b/src/analysis.ts @@ -179,12 +179,16 @@ export function analyzeCompilationDatabase(contents: string): CompilationDatabas argument.startsWith("-fmodule-file=") || argument.startsWith("-fprebuilt-module-path=") )); + const directory = typeof command.directory === "string" ? command.directory : undefined; + const sourceFile = typeof command.file === "string" + ? resolveCompilationSourceFile(directory, command.file) + : undefined; candidates.push({ kind, capability: kind === "llvm" ? "full" : "syntax-only", compilerPath: args[0], - sourceFile: typeof command.file === "string" ? command.file : undefined, - directory: typeof command.directory === "string" ? command.directory : undefined, + sourceFile, + directory, arguments: args, hasPrebuiltModules, reason: kind === "llvm" @@ -219,6 +223,16 @@ function isWithinDirectory(directory: string, file: string): boolean { || (relative !== ".." && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative)); } +function resolveCompilationSourceFile(directory: string | undefined, file: string): string { + if (directory === undefined) { + return file; + } + const windows = /^[A-Za-z]:[\\/]/.test(directory) || directory.includes("\\") + || /^[A-Za-z]:[\\/]/.test(file) || file.includes("\\"); + const pathApi = windows ? path.win32 : path.posix; + return pathApi.isAbsolute(file) ? file : pathApi.resolve(directory, file); +} + function isProjectSource(directory: string, file: string): boolean { if (!isWithinDirectory(directory, file)) { return false; @@ -252,13 +266,6 @@ function removeManagedArguments(arguments_: readonly string[]): string[] { return result; } -function hasValueFlag(arguments_: readonly string[], flag: string): boolean { - return arguments_.some((argument, index) => ( - (argument === flag && index + 1 < arguments_.length) - || argument.startsWith(`${flag}=`) - )); -} - function hasExplicitLibcxxPath(arguments_: readonly string[]): boolean { return arguments_.some((argument, index) => { const value = argument === "-isystem" @@ -276,8 +283,7 @@ function isHermeticClangCommand(arguments_: readonly string[] | undefined): bool } return arguments_.includes("--no-default-config") && arguments_.includes("-nostdinc++") - && hasExplicitLibcxxPath(arguments_) - && hasValueFlag(arguments_, "--sysroot"); + && hasExplicitLibcxxPath(arguments_); } function expandWorkspaceVariables(argument: string, workspaceFolder?: string): string { diff --git a/syntaxes/mcpp-modules.tmLanguage.json b/syntaxes/mcpp-modules.tmLanguage.json index 39bd45d..28c69e3 100644 --- a/syntaxes/mcpp-modules.tmLanguage.json +++ b/syntaxes/mcpp-modules.tmLanguage.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "name": "mcpp C++ Modules", "scopeName": "source.cpp.mcpp-modules", - "injectionSelector": "L:source.cpp", + "injectionSelector": "L:source.cpp, L:source.mcpp-build", "patterns": [ { "include": "#module-declaration" diff --git a/test/analysis.test.ts b/test/analysis.test.ts index c904c05..521b429 100644 --- a/test/analysis.test.ts +++ b/test/analysis.test.ts @@ -115,6 +115,35 @@ test("prefers a project source over an mcpp dependency cache source", () => { assert.equal(result.sourceFile, "/work/app/src/main.cpp"); }); +test("resolves relative CDB source files from their command directory", () => { + const result = analyzeCompilationDatabase(JSON.stringify([ + { + directory: "/work/app", + file: "/work/app/.mcpp/packages/redis/src/transaction.cpp", + arguments: [ + "/tools/clang++", + "-std=c++23", + "-fprebuilt-module-path=/work/app/target/pcm.cache", + "-c", + "/work/app/.mcpp/packages/redis/src/transaction.cpp", + ], + }, + { + directory: "/work/app", + file: "src/main.cpp", + arguments: [ + "/tools/clang++", + "-std=c++23", + "-fprebuilt-module-path=/work/app/target/pcm.cache", + "-c", + "src/main.cpp", + ], + }, + ])); + + assert.equal(result.sourceFile, "/work/app/src/main.cpp"); +}); + test("keeps Windows separators when reading a command-form CDB entry", () => { const result = analyzeCompilationDatabase(JSON.stringify([ { @@ -203,8 +232,10 @@ test("hermetic mcpp commands do not use query-driver discovery", () => { "/tools/clang++", "--no-default-config", "-nostdinc++", - "-isystem/tools/llvm/include/c++/v1", - "--sysroot=/platform/sdk", + "-isystem", + "/tools/llvm/include/c++/v1", + "-isystem/tools/llvm/include/x86_64-unknown-linux-gnu", + "-isystem/tools/llvm/include", "-c", "/work/app/src/main.cpp", ], diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index c69696e..b8a2912 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -137,12 +137,16 @@ test("ships an injection grammar with module-specific scopes", () => { const grammar = manifest.contributes?.grammars?.find((item) => item.scopeName === "source.cpp.mcpp-modules"); assert.deepEqual(grammar, { scopeName: "source.cpp.mcpp-modules", - injectTo: ["source.cpp"], + injectTo: ["source.cpp", "source.mcpp-build"], path: "./syntaxes/mcpp-modules.tmLanguage.json", }); const grammarText = readFileSync(path.join(root, "syntaxes/mcpp-modules.tmLanguage.json"), "utf8"); - const grammarDocument = JSON.parse(grammarText) as { repository?: Record }; + const grammarDocument = JSON.parse(grammarText) as { + injectionSelector?: string; + repository?: Record; + }; + assert.match(grammarDocument.injectionSelector ?? "", /source\.mcpp-build/); for (const key of ["module-declaration", "import-declaration", "module-name"]) { assert.ok(grammarDocument.repository?.[key], `missing grammar rule: ${key}`); } @@ -155,6 +159,12 @@ test("ships an injection grammar with module-specific scopes", () => { const importPattern = new RegExp(javascriptPattern); assert.match("import xxx", importPattern); assert.match("export import foo.bar;", importPattern); + + const importRuleWithCaptures = importRule as { + captures?: Record; + }; + assert.equal(importRuleWithCaptures.captures?.["2"]?.name, "keyword.control.import.cpp"); + assert.match("import mcpp;", importPattern); }); test("设置全局默认后先释放工具链锁再提供立即构建", () => { From 87ac991f2732bd1ece1e052a9df6b3b0938edc87 Mon Sep 17 00:00:00 2001 From: wellwei Date: Wed, 5 Aug 2026 18:54:45 +0800 Subject: [PATCH 019/119] docs: clarify hermetic clangd requirements --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- .../plans/2026-08-05-clangd-hermetic-cdb-build-script.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b3344a..17587c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ ## 0.2.4 - 修复 hermetic mcpp 编译数据库被插件追加 `--query-driver` 后导致的 clangd 标准库和 - 模块误诊断;仅在编译命令未自带 no-default-config 和显式标准库配置时补充 query - driver。 + 模块误诊断;仅在编译命令未自带 `--no-default-config`、`-nostdinc++` 和显式 libc++ + 路径时补充 query driver。 - 模块检查优先选择项目源码,避免误选 `.mcpp` 依赖缓存或 `target` 生成源码,改善补全、 跳转和诊断稳定性。 - 将 `build.mcpp` 作为独立的语法高亮语言处理,不再让 clangd 把 mcpp 构建脚本当作普通 diff --git a/README.md b/README.md index 8efa163..cfc88b6 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,8 @@ TextMate 语法规则提供。 2. 从显式设置、编译器目录、匹配的 xlings `llvm-tools` 目录和 `PATH` 中查找 clangd。 3. 执行编译器和 clangd 的版本命令,比较 LLVM 版本与 revision。 4. 保留用户已有的 `clangd.arguments`,展开 `${workspaceFolder}` 和 - `${workspaceRoot}`;只有在 CDB 未提供 no-default-config 和显式标准库配置时,才添加 - 精确的 `--query-driver`。 + `${workspaceRoot}`;只有在 CDB 未提供 `--no-default-config`、`-nostdinc++` 和显式 + libc++ 路径时,才添加精确的 `--query-driver`。 5. 根据 CDB 中是否已有显式 PCM 映射以及 `mcpp.modulesSupport` 设置,决定是否增加 clangd 实验模块参数。 6. 必要时重启官方 clangd 扩展,并执行一次最长 60 秒的 `clangd --check`。 diff --git a/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md b/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md index 991888e..7d74a3c 100644 --- a/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md +++ b/docs/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md @@ -143,5 +143,5 @@ Then run `code --list-extensions --show-versions` and verify `mcpp-community.mcp - [x] Extend module grammar injection to `source.mcpp-build` and cover the import capture scope. - [x] Resolve relative compilation-database source paths from each command directory. -- [x] Treat explicit libc++ and no-default-config commands as hermetic without requiring a sysroot. +- [x] Treat no-default-config, nostdinc++, and explicit libc++ commands as hermetic without requiring a sysroot. - [x] Keep the release instructions synchronized with the extension version. From 607e404ba8eca1abe1ae0e5479891ebe8554ac8a Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Wed, 5 Aug 2026 21:17:56 +0800 Subject: [PATCH 020/119] feat: add run/test buttons to editor title bar --- package.json | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 86b6b0c..e1e4cdf 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,20 @@ }, "main": "./dist/src/extension.js", "contributes": { + "menus": { + "editor/title": [ + { + "command": "mcpp.run", + "group": "navigation@1", + "when": "editorLangId == cpp" + }, + { + "command": "mcpp.test", + "group": "navigation@2", + "when": "editorLangId == cpp" + } + ] + }, "commands": [ { "command": "mcpp.showMenu", @@ -60,11 +74,13 @@ }, { "command": "mcpp.run", - "title": "mcpp: 运行" + "title": "mcpp: 运行", + "icon": "$(play)" }, { "command": "mcpp.test", - "title": "mcpp: 测试" + "title": "mcpp: 测试", + "icon":"$(beaker)" }, { "command": "mcpp.clean", From 551072eaec246533efa69f4ce47e85fd10d5f766 Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Thu, 6 Aug 2026 01:04:38 +0800 Subject: [PATCH 021/119] feat: gate editor title buttons behind mcpp project detection --- package.json | 6 +-- src/extension.ts | 8 ++++ src/inProject.ts | 38 +++++++++++++++ test/artifacts.test.ts | 18 ++++++- test/inProject.test.ts | 105 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 src/inProject.ts create mode 100644 test/inProject.test.ts diff --git a/package.json b/package.json index e1e4cdf..b43d185 100644 --- a/package.json +++ b/package.json @@ -54,12 +54,12 @@ { "command": "mcpp.run", "group": "navigation@1", - "when": "editorLangId == cpp" + "when": "mcpp.inProject" }, { "command": "mcpp.test", "group": "navigation@2", - "when": "editorLangId == cpp" + "when": "mcpp.inProject" } ] }, @@ -80,7 +80,7 @@ { "command": "mcpp.test", "title": "mcpp: 测试", - "icon":"$(beaker)" + "icon": "$(beaker)" }, { "command": "mcpp.clean", diff --git a/src/extension.ts b/src/extension.ts index 3749f5f..3ec9356 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -44,6 +44,7 @@ import { type ModuleSupportState, } from "./workflow"; import { classifyTaskExit, type TaskCompletion } from "./tasks"; +import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./inProject"; const COMMAND_CONFIGURE = "mcpp.configureClangd"; const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; @@ -849,6 +850,13 @@ async function autoConfigureModulesWizard( export async function activate(extensionContext: vscode.ExtensionContext): Promise { moduleStatusByProject.clear(); moduleCheckOperations.clear(); + extensionContext.subscriptions.push( + registerInProjectContext({ + findMcppManifests: () => vscode.workspace.findFiles(MCPP_MANIFEST_GLOB, null, 1), + setContextValue: (key, value) => vscode.commands.executeCommand("setContext", key, value), + createManifestWatcher: () => vscode.workspace.createFileSystemWatcher(MCPP_MANIFEST_GLOB), + }), + ); const output = vscode.window.createOutputChannel("mcpp"); const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); diff --git a/src/inProject.ts b/src/inProject.ts new file mode 100644 index 0000000..bcc0e44 --- /dev/null +++ b/src/inProject.ts @@ -0,0 +1,38 @@ +export const IN_PROJECT_CONTEXT_KEY = "mcpp.inProject"; +export const MCPP_MANIFEST_GLOB = "**/mcpp.toml"; + +export interface FileSystemWatcherLike { + onDidCreate(listener: () => void): { dispose(): unknown }; + onDidDelete(listener: () => void): { dispose(): unknown }; + dispose(): unknown; +} + +export interface InProjectEnvironment { + findMcppManifests(): PromiseLike; + setContextValue(key: string, value: boolean): PromiseLike; + createManifestWatcher(): FileSystemWatcherLike; +} + +export async function updateInProjectContext(env: InProjectEnvironment): Promise { + const manifests = await env.findMcppManifests(); + const inProject = manifests.length > 0; + await env.setContextValue(IN_PROJECT_CONTEXT_KEY, inProject); + return inProject; +} + +export function registerInProjectContext(env: InProjectEnvironment): { dispose(): unknown } { + void updateInProjectContext(env); + const watcher = env.createManifestWatcher(); + const disposables = [ + watcher, + watcher.onDidCreate(() => void updateInProjectContext(env)), + watcher.onDidDelete(() => void updateInProjectContext(env)), + ]; + return { + dispose: () => { + for (const disposable of disposables) { + disposable.dispose(); + } + }, + }; +} diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index b8a2912..26e0011 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -13,7 +13,8 @@ interface PackageManifest { activationEvents?: string[]; capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string } }; contributes?: { - commands?: Array<{ command: string }>; + commands?: Array<{ command: string; icon?: string }>; + menus?: { "editor/title"?: Array<{ command: string; group?: string; when?: string }> }; configuration?: { properties?: Record }; configurationDefaults?: Record; languages?: Array<{ id: string; aliases?: string[]; filenames?: string[]; configuration?: string }>; @@ -28,7 +29,7 @@ test("declares the official clangd dependency and mcpp commands", () => { assert.equal(manifest.version, "0.2.4"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); - assert.ok(manifest.activationEvents?.includes("onLanguage:mcpp-build")); + assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); assert.equal( manifest.capabilities?.untrustedWorkspaces?.description, @@ -61,6 +62,19 @@ test("declares the official clangd dependency and mcpp commands", () => { }); }); +test("shows editor title buttons only inside mcpp projects", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + assert.deepEqual(manifest.contributes?.menus?.["editor/title"], [ + { command: "mcpp.run", group: "navigation@1", when: "mcpp.inProject" }, + { command: "mcpp.test", group: "navigation@2", when: "mcpp.inProject" }, + ]); + + const commands = manifest.contributes?.commands ?? []; + assert.equal(commands.find((command) => command.command === "mcpp.run")?.icon, "$(play)"); + assert.equal(commands.find((command) => command.command === "mcpp.test")?.icon, "$(beaker)"); + assert.ok(!commands.some((command) => command.command === "mcpp.inProject")); +}); + test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; const associations = manifest.contributes?.configurationDefaults?.["files.associations"] as diff --git a/test/inProject.test.ts b/test/inProject.test.ts new file mode 100644 index 0000000..2476d11 --- /dev/null +++ b/test/inProject.test.ts @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + IN_PROJECT_CONTEXT_KEY, + MCPP_MANIFEST_GLOB, + registerInProjectContext, + updateInProjectContext, + type InProjectEnvironment, +} from "../src/inProject"; + +interface FakeState { + manifests: unknown[]; + contextValues: Map; + createListeners: Array<() => void>; + deleteListeners: Array<() => void>; + watcherDisposed: boolean; +} + +function fakeEnvironment(manifests: unknown[] = []): { env: InProjectEnvironment; state: FakeState } { + const state: FakeState = { + manifests: [...manifests], + contextValues: new Map(), + createListeners: [], + deleteListeners: [], + watcherDisposed: false, + }; + const env: InProjectEnvironment = { + findMcppManifests: () => Promise.resolve(state.manifests), + setContextValue: (key, value) => { + state.contextValues.set(key, value); + return Promise.resolve(); + }, + createManifestWatcher: () => ({ + onDidCreate: (listener) => { + state.createListeners.push(listener); + return { dispose: () => undefined }; + }, + onDidDelete: (listener) => { + state.deleteListeners.push(listener); + return { dispose: () => undefined }; + }, + dispose: () => { + state.watcherDisposed = true; + }, + }), + }; + return { env, state }; +} + +async function flushAsync(): Promise { + await new Promise((resolve) => setImmediate(resolve)); +} + +test("exposes the fixed context key and manifest glob", () => { + assert.equal(IN_PROJECT_CONTEXT_KEY, "mcpp.inProject"); + assert.equal(MCPP_MANIFEST_GLOB, "**/mcpp.toml"); +}); + +test("sets the context key to true when an mcpp.toml exists", async () => { + const { env, state } = fakeEnvironment(["/work/app/mcpp.toml"]); + const result = await updateInProjectContext(env); + assert.equal(result, true); + assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true); +}); + +test("sets the context key to false when no mcpp.toml exists", async () => { + const { env, state } = fakeEnvironment(); + const result = await updateInProjectContext(env); + assert.equal(result, false); + assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), false); +}); + +test("writes the context on registration and owns the watcher lifecycle", async () => { + const { env, state } = fakeEnvironment(["/work/app/mcpp.toml"]); + const registration = registerInProjectContext(env); + await flushAsync(); + assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true); + assert.equal(state.createListeners.length, 1); + assert.equal(state.deleteListeners.length, 1); + + registration.dispose(); + assert.equal(state.watcherDisposed, true); +}); + +test("re-evaluates the context when mcpp.toml is created or deleted", async () => { + const { env, state } = fakeEnvironment(); + registerInProjectContext(env); + await flushAsync(); + assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), false); + + state.manifests.push("/work/app/mcpp.toml"); + for (const listener of state.createListeners) { + listener(); + } + await flushAsync(); + assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true); + + state.manifests.length = 0; + for (const listener of state.deleteListeners) { + listener(); + } + await flushAsync(); + assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), false); +}); From f2da6bafac656566ffbe0a52f4c99e5ee856057a Mon Sep 17 00:00:00 2001 From: wellwei Date: Thu, 6 Aug 2026 13:13:57 +0800 Subject: [PATCH 022/119] fix: scope editor actions to active mcpp member --- src/discovery.ts | 22 ++++++++++++++- src/extension.ts | 45 +++++++++++++++++------------ src/inProject.ts | 18 ++++-------- test/discovery.test.ts | 36 ++++++++++++++++++++++++ test/inProject.test.ts | 64 ++++++++++++++++++------------------------ 5 files changed, 117 insertions(+), 68 deletions(-) diff --git a/src/discovery.ts b/src/discovery.ts index 837bc9b..e5d0efc 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -18,8 +18,21 @@ function unique(values: string[]): string[] { return values.filter((value, index) => values.indexOf(value) === index); } -export function findNearestMcppProject(startPath: string): McppProjectDiscovery | undefined { +function isPathWithin(candidate: string, root: string): boolean { + const relative = path.relative(root, candidate); + return relative === "" || ( + relative !== ".." + && !relative.startsWith(`..${path.sep}`) + && !path.isAbsolute(relative) + ); +} + +export function findNearestMcppProject( + startPath: string, + workspaceRoot?: string, +): McppProjectDiscovery | undefined { let current = path.resolve(startPath); + const boundary = workspaceRoot === undefined ? undefined : path.resolve(workspaceRoot); try { if (statSync(current).isFile()) { @@ -29,6 +42,10 @@ export function findNearestMcppProject(startPath: string): McppProjectDiscovery // 新创建的 VS Code 工作区路径可能尚不存在,此时按目录处理。 } + if (boundary !== undefined && !isPathWithin(current, boundary)) { + return undefined; + } + while (true) { const manifestPath = path.join(current, "mcpp.toml"); if (existsSync(manifestPath)) { @@ -39,6 +56,9 @@ export function findNearestMcppProject(startPath: string): McppProjectDiscovery }; } + if (boundary !== undefined && current === boundary) { + return undefined; + } const parent = path.dirname(current); if (parent === current) { return undefined; diff --git a/src/extension.ts b/src/extension.ts index 3ec9356..802b186 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -79,16 +79,21 @@ const moduleCheckOperations = createLatestOperationTracker(); let lastReconciledProjectRoot: string | undefined; function findCurrentProject(): McppProjectDiscovery | undefined { - const activePath = vscode.window.activeTextEditor?.document.uri.scheme === "file" - ? vscode.window.activeTextEditor.document.uri.fsPath - : undefined; - const searchPaths = [ - ...(activePath === undefined ? [] : [activePath]), - ...(vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath) ?? []), - ]; - - for (const searchPath of searchPaths) { - const project = findNearestMcppProject(searchPath); + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor !== undefined) { + const activeUri = activeEditor.document.uri; + if (activeUri.scheme !== "file") { + return undefined; + } + const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeUri); + if (workspaceFolder === undefined) { + return undefined; + } + return findNearestMcppProject(activeUri.fsPath, workspaceFolder.uri.fsPath); + } + + for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { + const project = findNearestMcppProject(workspaceFolder.uri.fsPath); if (project !== undefined) { return project; } @@ -850,13 +855,6 @@ async function autoConfigureModulesWizard( export async function activate(extensionContext: vscode.ExtensionContext): Promise { moduleStatusByProject.clear(); moduleCheckOperations.clear(); - extensionContext.subscriptions.push( - registerInProjectContext({ - findMcppManifests: () => vscode.workspace.findFiles(MCPP_MANIFEST_GLOB, null, 1), - setContextValue: (key, value) => vscode.commands.executeCommand("setContext", key, value), - createManifestWatcher: () => vscode.workspace.createFileSystemWatcher(MCPP_MANIFEST_GLOB), - }), - ); const output = vscode.window.createOutputChannel("mcpp"); const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); @@ -870,8 +868,18 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi void vscode.window.showErrorMessage(`mcpp:${message}`); } }; - const manifestWatcher = vscode.workspace.createFileSystemWatcher("**/mcpp.toml"); + const manifestWatcher = vscode.workspace.createFileSystemWatcher(MCPP_MANIFEST_GLOB); const compilationDatabaseWatcher = vscode.workspace.createFileSystemWatcher("**/compile_commands.json"); + const inProjectContext = registerInProjectContext({ + currentProject: findCurrentProject, + setContextValue: (key, value) => vscode.commands.executeCommand("setContext", key, value), + subscribe: (listener) => [ + vscode.window.onDidChangeActiveTextEditor(listener), + vscode.workspace.onDidChangeWorkspaceFolders(listener), + manifestWatcher.onDidCreate(listener), + manifestWatcher.onDidDelete(listener), + ], + }); const executeWithWorkspaceClangd = createSerialExecutor(); const reconcileProjectContext = async ( project: McppProjectDiscovery | undefined, @@ -1162,6 +1170,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi })), configurationWatcher, trustWatcher, + inProjectContext, vscode.window.onDidChangeActiveTextEditor(() => { refreshStatus(); const current = findCurrentProject(); diff --git a/src/inProject.ts b/src/inProject.ts index bcc0e44..7b901e1 100644 --- a/src/inProject.ts +++ b/src/inProject.ts @@ -1,33 +1,25 @@ export const IN_PROJECT_CONTEXT_KEY = "mcpp.inProject"; export const MCPP_MANIFEST_GLOB = "**/mcpp.toml"; -export interface FileSystemWatcherLike { - onDidCreate(listener: () => void): { dispose(): unknown }; - onDidDelete(listener: () => void): { dispose(): unknown }; +export interface DisposableLike { dispose(): unknown; } export interface InProjectEnvironment { - findMcppManifests(): PromiseLike; + currentProject(): unknown | undefined; setContextValue(key: string, value: boolean): PromiseLike; - createManifestWatcher(): FileSystemWatcherLike; + subscribe(listener: () => void): readonly DisposableLike[]; } export async function updateInProjectContext(env: InProjectEnvironment): Promise { - const manifests = await env.findMcppManifests(); - const inProject = manifests.length > 0; + const inProject = env.currentProject() !== undefined; await env.setContextValue(IN_PROJECT_CONTEXT_KEY, inProject); return inProject; } export function registerInProjectContext(env: InProjectEnvironment): { dispose(): unknown } { void updateInProjectContext(env); - const watcher = env.createManifestWatcher(); - const disposables = [ - watcher, - watcher.onDidCreate(() => void updateInProjectContext(env)), - watcher.onDidDelete(() => void updateInProjectContext(env)), - ]; + const disposables = env.subscribe(() => void updateInProjectContext(env)); return { dispose: () => { for (const disposable of disposables) { diff --git a/test/discovery.test.ts b/test/discovery.test.ts index 6a91d01..2a44bae 100644 --- a/test/discovery.test.ts +++ b/test/discovery.test.ts @@ -24,6 +24,42 @@ test("finds the nearest mcpp manifest and root compilation database", () => { } }); +test("selects the nearest member inside a multi-member workspace", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-members-")); + try { + const memberA = path.join(root, "A"); + const memberB = path.join(root, "B"); + const sourceA = path.join(memberA, "src"); + const sourceB = path.join(memberB, "src"); + mkdirSync(sourceA, { recursive: true }); + mkdirSync(sourceB, { recursive: true }); + writeFileSync(path.join(root, "mcpp.toml"), "[workspace]\nmembers = ['A', 'B']\n"); + writeFileSync(path.join(memberA, "mcpp.toml"), "[package]\nname = 'A'\n"); + writeFileSync(path.join(memberB, "mcpp.toml"), "[package]\nname = 'B'\n"); + + assert.equal(findNearestMcppProject(sourceA, root)?.root, memberA); + assert.equal(findNearestMcppProject(sourceB, root)?.root, memberB); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("does not discover an mcpp project outside the opened workspace folder", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-workspace-boundary-")); + try { + const openedMember = path.join(root, "A"); + const externalMemberSource = path.join(root, "B", "src"); + mkdirSync(openedMember, { recursive: true }); + mkdirSync(externalMemberSource, { recursive: true }); + writeFileSync(path.join(openedMember, "mcpp.toml"), "[package]\nname = 'A'\n"); + writeFileSync(path.join(root, "B", "mcpp.toml"), "[package]\nname = 'B'\n"); + + assert.equal(findNearestMcppProject(externalMemberSource, openedMember), undefined); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("derives sibling, xlings and PATH clangd candidates", () => { assert.deepEqual( deriveClangdCandidates("/tools/xim-x-llvm/22.1.8/bin/clang++"), diff --git a/test/inProject.test.ts b/test/inProject.test.ts index 2476d11..ba4e942 100644 --- a/test/inProject.test.ts +++ b/test/inProject.test.ts @@ -10,40 +10,33 @@ import { } from "../src/inProject"; interface FakeState { - manifests: unknown[]; + project: unknown | undefined; contextValues: Map; - createListeners: Array<() => void>; - deleteListeners: Array<() => void>; - watcherDisposed: boolean; + changeListeners: Array<() => void>; + disposed: boolean; } -function fakeEnvironment(manifests: unknown[] = []): { env: InProjectEnvironment; state: FakeState } { +function fakeEnvironment(project?: unknown): { env: InProjectEnvironment; state: FakeState } { const state: FakeState = { - manifests: [...manifests], + project, contextValues: new Map(), - createListeners: [], - deleteListeners: [], - watcherDisposed: false, + changeListeners: [], + disposed: false, }; const env: InProjectEnvironment = { - findMcppManifests: () => Promise.resolve(state.manifests), + currentProject: () => state.project, setContextValue: (key, value) => { state.contextValues.set(key, value); return Promise.resolve(); }, - createManifestWatcher: () => ({ - onDidCreate: (listener) => { - state.createListeners.push(listener); - return { dispose: () => undefined }; - }, - onDidDelete: (listener) => { - state.deleteListeners.push(listener); - return { dispose: () => undefined }; - }, - dispose: () => { - state.watcherDisposed = true; - }, - }), + subscribe: (listener) => { + state.changeListeners.push(listener); + return [{ + dispose: () => { + state.disposed = true; + }, + }]; + }, }; return { env, state }; } @@ -57,47 +50,46 @@ test("exposes the fixed context key and manifest glob", () => { assert.equal(MCPP_MANIFEST_GLOB, "**/mcpp.toml"); }); -test("sets the context key to true when an mcpp.toml exists", async () => { - const { env, state } = fakeEnvironment(["/work/app/mcpp.toml"]); +test("sets the context key to true when the active resource resolves to an mcpp project", async () => { + const { env, state } = fakeEnvironment({ root: "/work/A" }); const result = await updateInProjectContext(env); assert.equal(result, true); assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true); }); -test("sets the context key to false when no mcpp.toml exists", async () => { +test("sets the context key to false when the active resource has no mcpp project", async () => { const { env, state } = fakeEnvironment(); const result = await updateInProjectContext(env); assert.equal(result, false); assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), false); }); -test("writes the context on registration and owns the watcher lifecycle", async () => { - const { env, state } = fakeEnvironment(["/work/app/mcpp.toml"]); +test("writes the context on registration and owns the change subscriptions", async () => { + const { env, state } = fakeEnvironment({ root: "/work/A" }); const registration = registerInProjectContext(env); await flushAsync(); assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true); - assert.equal(state.createListeners.length, 1); - assert.equal(state.deleteListeners.length, 1); + assert.equal(state.changeListeners.length, 1); registration.dispose(); - assert.equal(state.watcherDisposed, true); + assert.equal(state.disposed, true); }); -test("re-evaluates the context when mcpp.toml is created or deleted", async () => { +test("re-evaluates the context when the active project changes", async () => { const { env, state } = fakeEnvironment(); registerInProjectContext(env); await flushAsync(); assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), false); - state.manifests.push("/work/app/mcpp.toml"); - for (const listener of state.createListeners) { + state.project = { root: "/work/A" }; + for (const listener of state.changeListeners) { listener(); } await flushAsync(); assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true); - state.manifests.length = 0; - for (const listener of state.deleteListeners) { + state.project = undefined; + for (const listener of state.changeListeners) { listener(); } await flushAsync(); From 81e3b767a05652b8565be15dc48f220c3aba3e22 Mon Sep 17 00:00:00 2001 From: wellwei Date: Thu, 6 Aug 2026 14:39:45 +0800 Subject: [PATCH 023/119] chore: release mcpp-vscode 0.2.5 --- CHANGELOG.md | 6 ++++++ README.md | 10 +++++----- package-lock.json | 4 ++-- package.json | 2 +- src/cliController.ts | 4 ++-- src/commands.ts | 3 ++- test/artifacts.test.ts | 2 +- test/commands.test.ts | 7 +++++-- 8 files changed, 24 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17587c8..a51eb06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +## 0.2.5 + +- 编辑器标题栏的 mcpp 运行/测试操作按当前活动文件所属的 mcpp member 作用域执行;工作区外 + 打开的文件不会显示这些按钮。 +- 将状态栏快捷菜单名称明确为 `$(tools) mcpp: 快捷菜单`,与模块可用性状态按钮区分。 + ## 0.2.4 - 修复 hermetic mcpp 编译数据库被插件追加 `--query-driver` 后导致的 clangd 标准库和 diff --git a/README.md b/README.md index cfc88b6..4e0d263 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -当前版本为 `0.2.4`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +当前版本为 `0.2.5`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 > 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 @@ -36,7 +36,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.2.4.vsix +code --install-extension /path/to/mcpp-vscode-0.2.5.vsix ``` 安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 @@ -131,7 +131,7 @@ Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明, ### mcpp CLI 与工具链管理 -状态栏中的 `$(tools) mcpp` 菜单提供: +状态栏中的 `$(tools) mcpp: 快捷菜单` 提供: - 在当前工程根目录执行 `mcpp build`、`run`、`test` 和 `clean`。 - 在 VS Code 任务终端中实时显示完整输出。 @@ -381,8 +381,8 @@ API、状态栏、任务和 clangd 集成。 版本完全一致的 tag: ```sh -git tag -a v0.2.4 -m "mcpp-vscode 0.2.4" -git push origin v0.2.4 +git tag -a v0.2.5 -m "mcpp-vscode 0.2.5" +git push origin v0.2.5 ``` `.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, diff --git a/package-lock.json b/package-lock.json index 9613a23..7cd3fcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.2.4", + "version": "0.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.2.4", + "version": "0.2.5", "license": "Apache-2.0", "devDependencies": { "@types/node": "^24.0.0", diff --git a/package.json b/package.json index b43d185..e83188c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.4", + "version": "0.2.5", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", diff --git a/src/cliController.ts b/src/cliController.ts index e3453df..77766cf 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -22,7 +22,7 @@ import { type ProjectTaskKind, type TaskCompletion, } from "./tasks"; -import { CLI_COMMANDS, quickMenuItems } from "./commands"; +import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; export interface McppCliControllerOptions { output: vscode.OutputChannel; @@ -74,7 +74,7 @@ export class McppCliController { public constructor(private readonly options: McppCliControllerOptions) { this.status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 40); this.status.command = CLI_COMMANDS.showMenu; - this.status.text = "$(tools) mcpp"; + this.status.text = quickMenuStatusText; this.status.tooltip = "打开 mcpp 项目和工具链快捷菜单"; } diff --git a/src/commands.ts b/src/commands.ts index 766b3b9..bd1c8fb 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -10,6 +10,8 @@ export const CLI_COMMANDS = { autoConfigureModules: "mcpp.autoConfigureModules", } as const; +export const quickMenuStatusText = "$(tools) mcpp: 快捷菜单"; + export interface QuickMenuItem { label: string; command: string; @@ -29,4 +31,3 @@ export const quickMenuItems: readonly QuickMenuItem[] = [ { label: "$(check) 检查模块支持", command: "mcpp.checkModuleSupport", group: "ide" }, { label: "$(rocket) 一键配置模块代码提示", command: CLI_COMMANDS.autoConfigureModules, group: "ide" }, ]; - diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 26e0011..f2d24cb 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -26,7 +26,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.4"); + assert.equal(manifest.version, "0.2.5"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); diff --git a/test/commands.test.ts b/test/commands.test.ts index 28f329b..6e59248 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -1,7 +1,11 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { CLI_COMMANDS, quickMenuItems } from "../src/commands"; +import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "../src/commands"; + +test("状态栏快捷菜单名称与模块状态易于区分", () => { + assert.equal(quickMenuStatusText, "$(tools) mcpp: 快捷菜单"); +}); test("CLI 命令覆盖项目、工具链和 IDE", () => { assert.deepEqual(Object.values(CLI_COMMANDS), [ @@ -33,4 +37,3 @@ test("CLI 命令覆盖项目、工具链和 IDE", () => { ); assert.ok(quickMenuItems.every((item) => item.label.length > 0)); }); - From a79cbe05bfaf97208cd127c5b950dfdc97bf5f9b Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Thu, 6 Aug 2026 17:38:12 +0800 Subject: [PATCH 024/119] feat: add mcpp.newProject command to scaffold and open projects --- package.json | 5 +++ src/cliController.ts | 70 ++++++++++++++++++++++++++++++++++++++++++ src/commands.ts | 1 + src/extension.ts | 17 +++++++++- test/artifacts.test.ts | 30 ++++++++++++++++++ test/commands.test.ts | 1 + 6 files changed, 123 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e83188c..071823f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "onCommand:mcpp.refreshCompilationDatabase", "onCommand:mcpp.checkModuleSupport", "onCommand:mcpp.showMenu", + "onCommand:mcpp.newProject", "onCommand:mcpp.build", "onCommand:mcpp.run", "onCommand:mcpp.test", @@ -68,6 +69,10 @@ "command": "mcpp.showMenu", "title": "mcpp: 打开快捷菜单" }, + { + "command": "mcpp.newProject", + "title": "mcpp: 新建工程" + }, { "command": "mcpp.build", "title": "mcpp: 构建" diff --git a/src/cliController.ts b/src/cliController.ts index 77766cf..5324bfd 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -1,3 +1,4 @@ +import { join } from "node:path"; import process from "node:process"; import * as vscode from "vscode"; @@ -24,6 +25,8 @@ import { } from "./tasks"; import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; +export const PENDING_NEW_PROJECT_KEY = "mcpp.pendingNewProject"; + export interface McppCliControllerOptions { output: vscode.OutputChannel; currentProject: () => McppProjectDiscovery | undefined; @@ -33,6 +36,7 @@ export interface McppCliControllerOptions { completion: TaskCompletion, ) => Promise; isTrusted: () => boolean; + globalState: vscode.Memento; } interface ToolchainPickItem extends vscode.QuickPickItem { @@ -82,6 +86,7 @@ export class McppCliController { const disposables: vscode.Disposable[] = [ this.status, vscode.commands.registerCommand(CLI_COMMANDS.showMenu, this.guarded(() => this.showMenu())), + vscode.commands.registerCommand(CLI_COMMANDS.newProject, this.guarded(() => this.newProject())), vscode.commands.registerCommand(CLI_COMMANDS.build, this.guarded(() => this.runProjectTask("build"))), vscode.commands.registerCommand(CLI_COMMANDS.run, this.guarded(() => this.runProjectTask("run"))), vscode.commands.registerCommand(CLI_COMMANDS.test, this.guarded(() => this.runProjectTask("test"))), @@ -493,6 +498,71 @@ export class McppCliController { } } + public async newProject(): Promise { + if (!this.requireTrusted()) { + return; + } + + const input = await vscode.window.showInputBox({ + title: "新建 mcpp 工程(1/2)", + prompt: "输入项目名,将在所选位置创建同名项目文件夹", + placeHolder: "hello-mcpp", + validateInput: (value) => { + const trimmed = value.trim(); + if (trimmed.length === 0) { + return "项目名不能为空"; + } + if (/[\\/]/.test(trimmed)) { + return "项目名不能包含路径分隔符"; + } + return undefined; + }, + }); + if (input === undefined) { + return; + } + const projectName = input.trim(); + + const picked = await vscode.window.showOpenDialog({ + title: "选择项目位置(2/2)", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "在此创建项目", + }); + const location = picked?.[0]; + if (location === undefined) { + return; + } + + const projectRoot = join(location.fsPath, projectName); + const confirmCreate = "创建并打开"; + const choice = await vscode.window.showWarningMessage( + `将在 ${location.fsPath} 执行 “mcpp new ${projectName}”,创建项目文件夹 ${projectRoot} 并打开它。`, + { modal: true }, + confirmCreate, + ); + if (choice !== confirmCreate) { + return; + } + + const executable = this.mcppExecutable(undefined); + const args = mcppCommandArguments("new", projectName); + const result = await runProcess(executable, args, location.fsPath); + this.appendShortCommand("新建工程", executable, args, result); + if (result.exitCode !== 0) { + await vscode.window.showErrorMessage( + `mcpp new ${projectName} 失败(退出码 ${result.exitCode})。请查看 mcpp 输出频道。`, + ); + return; + } + + // vscode.openFolder 会重载窗口;通过 globalState 把刷新请求带到新窗口, + // 由 activate() 在新窗口中执行 mcpp.refreshCompilationDatabase 并激活 clangd。 + await this.options.globalState.update(PENDING_NEW_PROJECT_KEY, projectRoot); + await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(projectRoot)); + } + private guarded(operation: () => Promise): () => Promise { return async () => { try { diff --git a/src/commands.ts b/src/commands.ts index bd1c8fb..a28796c 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,5 +1,6 @@ export const CLI_COMMANDS = { showMenu: "mcpp.showMenu", + newProject: "mcpp.newProject", build: "mcpp.build", run: "mcpp.run", test: "mcpp.test", diff --git a/src/extension.ts b/src/extension.ts index 802b186..1def283 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,7 +24,7 @@ import { xlingsInstallArgs, } from "./llvmTools"; import { CLI_COMMANDS } from "./commands"; -import { McppCliController } from "./cliController"; +import { McppCliController, PENDING_NEW_PROJECT_KEY } from "./cliController"; import { runClangdCheck, runToolVersion, type ToolVersionResult } from "./process"; import { configurationReadyAfterRestart, @@ -1022,6 +1022,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi currentProject: findCurrentProject, afterProjectTask, isTrusted: () => vscode.workspace.isTrusted, + globalState: extensionContext.globalState, }); extensionContext.subscriptions.push( @@ -1218,6 +1219,20 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi const message = error instanceof Error ? error.message : String(error); appendOutputLine(output, `[自动配置] ${message}`); } + + // 新建工程流程在打开文件夹前排入了刷新请求;窗口重载后在这里兑现。 + // 刷新会跑完整 mcpp build,不能 await:activate 未完成时按钮等命令会排队, + // 表现为编辑器标题按钮点击无反应。 + const pendingNewProject = extensionContext.globalState.get(PENDING_NEW_PROJECT_KEY); + if (pendingNewProject !== undefined) { + await extensionContext.globalState.update(PENDING_NEW_PROJECT_KEY, undefined); + if (findCurrentProject()?.root === pendingNewProject) { + void vscode.commands.executeCommand(COMMAND_REFRESH).then(undefined, (error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[新建工程] ${message}`); + }); + } + } } export function deactivate(): void { diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index f2d24cb..e3373db 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -39,6 +39,7 @@ test("declares the official clangd dependency and mcpp commands", () => { manifest.contributes?.commands?.map((command) => command.command), [ "mcpp.showMenu", + "mcpp.newProject", "mcpp.build", "mcpp.run", "mcpp.test", @@ -231,6 +232,35 @@ test("泛化 triple 工具链由 mcpp 最终校验", () => { assert.match(method, /可能携带 target 语义.*最终由 mcpp 校验/s); }); +test("新建工程选定位置后先确认再创建,打开前排入刷新请求", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const start = source.indexOf("public async newProject"); + const end = source.indexOf("private guarded", start); + assert.notEqual(start, -1); + assert.notEqual(end, -1); + + const method = source.slice(start, end); + const locationPick = method.indexOf("showOpenDialog"); + const confirm = method.indexOf("showWarningMessage"); + const create = method.indexOf("runProcess"); + const pending = method.indexOf("globalState.update(PENDING_NEW_PROJECT_KEY"); + const open = method.indexOf('executeCommand("vscode.openFolder"'); + assert.ok(locationPick >= 0 && locationPick < confirm); + assert.ok(confirm >= 0 && confirm < create); + assert.ok(create >= 0 && create < pending); + assert.ok(pending >= 0 && pending < open); +}); + +test("打开新建工程后刷新编译数据库", () => { + const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); + const start = source.indexOf("globalState.get(PENDING_NEW_PROJECT_KEY)"); + assert.notEqual(start, -1); + + // 不能 await 刷新:activate 未完成时按钮等命令会排队,表现为点击无反应。 + const refresh = source.indexOf("void vscode.commands.executeCommand(COMMAND_REFRESH)", start); + assert.ok(refresh >= 0); +}); + test("声明 GitHub 仓库和扩展图标", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; assert.equal(manifest.icon, "images/logo.png"); diff --git a/test/commands.test.ts b/test/commands.test.ts index 6e59248..843a2fc 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -10,6 +10,7 @@ test("状态栏快捷菜单名称与模块状态易于区分", () => { test("CLI 命令覆盖项目、工具链和 IDE", () => { assert.deepEqual(Object.values(CLI_COMMANDS), [ "mcpp.showMenu", + "mcpp.newProject", "mcpp.build", "mcpp.run", "mcpp.test", From d8db658746295c0a2fb3b2fbb1b3dbcb48fc573c Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Fri, 7 Aug 2026 22:23:34 +0800 Subject: [PATCH 025/119] fix: harden new-project validation, drop pending auto-build state --- src/cliController.ts | 25 +++++++++---------------- src/extension.ts | 17 +---------------- src/newProject.ts | 23 +++++++++++++++++++++++ test/artifacts.test.ts | 24 +++++++++++------------- test/newProject.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 45 deletions(-) create mode 100644 src/newProject.ts create mode 100644 test/newProject.test.ts diff --git a/src/cliController.ts b/src/cliController.ts index 5324bfd..56d880c 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -1,3 +1,4 @@ +import { existsSync } from "node:fs"; import { join } from "node:path"; import process from "node:process"; @@ -24,8 +25,7 @@ import { type TaskCompletion, } from "./tasks"; import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; - -export const PENDING_NEW_PROJECT_KEY = "mcpp.pendingNewProject"; +import { validateNewProjectName } from "./newProject"; export interface McppCliControllerOptions { output: vscode.OutputChannel; @@ -36,7 +36,6 @@ export interface McppCliControllerOptions { completion: TaskCompletion, ) => Promise; isTrusted: () => boolean; - globalState: vscode.Memento; } interface ToolchainPickItem extends vscode.QuickPickItem { @@ -507,16 +506,7 @@ export class McppCliController { title: "新建 mcpp 工程(1/2)", prompt: "输入项目名,将在所选位置创建同名项目文件夹", placeHolder: "hello-mcpp", - validateInput: (value) => { - const trimmed = value.trim(); - if (trimmed.length === 0) { - return "项目名不能为空"; - } - if (/[\\/]/.test(trimmed)) { - return "项目名不能包含路径分隔符"; - } - return undefined; - }, + validateInput: validateNewProjectName, }); if (input === undefined) { return; @@ -536,6 +526,10 @@ export class McppCliController { } const projectRoot = join(location.fsPath, projectName); + if (existsSync(projectRoot)) { + await vscode.window.showErrorMessage(`目标路径已存在:${projectRoot}。请更换项目名或位置。`); + return; + } const confirmCreate = "创建并打开"; const choice = await vscode.window.showWarningMessage( `将在 ${location.fsPath} 执行 “mcpp new ${projectName}”,创建项目文件夹 ${projectRoot} 并打开它。`, @@ -557,9 +551,8 @@ export class McppCliController { return; } - // vscode.openFolder 会重载窗口;通过 globalState 把刷新请求带到新窗口, - // 由 activate() 在新窗口中执行 mcpp.refreshCompilationDatabase 并激活 clangd。 - await this.options.globalState.update(PENDING_NEW_PROJECT_KEY, projectRoot); + // 契约:创建并打开工程。打开后的构建交给用户手动触发(或后续 #5 的 + // IDE configure 流程),避免与缺少 CDB 时的 configure 重复执行。 await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(projectRoot)); } diff --git a/src/extension.ts b/src/extension.ts index 1def283..802b186 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,7 +24,7 @@ import { xlingsInstallArgs, } from "./llvmTools"; import { CLI_COMMANDS } from "./commands"; -import { McppCliController, PENDING_NEW_PROJECT_KEY } from "./cliController"; +import { McppCliController } from "./cliController"; import { runClangdCheck, runToolVersion, type ToolVersionResult } from "./process"; import { configurationReadyAfterRestart, @@ -1022,7 +1022,6 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi currentProject: findCurrentProject, afterProjectTask, isTrusted: () => vscode.workspace.isTrusted, - globalState: extensionContext.globalState, }); extensionContext.subscriptions.push( @@ -1219,20 +1218,6 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi const message = error instanceof Error ? error.message : String(error); appendOutputLine(output, `[自动配置] ${message}`); } - - // 新建工程流程在打开文件夹前排入了刷新请求;窗口重载后在这里兑现。 - // 刷新会跑完整 mcpp build,不能 await:activate 未完成时按钮等命令会排队, - // 表现为编辑器标题按钮点击无反应。 - const pendingNewProject = extensionContext.globalState.get(PENDING_NEW_PROJECT_KEY); - if (pendingNewProject !== undefined) { - await extensionContext.globalState.update(PENDING_NEW_PROJECT_KEY, undefined); - if (findCurrentProject()?.root === pendingNewProject) { - void vscode.commands.executeCommand(COMMAND_REFRESH).then(undefined, (error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[新建工程] ${message}`); - }); - } - } } export function deactivate(): void { diff --git a/src/newProject.ts b/src/newProject.ts new file mode 100644 index 0000000..bffc4d8 --- /dev/null +++ b/src/newProject.ts @@ -0,0 +1,23 @@ +/** + * 新建工程的项目名校验。返回错误提示;undefined 表示合法。 + * + * 项目名随后作为 `mcpp new ` 的 argv 传入:参数数组只能防 shell 注入, + * 不能阻止 mcpp 自身把名字解析为 CLI 选项(如 --template),所以这里拒绝 + * `-` 前缀以及 `.`、`..`。 + */ +export function validateNewProjectName(input: string): string | undefined { + const name = input.trim(); + if (name.length === 0) { + return "项目名不能为空"; + } + if (/[\\/]/.test(name)) { + return "项目名不能包含路径分隔符"; + } + if (name.startsWith("-")) { + return "项目名不能以 - 开头,否则会被 mcpp 解析为命令行选项"; + } + if (name === "." || name === "..") { + return "项目名不能是 . 或 .."; + } + return undefined; +} diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index e3373db..47eae62 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -232,7 +232,7 @@ test("泛化 triple 工具链由 mcpp 最终校验", () => { assert.match(method, /可能携带 target 语义.*最终由 mcpp 校验/s); }); -test("新建工程选定位置后先确认再创建,打开前排入刷新请求", () => { +test("新建工程先校验目标路径再确认创建,成功后只打开不构建", () => { const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); const start = source.indexOf("public async newProject"); const end = source.indexOf("private guarded", start); @@ -241,24 +241,22 @@ test("新建工程选定位置后先确认再创建,打开前排入刷新请 const method = source.slice(start, end); const locationPick = method.indexOf("showOpenDialog"); + const exists = method.indexOf("existsSync(projectRoot)"); const confirm = method.indexOf("showWarningMessage"); const create = method.indexOf("runProcess"); - const pending = method.indexOf("globalState.update(PENDING_NEW_PROJECT_KEY"); const open = method.indexOf('executeCommand("vscode.openFolder"'); - assert.ok(locationPick >= 0 && locationPick < confirm); + assert.match(method, /validateNewProjectName/); + assert.ok(locationPick >= 0 && locationPick < exists); + assert.ok(exists >= 0 && exists < confirm); assert.ok(confirm >= 0 && confirm < create); - assert.ok(create >= 0 && create < pending); - assert.ok(pending >= 0 && pending < open); + assert.ok(create >= 0 && create < open); }); -test("打开新建工程后刷新编译数据库", () => { - const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); - const start = source.indexOf("globalState.get(PENDING_NEW_PROJECT_KEY)"); - assert.notEqual(start, -1); - - // 不能 await 刷新:activate 未完成时按钮等命令会排队,表现为点击无反应。 - const refresh = source.indexOf("void vscode.commands.executeCommand(COMMAND_REFRESH)", start); - assert.ok(refresh >= 0); +test("新建工程契约是创建并打开,不自动构建", () => { + const controller = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const extension = readFileSync(path.join(root, "src/extension.ts"), "utf8"); + assert.doesNotMatch(controller, /globalState|PENDING_NEW_PROJECT/); + assert.doesNotMatch(extension, /PENDING_NEW_PROJECT/); }); test("声明 GitHub 仓库和扩展图标", () => { diff --git a/test/newProject.test.ts b/test/newProject.test.ts new file mode 100644 index 0000000..615f7ca --- /dev/null +++ b/test/newProject.test.ts @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { validateNewProjectName } from "../src/newProject"; + +test("拒绝空值和纯空白项目名", () => { + for (const name of ["", " "]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${JSON.stringify(name)}`); + } +}); + +test("拒绝包含路径分隔符的项目名", () => { + for (const name of ["a/b", "a\\b", "/abs", "..\\up"]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${name}`); + } +}); + +test("拒绝会被 mcpp 解析为 CLI 选项的项目名", () => { + // 参数数组只能防 shell 注入;`-` 前缀会被 mcpp 自身解析为 + // --template、--list-templates 等选项,可能成功退出却没有创建工程。 + for (const name of ["-x", "--template", "--list-templates"]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${name}`); + } +}); + +test("拒绝相对路径名 . 和 ..", () => { + for (const name of [".", ".."]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${name}`); + } +}); + +test("接受常规项目名,前后空白忽略", () => { + for (const name of ["hello", "hello-mcpp", "my_project", "a.b.c", "项目", " padded "]) { + assert.equal(validateNewProjectName(name), undefined, `should accept: ${name}`); + } +}); From c239dc09d4a261fb10c2f398479fedd8d605d0da Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Sat, 8 Aug 2026 14:06:11 +0800 Subject: [PATCH 026/119] fix: reject names that corrupt mcpp-generated project files --- src/newProject.ts | 20 ++++++++++++++++++++ test/newProject.test.ts | 19 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/newProject.ts b/src/newProject.ts index bffc4d8..7b47764 100644 --- a/src/newProject.ts +++ b/src/newProject.ts @@ -1,9 +1,17 @@ +const CONTROL_CHARS = /[\u0000-\u001F\u007F]/; +const WINDOWS_RESERVED_CHARS = /[<>:"|?*]/; +const WINDOWS_DEVICE_NAMES = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i; + /** * 新建工程的项目名校验。返回错误提示;undefined 表示合法。 * * 项目名随后作为 `mcpp new ` 的 argv 传入:参数数组只能防 shell 注入, * 不能阻止 mcpp 自身把名字解析为 CLI 选项(如 --template),所以这里拒绝 * `-` 前缀以及 `.`、`..`。 + * + * mcpp 模板把项目名直接写进 mcpp.toml 的 `name = "{}"` 和 main.cpp,不做 + * TOML/C++ 转义,所以拒绝双引号和控制字符;Windows 保留字符、保留设备名和 + * 尾随点一并按跨平台策略拒绝。根本修复应在 mcpp CLI 自身完成。 */ export function validateNewProjectName(input: string): string | undefined { const name = input.trim(); @@ -19,5 +27,17 @@ export function validateNewProjectName(input: string): string | undefined { if (name === "." || name === "..") { return "项目名不能是 . 或 .."; } + if (CONTROL_CHARS.test(name)) { + return "项目名不能包含控制字符"; + } + if (WINDOWS_RESERVED_CHARS.test(name)) { + return '项目名不能包含 <>:"|?* 等保留字符'; + } + if (name.endsWith(".")) { + return "项目名不能以 . 结尾(Windows 不支持)"; + } + if (WINDOWS_DEVICE_NAMES.test(name)) { + return "项目名不能是 Windows 保留设备名"; + } return undefined; } diff --git a/test/newProject.test.ts b/test/newProject.test.ts index 615f7ca..80daec7 100644 --- a/test/newProject.test.ts +++ b/test/newProject.test.ts @@ -29,8 +29,25 @@ test("拒绝相对路径名 . 和 ..", () => { } }); +test("拒绝双引号和控制字符,避免破坏 mcpp 生成的 TOML 和 C++ 源码", () => { + // mcpp 模板把项目名直接写进 mcpp.toml 的 name = "{}" 和 main.cpp, + // 不做 TOML/C++ 转义,这些输入会生成坏工程。 + for (const name of ['bad"name', "bad\tname", "bad\nname", "bad\rname", "bad\u001Fname", "bad\u007Fname"]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${JSON.stringify(name)}`); + } +}); + +test("按跨平台策略拒绝 Windows 保留字符、设备名和尾随点", () => { + for (const name of ["ab", "a:b", "a|b", "a?b", "a*b", "name."]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${name}`); + } + for (const name of ["CON", "con", "PRN", "AUX", "NUL", "COM1", "com9", "LPT1"]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${name}`); + } +}); + test("接受常规项目名,前后空白忽略", () => { - for (const name of ["hello", "hello-mcpp", "my_project", "a.b.c", "项目", " padded "]) { + for (const name of ["hello", "hello-mcpp", "my_project", "a.b.c", "项目", "console", "com10", " padded "]) { assert.equal(validateNewProjectName(name), undefined, `should accept: ${name}`); } }); From 3aa5ca0799df596cc02e35acf1eb600f4d6952e2 Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Sat, 8 Aug 2026 14:17:41 +0800 Subject: [PATCH 027/119] test: cover new-project flow with injectable dependencies --- src/cliController.ts | 48 ++++++++++------------- src/newProject.ts | 42 ++++++++++++++++++++ test/artifacts.test.ts | 14 ++++--- test/newProject.test.ts | 86 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 34 deletions(-) diff --git a/src/cliController.ts b/src/cliController.ts index 56d880c..2e53172 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -25,7 +25,7 @@ import { type TaskCompletion, } from "./tasks"; import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; -import { validateNewProjectName } from "./newProject"; +import { runNewProjectFlow, validateNewProjectName } from "./newProject"; export interface McppCliControllerOptions { output: vscode.OutputChannel; @@ -526,34 +526,26 @@ export class McppCliController { } const projectRoot = join(location.fsPath, projectName); - if (existsSync(projectRoot)) { - await vscode.window.showErrorMessage(`目标路径已存在:${projectRoot}。请更换项目名或位置。`); - return; - } const confirmCreate = "创建并打开"; - const choice = await vscode.window.showWarningMessage( - `将在 ${location.fsPath} 执行 “mcpp new ${projectName}”,创建项目文件夹 ${projectRoot} 并打开它。`, - { modal: true }, - confirmCreate, - ); - if (choice !== confirmCreate) { - return; - } - - const executable = this.mcppExecutable(undefined); - const args = mcppCommandArguments("new", projectName); - const result = await runProcess(executable, args, location.fsPath); - this.appendShortCommand("新建工程", executable, args, result); - if (result.exitCode !== 0) { - await vscode.window.showErrorMessage( - `mcpp new ${projectName} 失败(退出码 ${result.exitCode})。请查看 mcpp 输出频道。`, - ); - return; - } - - // 契约:创建并打开工程。打开后的构建交给用户手动触发(或后续 #5 的 - // IDE configure 流程),避免与缺少 CDB 时的 configure 重复执行。 - await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(projectRoot)); + await runNewProjectFlow(projectName, location.fsPath, projectRoot, { + exists: existsSync, + confirm: async (message) => + (await vscode.window.showWarningMessage(message, { modal: true }, confirmCreate)) + === confirmCreate, + run: async (name, cwd) => { + const executable = this.mcppExecutable(undefined); + const args = mcppCommandArguments("new", name); + const result = await runProcess(executable, args, cwd); + this.appendShortCommand("新建工程", executable, args, result); + return result.exitCode; + }, + openFolder: async (path) => { + await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(path)); + }, + showError: async (message) => { + await vscode.window.showErrorMessage(message); + }, + }); } private guarded(operation: () => Promise): () => Promise { diff --git a/src/newProject.ts b/src/newProject.ts index 7b47764..e57543a 100644 --- a/src/newProject.ts +++ b/src/newProject.ts @@ -1,3 +1,45 @@ +export interface NewProjectActions { + exists(path: string): boolean; + confirm(message: string): Promise; + run(name: string, cwd: string): Promise; + openFolder(path: string): Promise; + showError(message: string): Promise | void; +} + +export type NewProjectOutcome = "exists" | "declined" | "failed" | "opened"; + +/** + * 新建工程的核心流程,依赖全部注入以便单测。契约:创建并打开工程—— + * 打开后的构建交给用户手动触发(或后续 #5 的 IDE configure 流程), + * 避免与缺少 CDB 时的 configure 重复执行。 + */ +export async function runNewProjectFlow( + projectName: string, + location: string, + projectRoot: string, + actions: NewProjectActions, +): Promise { + if (actions.exists(projectRoot)) { + await actions.showError(`目标路径已存在:${projectRoot}。请更换项目名或位置。`); + return "exists"; + } + const confirmed = await actions.confirm( + `将在 ${location} 执行 “mcpp new ${projectName}”,创建项目文件夹 ${projectRoot} 并打开它。`, + ); + if (!confirmed) { + return "declined"; + } + const exitCode = await actions.run(projectName, location); + if (exitCode !== 0) { + await actions.showError( + `mcpp new ${projectName} 失败(退出码 ${exitCode})。请查看 mcpp 输出频道。`, + ); + return "failed"; + } + await actions.openFolder(projectRoot); + return "opened"; +} + const CONTROL_CHARS = /[\u0000-\u001F\u007F]/; const WINDOWS_RESERVED_CHARS = /[<>:"|?*]/; const WINDOWS_DEVICE_NAMES = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i; diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 47eae62..e9f304b 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -239,14 +239,16 @@ test("新建工程先校验目标路径再确认创建,成功后只打开不 assert.notEqual(start, -1); assert.notEqual(end, -1); + // 控制流本身由 test/newProject.test.ts 对 runNewProjectFlow 的行为级测试覆盖; + // 这里只验证控制器把 UI/进程依赖注入流程函数。 const method = source.slice(start, end); - const locationPick = method.indexOf("showOpenDialog"); - const exists = method.indexOf("existsSync(projectRoot)"); - const confirm = method.indexOf("showWarningMessage"); - const create = method.indexOf("runProcess"); - const open = method.indexOf('executeCommand("vscode.openFolder"'); assert.match(method, /validateNewProjectName/); - assert.ok(locationPick >= 0 && locationPick < exists); + assert.match(method, /runNewProjectFlow/); + const flow = method.indexOf("runNewProjectFlow"); + const exists = method.indexOf("existsSync", flow); + const confirm = method.indexOf("showWarningMessage", flow); + const create = method.indexOf("runProcess", flow); + const open = method.indexOf('executeCommand("vscode.openFolder"', flow); assert.ok(exists >= 0 && exists < confirm); assert.ok(confirm >= 0 && confirm < create); assert.ok(create >= 0 && create < open); diff --git a/test/newProject.test.ts b/test/newProject.test.ts index 80daec7..9b2f82c 100644 --- a/test/newProject.test.ts +++ b/test/newProject.test.ts @@ -51,3 +51,89 @@ test("接受常规项目名,前后空白忽略", () => { assert.equal(validateNewProjectName(name), undefined, `should accept: ${name}`); } }); +import { runNewProjectFlow, type NewProjectActions } from "../src/newProject"; + +function recordingActions(overrides: Partial, calls: string[]): NewProjectActions { + return { + exists: (path) => { + calls.push(`exists:${path}`); + return overrides.exists?.(path) ?? false; + }, + confirm: async (message) => { + calls.push(`confirm:${message}`); + return overrides.confirm?.(message) ?? true; + }, + run: async (name, cwd) => { + calls.push(`run:${name}@${cwd}`); + return overrides.run?.(name, cwd) ?? 0; + }, + openFolder: async (path) => { + calls.push(`openFolder:${path}`); + await overrides.openFolder?.(path); + }, + showError: (message) => { + calls.push(`showError:${message}`); + }, + }; +} + +test("目标路径已存在时报错且不确认、不创建、不打开", async () => { + const calls: string[] = []; + const outcome = await runNewProjectFlow( + "demo", + "/parent", + "/parent/demo", + recordingActions({ exists: () => true }, calls), + ); + assert.equal(outcome, "exists"); + assert.deepEqual(calls, [ + "exists:/parent/demo", + "showError:目标路径已存在:/parent/demo。请更换项目名或位置。", + ]); +}); + +test("用户取消确认时不创建、不打开", async () => { + const calls: string[] = []; + const outcome = await runNewProjectFlow( + "demo", + "/parent", + "/parent/demo", + recordingActions({ confirm: async () => false }, calls), + ); + assert.equal(outcome, "declined"); + assert.deepEqual(calls.map((call) => call.split(":", 1)[0]), ["exists", "confirm"]); +}); + +test("mcpp new 失败时报错且不打开", async () => { + const calls: string[] = []; + const outcome = await runNewProjectFlow( + "demo", + "/parent", + "/parent/demo", + recordingActions({ run: async () => 2 }, calls), + ); + assert.equal(outcome, "failed"); + assert.deepEqual(calls, [ + "exists:/parent/demo", + "confirm:将在 /parent 执行 “mcpp new demo”,创建项目文件夹 /parent/demo 并打开它。", + "run:demo@/parent", + "showError:mcpp new demo 失败(退出码 2)。请查看 mcpp 输出频道。", + ]); +}); + +test("创建成功后只打开项目文件夹,不自动构建", async () => { + const calls: string[] = []; + const outcome = await runNewProjectFlow( + "demo", + "/parent", + "/parent/demo", + recordingActions({}, calls), + ); + assert.equal(outcome, "opened"); + assert.deepEqual(calls, [ + "exists:/parent/demo", + "confirm:将在 /parent 执行 “mcpp new demo”,创建项目文件夹 /parent/demo 并打开它。", + "run:demo@/parent", + "openFolder:/parent/demo", + ]); +}); From bfbaffe585e1a3339c9f58605aca04d1577e0f78 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 15:38:44 +0800 Subject: [PATCH 028/119] fix: reject unsafe new-project names --- src/newProject.ts | 7 ++++++- test/newProject.test.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/newProject.ts b/src/newProject.ts index e57543a..07cdead 100644 --- a/src/newProject.ts +++ b/src/newProject.ts @@ -42,7 +42,8 @@ export async function runNewProjectFlow( const CONTROL_CHARS = /[\u0000-\u001F\u007F]/; const WINDOWS_RESERVED_CHARS = /[<>:"|?*]/; -const WINDOWS_DEVICE_NAMES = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i; +const WINDOWS_DEVICE_NAMES = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i; +const MCPP_BUILTIN_TEMPLATE_MARKER = "PROJECT"; /** * 新建工程的项目名校验。返回错误提示;undefined 表示合法。 @@ -69,6 +70,10 @@ export function validateNewProjectName(input: string): string | undefined { if (name === "." || name === "..") { return "项目名不能是 . 或 .."; } + // mcpp#380:当前内置模板会重复扫描替换结果,名称包含该标记时不会终止。 + if (name.includes(MCPP_BUILTIN_TEMPLATE_MARKER)) { + return "项目名不能包含 PROJECT,否则会触发当前 mcpp 模板替换缺陷"; + } if (CONTROL_CHARS.test(name)) { return "项目名不能包含控制字符"; } diff --git a/test/newProject.test.ts b/test/newProject.test.ts index 9b2f82c..420c642 100644 --- a/test/newProject.test.ts +++ b/test/newProject.test.ts @@ -37,6 +37,13 @@ test("拒绝双引号和控制字符,避免破坏 mcpp 生成的 TOML 和 C++ } }); +test("拒绝会触发 mcpp 内置模板无限替换的项目名", () => { + // mcpp 当前会循环替换字面量 PROJECT;插入值仍包含该标记时不会终止。 + for (const name of ["PROJECT", "myPROJECTname", "demo-PROJECT-app"]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${name}`); + } +}); + test("按跨平台策略拒绝 Windows 保留字符、设备名和尾随点", () => { for (const name of ["ab", "a:b", "a|b", "a?b", "a*b", "name."]) { assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${name}`); @@ -46,6 +53,12 @@ test("按跨平台策略拒绝 Windows 保留字符、设备名和尾随点", () } }); +test("Windows 保留设备名添加扩展后仍然拒绝", () => { + for (const name of ["CON.txt", "con.json", "AUX.md", "LPT1.log", "COM9.tar.gz"]) { + assert.ok(validateNewProjectName(name) !== undefined, `should reject: ${name}`); + } +}); + test("接受常规项目名,前后空白忽略", () => { for (const name of ["hello", "hello-mcpp", "my_project", "a.b.c", "项目", "console", "com10", " padded "]) { assert.equal(validateNewProjectName(name), undefined, `should accept: ${name}`); From 4b97d688d64854620df07f49da2317f360bc5286 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 15:01:38 +0800 Subject: [PATCH 029/119] docs: design issue 6 and 7 changes --- ...026-08-08-one-click-module-setup-design.md | 143 ++++++++++++++++++ .../specs/2026-08-08-pr-ci-design.md | 112 ++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-08-one-click-module-setup-design.md create mode 100644 docs/superpowers/specs/2026-08-08-pr-ci-design.md diff --git a/docs/superpowers/specs/2026-08-08-one-click-module-setup-design.md b/docs/superpowers/specs/2026-08-08-one-click-module-setup-design.md new file mode 100644 index 0000000..79eadd0 --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-one-click-module-setup-design.md @@ -0,0 +1,143 @@ +# Issue #6 一键配置模块代码提示设计 + +## 背景 + +当前 `mcpp.autoConfigureModules` 把 LLVM 工具链安装、全局默认切换、 +`compile_commands.json` 刷新和 clangd 安装拆成多轮选择与确认。Issue #6 要求把 +入口收敛为“用户主动执行命令,再确认一次”,确认后自动完成其余步骤。 + +本设计基于 `origin/main@1a8ae33`。该基线不包含 `mcpp ide configure` 集成,因此 +CDB 仍由 `mcpp build` 生成;不依赖尚未进入 mcpp 主线的 IDE snapshot 协议。 + +## 目标 + +- 从命令面板或现有快捷入口执行“一键配置模块代码提示”后,最多出现一次模态确认。 +- 确认后自动安装所需 LLVM 工具链、切换全局默认、构建并刷新 CDB、安装匹配的 + clangd、重新配置语言服务并检查模块状态。 +- 不静默修改 `mcpp.toml`、workspace manifest 或 target 配置。 +- 安装、default、clangd 安装或配置失败后立即停止;构建失败仅在产生可用 LLVM CDB + 时进入明确的降级刷新路径,不继续使用旧上下文宣称成功。 +- 保留现有手动“安装工具链”“选择全局默认”“刷新编译数据库”命令。 + +## 非目标 + +- 不引入或回移 `mcpp ide configure`。 +- 不自动安装 xlings 本体,不执行在线安装脚本。 +- 不覆盖项目 `[toolchain]` 或 `[target.*].toolchain`。 +- 不在本次改动中解决真实 LLVM/clangd 跨平台兼容矩阵。 +- 不升级与 issue 无关的 npm 依赖或修复现有 `npm audit` 报告。 + +## 用户流程 + +### 预检 + +扩展在显示确认框前完成以下只读检查: + +1. 当前资源属于 mcpp 工程且工作区已受信任。 +2. 没有其他 mcpp 操作正在运行。 +3. `mcpp toolchain list` 能被当前解析器识别。 +4. 当前项目是否通过 manifest 或 target 覆盖全局默认。 +5. host target 是否已有可用 LLVM 工具链。 + +若当前项目显式固定 GCC/MSVC,流程直接停止并提示用户手动修改项目配置。扩展不能 +通过修改全局默认伪装成切换成功,也不能改写受版本控制的 manifest。 + +### 唯一确认 + +预检通过后显示一个居中的 modal。主文案说明当前工具链和目标 LLVM;detail 列出 +即将执行的动作:可能下载 LLVM、修改 mcpp 全局默认及 host target、执行完整构建、 +可能下载匹配的 llvm-tools,并重启 clangd。用户取消后不产生任何修改。 + +已使用 LLVM 但 clangd/CDB 需要修复时沿用同一 modal,只隐藏不需要的工具链动作。 + +### 自动执行 + +确认后按顺序执行: + +1. host LLVM 不存在时运行 `mcpp toolchain install llvm`,由 mcpp 选择当前索引中的 + 最高可用版本,扩展不复制版本选择策略。 +2. 当前有效工具链不是 LLVM 时运行 `mcpp toolchain default llvm`。mcpp 从已安装 + 版本中选择最高匹配并清空旧的 global `default_target`。 +3. 运行 `mcpp build` 生成与新工具链一致的 CDB 和模块产物。 +4. 构建结束后重新发现工程并重新分析 CDB;禁止继续使用切换前的 `ProjectContext`。 +5. 若没有兼容 clangd,通过已安装的 xlings 执行匹配 LLVM 版本的 + `xim:llvm-tools` 安装。 +6. 再次解析 clangd,写入官方 clangd 扩展配置,重启语言服务并运行模块检查。 +7. 只显示一次最终成功通知;任务过程和失败细节写入现有 mcpp 输出频道/任务终端。 + +构建失败时仍重新读取 CDB:若 CDB 已生成且 clangd 可配置,可以保留编辑能力,但 +最终结果必须说明“构建失败,语言服务已刷新”,不能报告完整成功。 + +## 代码结构 + +### `src/moduleSetup.ts` + +新增纯 TypeScript 模块,负责: + +- 根据工具链 inventory 和当前 capability 生成 `ModuleSetupPlan`。 +- 返回 `project-toolchain-override`、`busy`、`unrecognized-inventory` 等明确阻断原因。 +- 生成 modal 所需的动作摘要。 +- 编排注入的安装、切换、构建、重载、clangd 配置和检查操作。安装/default/clangd + 失败立即终止;构建失败只允许先重载并验证新 CDB,再决定终止或降级继续。 + +该模块不导入 `vscode`,Node 单元测试可以覆盖完整状态机。 + +### `src/cliController.ts` + +增加供一键流程使用的非交互复合操作,不复用会弹 QuickPick 的公开命令。复合操作 +持有一个全局 operation token,直接使用现有 `executeTask`/`runProcess` 依次执行 +LLVM 安装、全局默认切换和构建,完成后释放 token并返回每一阶段的 +`TaskCompletion`。构建后的 clangd 重协调在 token 释放后进行,避免现有 +`McppOperationRegistry` 的 global/project 互斥形成重入死锁。 + +手动命令保持原行为;公共底层执行逻辑只在确实消除重复时抽取。 + +### `src/extension.ts` + +`autoConfigureModulesWizard` 缩减为 VS Code 适配层:加载 inventory、调用 planner、 +显示唯一 modal、执行计划、重新加载 context、调用现有 clangd 配置与模块检查。 +现有 workspace clangd 串行器继续防止两个向导同时修改窗口级 clangd 设置。 + +### `src/cli.ts` 与 `src/tasks.ts` + +只增加 planner 需要的纯判断或执行结果类型;不改变当前 CLI 输出兼容解析规则。 + +## 并发与错误处理 + +- 预检和执行前都检查 operation registry,消除确认期间出现的竞争窗口。 +- mcpp 安装、default 和 build 对其他项目/全局操作表现为一个独占事务。 +- xlings/clangd 阶段仍受 workspace clangd 串行器保护。 +- `cancelled` 与 `failed` 分开呈现;取消不显示错误。 +- xlings 缺失时停止并提供官网链接或设置入口,但不再进入第二轮“安装/跳过”选择。 +- LLVM 安装后仍无法切换、构建后仍是 GCC/MSVC、或 clangd identity 不兼容时均视为失败。 +- 未受信任工作区不执行任何外部程序。 + +## 测试 + +### 单元测试 + +新增 `test/moduleSetup.test.ts`,至少覆盖: + +- 已是 LLVM 且 clangd 就绪,只构建、重载和检查。 +- GCC 且未安装 LLVM,顺序为 install -> default -> build -> reload -> clangd -> check。 +- GCC 且已有 LLVM,跳过 install。 +- 项目显式固定 GCC/MSVC 时阻断且没有任何副作用。 +- 用户取消、operation busy、inventory 不可识别。 +- 安装/default/build/重载/clangd 任一步失败后不执行后续步骤。 +- 构建失败但产生可用 LLVM CDB 时返回降级结果而非完整成功。 + +扩展现有 controller/workflow 测试补充命令参数、锁释放顺序和旧上下文失效行为。 + +### Extension Host E2E + +Issue #7 引入的 Extension Host smoke test 使用 fake mcpp CLI 验证扩展可激活、命令已注册、 +普通项目任务可从配置的 `mcpp.path` 执行。modal 选择和下载分支由纯状态机单测覆盖, +不在 PR CI 中下载真实工具链。 + +## 验收标准 + +- 普通未固定 GCC/MSVC 工程从命令入口到开始执行只有一次 modal 确认。 +- 确认后不出现工具链 QuickPick、默认选择或“是否刷新 CDB”提示。 +- 显式固定工具链的工程不会修改任何 manifest 或全局默认。 +- 自动流程结束时使用重新加载后的 LLVM CDB 和 clangd identity。 +- 所有新增单元测试、现有 `npm test`、VSIX 打包和 Extension Host smoke E2E 通过。 diff --git a/docs/superpowers/specs/2026-08-08-pr-ci-design.md b/docs/superpowers/specs/2026-08-08-pr-ci-design.md new file mode 100644 index 0000000..427a1be --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-pr-ci-design.md @@ -0,0 +1,112 @@ +# Issue #7 PR CI 与 Extension Host E2E 设计 + +## 背景 + +仓库当前只有 `.github/workflows/release.yml`,仅在 `v*` tag push 时执行测试、打包和 +Release 上传。普通 pull request 没有自动检查。现有 `npm test` 是 TypeScript 编译加 +Node `node:test`,没有启动真实 VS Code Extension Host 的 E2E。 + +Issue #7 要求 PR 自动触发单元测试和 E2E,提前发现扩展打包、激活和 VS Code API +接线问题。 + +## 目标 + +- 对 pull request 自动运行编译、全部 Node 单元测试、VSIX 打包校验和 Extension Host + smoke E2E。 +- 对 `main` push 重跑相同检查,确保合并后的提交仍可复现。 +- 单元/打包与 E2E 使用独立 job,失败来源清晰,可分别设为 required check。 +- E2E 在干净 runner 上可重复,不依赖用户缓存或真实工具链下载。 + +## 非目标 + +- 不替代 tag release workflow。 +- 不在每个 PR 下载 mcpp LLVM 工具链、xlings 包或真实 clangd。 +- 不承诺 macOS/Windows 的真实语言服务集成覆盖;本次 smoke lane 使用 Linux。 +- 不在 workflow 中自动修改依赖或执行 `npm audit fix`。 + +## Workflow + +新增 `.github/workflows/ci.yml`: + +- 触发:`pull_request` 和 `push` 到 `main`。 +- 权限:`contents: read`。 +- concurrency:同一 workflow/ref 只保留最新运行,PR 更新时取消旧任务。 +- Node:与发布流程一致使用 Node.js 22 和 npm cache。 + +### `unit-and-package` + +1. `actions/checkout@v4` +2. `actions/setup-node@v4`,Node 22,`cache: npm` +3. `npm ci` +4. `npm test` +5. `npm run package` +6. 校验只生成一个与 `package.json.version` 一致的 VSIX,并执行 ZIP 完整性检查 + +该 job 复用发布前的核心检查,但不创建 Release,也不需要写权限。 + +### `extension-host-e2e` + +1. checkout、setup-node、`npm ci` +2. `npm run compile` +3. 通过 `xvfb-run -a npm run test:e2e` 启动 VS Code Extension Host + +`@vscode/test-electron` 负责下载并启动稳定版 VS Code。首次实现固定 npm lockfile 中的 +测试 runner 版本,不额外固定 VS Code patch;runner 下载失败应表现为明确的 job 失败。 + +## E2E 结构 + +### `test/e2e/runTest.ts` + +Node 启动器解析扩展根目录、编译后的测试入口和 fixture workspace,调用 +`@vscode/test-electron` 的 `runTests`。启动参数关闭更新和遥测,并使用隔离的临时 +user-data/extensions 目录,防止本机配置污染。隔离 extensions 目录只放置测试 fixture +提供的 `llvm-vs-code-extensions.vscode-clangd` stub,满足被测扩展的 dependency,而不 +从 Marketplace 下载真实 clangd 扩展。 + +### `test/e2e/suite/index.ts` + +Extension Host 内的测试入口,负责发现和运行 smoke cases,并在失败时返回非零退出码。 +测试框架只服务 Extension Host E2E;现有纯 Node 测试继续使用 `node:test`。 + +### `test/e2e/suite/extension.test.ts` + +首批 smoke cases: + +- 在包含 `mcpp.toml` 的 fixture 中找到并激活 `mcpp-community.mcpp-vscode`。 +- 验证 `mcpp.autoConfigureModules`、`mcpp.build` 和其他 manifest 命令实际注册到 VS Code。 +- 设置 workspace scoped `mcpp.path` 指向 fake CLI,执行一个无 modal 的项目命令,验证 + fake CLI 收到参数且扩展没有依赖开发机 PATH。 +- 验证 mcpp 工程 context key 驱动的命令路径在 Extension Host 中可用。 + +### `test/e2e/fixtures/` + +提供最小 `mcpp.toml`、源文件、可执行 fake mcpp 和 no-op clangd dependency stub。 +fake CLI 只实现 smoke case 需要的参数,把 argv 写入 fixture 临时记录文件并返回确定 +退出码;stub 只提供扩展 id 和 `clangd.restart` 命令。二者都不伪装真实编译、CDB 或 +clangd 兼容性。 + +## npm Scripts 与依赖 + +- 增加 `test:e2e`,只运行 Extension Host 测试。 +- 保留 `test` 的现有含义,避免本地每次单测都下载 VS Code。 +- 增加 `test:all` 作为本地完整验证入口:`npm test && npm run test:e2e`。 +- 开发依赖增加当前稳定的 `@vscode/test-electron` 及其测试 runner 所需的最小依赖; + 所有版本写入 `package-lock.json`。 + +## 稳定性与安全边界 + +- PR E2E 不访问真实 mcpp/xlings registry,不依赖缓存命中。 +- fake CLI 的路径由测试显式设置,不修改用户级 VS Code 配置。 +- fixture 和日志写入测试临时目录,测试结束后清理。 +- CI 不接收 secrets,不运行来自 issue/PR 文本的 shell 内容。 +- workflow shell 命令只处理仓库内固定路径和从 `package.json` 读取的版本。 +- E2E 超时必须有限,Extension Host 退出失败不能被吞掉。 + +## 验收标准 + +- 新 PR 自动出现独立的 `unit-and-package` 与 `extension-host-e2e` checks。 +- 任一单元测试、TypeScript 编译、VSIX 打包或 Extension Host smoke case 失败时 workflow + 返回失败。 +- 本地 `npm test` 继续快速运行且不下载 VS Code。 +- 本地 `npm run test:e2e` 可在支持的 macOS/Linux 图形或 Xvfb 环境复现。 +- tag release workflow 保持原发布职责,不混入 PR 权限或 E2E 副作用。 From a5445f4e677b3efe44127ab5914bcb18a64e98ea Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 15:09:06 +0800 Subject: [PATCH 030/119] docs: add issues 6 and 7 implementation plan --- .../2026-08-08-issues-6-7-implementation.md | 411 ++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-08-issues-6-7-implementation.md diff --git a/docs/superpowers/plans/2026-08-08-issues-6-7-implementation.md b/docs/superpowers/plans/2026-08-08-issues-6-7-implementation.md new file mode 100644 index 0000000..eebb40f --- /dev/null +++ b/docs/superpowers/plans/2026-08-08-issues-6-7-implementation.md @@ -0,0 +1,411 @@ +# Issues 6 And 7 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make module code completion genuinely one-click and add deterministic PR unit/E2E CI for the VS Code extension. + +**Architecture:** Issue #6 gets a pure `moduleSetup` planner/state machine plus a non-interactive, locked mcpp preparation method. The VS Code layer performs one confirmation, then reloads the new CDB before configuring clangd. Issue #7 adds a Linux Extension Host smoke lane using fake mcpp and a stub clangd dependency, while the existing Node tests and VSIX packaging run in a separate CI job. + +**Tech Stack:** TypeScript, Node `node:test`, Mocha inside `@vscode/test-electron`, GitHub Actions, VS Code extension APIs. + +--- + +## File Map + +| File | Responsibility | +| --- | --- | +| `src/moduleSetup.ts` | Pure plan construction, command list, confirmation text, and injected execution state machine. | +| `test/moduleSetup.test.ts` | RED/GREEN coverage for plan branches, command order, failure stops, and build degradation. | +| `src/cliController.ts` | Non-interactive locked execution of `toolchain install llvm`, `toolchain default llvm`, and `build`. | +| `test/artifacts.test.ts` | Manifest/source assertions for the new one-click flow and no extra interactive prompt. | +| `src/extension.ts` | Replace the eight-step wizard with the single-confirmation adapter and reload the post-build context. | +| `README.md` | Update command behavior and explain the explicit project-toolchain boundary. | +| `package.json`, `package-lock.json` | Add E2E scripts and runner dependencies. | +| `test/e2e/runTest.ts` | Download/start VS Code and prepare isolated user/extensions directories. | +| `test/e2e/suite/index.ts`, `test/e2e/suite/extension.test.ts` | Extension Host smoke test entry and cases. | +| `test/e2e/fixtures/project/mcpp.toml` | Minimal workspace fixture. | +| `test/e2e/fixtures/fake-mcpp.js` | Deterministic executable recording argv and returning success. | +| `test/e2e/fixtures/clangd-stub/` | Installed-looking `llvm-vs-code-extensions.vscode-clangd` dependency stub. | +| `.github/workflows/ci.yml` | PR/main checks for unit/package and Extension Host E2E. | + +## Task 1: Add The Failing Pure Workflow Tests + +**Files:** +- Create: `test/moduleSetup.test.ts` +- Create: `src/moduleSetup.ts` only after the RED command fails + +- [ ] **Step 1: Define test fixtures and the planner contract** + +Add a minimal inventory factory using the existing `ToolchainInventory` shape and tests that import the not-yet-existing functions: + +```ts +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + buildModuleSetupPlan, + executeModuleSetup, + mcppModuleSetupCommands, + type ModuleSetupOperations, +} from "../src/moduleSetup"; +import type { ToolchainInventory } from "../src/cli"; + +function inventory(overrides: Partial = {}): ToolchainInventory { + return { + installed: [{ family: "gcc", version: "16.1.0", spec: "gcc@16.1.0", source: "managed", effective: true }], + available: [{ family: "llvm", version: "22.1.8", spec: "llvm@22.1.8", source: "managed", effective: false }], + targets: [], + effective: { family: "gcc", version: "16.1.0", spec: "gcc@16.1.0", source: "managed", effective: true }, + effectiveTarget: undefined, + globalDefaultSpec: "gcc@16.1.0", + projectOverridesGlobal: false, + recognized: true, + rawOutput: "fixture", + ...overrides, + }; +} + +test("plans install, switch and build for an unfixed GCC project", () => { + const plan = buildModuleSetupPlan(inventory(), "syntax-only", true, false); + assert.deepEqual(plan, { state: "ready", installLlvm: true, switchDefault: true }); + if (plan.state !== "ready") throw new Error("expected a ready setup plan"); + assert.deepEqual( + mcppModuleSetupCommands(plan), + [ + { stage: "install", mode: "task", args: ["toolchain", "install", "llvm"] }, + { stage: "default", mode: "process", args: ["toolchain", "default", "llvm"] }, + { stage: "build", mode: "task", args: ["build"] }, + ], + ); +}); + +test("blocks a project-pinned non-LLVM toolchain without operations", () => { + const decision = buildModuleSetupPlan(inventory({ projectOverridesGlobal: true }), "syntax-only", true, false); + assert.deepEqual(decision, { state: "blocked", reason: "project-toolchain-override" }); +}); +``` + +- [ ] **Step 2: Add execution-order and failure tests** + +Use an injected operation recorder. Cover an installed LLVM plan, a cancelled install, a failed default, a failed build with no usable CDB, and a failed build whose reload returns a full LLVM context. Assert that later operations are absent and that the latter returns `degraded` after clangd/check succeed. + +```ts +function recorder(log: string[], result: Partial = {}): ModuleSetupOperations { + return { + prepare: async () => { log.push("prepare"); return { state: "succeeded", stage: "build" }; }, + reload: async () => { log.push("reload"); return { state: "succeeded" }; }, + ensureClangd: async () => { log.push("clangd"); return { state: "succeeded" }; }, + checkModules: async () => { log.push("check"); return { state: "succeeded" }; }, + ...result, + }; +} + +test("executes prepare, reload, clangd and module check exactly once", async () => { + const log: string[] = []; + const outcome = await executeModuleSetup( + { state: "ready", installLlvm: false, switchDefault: false }, + recorder(log), + ); + assert.equal(outcome.state, "succeeded"); + assert.deepEqual(log, ["prepare", "reload", "clangd", "check"]); +}); + +test("does not continue after a non-build preparation failure", async () => { + const log: string[] = []; + const outcome = await executeModuleSetup( + { state: "ready", installLlvm: true, switchDefault: true }, + recorder(log, { prepare: async () => { log.push("prepare"); return { state: "failed", stage: "default" }; } }), + ); + assert.equal(outcome.state, "failed"); + assert.deepEqual(log, ["prepare"]); +}); +``` + +- [ ] **Step 3: Run the focused test and verify RED** + +Run: `npm run clean && npm run compile && node --test dist/test/moduleSetup.test.js` + +Expected: TypeScript compilation fails because `src/moduleSetup.ts` and its exported contract do not exist. Do not add implementation before observing this failure. + +## Task 2: Implement The Pure Workflow + +**Files:** +- Create: `src/moduleSetup.ts` +- Modify: `test/moduleSetup.test.ts` only for assertions clarified by the implementation contract + +- [ ] **Step 1: Implement inventory planning** + +Export the following types and function signatures: + +```ts +export type ModuleSetupStage = "install" | "default" | "build" | "reload" | "clangd" | "check"; +export type ModuleSetupStepState = "succeeded" | "failed" | "cancelled"; +export type ModuleSetupBlockedReason = "untrusted" | "busy" | "project-toolchain-override" | "unrecognized-inventory"; +export type ModuleSetupDecision = + | { state: "ready"; installLlvm: boolean; switchDefault: boolean } + | { state: "blocked"; reason: ModuleSetupBlockedReason }; + +export function buildModuleSetupPlan( + inventory: ToolchainInventory, + capability: "full" | "syntax-only" | "unavailable", + trusted: boolean, + busy: boolean, +): ModuleSetupDecision; +``` + +Return `untrusted` before reading mutable state, `unrecognized-inventory` before reading inventory fields, and `busy` before planning mutations. Block project overrides only when `projectOverridesGlobal` is true and the effective toolchain is not LLVM. Treat an installed host LLVM as `family.toLowerCase() === "llvm"`; the available list does not count as installed. + +- [ ] **Step 2: Implement fixed command construction and Chinese confirmation text** + +Export: + +```ts +export type ModuleSetupCommand = { + stage: "install" | "default" | "build"; + mode: "task" | "process"; + args: string[]; +}; + +export function mcppModuleSetupCommands( + plan: Extract, +): ModuleSetupCommand[]; + +export function moduleSetupConfirmation( + capability: "full" | "syntax-only" | "unavailable", + plan: Extract, +): { message: string; detail: string }; +``` + +Always append `{ stage: "build", mode: "task", args: ["build"] }`. Use argv arrays, never shell strings. The detail must state that global default changes are not project manifest changes and mention the possible LLVM/llvm-tools download. + +- [ ] **Step 3: Implement injected execution and the build-degradation exception** + +Export: + +```ts +export interface ModuleSetupStepResult { + state: ModuleSetupStepState; + stage?: ModuleSetupStage; + message?: string; +} + +export interface ModuleSetupOperations { + prepare: () => Promise; + reload: () => Promise; + ensureClangd: () => Promise; + checkModules: () => Promise; +} + +export type ModuleSetupOutcome = ModuleSetupStepResult & { degraded?: boolean }; + +export function executeModuleSetup( + decision: Extract, + operations: ModuleSetupOperations, +): Promise; +``` + +Stop on cancelled or failed `prepare` except a failed build; invoke `reload` after a failed build to determine whether the new CDB is usable. Only a successful reload, clangd configuration, and module check may produce `succeeded`; a successful remainder after a failed build produces `degraded: true`. Never call `ensureClangd` or `checkModules` after a failed reload. + +- [ ] **Step 4: Run the focused tests and the full Node suite** + +Run: `npm test` + +Expected: the new planner/state-machine tests and all pre-existing tests pass. If a test fails, fix the implementation or its contract before proceeding to VS Code integration. + +## Task 3: Add The Locked Non-Interactive Controller Operation + +**Files:** +- Modify: `src/cliController.ts` +- Modify: `test/artifacts.test.ts` +- Modify: `test/tasks.test.ts` if lock behavior needs a focused pure assertion + +- [ ] **Step 1: Add a failing source-contract test** + +Assert that `src/cliController.ts` exposes an automated setup method which uses `mcppModuleSetupCommands`, calls `beginGlobal` before the first command, and calls `finishGlobal` in a `finally` block. Also assert that the method does not call `pickInstallSpec`, `selectDefaultToolchainFromInventory`, `showQuickPick`, or `showWarningMessage`. + +- [ ] **Step 2: Implement the method with one operation token** + +Make the existing inventory reader public without changing its parsing/error behavior, then add: + +```ts +public async readToolchainInventory( + project: McppProjectDiscovery | undefined = this.options.currentProject(), +): Promise; + +public async runAutomaticModuleSetup( + plan: Extract, +): Promise; +``` + +The method must require a project and trust, acquire one global token, and execute `mcppModuleSetupCommands(plan)` in order. `task` commands use the existing `executeTask`; `process` commands use `runProcess`; append all output through the existing helpers. Return the exact failing stage and distinguish `cancelled` from `failed`. The `build` task executes directly under the global token instead of calling `runProjectTask`, avoiding a nested project-lock attempt. Release the token before returning even when `executeTask` throws. + +Use Chinese comments only for the lock lifetime and the direct-build reason; the method itself should remain self-explanatory. + +- [ ] **Step 3: Run focused controller/task tests** + +Run: `npm run clean && npm run compile && node --test dist/test/artifacts.test.js dist/test/tasks.test.js dist/test/moduleSetup.test.js` + +Expected: PASS with the new source/lock assertions and no regression in existing task-lock tests. + +## Task 4: Replace The Eight-Step Wizard + +**Files:** +- Modify: `src/extension.ts` +- Modify: `test/artifacts.test.ts` +- Modify: `README.md` + +- [ ] **Step 1: Add failing source assertions for the user contract** + +Extend `test/artifacts.test.ts` to assert that the one-click path calls `readToolchainInventory`, `buildModuleSetupPlan`, `moduleSetupConfirmation`, `runAutomaticModuleSetup`, and `executeModuleSetup`; assert that the wizard no longer calls the generic install/default commands or displays the post-build “刷新编译数据库” choice. + +- [ ] **Step 2: Add the planner/controller imports and a post-build context holder** + +Import the pure module types/functions. Keep the existing `ProjectContext` local to the extension and use `let currentContext = context` inside the workflow closure. The reload operation must call `loadProjectContext(context.project)` after the controller returns; it must update status before clangd resolution. + +- [ ] **Step 3: Implement one preflight modal** + +Replace the current `autoConfigureModulesWizard` body with this sequence: + +```ts +const inventory = await cliController.readToolchainInventory(context.project); +if (inventory === undefined) return; +const decision = buildModuleSetupPlan( + inventory, + context.analysis.capability, + vscode.workspace.isTrusted, + cliController.isBusy(), +); +if (decision.state === "blocked") { + await vscode.window.showWarningMessage(moduleSetupBlockedMessage(decision.reason)); + return; +} +const confirmation = moduleSetupConfirmation(context.analysis.capability, decision); +const choice = await vscode.window.showWarningMessage( + confirmation.message, + { modal: true, detail: confirmation.detail }, + "确认一键配置", +); +if (choice !== "确认一键配置") return; +``` + +The preflight modal is the only confirmation. Do not call the existing interactive install/default commands from this path. + +Add a local pure mapping helper with exhaustive cases: + +```ts +function moduleSetupBlockedMessage(reason: ModuleSetupBlockedReason): string { + switch (reason) { + case "project-toolchain-override": + return "当前项目显式固定了非 LLVM 工具链;一键配置不会修改 mcpp.toml,请先手动切换项目工具链。"; + case "untrusted": + return "当前工作区未受信任,不会执行外部程序。请先信任工作区。"; + case "busy": + return "已有 mcpp 操作正在运行,请等待完成后再试。"; + case "unrecognized-inventory": + return "无法识别 mcpp 工具链状态,请查看 mcpp 输出频道并检查 mcpp 版本。"; + } +} +``` + +- [ ] **Step 4: Implement the injected execution closures** + +Use `executeModuleSetup` with: + +- `prepare`: `cliController.runAutomaticModuleSetup(decision)`. +- `reload`: reload the project context, require a full LLVM analysis and return a failed result when the CDB is missing or remains GCC/MSVC. +- `ensureClangd`: resolve current clangd; if compatible, call `configureClangd(currentContext, status, output, "automatic", true)`. Otherwise require `findXlingsExecutable()` and a compiler LLVM identity, run the existing xlings task with `xlingsInstallArgs`, reload the context again, then configure clangd automatically. +- `checkModules`: call `runModuleSupportCheck(currentContext, status, output, "automatic")` and return failed when no available module status is produced. + +The closure must not call `maybeDisableCppTools`, `showQuickPick`, `showInputBox`, or any interactive clangd mode. + +- [ ] **Step 5: Report one final result and update README** + +Map `succeeded` to one information message, `degraded` to one warning containing “构建失败,语言服务已刷新”, and `failed/cancelled` to one error/warning. Update the README command table and workflow section to say the command performs a single confirmation and refuses project-pinned toolchains instead of claiming it can silently switch them. + +- [ ] **Step 6: Run the full Node suite and package build** + +Run: `npm test && npm run package` + +Expected: all tests pass and `vsce package` creates a version-matched VSIX without including `test/**` or `dist/test/**`. + +## Task 5: Add Deterministic Extension Host E2E + +**Files:** +- Modify: `package.json`, `package-lock.json` +- Create: `test/e2e/runTest.ts` +- Create: `test/e2e/suite/index.ts` +- Create: `test/e2e/suite/extension.test.ts` +- Create: `test/e2e/fixtures/project/mcpp.toml` +- Create: `test/e2e/fixtures/project/main.cpp` +- Create: `test/e2e/fixtures/fake-mcpp.js` +- Create: `test/e2e/fixtures/clangd-stub/package.json` +- Create: `test/e2e/fixtures/clangd-stub/extension.js` + +- [ ] **Step 1: Add the E2E dependencies and scripts** + +Add `@vscode/test-electron`, `mocha`, and `@types/mocha` as dev dependencies. Add scripts: + +```json +"test:e2e": "npm run compile && node dist/test/e2e/runTest.js", +"test:all": "npm test && npm run test:e2e" +``` + +Run `npm install --save-dev @vscode/test-electron@3.1.0 mocha @types/mocha` and verify only the intended manifest/lockfile changes occur. + +- [ ] **Step 2: Add the fake CLI and dependency stub** + +The fake CLI writes its `process.argv.slice(2).join(" ")` to `process.env.MCPP_E2E_LOG`, exits zero for `build`, and exits one for no other unneeded command. The stub package id is `llvm-vs-code-extensions.vscode-clangd`, its activation registers `clangd.restart`, and it has no external process behavior. `runTest.ts` copies/chmods the fake CLI and places the stub under an isolated extensions directory with a versioned folder name. + +- [ ] **Step 3: Add the Extension Host runner and suite** + +`runTest.ts` calls `runTests` with the compiled extension root, compiled suite path, fixture workspace, `--user-data-dir`, `--extensions-dir`, `--disable-updates`, `--skip-welcome`, and `--disable-workspace-trust` launch args. Set `MCPP_E2E_FAKE_MCPP` and `MCPP_E2E_LOG` through `extensionTestsEnv`. + +The suite activates the mcpp extension, asserts `mcpp.build` and `mcpp.autoConfigureModules` are registered, writes workspace-scoped `mcpp.path` to the fake executable, executes `mcpp.build`, waits for the log file, and asserts the recorded argv is exactly `build`. + +- [ ] **Step 4: Run E2E locally and fix only harness issues** + +Run: `npm run test:e2e` + +Expected: VS Code downloads once, the Extension Host exits zero, and the suite reports all smoke cases passing. A missing display must be handled by running `xvfb-run -a npm run test:e2e`; do not weaken the test to skip activation. + +## Task 6: Add PR CI + +**Files:** +- Create: `.github/workflows/ci.yml` +- Modify: `test/artifacts.test.ts` if workflow assertions are useful + +- [ ] **Step 1: Add workflow source assertions** + +Add tests that parse `.github/workflows/ci.yml` as text and assert `pull_request`, `push` to `main`, `npm ci`, `npm test`, `npm run package`, `npm run test:e2e`, `xvfb-run`, `contents: read`, and a concurrency group are present. Keep the release workflow test unchanged. + +- [ ] **Step 2: Implement the two-job workflow** + +Use Node 22 and `actions/checkout@v4`/`actions/setup-node@v4`. The `unit-and-package` job runs `npm ci`, `npm test`, `npm run package`, and checks the generated VSIX. The `extension-host-e2e` job runs `npm ci`, then `xvfb-run -a npm run test:e2e`. Set `permissions: contents: read` and cancel superseded runs per PR/ref. + +- [ ] **Step 3: Run local workflow-adjacent verification** + +Run: `npm test && npm run package && npm run test:e2e` (with `xvfb-run -a` when needed), then `git diff --check` and `git status --short`. + +Expected: both local jobs' command sequences pass, the VSIX remains valid, and no unrelated files are modified. + +## Task 7: Final Verification And Commits + +- [ ] **Step 1: Run the complete local command** + +Run: `npm run test:all` (prefix the E2E command with `xvfb-run -a` on headless Linux), followed by `npm run package` and `unzip -t mcpp-vscode-*.vsix`. + +- [ ] **Step 2: Review the diff against the approved specs** + +Check that #6 has one modal only, never edits project manifests, reloads context after build, and keeps manual commands unchanged. Check that #7 adds only PR/main CI and deterministic smoke dependencies. + +- [ ] **Step 3: Commit in focused changes** + +```bash +git add src/moduleSetup.ts test/moduleSetup.test.ts src/cliController.ts src/extension.ts test/artifacts.test.ts README.md +git commit -m "feat: make module setup one click" + +git add package.json package-lock.json test/e2e .github/workflows/ci.yml test/artifacts.test.ts +git commit -m "test: add pull request extension checks" +``` + +Do not push or create a PR until the user requests that integration step. From 6ebc6e6b498bf73fa0e2bf2db366b735591f8445 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 15:42:47 +0800 Subject: [PATCH 031/119] feat: add module setup workflow --- src/moduleSetup.ts | 186 +++++++++++++++++++ test/moduleSetup.test.ts | 379 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 565 insertions(+) create mode 100644 src/moduleSetup.ts create mode 100644 test/moduleSetup.test.ts diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts new file mode 100644 index 0000000..e32e238 --- /dev/null +++ b/src/moduleSetup.ts @@ -0,0 +1,186 @@ +import { hostDefaultToolchains, type ToolchainInventory } from "./cli"; + +export type ModuleSetupStage = "install" | "default" | "build" | "reload" | "clangd" | "check"; +export type ModuleSetupStepState = "succeeded" | "failed" | "cancelled"; +export type ModuleSetupBlockedReason = + | "untrusted" + | "busy" + | "project-toolchain-override" + | "unrecognized-inventory"; + +export type ModuleSetupDecision = + | { + kind: "ready"; + installLlvm: boolean; + switchDefault: boolean; + } + | { + kind: "blocked"; + reason: ModuleSetupBlockedReason; + }; + +type ModuleSetupPlan = Extract; +type ModuleCapability = "full" | "syntax-only" | "unavailable"; + +export function buildModuleSetupPlan( + inventory: ToolchainInventory, + _capability: ModuleCapability, + trusted: boolean, + busy: boolean, +): ModuleSetupDecision { + if (!trusted) { + return { kind: "blocked", reason: "untrusted" }; + } + if (!inventory.recognized) { + return { kind: "blocked", reason: "unrecognized-inventory" }; + } + if (busy) { + return { kind: "blocked", reason: "busy" }; + } + + const effectiveIsLlvm = inventory.effective?.family.toLowerCase() === "llvm"; + if (inventory.projectOverridesGlobal && inventory.effective !== undefined && !effectiveIsLlvm) { + return { kind: "blocked", reason: "project-toolchain-override" }; + } + + // target-only payload 不能成为 host 默认工具链,因此必须复用现有筛选规则。 + const installedHostLlvm = hostDefaultToolchains(inventory) + .some((toolchain) => toolchain.family.toLowerCase() === "llvm"); + return { + kind: "ready", + installLlvm: !installedHostLlvm, + switchDefault: !effectiveIsLlvm, + }; +} + +export interface ModuleSetupCommand { + stage: "install" | "default" | "build"; + mode: "task" | "process"; + args: string[]; +} + +export function mcppModuleSetupCommands(plan: ModuleSetupPlan): ModuleSetupCommand[] { + const commands: ModuleSetupCommand[] = []; + if (plan.installLlvm) { + commands.push({ + stage: "install", + mode: "process", + args: ["toolchain", "install", "llvm"], + }); + } + if (plan.switchDefault) { + commands.push({ + stage: "default", + mode: "process", + args: ["toolchain", "default", "llvm"], + }); + } + commands.push({ stage: "build", mode: "task", args: ["build"] }); + return commands; +} + +export interface ModuleSetupConfirmation { + message: string; + detail: string; +} + +export function moduleSetupConfirmation( + capability: ModuleCapability, + plan: ModuleSetupPlan, +): ModuleSetupConfirmation { + const message = capability === "full" + ? "当前已具备 LLVM 模块能力,是否重新构建并配置模块代码提示?" + : capability === "syntax-only" + ? "当前仅提供模块语法高亮,是否切换到 LLVM 并配置模块代码提示?" + : "当前缺少可用的模块编译数据库,是否配置 LLVM 并构建项目?"; + const installDetail = plan.installLlvm + ? "流程可能下载 LLVM/llvm-tools" + : "将复用已安装的 LLVM;补齐配套组件时仍可能下载 LLVM/llvm-tools"; + const defaultDetail = plan.switchDefault + ? "并把 LLVM 设为全局默认工具链" + : "当前 LLVM 已生效,不修改全局默认工具链"; + return { + message, + detail: `${installDetail},${defaultDetail};不会编辑项目清单 mcpp.toml。`, + }; +} + +export interface ModuleSetupStepResult { + stage: ModuleSetupStage; + state: ModuleSetupStepState; + detail?: string; + exitCode?: number; +} + +export interface ModuleSetupOperations { + prepare(command: ModuleSetupCommand): Promise; + reload(): Promise; + ensureClangd(): Promise; + checkModules(): Promise; +} + +export type ModuleSetupOutcome = + | { + state: "succeeded"; + degraded: boolean; + steps: ModuleSetupStepResult[]; + } + | { + state: "failed" | "cancelled"; + stage: ModuleSetupStage; + degraded: false; + steps: ModuleSetupStepResult[]; + }; + +function stoppedOutcome( + result: ModuleSetupStepResult, + steps: ModuleSetupStepResult[], +): Exclude { + return { + state: result.state === "cancelled" ? "cancelled" : "failed", + stage: result.stage, + degraded: false, + steps, + }; +} + +export async function executeModuleSetup( + plan: ModuleSetupPlan, + operations: ModuleSetupOperations, +): Promise { + const steps: ModuleSetupStepResult[] = []; + let buildFailed = false; + + for (const command of mcppModuleSetupCommands(plan)) { + const result = await operations.prepare(command); + steps.push(result); + if (result.state === "succeeded") { + continue; + } + if (command.stage === "build" && result.state === "failed") { + buildFailed = true; + break; + } + return stoppedOutcome(result, steps); + } + + // 构建失败后仍尝试恢复现有 CDB;只有完整恢复链成功才算降级成功。 + const recoveryOperations: Array<() => Promise> = [ + () => operations.reload(), + () => operations.ensureClangd(), + () => operations.checkModules(), + ]; + for (const operation of recoveryOperations) { + const result = await operation(); + steps.push(result); + if (result.state !== "succeeded") { + return stoppedOutcome(result, steps); + } + } + + return { + state: "succeeded", + degraded: buildFailed, + steps, + }; +} diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts new file mode 100644 index 0000000..c1b4351 --- /dev/null +++ b/test/moduleSetup.test.ts @@ -0,0 +1,379 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { ToolchainInventory, ToolchainItem } from "../src/cli"; +import { + buildModuleSetupPlan, + executeModuleSetup, + mcppModuleSetupCommands, + moduleSetupConfirmation, + type ModuleSetupDecision, + type ModuleSetupOperations, + type ModuleSetupStage, + type ModuleSetupStepResult, + type ModuleSetupStepState, +} from "../src/moduleSetup"; + +function toolchain(family: string, effective = false): ToolchainItem { + return { + family, + version: "1.0.0", + spec: `${family}@1.0.0`, + source: "managed", + effective, + }; +} + +function inventory( + overrides: Partial = {}, +): ToolchainInventory { + return { + installed: [], + available: [], + targets: [], + effective: undefined, + effectiveTarget: undefined, + globalDefaultSpec: undefined, + projectOverridesGlobal: false, + recognized: true, + rawOutput: "", + ...overrides, + }; +} + +function ready(decision: ModuleSetupDecision): Extract { + if (decision.kind !== "ready") { + assert.fail(`expected ready plan, got ${decision.reason}`); + } + return decision; +} + +test("GCC 生效且没有已安装的 host LLVM 时计划安装并切换默认工具链", () => { + const gcc = toolchain("gcc", true); + const plan = buildModuleSetupPlan( + inventory({ installed: [gcc], effective: gcc }), + "unavailable", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: true, + switchDefault: true, + }); +}); + +test("已安装 host LLVM 但 GCC 生效时只切换默认工具链", () => { + const gcc = toolchain("gcc", true); + const llvm = toolchain("llvm"); + const plan = buildModuleSetupPlan( + inventory({ installed: [gcc, llvm], effective: gcc }), + "syntax-only", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: false, + switchDefault: true, + }); +}); + +test("LLVM 已生效时不重复安装或切换", () => { + const llvm = toolchain("llvm", true); + const plan = buildModuleSetupPlan( + inventory({ installed: [llvm], effective: llvm }), + "full", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: false, + switchDefault: false, + }); +}); + +test("available LLVM 不算已安装的 host 工具链", () => { + const gcc = toolchain("gcc", true); + const availableLlvm = toolchain("llvm"); + const plan = buildModuleSetupPlan( + inventory({ installed: [gcc], available: [availableLlvm], effective: gcc }), + "syntax-only", + true, + false, + ); + + assert.equal(ready(plan).installLlvm, true); +}); + +test("只有 target payload 的 LLVM 不算可设为默认的 host 工具链", () => { + const gcc = toolchain("gcc", true); + const llvm = toolchain("llvm"); + const plan = buildModuleSetupPlan( + inventory({ + installed: [gcc, llvm], + effective: gcc, + targets: [{ + target: "x86_64-linux-musl", + note: "static", + toolchainSpec: llvm.spec, + status: "installed", + effective: false, + }], + }), + "syntax-only", + true, + false, + ); + + assert.equal(ready(plan).installLlvm, true); +}); + +test("项目显式固定非 LLVM 时阻止修改全局默认", () => { + const gcc = toolchain("gcc", true); + const plan = buildModuleSetupPlan( + inventory({ + installed: [gcc], + effective: gcc, + projectOverridesGlobal: true, + }), + "syntax-only", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "blocked", + reason: "project-toolchain-override", + }); +}); + +test("项目显式固定 LLVM 时仍允许继续配置", () => { + const llvm = toolchain("llvm", true); + const plan = buildModuleSetupPlan( + inventory({ + installed: [llvm], + effective: llvm, + projectOverridesGlobal: true, + }), + "full", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: false, + switchDefault: false, + }); +}); + +test("阻止原因按未信任、未知清单、忙碌、项目覆盖的顺序判定", () => { + const gcc = toolchain("gcc", true); + const conflicting = inventory({ + installed: [gcc], + effective: gcc, + projectOverridesGlobal: true, + recognized: false, + }); + + assert.deepEqual( + buildModuleSetupPlan(conflicting, "unavailable", false, true), + { kind: "blocked", reason: "untrusted" }, + ); + assert.deepEqual( + buildModuleSetupPlan(conflicting, "unavailable", true, true), + { kind: "blocked", reason: "unrecognized-inventory" }, + ); + assert.deepEqual( + buildModuleSetupPlan({ ...conflicting, recognized: true }, "unavailable", true, true), + { kind: "blocked", reason: "busy" }, + ); +}); + +test("模块设置命令使用固定 argv 数组且始终构建", () => { + assert.deepEqual( + mcppModuleSetupCommands({ kind: "ready", installLlvm: true, switchDefault: true }), + [ + { stage: "install", mode: "process", args: ["toolchain", "install", "llvm"] }, + { stage: "default", mode: "process", args: ["toolchain", "default", "llvm"] }, + { stage: "build", mode: "task", args: ["build"] }, + ], + ); + assert.deepEqual( + mcppModuleSetupCommands({ kind: "ready", installLlvm: false, switchDefault: false }), + [{ stage: "build", mode: "task", args: ["build"] }], + ); +}); + +test("确认文案按能力解释现状并完整说明副作用边界", () => { + const plan = { kind: "ready", installLlvm: true, switchDefault: true } as const; + const confirmations = [ + moduleSetupConfirmation("full", plan), + moduleSetupConfirmation("syntax-only", plan), + moduleSetupConfirmation("unavailable", plan), + ]; + + assert.match(confirmations[0].message, /模块代码提示/); + assert.match(confirmations[1].message, /语法高亮/); + assert.match(confirmations[2].message, /编译数据库/); + assert.equal(new Set(confirmations.map((item) => item.message)).size, 3); + for (const confirmation of confirmations) { + assert.match(confirmation.detail, /LLVM\/llvm-tools/); + assert.match(confirmation.detail, /全局默认/); + assert.match(confirmation.detail, /不会编辑项目.*mcpp\.toml/); + } +}); + +interface OperationFixture { + calls: ModuleSetupStage[]; + operations: ModuleSetupOperations; +} + +function result(stage: ModuleSetupStage, state: ModuleSetupStepState): ModuleSetupStepResult { + return { stage, state }; +} + +function operationsWith( + states: Partial> = {}, +): OperationFixture { + const calls: ModuleSetupStage[] = []; + const run = async (stage: ModuleSetupStage): Promise => { + calls.push(stage); + return result(stage, states[stage] ?? "succeeded"); + }; + + return { + calls, + operations: { + prepare: async (command: { stage: ModuleSetupStage }) => run(command.stage), + reload: async () => run("reload"), + ensureClangd: async () => run("clangd"), + checkModules: async () => run("check"), + }, + }; +} + +test("成功流程严格按安装、默认、构建、重载、clangd、检查执行", async () => { + const fixture = operationsWith(); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: true, switchDefault: true }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["install", "default", "build", "reload", "clangd", "check"]); + assert.equal(outcome.state, "succeeded"); + assert.equal(outcome.degraded, false); + assert.deepEqual( + outcome.steps.map((step: ModuleSetupStepResult) => step.stage), + fixture.calls, + ); +}); + +test("安装或默认步骤取消、失败后立即停止", async () => { + const cases: Array<{ + stage: "install" | "default"; + state: "cancelled" | "failed"; + plan: Extract; + expectedCalls: ModuleSetupStage[]; + }> = [ + { + stage: "install", + state: "cancelled", + plan: { kind: "ready", installLlvm: true, switchDefault: true }, + expectedCalls: ["install"], + }, + { + stage: "install", + state: "failed", + plan: { kind: "ready", installLlvm: true, switchDefault: true }, + expectedCalls: ["install"], + }, + { + stage: "default", + state: "cancelled", + plan: { kind: "ready", installLlvm: false, switchDefault: true }, + expectedCalls: ["default"], + }, + { + stage: "default", + state: "failed", + plan: { kind: "ready", installLlvm: false, switchDefault: true }, + expectedCalls: ["default"], + }, + ]; + + for (const item of cases) { + const fixture = operationsWith({ [item.stage]: item.state }); + const outcome = await executeModuleSetup(item.plan, fixture.operations); + assert.deepEqual(fixture.calls, item.expectedCalls); + assert.equal(outcome.state, item.state); + assert.equal(outcome.degraded, false); + assert.equal(outcome.stage, item.stage); + } +}); + +test("构建取消后停止,构建失败则尝试后续恢复", async () => { + const fixture = operationsWith({ build: "cancelled" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build"]); + assert.equal(outcome.state, "cancelled"); + assert.equal(outcome.degraded, false); +}); + +test("构建失败后的重载失败会终止恢复流程", async () => { + const fixture = operationsWith({ build: "failed", reload: "failed" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build", "reload"]); + assert.equal(outcome.state, "failed"); + assert.equal(outcome.degraded, false); + assert.equal(outcome.stage, "reload"); +}); + +test("构建失败但 IDE 恢复链成功时返回降级成功", async () => { + const fixture = operationsWith({ build: "failed" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build", "reload", "clangd", "check"]); + assert.equal(outcome.state, "succeeded"); + assert.equal(outcome.degraded, true); +}); + +test("clangd 失败后不检查模块", async () => { + const fixture = operationsWith({ clangd: "failed" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build", "reload", "clangd"]); + assert.equal(outcome.state, "failed"); + assert.equal(outcome.stage, "clangd"); +}); + +test("模块检查失败时返回检查阶段", async () => { + const fixture = operationsWith({ check: "failed" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build", "reload", "clangd", "check"]); + assert.equal(outcome.state, "failed"); + assert.equal(outcome.stage, "check"); +}); From 4f80fc4e782329a490741840182d4c73659da511 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 15:52:51 +0800 Subject: [PATCH 032/119] fix: avoid default switch without effective toolchain --- src/moduleSetup.ts | 2 +- test/moduleSetup.test.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts index e32e238..e08db09 100644 --- a/src/moduleSetup.ts +++ b/src/moduleSetup.ts @@ -49,7 +49,7 @@ export function buildModuleSetupPlan( return { kind: "ready", installLlvm: !installedHostLlvm, - switchDefault: !effectiveIsLlvm, + switchDefault: inventory.effective !== undefined && !effectiveIsLlvm, }; } diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts index c1b4351..c41abf6 100644 --- a/test/moduleSetup.test.ts +++ b/test/moduleSetup.test.ts @@ -97,6 +97,21 @@ test("LLVM 已生效时不重复安装或切换", () => { }); }); +test("没有 effective 工具链时不推断需要切换全局默认", () => { + const plan = buildModuleSetupPlan( + inventory(), + "unavailable", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: true, + switchDefault: false, + }); +}); + test("available LLVM 不算已安装的 host 工具链", () => { const gcc = toolchain("gcc", true); const availableLlvm = toolchain("llvm"); @@ -229,6 +244,26 @@ test("确认文案按能力解释现状并完整说明副作用边界", () => { } }); +test("确认文案说明将复用已安装的 LLVM", () => { + const confirmation = moduleSetupConfirmation( + "syntax-only", + { kind: "ready", installLlvm: false, switchDefault: true }, + ); + + assert.match(confirmation.detail, /将复用已安装的 LLVM/); + assert.match(confirmation.detail, /补齐配套组件时仍可能下载 LLVM\/llvm-tools/); +}); + +test("确认文案在无需切换时明确不修改全局默认值", () => { + const confirmation = moduleSetupConfirmation( + "unavailable", + { kind: "ready", installLlvm: true, switchDefault: false }, + ); + + assert.match(confirmation.detail, /不修改全局默认工具链/); + assert.doesNotMatch(confirmation.detail, /设为全局默认工具链/); +}); + interface OperationFixture { calls: ModuleSetupStage[]; operations: ModuleSetupOperations; From 64b7e5752d0b26a8ab66356014c42309da9bac78 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 16:15:21 +0800 Subject: [PATCH 033/119] fix: make module setup confirmation neutral --- src/moduleSetup.ts | 2 +- test/moduleSetup.test.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts index e08db09..92d2dde 100644 --- a/src/moduleSetup.ts +++ b/src/moduleSetup.ts @@ -98,7 +98,7 @@ export function moduleSetupConfirmation( : "将复用已安装的 LLVM;补齐配套组件时仍可能下载 LLVM/llvm-tools"; const defaultDetail = plan.switchDefault ? "并把 LLVM 设为全局默认工具链" - : "当前 LLVM 已生效,不修改全局默认工具链"; + : "不会修改全局默认工具链"; return { message, detail: `${installDetail},${defaultDetail};不会编辑项目清单 mcpp.toml。`, diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts index c41abf6..9fe9968 100644 --- a/test/moduleSetup.test.ts +++ b/test/moduleSetup.test.ts @@ -255,13 +255,15 @@ test("确认文案说明将复用已安装的 LLVM", () => { }); test("确认文案在无需切换时明确不修改全局默认值", () => { + const plan = ready(buildModuleSetupPlan(inventory(), "unavailable", true, false)); const confirmation = moduleSetupConfirmation( "unavailable", - { kind: "ready", installLlvm: true, switchDefault: false }, + plan, ); - assert.match(confirmation.detail, /不修改全局默认工具链/); + assert.match(confirmation.detail, /不会修改全局默认工具链/); assert.doesNotMatch(confirmation.detail, /设为全局默认工具链/); + assert.doesNotMatch(confirmation.detail, /当前 LLVM 已生效/); }); interface OperationFixture { From c90bf6af2b04bc5a907fe0921f6cb51b98f47fa8 Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Sat, 8 Aug 2026 15:50:43 +0800 Subject: [PATCH 034/119] feat: add structural completion for mcpp.toml Section-header snippets and writing templates for free-vocabulary sections, computed from a tolerant TOML parser that yields cursor contexts with explicit replacement ranges (unterminated input, CRLF, quoted/dotted keys, nested inline tables). Conditional sections such as [target..dependencies] resolve to their base group. No static field keys/enums (waiting for a versioned upstream manifest schema) and no dynamic dependency data (waiting for a batch catalog interface); the completion query layer is provider-dispatch so both can be added back without touching the parser. Contract tests run every registered section header and template key through real mcpp builds (skipped where mcpp is absent). Gated by mcpp.tomlCompletion (default on). --- package.json | 9 +- src/extension.ts | 47 ++ src/mcppTomlCompletion.ts | 223 ++++++++ src/mcppTomlParser.ts | 890 ++++++++++++++++++++++++++++++++ test/artifacts.test.ts | 2 +- test/mcppTomlCompletion.test.ts | 93 ++++ test/mcppTomlContract.test.ts | 228 ++++++++ test/mcppTomlParser.test.ts | 461 +++++++++++++++++ 8 files changed, 1951 insertions(+), 2 deletions(-) create mode 100644 src/mcppTomlCompletion.ts create mode 100644 src/mcppTomlParser.ts create mode 100644 test/mcppTomlCompletion.test.ts create mode 100644 test/mcppTomlContract.test.ts create mode 100644 test/mcppTomlParser.test.ts diff --git a/package.json b/package.json index e83188c..84d4538 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "workspaceContains:mcpp.toml", "onLanguage:cpp", "onLanguage:mcpp-build", + "onLanguage:mcpp-toml", "onCommand:mcpp.configureClangd", "onCommand:mcpp.refreshCompilationDatabase", "onCommand:mcpp.checkModuleSupport", @@ -44,7 +45,7 @@ "capabilities": { "untrustedWorkspaces": { "supported": "limited", - "description": "未受信任工作区仅启用模块语法高亮,不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。" + "description": "未受信任工作区仅启用模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。" } }, "main": "./dist/src/extension.js", @@ -146,6 +147,12 @@ "default": true, "scope": "resource", "description": "配置 clangd 时,是否询问关闭 Microsoft C/C++ IntelliSense。" + }, + "mcpp.tomlCompletion": { + "type": "boolean", + "default": true, + "scope": "resource", + "description": "为 mcpp.toml 提供结构补全:段头与写法模板(snippet)。所有建议带显式替换范围,并经真实 mcpp 契约测试验证。" } } }, diff --git a/src/extension.ts b/src/extension.ts index 802b186..71bd556 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -45,6 +45,7 @@ import { } from "./workflow"; import { classifyTaskExit, type TaskCompletion } from "./tasks"; import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./inProject"; +import { computeMcppTomlCompletions } from "./mcppTomlCompletion"; const COMMAND_CONFIGURE = "mcpp.configureClangd"; const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; @@ -852,6 +853,47 @@ async function autoConfigureModulesWizard( appendOutputLine(output, `[一键配置] 一键配置完成。clangd:${resolvedClangd.path}`); } +// mcpp.toml 结构补全:建议由纯函数 computeMcppTomlCompletions 计算,这里只做 +// vscode 类型映射。范围:段头 snippet + 开放词汇段的写法模板;不含字段键/枚举 +// 与依赖数据(分别等上游版本化 schema 与批量 catalog 接口)。 +const mcppTomlCompletionKinds = { + section: vscode.CompletionItemKind.Folder, + template: vscode.CompletionItemKind.Snippet, +} as const; + +const mcppTomlCompletionProvider: vscode.CompletionItemProvider = { + provideCompletionItems(document, position) { + // mcpp.toml 结构补全由 mcpp.tomlCompletion 控制,按文档作用域读取。 + if (!vscode.workspace.getConfiguration("mcpp", document.uri).get("tomlCompletion", true)) { + return undefined; + } + const lines: string[] = []; + for (let line = 0; line <= position.line; line += 1) { + lines.push(document.lineAt(line).text); + } + return computeMcppTomlCompletions(lines, position.line, position.character).map((suggestion) => { + const item = new vscode.CompletionItem( + suggestion.label, + mcppTomlCompletionKinds[suggestion.kind], + ); + item.detail = suggestion.detail; + if (suggestion.documentation !== undefined) { + item.documentation = new vscode.MarkdownString(suggestion.documentation); + } + if (suggestion.insertSnippet !== undefined) { + item.insertText = new vscode.SnippetString(suggestion.insertSnippet); + } + item.range = new vscode.Range( + position.line, + suggestion.range.startCharacter, + position.line, + suggestion.range.endCharacter, + ); + return item; + }); + }, +}; + export async function activate(extensionContext: vscode.ExtensionContext): Promise { moduleStatusByProject.clear(); moduleCheckOperations.clear(); @@ -1028,6 +1070,11 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi output, status, ...cliController.register(), + vscode.languages.registerCompletionItemProvider( + { language: "mcpp-toml" }, + mcppTomlCompletionProvider, + "[", + ), vscode.commands.registerCommand(COMMAND_CONFIGURE, runGuarded(async () => { const project = findCurrentProject(); if (project === undefined) { diff --git a/src/mcppTomlCompletion.ts b/src/mcppTomlCompletion.ts new file mode 100644 index 0000000..8694846 --- /dev/null +++ b/src/mcppTomlCompletion.ts @@ -0,0 +1,223 @@ +// mcpp.toml 的代码补全查询层(结构补全版)。 +// +// 范围:段头结构建议 + 开放词汇段的写法模板。每条建议携带显式替换范围。 +// 依赖包名/版本等动态数据补全与静态字段键/枚举补全均不在本版——前者等上游 +// 批量 catalog 接口,后者等版本化 manifest schema(见设计 issue #8 与 +// mcpp RFC #379)。 +// +// 本模块不依赖 vscode API;上下文来自 mcppTomlParser 的 contextAt(容错解析)。 + +import { + contextAt, + type ReplaceRange, + type SectionResolution, +} from "./mcppTomlParser"; + +export type McppTomlSuggestionKind = "section" | "template"; + +export interface McppTomlSuggestion { + label: string; + kind: McppTomlSuggestionKind; + detail: string; + documentation?: string; + /** 插入文本;含 $1 等 snippet 占位符。缺省时插入 label。 */ + insertSnippet?: string; + /** 替换范围(光标所在行的起止列)。 */ + range: ReplaceRange; +} + +export interface SectionHeaderSpec { + group: string; + label: string; + /** snippet 形式的段头(含 ${1:...} 占位)。 */ + header: string; + detail: string; +} + +// 段头结构清单:TOML 结构语法,非字段语义。出处:mcpp 文档 02/03/05/06 +// 与 src/manifest/toml.cppm 的段清单(契约测试用真实 mcpp 逐段验证)。 +export const SECTION_HEADERS: readonly SectionHeaderSpec[] = [ + { group: "package", label: "[package]", header: "[package]", detail: "包元数据" }, + { group: "lib", label: "[lib]", header: "[lib]", detail: "库根模块约定" }, + { group: "build", label: "[build]", header: "[build]", detail: "构建配置" }, + { group: "generated_files", label: "[generated_files]", header: "[generated_files]", detail: "生成文件(路径 → 内容)" }, + { group: "dependencies", label: "[dependencies]", header: "[dependencies]", detail: "运行时依赖" }, + { group: "dev-dependencies", label: "[dev-dependencies]", header: "[dev-dependencies]", detail: "开发/测试依赖" }, + { group: "workspace", label: "[workspace]", header: "[workspace]", detail: "工作空间成员声明" }, + { group: "workspace.dependencies", label: "[workspace.dependencies]", header: "[workspace.dependencies]", detail: "集中声明依赖版本,成员用 workspace = true 继承" }, + { group: "features", label: "[features]", header: "[features]", detail: "feature 定义" }, + { group: "feature-deps", label: "[feature-deps.]", header: "[feature-deps.${1:name}]", detail: "由 feature 拉取的可选依赖" }, + { group: "capabilities", label: "[capabilities]", header: "[capabilities]", detail: "capability 绑定(provider 选择)" }, + { group: "targets", label: "[targets.]", header: "[targets.${1:name}]", detail: "构建目标" }, + { group: "profile", label: "[profile.]", header: "[profile.${1:name}]", detail: "构建档案" }, + { group: "runtime", label: "[runtime]", header: "[runtime]", detail: "主机运行时能力" }, + { group: "resources", label: "[resources]", header: "[resources]", detail: "编译进产物的元数据与资产(仅 PE 目标)" }, + { group: "toolchain", label: "[toolchain]", header: "[toolchain]", detail: "编译器工具链简写" }, + { group: "xlings", label: "[xlings]", header: "[xlings]", detail: "构建环境(xlings 供给)" }, + { group: "xlings.workspace", label: "[xlings.workspace]", header: "[xlings.workspace]", detail: "固定工具版本" }, + { group: "xlings.envs", label: "[xlings.envs]", header: "[xlings.envs]", detail: "工具环境的环境变量" }, + { group: "target", label: "[target.]", header: "[target.${1:x86_64-linux-gnu}]", detail: "按目标三元组的配置" }, + { group: "pack", label: "[pack]", header: "[pack]", detail: "mcpp pack 打包配置" }, + { group: "pack.bundle-project", label: "[pack.bundle-project]", header: "[pack.bundle-project]", detail: "vendored 过滤策略微调" }, + { group: "indices", label: "[indices]", header: "[indices]", detail: "项目级索引重定向" }, + { group: "tools.overrides", label: "[tools.overrides]", header: "[tools.overrides]", detail: "host 工具二进制覆盖" }, + { group: "language", label: "[language]", header: "[language]", detail: "旧版兼容字段;新项目请用 [package].standard" }, +]; + +/** 依赖类段(键位置给依赖写法模板)。 */ +const DEPENDENCY_GROUPS: ReadonlySet = new Set([ + "dependencies", + "dev-dependencies", + "build-dependencies", + "workspace.dependencies", + "feature-deps", +]); + +interface TemplateSpec { + label: string; + detail: string; + documentation?: string; + insertSnippet: string; +} + +const DEPENDENCY_TEMPLATES: readonly TemplateSpec[] = [ + { + label: 'name = "version"', + detail: "SemVer 版本依赖", + documentation: "默认 caret 约束(^);也支持 ~、= 与 \">=1.0, <2.0\" 范围组合。", + insertSnippet: '${1:name} = "${2:1.0.0}"', + }, + { + label: "name = { path = ... }", + detail: "路径依赖(本地开发)", + insertSnippet: '${1:name} = { path = "${2:../mylib}" }', + }, + { + label: "name = { git = ..., tag = ... }", + detail: "Git 依赖(tag / branch / rev 三选一)", + insertSnippet: '${1:name} = { git = "${2:https://github.com/user/repo.git}", tag = "${3:v1.0.0}" }', + }, + { + label: "name = { version = ..., features = [...] }", + detail: "长式 dep spec:请求该依赖的 feature", + insertSnippet: '${1:name} = { version = "${2:1.0}", features = ["${3:feature}"] }', + }, + { + label: "name = { version = ..., tools = [...] }", + detail: "依赖产出的 host 工具(须为该包的 bin target)", + insertSnippet: '${1:name} = { version = "${2:1.0}", tools = ["${3:protoc}"] }', + }, +]; + +const FEATURE_TEMPLATES: readonly TemplateSpec[] = [ + { label: "name = [...]", detail: "数组简写:仅隐含 feature", insertSnippet: "${1:name} = [${2}]" }, + { label: "name = { defines = [...] }", detail: "表形式:激活时贡献包自有宏", insertSnippet: '${1:name} = { defines = ["${2:MACRO}"] }' }, + { label: "name = { requires = [...] }", detail: "表形式:需要 capability", insertSnippet: '${1:name} = { requires = ["${2:blas}"] }' }, + { label: "name = { sources = [...] }", detail: "表形式:feature 门控的源 glob", insertSnippet: '${1:name} = { sources = ["${2:src/simd/**}"] }' }, +]; + +const GENERATED_FILE_TEMPLATES: readonly TemplateSpec[] = [ + { + label: '"path" = "content"', + detail: "生成文件(相对路径 → 内容,进指纹)", + insertSnippet: '"${1:src/gen/wrap.cppm}" = """\n${2:}\n"""', + }, +]; + +const CAPABILITY_TEMPLATES: readonly TemplateSpec[] = [ + { + label: 'capability = "provider"', + detail: "capability 绑定(等价于 --cap)", + insertSnippet: '${1:blas} = "${2:compat.openblas}"', + }, +]; + +const XLINGS_WORKSPACE_TEMPLATES: readonly TemplateSpec[] = [ + { label: 'tool = "version"', detail: "固定工具版本", insertSnippet: '${1:clang} = "${2:20.1.7}"' }, +]; + +const XLINGS_ENVS_TEMPLATES: readonly TemplateSpec[] = [ + { label: 'NAME = "value"', detail: "应用到工具环境的环境变量", insertSnippet: '${1:NAME} = "${2:value}"' }, +]; + +const TOOLS_OVERRIDES_TEMPLATES: readonly TemplateSpec[] = [ + { + label: '"pkg:tool" = "path"', + detail: "用已有二进制覆盖 host 工具(跳过构建)", + insertSnippet: '"${1:compat.protobuf:protoc}" = "${2:/usr/bin/protoc}"', + }, +]; + +const TEMPLATES_BY_GROUP: Record = { + "features": FEATURE_TEMPLATES, + "generated_files": GENERATED_FILE_TEMPLATES, + "capabilities": CAPABILITY_TEMPLATES, + "xlings.workspace": XLINGS_WORKSPACE_TEMPLATES, + "xlings.envs": XLINGS_ENVS_TEMPLATES, + "tools.overrides": TOOLS_OVERRIDES_TEMPLATES, +}; + +function sectionHeaderSuggestions(range: ReplaceRange): McppTomlSuggestion[] { + return SECTION_HEADERS.map((section) => ({ + label: section.label, + kind: "section", + detail: section.detail, + insertSnippet: section.header, + range, + })); +} + +function templateSuggestions(templates: readonly TemplateSpec[], range: ReplaceRange): McppTomlSuggestion[] { + return templates.map((template) => ({ + label: template.label, + kind: "template", + detail: template.detail, + documentation: template.documentation, + insertSnippet: template.insertSnippet, + range, + })); +} + +/** + * 计算 mcpp.toml 在指定位置的补全建议(结构补全:段头 + 写法模板)。 + */ +export function computeMcppTomlCompletions( + lines: readonly string[], + line: number, + character: number, +): McppTomlSuggestion[] { + const context = contextAt(lines, line, character); + + if (context.kind === "section-header") { + // parser 的替换范围从段名 token 开始;段头建议插入的是完整 "[xxx]", + // 需要把范围扩展到本行的 "[",避免留下 "[["。仅当 "[" 是行内首个 + // 非空白字符时才扩展(section-header 上下文正常都满足,防御奇怪输入)。 + const lineText = (lines[line] ?? "").replace(/\r$/, ""); + const bracket = lineText.indexOf("["); + const firstNonWs = lineText.search(/\S/); + const range = bracket >= 0 && bracket === firstNonWs + ? { startCharacter: bracket, endCharacter: context.replaceRange.endCharacter } + : context.replaceRange; + return sectionHeaderSuggestions(range); + } + + if (context.kind === "key") { + const { section, containerPath, replaceRange } = context; + // 文档顶部(尚无段头):提示段头。未知段:不提供建议 + // (附录 A:不支持包自定义 toml 键)。 + if (section.kind === "top") { + return sectionHeaderSuggestions(replaceRange); + } + if (section.kind !== "known" || containerPath.length > 0) { + return []; + } + if (DEPENDENCY_GROUPS.has(section.group)) { + return templateSuggestions(DEPENDENCY_TEMPLATES, replaceRange); + } + const templates = TEMPLATES_BY_GROUP[section.group]; + return templates === undefined ? [] : templateSuggestions(templates, replaceRange); + } + + // 值位置:自由格式值不瞎猜(版本候选等动态数据层落地后再说)。 + return []; +} diff --git a/src/mcppTomlParser.ts b/src/mcppTomlParser.ts new file mode 100644 index 0000000..1276eca --- /dev/null +++ b/src/mcppTomlParser.ts @@ -0,0 +1,890 @@ +// mcpp.toml 的容错解析层:为代码补全提供结构化信息。 +// 设计目标是容错而非校验——用户在编辑器里输入到一半(未闭合的 `[`、字符串、 +// 内联表)时不抛异常,而是把已识别的结构连同精确的 0 基行列范围返回。 +// 本模块不依赖 vscode API,可在 node --test 下直接测试;extension.ts 负责 +// 把这里的纯数据上下文映射为 CompletionItem。 +// +// 覆盖的 TOML 子集:段头(含 [[...]] 数组表、单/双引号段)、裸键/引号键/ +// 点分键、字符串(含三引号多行串)、整数、布尔、数组与内联表(可嵌套、 +// 可跨行)、行注释。CRLF 行尾在进入解析前剥除,列号按剥除后的文本计算。 + +/** 0 基行列范围,end 为开区间(与 VS Code Range 同构)。 */ +export interface TomlRange { + startLine: number; + startCharacter: number; + endLine: number; + endCharacter: number; +} + +/** 键/段路径中的一段:已去引号的名字 + 原始 token 的范围(含引号)。 */ +export interface TomlKeySegment { + name: string; + quoted: boolean; + range: TomlRange; +} + +export type TomlValueKind = "string" | "integer" | "boolean" | "array" | "inlineTable" | "unknown"; + +/** 值节点。open = true 表示结构未闭合(输入到一半),range 延伸到文档末尾。 */ +export interface TomlValueNode { + kind: TomlValueKind; + range: TomlRange; + /** 未闭合(字符串缺右引号、数组缺 ]、内联表缺 })。 */ + open: boolean; + /** 标量原文:字符串为去引号内容(未做转义还原),整数/布尔为原文 token。 */ + text?: string; + /** 字符串节点:引号字符。 */ + quote?: '"' | "'"; + /** 字符串节点:是否三引号多行串。 */ + multiline?: boolean; + /** 字符串节点:内容范围(不含引号;未闭合时延伸到扫描终点)。 */ + contentRange?: TomlRange; + /** 数组节点:已解析出的元素。 */ + elements?: TomlValueNode[]; + /** 内联表节点:已解析出的键值条目。 */ + entries?: TomlKeyValueNode[]; +} + +/** 键值对节点:keyPath 为点分键拆出的段数组(如 capi.lua → [capi, lua])。 */ +export interface TomlKeyValueNode { + type: "keyValue"; + keyPath: TomlKeySegment[]; + range: TomlRange; + /** 缺省表示 `=` 之后没有可用值(含尚未输入的情况)。 */ + value?: TomlValueNode; +} + +/** 段头节点。open = true 表示 `]` 尚未输入。 */ +export interface TomlSectionNode { + type: "section"; + /** 段路径段(已去引号,如 [target.'cfg(windows)'.build] → target / cfg(windows) / build)。 */ + segments: TomlKeySegment[]; + /** 是否为 [[...]] 数组表段头。 */ + isArray: boolean; + open: boolean; + range: TomlRange; + /** 段头所在行(0 基)。 */ + line: number; +} + +export type TomlNode = TomlSectionNode | TomlKeyValueNode; + +export interface TomlDocument { + nodes: TomlNode[]; +} + +/** 段归属语义:top = 文档顶部尚无段头;unknown = 未识别的自定义段;known = 已知段组。 */ +export type SectionResolution = + | { kind: "top" } + | { kind: "unknown"; segments: string[] } + | { kind: "known"; group: string }; + +/** 当前 token 的替换范围(与光标同行的起止列),供补全做显式 range。 */ +export interface ReplaceRange { + startCharacter: number; + endCharacter: number; +} + +/** 光标的结构化上下文(纯数据)。 */ +export type TomlCursorContext = + | { + kind: "section-header"; + /** 光标所在 token 之前已解析出的段路径段(去引号)。 */ + segments: string[]; + isArray: boolean; + replaceRange: ReplaceRange; + } + | { + kind: "key"; + section: SectionResolution; + /** 容器路径:顶层语句为 [];内联表内为从语句根键开始的路径(如 simd = { flags = [ { … 深处为 ["simd","flags"])。 */ + containerPath: string[]; + /** 同一行点分键中光标 token 之前已输入的段(如 capi.la| → ["capi"])。 */ + keyPrefix: string[]; + replaceRange: ReplaceRange; + } + | { + kind: "value"; + section: SectionResolution; + /** 所属键的完整路径(含内联表/数组下钻)。 */ + keyPath: string[]; + /** 光标处值的种类;值尚未开始或不可判定时为 undefined。 */ + valueKind: TomlValueKind | undefined; + insideString: boolean; + /** 所在字符串未闭合(不在字符串内时为 false)。 */ + stringOpen: boolean; + replaceRange: ReplaceRange; + }; + +/** 精确匹配的已知段(与 mcpp manifest 层一致)。 */ +const KNOWN_SECTIONS: ReadonlySet = new Set([ + "package", + "lib", + "build", + "generated_files", + "dependencies", + "dev-dependencies", + "build-dependencies", + "features", + "indices", + "capabilities", + "runtime", + "resources", + "toolchain", + "xlings", + "xlings.workspace", + "xlings.envs", + "workspace", + "workspace.dependencies", + "pack", + "pack.bundle-project", + "language", + "tools.overrides", +]); + +/** 参数化段基组:. 归入 (dependencies 系支持命名空间再嵌套)。 */ +const PARAMETERIZED_BASES: ReadonlySet = new Set([ + "targets", + "profile", + "feature-deps", + "dependencies", + "dev-dependencies", +]); + +function resolveGroup(segments: readonly string[]): string | undefined { + const joined = segments.join("."); + if (KNOWN_SECTIONS.has(joined)) { + return joined; + } + const head = segments[0]; + if (head === "target") { + // [target.] 或 [target..<子表>];sel 是三元组或去引号后的 + // cfg 表达式,本身不含未引号点,因此子表部分从第三段开始。 + if (segments.length <= 2) { + return "target"; + } + return resolveGroup(segments.slice(2)); + } + if (head === "runtime" && segments.length === 2) { + // [runtime.""]:带点 capability 名的显式 provider 子表。 + return "runtime.capability"; + } + if (PARAMETERIZED_BASES.has(head) && segments.length >= 2) { + return head; + } + if (head === "workspace" && segments[1] === "dependencies" && segments.length >= 3) { + return "workspace.dependencies"; + } + return undefined; +} + +/** + * 把解析出的段路径规范化为语义组。空 segments 表示文档顶部(尚无段头), + * 未识别的段返回 unknown,由调用方区分这两种情况。 + */ +export function resolveSection(segments: readonly string[]): SectionResolution { + if (segments.length === 0) { + return { kind: "top" }; + } + const group = resolveGroup(segments); + return group === undefined + ? { kind: "unknown", segments: [...segments] } + : { kind: "known", group }; +} + +interface Pos { + line: number; + col: number; +} + +/** 内部控制流信号:光标上下文已捕获,提前结束扫描。解析本身永不抛异常。 */ +const CONTEXT_FOUND = Symbol("mcppTomlContextFound"); + +function isBareKeyChar(ch: string): boolean { + return ( + (ch >= "a" && ch <= "z") || + (ch >= "A" && ch <= "Z") || + (ch >= "0" && ch <= "9") || + ch === "_" || + ch === "-" + ); +} + +/** 逐行容错扫描器。行列均为 0 基,列按剥除 \r 后的文本计算。 */ +class Scanner { + private readonly lines: string[]; + private line = 0; + private col = 0; + private readonly cursor: Pos | undefined; + /** 光标上下文捕获结果。 */ + context: TomlCursorContext | undefined; + /** 扫描过程中最近经过的段头,用于给键/值上下文标注段归属。 */ + private currentSection: TomlSectionNode | undefined; + + constructor(lines: readonly string[], cursor?: Pos) { + this.lines = lines + .map((text) => (text.endsWith("\r") ? text.slice(0, -1) : text)); + if (this.lines.length === 0) { + this.lines.push(""); + } + if (cursor !== undefined) { + // 越界坐标钳制到文档内,保证补全在任意光标位置都能得到上下文。 + const line = Math.min(Math.max(0, cursor.line), this.lines.length - 1); + const col = Math.min(Math.max(0, cursor.col), this.lines[line].length); + this.cursor = { line, col }; + } + } + + // ---- 基础游标操作 ---- + + private pos(): Pos { + return { line: this.line, col: this.col }; + } + + private eof(): boolean { + return this.line >= this.lines.length; + } + + private atEol(): boolean { + return this.eof() || this.col >= this.lines[this.line].length; + } + + private peek(): string { + return this.atEol() ? "" : this.lines[this.line][this.col]; + } + + private peekAt(offset: number): string { + if (this.eof()) { + return ""; + } + const text = this.lines[this.line]; + return this.col + offset < text.length ? text[this.col + offset] : ""; + } + + private advance(): void { + if (this.eof()) { + return; + } + this.col += 1; + if (this.col > this.lines[this.line].length) { + this.line += 1; + this.col = 0; + } + } + + private samePos(a: Pos, b: Pos): boolean { + return a.line === b.line && a.col === b.col; + } + + private cmpPos(a: Pos, b: Pos): number { + return a.line - b.line || a.col - b.col; + } + + private skipInlineWs(): void { + while (this.peek() === " " || this.peek() === "\t") { + this.col += 1; + } + } + + /** 跳过空白、换行与行注释(# 到行尾;字符串内的 # 不会走到这里)。 */ + private skipTrivia(): void { + while (!this.eof()) { + const ch = this.peek(); + if (ch === " " || ch === "\t") { + this.col += 1; + continue; + } + if (this.atEol()) { + this.line += 1; + this.col = 0; + continue; + } + if (ch === "#") { + while (!this.atEol()) { + this.col += 1; + } + continue; + } + break; + } + } + + private rangeFrom(start: Pos): TomlRange { + return { + startLine: start.line, + startCharacter: start.col, + endLine: this.line, + endCharacter: this.col, + }; + } + + private sliceText(a: Pos, b: Pos): string { + if (a.line === b.line) { + return this.lines[a.line].slice(a.col, b.col); + } + const parts = [this.lines[a.line].slice(a.col)]; + for (let l = a.line + 1; l < b.line; l += 1) { + parts.push(this.lines[l]); + } + parts.push(this.lines[b.line].slice(0, b.col)); + return parts.join("\n"); + } + + // ---- 光标上下文捕获 ---- + + private cursorReached(): boolean { + const cursor = this.cursor; + return cursor !== undefined && this.cmpPos(this.pos(), cursor) >= 0; + } + + /** 光标是否落在范围内(端点含尾:补全时光标常贴在 token 末尾)。 */ + private cursorWithin(range: TomlRange): boolean { + const cursor = this.cursor; + if (cursor === undefined) { + return false; + } + if (cursor.line < range.startLine || cursor.line > range.endLine) { + return false; + } + if (cursor.line === range.startLine && cursor.col < range.startCharacter) { + return false; + } + if (cursor.line === range.endLine && cursor.col > range.endCharacter) { + return false; + } + return true; + } + + /** 在光标行上向左/右扩展 token(遇 stop 字符停),用于字符串内容等无预扫描范围的替换区间。 */ + private expandOnLine(stop: (ch: string) => boolean): ReplaceRange { + const cursor = this.cursor; + if (cursor === undefined) { + return { startCharacter: 0, endCharacter: 0 }; + } + const text = this.lines[cursor.line] ?? ""; + const at = Math.min(cursor.col, text.length); + let start = at; + let end = at; + while (start > 0 && !stop(text[start - 1])) { + start -= 1; + } + while (end < text.length && !stop(text[end])) { + end += 1; + } + return { startCharacter: start, endCharacter: end }; + } + + private emptyReplaceRange(): ReplaceRange { + const cursor = this.cursor; + const col = cursor?.col ?? 0; + return { startCharacter: col, endCharacter: col }; + } + + private static tokenReplaceRange(range: TomlRange): ReplaceRange { + // 键/段 token 保证单行(引号键跨行即视为未闭合而截断在行尾)。 + return { startCharacter: range.startCharacter, endCharacter: range.endCharacter }; + } + + /** 单行范围转替换范围;跨行值(数组/多行串)返回 undefined,由调用方回退。 */ + private static singleLineReplaceRange(range: TomlRange): ReplaceRange | undefined { + if (range.startLine !== range.endLine) { + return undefined; + } + return { startCharacter: range.startCharacter, endCharacter: range.endCharacter }; + } + + private sectionResolution(): SectionResolution { + if (this.currentSection === undefined) { + return { kind: "top" }; + } + return resolveSection(this.currentSection.segments.map((segment) => segment.name)); + } + + private capture(context: TomlCursorContext): never { + this.context = context; + throw CONTEXT_FOUND; + } + + private captureKey(containerPath: readonly string[], keyPrefix: readonly string[], token?: TomlRange): never { + this.capture({ + kind: "key", + section: this.sectionResolution(), + containerPath: [...containerPath], + keyPrefix: [...keyPrefix], + replaceRange: token !== undefined ? Scanner.tokenReplaceRange(token) : this.emptyReplaceRange(), + }); + } + + private captureValue( + keyPath: readonly string[], + valueKind: TomlValueKind | undefined, + options?: { insideString?: boolean; stringOpen?: boolean; replaceRange?: ReplaceRange }, + ): never { + this.capture({ + kind: "value", + section: this.sectionResolution(), + keyPath: [...keyPath], + valueKind, + insideString: options?.insideString ?? false, + stringOpen: options?.stringOpen ?? false, + replaceRange: options?.replaceRange ?? this.emptyReplaceRange(), + }); + } + + private captureHeader(segments: readonly TomlKeySegment[], isArray: boolean, token?: TomlRange): never { + this.capture({ + kind: "section-header", + segments: segments.map((segment) => segment.name), + isArray, + replaceRange: token !== undefined ? Scanner.tokenReplaceRange(token) : this.emptyReplaceRange(), + }); + } + + /** 扫描到文档末尾仍未捕获光标上下文时的兜底:当前段内的顶层键位置。 */ + finalContext(): TomlCursorContext { + return { + kind: "key", + section: this.sectionResolution(), + containerPath: [], + keyPrefix: [], + replaceRange: this.emptyReplaceRange(), + }; + } + + // ---- 结构扫描 ---- + + scan(): TomlNode[] { + const nodes: TomlNode[] = []; + while (!this.eof()) { + this.skipTrivia(); + // 光标落在语句之间的空白/注释里:视为顶层键位置(新语句的起点)。 + if (this.cursorReached()) { + this.captureKey([], []); + } + if (this.eof()) { + break; + } + const start = this.pos(); + if (this.peek() === "[") { + nodes.push(this.scanHeader(start)); + } else { + const keyValue = this.scanKeyValue([]); + if (keyValue !== undefined) { + nodes.push(keyValue); + } + } + // 进度保护:任何无法识别的垃圾字符都必须被消费,保证容错扫描必然终止。 + if (this.samePos(this.pos(), start)) { + this.advance(); + } + } + return nodes; + } + + /** 段头:[a.b] / [[a.b]] / 引号段;未闭合(缺 ])时 open = true 并跳过该行剩余内容。 */ + private scanHeader(start: Pos): TomlSectionNode { + this.advance(); // [ + let isArray = false; + if (this.peek() === "[") { + isArray = true; + this.advance(); + } + const segments: TomlKeySegment[] = []; + let open = true; + for (;;) { + this.skipInlineWs(); + // 光标在段之间的空白/点号附近(如 `[targets.` 之后)。 + if (this.cursorReached()) { + this.captureHeader(segments, isArray); + } + const segment = this.scanKeySegment(); + if (segment === undefined) { + break; + } + if (this.cursorWithin(segment.range)) { + this.captureHeader(segments, isArray, segment.range); + } + segments.push(segment); + this.skipInlineWs(); + if (this.peek() === ".") { + this.advance(); + continue; + } + break; + } + this.skipInlineWs(); + if (this.cursorReached()) { + this.captureHeader(segments, isArray); + } + if (this.peek() === "]") { + this.advance(); + if (isArray && this.peek() === "]") { + this.advance(); + } + // 容错:[[x] 只写了一个 ] 也视为已闭合。 + open = false; + } + if (open) { + // 未闭合段头:跳过该行剩余内容,避免残余字符被当作下一条语句。 + while (!this.atEol()) { + this.col += 1; + } + } + const node: TomlSectionNode = { + type: "section", + segments, + isArray, + open, + range: this.rangeFrom(start), + line: start.line, + }; + this.currentSection = node; + return node; + } + + /** 单个键/段路径段:裸键或单/双引号键(引号键跨行即按未闭合截断)。 */ + private scanKeySegment(): TomlKeySegment | undefined { + const start = this.pos(); + const quote = this.peek(); + if (quote === '"' || quote === "'") { + this.advance(); + const contentStart = this.pos(); + while (!this.atEol() && this.peek() !== quote) { + // 双引号键内的转义(如 \")不结束键。 + if (quote === '"' && this.peek() === "\\" && this.col + 1 < this.lines[this.line].length) { + this.col += 2; + continue; + } + this.col += 1; + } + const contentEnd = this.pos(); + if (this.peek() === quote) { + this.advance(); + } + return { + name: this.sliceText(contentStart, contentEnd), + quoted: true, + range: this.rangeFrom(start), + }; + } + if (!isBareKeyChar(quote)) { + return undefined; + } + while (isBareKeyChar(this.peek())) { + this.col += 1; + } + return { + name: this.sliceText(start, this.pos()), + quoted: false, + range: this.rangeFrom(start), + }; + } + + /** + * 键值对:点分键 [= 值]。containerPath 是内联表递归时下钻的键路径 + * (顶层语句为 [])。无法识别出键也没有值时返回 undefined。 + */ + private scanKeyValue(containerPath: readonly string[]): TomlKeyValueNode | undefined { + const start = this.pos(); + const segments: TomlKeySegment[] = []; + for (;;) { + this.skipInlineWs(); + // 光标在键槽位的空白处(如 `{ ` 之后、键未开始时)。 + if (this.cursorReached()) { + this.captureKey(containerPath, segments.map((segment) => segment.name)); + } + const segment = this.scanKeySegment(); + if (segment === undefined) { + break; + } + if (this.cursorWithin(segment.range)) { + this.captureKey(containerPath, segments.map((segment) => segment.name), segment.range); + } + segments.push(segment); + this.skipInlineWs(); + if (this.peek() === ".") { + this.advance(); + continue; + } + break; + } + this.skipInlineWs(); + // 光标在键与 = 之间(如 `name |`):仍算键位置。 + if (this.cursorReached()) { + this.captureKey(containerPath, segments.map((segment) => segment.name)); + } + const keyNames = segments.map((segment) => segment.name); + let value: TomlValueNode | undefined; + if (this.peek() === "=") { + this.advance(); + this.skipInlineWs(); + const valuePath = containerPath.concat(keyNames); + // 光标在 = 之后、值未开始(如 `kind = |`)。 + if (this.cursorReached()) { + // 光标后方同行还有值 token(光标恰在 token 首字符、或在 = 与 token + // 之间的空白上)时,替换范围要覆盖整个 token,否则补全插入后原文残留。 + if (!this.atEol() && this.peek() !== "#") { + const ahead = this.scanValue(valuePath); // 光标落在 token 内时由 scanValue 捕获 + this.captureValue(valuePath, ahead.kind, { + replaceRange: Scanner.singleLineReplaceRange(ahead.range) ?? this.emptyReplaceRange(), + }); + } + this.captureValue(valuePath, undefined); + } + // 值必须在同行开始(TOML 本就如此);行尾没有值则按缺失处理。 + if (!this.atEol() && this.peek() !== "#") { + value = this.scanValue(valuePath); + } + } + if (segments.length === 0 && value === undefined) { + return undefined; + } + return { type: "keyValue", keyPath: segments, range: this.rangeFrom(start), value }; + } + + private scanValue(keyPath: readonly string[]): TomlValueNode { + const ch = this.peek(); + if (ch === '"' || ch === "'") { + return this.scanStringValue(keyPath); + } + if (ch === "[") { + return this.scanArrayValue(keyPath); + } + if (ch === "{") { + return this.scanInlineTableValue(keyPath); + } + return this.scanBareValue(keyPath); + } + + /** 字符串:单/双引号单行串与三引号多行串;未闭合时 open = true。 */ + private scanStringValue(keyPath: readonly string[]): TomlValueNode { + const start = this.pos(); + const quote = this.peek() as '"' | "'"; + this.advance(); + let multiline = false; + let open = true; + let contentStart = this.pos(); + let contentEnd = this.pos(); + if (this.peek() === quote) { + this.advance(); + if (this.peek() === quote) { + // 三引号多行串。 + multiline = true; + this.advance(); + contentStart = this.pos(); + } else { + // 空字符串 "" / ''。 + open = false; + } + } + let closeStart: Pos | undefined; + if (open) { + while (!this.eof()) { + if (!multiline && this.atEol()) { + break; // 单行串跨行 → 未闭合 + } + const ch = this.peek(); + if (ch === "\\" && quote === '"') { + // 双引号串内的转义:跳过下一个字符(\" 不结束串)。 + this.advance(); + if (!this.eof() && (multiline || !this.atEol())) { + this.advance(); + } + continue; + } + if (ch === quote) { + if (multiline) { + if (this.peekAt(1) === quote && this.peekAt(2) === quote) { + contentEnd = this.pos(); + closeStart = this.pos(); + this.advance(); + this.advance(); + this.advance(); + open = false; + break; + } + this.advance(); + continue; + } + contentEnd = this.pos(); + closeStart = this.pos(); + this.advance(); + open = false; + break; + } + this.advance(); + } + if (open) { + contentEnd = this.pos(); + } + } + const node: TomlValueNode = { + kind: "string", + range: this.rangeFrom(start), + open, + text: this.sliceText(contentStart, contentEnd), + quote, + multiline, + contentRange: { + startLine: contentStart.line, + startCharacter: contentStart.col, + endLine: contentEnd.line, + endCharacter: contentEnd.col, + }, + }; + if (this.cursorWithin(node.range)) { + const cursor = this.cursor; + if (cursor !== undefined) { + const afterOpen = this.cmpPos(cursor, contentStart) >= 0; + const beforeClose = closeStart === undefined || this.cmpPos(cursor, closeStart) <= 0; + const insideString = afterOpen && beforeClose; + // 字符串内容的替换范围:扩展到本行引号边界,并钳制在内容范围内。 + let replaceRange = this.emptyReplaceRange(); + if (insideString) { + const expanded = this.expandOnLine((ch) => ch === quote); + const minCol = contentStart.line === cursor.line ? contentStart.col : 0; + const maxCol = + contentEnd.line === cursor.line ? contentEnd.col : this.lines[cursor.line]?.length ?? 0; + replaceRange = { + startCharacter: Math.max(expanded.startCharacter, minCol), + endCharacter: Math.min(Math.max(expanded.endCharacter, minCol), maxCol), + }; + } + this.captureValue(keyPath, "string", { + insideString, + stringOpen: open && afterOpen, + replaceRange, + }); + } + } + return node; + } + + /** 数组:可跨行,元素递归解析;缺 ] 时 open = true。 */ + private scanArrayValue(keyPath: readonly string[]): TomlValueNode { + const start = this.pos(); + this.advance(); // [ + const elements: TomlValueNode[] = []; + let open = true; + for (;;) { + this.skipTrivia(); + // 光标在元素槽位(如 `sources = [ ` 之后)。 + if (this.cursorReached()) { + this.captureValue(keyPath, undefined); + } + if (this.eof()) { + break; + } + const ch = this.peek(); + if (ch === "]") { + this.advance(); + open = false; + break; + } + if (ch === ",") { + this.advance(); + continue; + } + const elementStart = this.pos(); + elements.push(this.scanValue(keyPath)); + if (this.samePos(this.pos(), elementStart)) { + this.advance(); // 进度保护 + } + } + return { kind: "array", range: this.rangeFrom(start), open, elements }; + } + + /** 内联表:可嵌套、可跨行,条目是完整键值对;缺 } 时 open = true。 */ + private scanInlineTableValue(keyPath: readonly string[]): TomlValueNode { + const start = this.pos(); + this.advance(); // { + const entries: TomlKeyValueNode[] = []; + let open = true; + for (;;) { + this.skipTrivia(); + // 光标在条目键槽位(如 `simd = { flags = [ { ` 深处)。 + if (this.cursorReached()) { + this.captureKey(keyPath, []); + } + if (this.eof()) { + break; + } + const ch = this.peek(); + if (ch === "}") { + this.advance(); + open = false; + break; + } + if (ch === ",") { + this.advance(); + continue; + } + const entryStart = this.pos(); + const entry = this.scanKeyValue(keyPath); + if (entry !== undefined) { + entries.push(entry); + } + if (this.samePos(this.pos(), entryStart)) { + this.advance(); // 进度保护 + } + } + return { kind: "inlineTable", range: this.rangeFrom(start), open, entries }; + } + + /** 裸值 token:布尔、整数,其余归为 unknown(容错,不校验合法性)。 */ + private scanBareValue(keyPath: readonly string[]): TomlValueNode { + const start = this.pos(); + let end = this.pos(); + while (!this.atEol()) { + const ch = this.peek(); + if (ch === "," || ch === "]" || ch === "}" || ch === "[" || ch === "{" || ch === "#") { + break; + } + this.col += 1; + if (ch !== " " && ch !== "\t") { + end = this.pos(); + } + } + const text = this.sliceText(start, end); + let kind: TomlValueKind = "unknown"; + if (text === "true" || text === "false") { + kind = "boolean"; + } else if (/^[+-]?\d[\d_]*$/.test(text)) { + kind = "integer"; + } + const range: TomlRange = { + startLine: start.line, + startCharacter: start.col, + endLine: end.line, + endCharacter: end.col, + }; + if (this.cursorWithin(range)) { + this.captureValue(keyPath, kind, { replaceRange: Scanner.tokenReplaceRange(range) }); + } + return { kind, range, open: false, text }; + } +} + +/** 容错解析整个文档,产出带 0 基行列范围的节点树。永不抛异常。 */ +export function parseMcppToml(lines: readonly string[]): TomlDocument { + const scanner = new Scanner(lines); + return { nodes: scanner.scan() }; +} + +/** + * 计算光标的结构化上下文。line / character 均为 0 基(character 按剥除 + * \r 后的文本计算);越界坐标会被钳制到文档内。永不抛异常。 + */ +export function contextAt(lines: readonly string[], line: number, character: number): TomlCursorContext { + const scanner = new Scanner(lines, { line, col: character }); + try { + scanner.scan(); + } catch (error) { + if (error === CONTEXT_FOUND && scanner.context !== undefined) { + return scanner.context; + } + throw error; + } + // 扫描结束仍未命中:光标在文档末尾的空白处,视为当前段内的顶层键位置。 + return scanner.finalContext(); +} diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index f2d24cb..840ecce 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -33,7 +33,7 @@ test("declares the official clangd dependency and mcpp commands", () => { assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); assert.equal( manifest.capabilities?.untrustedWorkspaces?.description, - "未受信任工作区仅启用模块语法高亮,不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。", + "未受信任工作区仅启用模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。", ); assert.deepEqual( manifest.contributes?.commands?.map((command) => command.command), diff --git a/test/mcppTomlCompletion.test.ts b/test/mcppTomlCompletion.test.ts new file mode 100644 index 0000000..e8924eb --- /dev/null +++ b/test/mcppTomlCompletion.test.ts @@ -0,0 +1,93 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + computeMcppTomlCompletions, + type McppTomlSuggestion, +} from "../src/mcppTomlCompletion"; + +function labels(suggestions: McppTomlSuggestion[]): string[] { + return suggestions.map((suggestion) => suggestion.label); +} + +test("suggests section headers on a partial bracket line", () => { + const suggestions = computeMcppTomlCompletions(["[dep"], 0, 4); + assert.ok(suggestions.length > 0); + assert.ok(suggestions.every((suggestion) => suggestion.kind === "section")); + assert.ok(labels(suggestions).includes("[dependencies]")); + assert.ok(labels(suggestions).includes("[workspace]")); + assert.ok(labels(suggestions).includes("[indices]")); + // 每条建议都带显式替换范围(覆盖已输入的 "[dep")。 + for (const suggestion of suggestions) { + assert.deepEqual(suggestion.range, { startCharacter: 0, endCharacter: 4 }); + } + // 参数化段插入 snippet。 + const targets = suggestions.find((suggestion) => suggestion.label === "[targets.]"); + assert.equal(targets?.insertSnippet, "[targets.${1:name}]"); +}); + +test("suggests section headers at the top of the document", () => { + const suggestions = computeMcppTomlCompletions([""], 0, 0); + assert.ok(suggestions.length > 0); + assert.ok(suggestions.every((suggestion) => suggestion.kind === "section")); +}); + +test("offers nothing in unknown sections", () => { + // 附录 A:不支持包自定义 toml 键;未知段不提供任何建议。 + assert.deepEqual(computeMcppTomlCompletions(["[mytool]", ""], 1, 0), []); + assert.deepEqual(computeMcppTomlCompletions(["[mytool]", "key = "], 1, 6), []); +}); + +test("offers no static field keys (removed, waiting for upstream schema)", () => { + // 静态字段键/枚举刻意不做:等上游版本化 manifest schema。 + assert.deepEqual(computeMcppTomlCompletions(["[package]", ""], 1, 0), []); + assert.deepEqual(computeMcppTomlCompletions(["[package]", "standard = "], 1, 11), []); + assert.deepEqual(computeMcppTomlCompletions(["[targets.app]", "kind = "], 1, 7), []); +}); + +test("suggests dependency writing templates", () => { + const suggestions = computeMcppTomlCompletions(["[dependencies]", ""], 1, 0); + assert.ok(suggestions.length > 0); + assert.ok(suggestions.every((suggestion) => suggestion.kind === "template")); + const names = labels(suggestions); + assert.ok(names.includes('name = "version"')); + assert.ok(names.includes("name = { git = ..., tag = ... }")); + assert.ok(names.includes("name = { version = ..., tools = [...] }")); + assert.deepEqual(suggestions[0].range, { startCharacter: 0, endCharacter: 0 }); +}); + +test("suggests dependency templates in conditional dependency sections", () => { + const suggestions = computeMcppTomlCompletions(["[target.'cfg(windows)'.dependencies]", ""], 1, 0); + assert.ok(labels(suggestions).includes('name = "version"')); +}); + +test("suggests templates in free-key sections", () => { + const features = computeMcppTomlCompletions(["[features]", ""], 1, 0); + assert.ok(features.every((suggestion) => suggestion.kind === "template")); + assert.ok(labels(features).includes("name = { defines = [...] }")); + + const capabilities = computeMcppTomlCompletions(["[capabilities]", ""], 1, 0); + assert.ok(labels(capabilities).includes('capability = "provider"')); + + const generated = computeMcppTomlCompletions(["[generated_files]", ""], 1, 0); + assert.ok(labels(generated).includes('"path" = "content"')); +}); + +test("offers nothing at value positions", () => { + // 版本候选等动态数据层落地前,值位置不出建议。 + assert.deepEqual(computeMcppTomlCompletions(["[dependencies]", 'zlib = "'], 1, 8), []); + assert.deepEqual(computeMcppTomlCompletions(["[package]", 'name = "'], 1, 7), []); +}); + +test("offers nothing inside nested inline tables", () => { + // containerPath 非空(内联表深处)不出建议。 + assert.deepEqual(computeMcppTomlCompletions(["[features]", "simd = { flags = [ { "], 1, 21), []); +}); + +test("replacement range covers a partially typed key", () => { + const suggestions = computeMcppTomlCompletions(["[dependencies]", "na"], 1, 2); + assert.ok(suggestions.length > 0); + for (const suggestion of suggestions) { + assert.deepEqual(suggestion.range, { startCharacter: 0, endCharacter: 2 }); + } +}); diff --git a/test/mcppTomlContract.test.ts b/test/mcppTomlContract.test.ts new file mode 100644 index 0000000..dc1c4e3 --- /dev/null +++ b/test/mcppTomlContract.test.ts @@ -0,0 +1,228 @@ +// mcpp.toml 契约测试:用真实 mcpp 二进制验证扩展手写的 manifest 语义 +// (段头清单、模板键、条件段规则)与权威 parser 一致。 +// +// 没有 mcpp 的环境整个文件 skip。mcpp 调用每次 60s 超时,全程串行。 + +import assert from "node:assert/strict"; +import { spawn, spawnSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import test from "node:test"; + +import { SECTION_HEADERS, type SectionHeaderSpec } from "../src/mcppTomlCompletion"; + +/** 探测 mcpp 是否可用;不可用则全部跳过。 */ +function detectMcpp(): string | undefined { + try { + const result = spawnSync("mcpp", ["--version"], { timeout: 10_000 }); + if (result.error !== undefined || result.status !== 0) { + return undefined; + } + return String(result.stdout).trim(); + } catch { + return undefined; + } +} + +const mcppVersion = detectMcpp(); +const skipReason = mcppVersion === undefined ? "未检测到 mcpp 二进制,跳过契约测试" : false; +if (skipReason !== false) { + console.log(skipReason); +} else { + console.log(`契约测试使用 ${mcppVersion}`); +} + +const MCPP_TIMEOUT_MS = 60_000; + +interface McppRun { + code: number; + output: string; +} + +/** 在指定工程目录串行执行 `mcpp build`,汇总 stdout+stderr。 */ +function runMcppBuild(projectDir: string): Promise { + return new Promise((resolve, reject) => { + const child = spawn("mcpp", ["build"], { cwd: projectDir, timeout: MCPP_TIMEOUT_MS }); + let output = ""; + child.stdout.on("data", (chunk: Buffer) => { output += chunk.toString(); }); + child.stderr.on("data", (chunk: Buffer) => { output += chunk.toString(); }); + child.on("error", reject); + child.on("close", (code, signal) => { + if (signal !== null) { + reject(new Error(`mcpp build 被信号 ${signal} 终止(疑似超时):\n${output}`)); + return; + } + resolve({ code: code ?? -1, output }); + }); + }); +} + +/** 每次新建唯一临时工程目录,注册到清理列表。 */ +const tempDirs: string[] = []; +function makeProject(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mcpp-contract-")); + tempDirs.push(dir); + fs.mkdirSync(path.join(dir, "src"), { recursive: true }); + fs.writeFileSync(path.join(dir, "src", "main.cpp"), "int main() { return 0; }\n"); + return dir; +} + +process.on("exit", () => { + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +const BASE_MANIFEST = '[package]\nname = "probe"\nversion = "0.1.0"\n'; + +/** 写 manifest(基线 + 附加内容)与额外卖文件,然后跑 mcpp build。 */ +function buildWith(extraManifest: string, files: Record = {}): Promise { + const dir = makeProject(); + fs.writeFileSync(path.join(dir, "mcpp.toml"), `${BASE_MANIFEST}\n${extraManifest}`); + for (const [relative, content] of Object.entries(files)) { + const target = path.join(dir, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, content); + } + return runMcppBuild(dir); +} + +/** 核心断言:构建成功且输出无 unsupported / error(大小写不敏感)。 */ +function assertClean(run: McppRun, what: string): void { + assert.equal(run.code, 0, `${what}:mcpp build 退出码 ${run.code}\n${run.output}`); + assert.ok( + !/unsupported|error/i.test(run.output), + `${what}:输出含 unsupported/error 诊断\n${run.output}`, + ); +} + +/** + * 把段头注册表条目变成可构建的具体 manifest 片段。 + * snippet 占位符取默认值(如 [targets.${1:name}] → [targets.name])。 + */ +function fixtureFor(entry: SectionHeaderSpec): { manifest: string; files: Record } { + const header = entry.header.replace(/\$\{\d+:([^}]*)\}/g, "$1"); + assert.ok(!header.includes("$"), `段头 ${entry.label} 的占位符未能全部展开`); + if (entry.group === "package") { + // [package] 永远在基线工程里,直接验证基线本身。 + return { manifest: "", files: {} }; + } + if (entry.group === "targets") { + // [targets.] 需要 kind;kind = "lib" 需要 src/.cppm 消除 lib-root 警告。 + return { + manifest: `${header}\nkind = "lib"\n`, + files: { "src/name.cppm": "export module name;\n" }, + }; + } + return { manifest: `${header}\n`, files: {} }; +} + +test("段头注册表形态:数量与关键段齐全(防止测试与实现脱节)", { skip: skipReason }, () => { + assert.ok(SECTION_HEADERS.length >= 20, `段头注册表只有 ${SECTION_HEADERS.length} 条(期望 ≥ 20)`); + const groups = new Set(SECTION_HEADERS.map((entry) => entry.group)); + for (const key of ["package", "dependencies", "workspace", "pack"]) { + assert.ok(groups.has(key), `段头注册表缺少关键段 ${key}`); + } + const labels = SECTION_HEADERS.map((entry) => entry.label); + assert.equal(new Set(labels).size, labels.length, "段头注册表存在重复 label"); +}); + +test("段头清单契约:每个注册段 mcpp 都接受、无诊断", { skip: skipReason, timeout: 600_000 }, async (t) => { + for (const entry of SECTION_HEADERS) { + await t.test(entry.label, async () => { + const fixture = fixtureFor(entry); + const run = await buildWith(fixture.manifest, fixture.files); + assertClean(run, `段 ${entry.label}`); + }); + } +}); + +test("[indices] 带 path 条目被接受(项目级索引重定向)", { skip: skipReason, timeout: 120_000 }, async () => { + const run = await buildWith('[indices]\nmyidx = { path = "/tmp/mcpp-contract-nonexistent-index" }\n'); + assertClean(run, "[indices] 索引重定向"); +}); + +// 依赖 spec 的 12 个键:与 src/mcppTomlCompletion.ts 的 DEPENDENCY_TEMPLATES +// 保持同步(模板未逐一列出键名,此处按 mcpp manifest schema 硬编码)。 +// 注意:features/backend/tools/host-module/reexport 不是「锚定键」——单独出现 +// 时 mcpp 会把内联表当成嵌套依赖表报错,必须搭配 version/path/git/workspace +// 之一;tag/branch/rev 则必须搭配 git。写在 [feature-deps.] 下只解析、 +// 不下载(feature 未激活),适合断言「键被接受、无诊断」。 +const DEP_SPEC_CASES: ReadonlyArray = [ + ["version", 'dep = { version = "1.0.0" }'], + ["path", 'dep = { path = "../x" }'], + ["git+tag", 'dep = { git = "https://example.com/r.git", tag = "v1" }'], + ["git+branch", 'dep = { git = "https://example.com/r.git", branch = "main" }'], + ["git+rev", 'dep = { git = "https://example.com/r.git", rev = "abc123" }'], + ["features", 'dep = { version = "1.0.0", features = ["f"] }'], + ["backend", 'dep = { version = "1.0.0", backend = "cmake" }'], + ["tools", 'dep = { version = "1.0.0", tools = ["t"] }'], + ["host-module", 'dep = { version = "1.0.0", host-module = true }'], + ["reexport", 'dep = { version = "1.0.0", reexport = true }'], + ["workspace", "dep = { workspace = true }"], +]; + +test("模板键契约:[feature-deps] 下依赖 spec 各键被接受", { skip: skipReason, timeout: 600_000 }, async (t) => { + for (const [name, spec] of DEP_SPEC_CASES) { + await t.test(name, async () => { + const run = await buildWith(`[feature-deps.f1]\n${spec}\n`); + assertClean(run, `[feature-deps.f1] 依赖 spec 键 ${name}`); + }); + } +}); + +test("模板键契约:[features] 表形式与数组简写被接受", { skip: skipReason, timeout: 120_000 }, async () => { + const run = await buildWith([ + "[features]", + 'f1 = { defines = ["X"], implies = [], sources = ["src/**"] }', + 'f2 = ["f1"]', + 'f3 = { requires = ["blas"] }', + "", + ].join("\n")); + assertClean(run, "[features] 表形式键"); +}); + +// 其余写法模板的完整实例(依赖/feature 之外的模板条目)。capabilities 的 +// provider 绑定在「无包 require 该能力」时不解析,静默通过;generated_files +// 的条目会在源 glob 展开前写入工程树,这里同时验证生成机制生效(编译成功 +// 即说明模块文件被正常纳入构建)。 +test("模板实例契约:capabilities / xlings / tools.overrides / generated_files", { skip: skipReason, timeout: 120_000 }, async () => { + const run = await buildWith( + [ + "[capabilities]", + 'blas = "compat.openblas"', + "", + "[xlings.workspace]", + 'clang = "20.1.7"', + "", + "[xlings.envs]", + 'FOO = "1"', + "", + "[tools.overrides]", + '"compat.protobuf:protoc" = "/usr/bin/protoc"', + "", + "[generated_files]", + '"src/gen/wrap.cppm" = """', + "export module wrap;", + '"""', + "", + ].join("\n"), + ); + assertClean(run, "模板实例(capabilities/xlings/tools.overrides/generated_files)"); +}); + +test("条件段规则:[target.'cfg(windows)'.build] 接受 build inputs", { skip: skipReason, timeout: 120_000 }, async () => { + const run = await buildWith("[target.'cfg(windows)'.build]\ndefines = [\"A=1\"]\n"); + assertClean(run, "条件段 build inputs"); +}); + +test("条件段规则:反向断言——cache 在条件段必须报 unsupported", { skip: skipReason, timeout: 120_000 }, async () => { + // 钉住「条件段只接受 build inputs」:cache 属于 profile 设置,必须告警。 + const run = await buildWith("[target.'cfg(windows)'.build]\ncache = \"off\"\n"); + assert.equal(run.code, 0, `条件段 cache:mcpp build 退出码 ${run.code}\n${run.output}`); + assert.ok( + /unsupported key 'cache'/i.test(run.output), + `条件段 cache:期望 unsupported-key 警告\n${run.output}`, + ); +}); diff --git a/test/mcppTomlParser.test.ts b/test/mcppTomlParser.test.ts new file mode 100644 index 0000000..fb29820 --- /dev/null +++ b/test/mcppTomlParser.test.ts @@ -0,0 +1,461 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + contextAt, + parseMcppToml, + resolveSection, + type TomlKeyValueNode, + type TomlSectionNode, +} from "../src/mcppTomlParser"; + +function sectionAt(lines: string[], index: number): TomlSectionNode { + const node = parseMcppToml(lines).nodes[index]; + assert.equal(node?.type, "section"); + return node as TomlSectionNode; +} + +function keyValueAt(lines: string[], index: number): TomlKeyValueNode { + const node = parseMcppToml(lines).nodes[index]; + assert.equal(node?.type, "keyValue"); + return node as TomlKeyValueNode; +} + +// ---- 段头解析 ---- + +test("解析普通段头及其范围", () => { + const section = sectionAt(["[package]"], 0); + assert.deepEqual(section.segments.map((s) => s.name), ["package"]); + assert.equal(section.segments[0].quoted, false); + assert.equal(section.isArray, false); + assert.equal(section.open, false); + assert.equal(section.line, 0); + assert.deepEqual(section.range, { startLine: 0, startCharacter: 0, endLine: 0, endCharacter: 9 }); + assert.deepEqual(section.segments[0].range, { startLine: 0, startCharacter: 1, endLine: 0, endCharacter: 8 }); +}); + +test("解析参数化段头 [targets.myapp]", () => { + const section = sectionAt(["[targets.myapp]"], 0); + assert.deepEqual(section.segments.map((s) => s.name), ["targets", "myapp"]); + assert.deepEqual(section.segments[1].range, { startLine: 0, startCharacter: 9, endLine: 0, endCharacter: 14 }); +}); + +test("解析单引号段 [target.'cfg(windows)'.build]", () => { + const section = sectionAt(["[target.'cfg(windows)'.build]"], 0); + assert.deepEqual(section.segments.map((s) => s.name), ["target", "cfg(windows)", "build"]); + assert.equal(section.segments[1].quoted, true); + assert.deepEqual(section.segments[1].range, { startLine: 0, startCharacter: 8, endLine: 0, endCharacter: 22 }); +}); + +test("解析双引号段 [runtime.\"opengl.glx.driver\"](带点的段名是一段)", () => { + const section = sectionAt(['[runtime."opengl.glx.driver"]'], 0); + assert.deepEqual(section.segments.map((s) => s.name), ["runtime", "opengl.glx.driver"]); + assert.equal(section.segments[1].quoted, true); + assert.deepEqual(section.segments[1].range, { startLine: 0, startCharacter: 9, endLine: 0, endCharacter: 28 }); +}); + +test("解析数组表段头 [[build.flags]]", () => { + const section = sectionAt(["[[build.flags]]"], 0); + assert.equal(section.isArray, true); + assert.equal(section.open, false); + assert.deepEqual(section.segments.map((s) => s.name), ["build", "flags"]); + assert.deepEqual(section.range, { startLine: 0, startCharacter: 0, endLine: 0, endCharacter: 15 }); +}); + +test("未闭合段头解析为 open 状态且不抛错", () => { + const document = parseMcppToml(["[dep", 'name = "x"']); + const section = document.nodes[0] as TomlSectionNode; + assert.equal(section.type, "section"); + assert.equal(section.open, true); + assert.deepEqual(section.segments.map((s) => s.name), ["dep"]); + // 后续行继续正常解析。 + const keyValue = document.nodes[1] as TomlKeyValueNode; + assert.equal(keyValue.type, "keyValue"); + assert.deepEqual(keyValue.keyPath.map((s) => s.name), ["name"]); +}); + +// ---- 键值解析 ---- + +test("解析裸键字符串键值对及其范围", () => { + const keyValue = keyValueAt(["[package]", 'name = "demo"'], 1); + assert.deepEqual(keyValue.keyPath.map((s) => s.name), ["name"]); + assert.deepEqual(keyValue.keyPath[0].range, { startLine: 1, startCharacter: 0, endLine: 1, endCharacter: 4 }); + assert.equal(keyValue.value?.kind, "string"); + assert.equal(keyValue.value?.open, false); + assert.equal(keyValue.value?.text, "demo"); + assert.deepEqual(keyValue.value?.range, { startLine: 1, startCharacter: 7, endLine: 1, endCharacter: 13 }); + assert.deepEqual(keyValue.value?.contentRange, { startLine: 1, startCharacter: 8, endLine: 1, endCharacter: 12 }); +}); + +test("解析引号键(键名带点仍是一段)", () => { + const keyValue = keyValueAt(['"chriskohlhoff.asio" = "1.28"'], 0); + assert.equal(keyValue.keyPath.length, 1); + assert.equal(keyValue.keyPath[0].name, "chriskohlhoff.asio"); + assert.equal(keyValue.keyPath[0].quoted, true); + assert.equal(keyValue.value?.text, "1.28"); +}); + +test("解析点分键 capi.lua", () => { + const keyValue = keyValueAt(['capi.lua = "1.0"'], 0); + assert.deepEqual(keyValue.keyPath.map((s) => s.name), ["capi", "lua"]); + assert.deepEqual(keyValue.keyPath[1].range, { startLine: 0, startCharacter: 5, endLine: 0, endCharacter: 8 }); +}); + +test("解析整数与布尔值", () => { + const integer = keyValueAt(["opt = 2"], 0); + assert.equal(integer.value?.kind, "integer"); + assert.equal(integer.value?.text, "2"); + assert.deepEqual(integer.value?.range, { startLine: 0, startCharacter: 6, endLine: 0, endCharacter: 7 }); + + const boolean = keyValueAt(["debug = true"], 0); + assert.equal(boolean.value?.kind, "boolean"); + assert.equal(boolean.value?.text, "true"); + assert.deepEqual(boolean.value?.range, { startLine: 0, startCharacter: 8, endLine: 0, endCharacter: 12 }); +}); + +test("解析跨行数组", () => { + const keyValue = keyValueAt(["sources = [", ' "a",', ' "b",', "]"], 0); + assert.equal(keyValue.value?.kind, "array"); + assert.equal(keyValue.value?.open, false); + assert.deepEqual(keyValue.value?.range, { startLine: 0, startCharacter: 10, endLine: 3, endCharacter: 1 }); + assert.deepEqual(keyValue.value?.elements?.map((element) => element.text), ["a", "b"]); +}); + +test("未闭合数组解析为 open 状态", () => { + const keyValue = keyValueAt(["flags = [ { glob = \"x\" },"], 0); + assert.equal(keyValue.value?.kind, "array"); + assert.equal(keyValue.value?.open, true); + assert.equal(keyValue.value?.elements?.length, 1); + assert.equal(keyValue.value?.elements?.[0].kind, "inlineTable"); +}); + +test("解析嵌套内联表", () => { + const keyValue = keyValueAt(['dep = { version = "1", opts = { a = 1 } }'], 0); + assert.equal(keyValue.value?.kind, "inlineTable"); + assert.equal(keyValue.value?.open, false); + const entries = keyValue.value?.entries ?? []; + assert.deepEqual(entries.map((entry) => entry.keyPath.map((s) => s.name)), [["version"], ["opts"]]); + assert.equal(entries[0].value?.kind, "string"); + assert.equal(entries[1].value?.kind, "inlineTable"); + assert.equal(entries[1].value?.entries?.[0].value?.kind, "integer"); +}); + +test("未闭合内联表解析为 open 状态并保留已输入条目", () => { + const keyValue = keyValueAt(['dep = { version = "1"'], 0); + assert.equal(keyValue.value?.kind, "inlineTable"); + assert.equal(keyValue.value?.open, true); + assert.equal(keyValue.value?.entries?.length, 1); + assert.deepEqual(keyValue.value?.entries?.[0].keyPath.map((s) => s.name), ["version"]); +}); + +// ---- 注释、字符串与行尾 ---- + +test("注释被跳过,字符串内的 # 不算注释", () => { + const keyValue = keyValueAt(["# 顶部注释", 'name = "a # b" # 行尾注释'], 0); + assert.equal(keyValue.value?.kind, "string"); + assert.equal(keyValue.value?.text, "a # b"); +}); + +test("字符串内的转义引号不结束字符串", () => { + const keyValue = keyValueAt(['desc = "a \\"b\\" c"'], 0); + assert.equal(keyValue.value?.kind, "string"); + assert.equal(keyValue.value?.open, false); + assert.equal(keyValue.value?.text, 'a \\"b\\" c'); +}); + +test("解析三引号多行字符串", () => { + const keyValue = keyValueAt(['text = """', "hello", '"""'], 0); + assert.equal(keyValue.value?.kind, "string"); + assert.equal(keyValue.value?.multiline, true); + assert.equal(keyValue.value?.open, false); + assert.equal(keyValue.value?.text, "\nhello\n"); +}); + +test("未闭合字符串解析为 open 状态", () => { + const keyValue = keyValueAt(['name = "hel'], 0); + assert.equal(keyValue.value?.kind, "string"); + assert.equal(keyValue.value?.open, true); + assert.equal(keyValue.value?.text, "hel"); +}); + +test("CRLF 行尾被剥除,列号按剥除后计算", () => { + const document = parseMcppToml(["[package]\r", 'name = "demo"\r']); + const section = document.nodes[0] as TomlSectionNode; + assert.deepEqual(section.range, { startLine: 0, startCharacter: 0, endLine: 0, endCharacter: 9 }); + const keyValue = document.nodes[1] as TomlKeyValueNode; + assert.deepEqual(keyValue.keyPath[0].range, { startLine: 1, startCharacter: 0, endLine: 1, endCharacter: 4 }); + assert.deepEqual(keyValue.value?.contentRange, { startLine: 1, startCharacter: 8, endLine: 1, endCharacter: 12 }); +}); + +test("无法识别的垃圾行不抛错、不中断后续解析", () => { + const document = parseMcppToml(["!!garbage!!", "[package]"]); + const section = document.nodes.find((node) => node.type === "section") as TomlSectionNode; + assert.deepEqual(section.segments.map((s) => s.name), ["package"]); +}); + +// ---- resolveSection 段归属语义 ---- + +test("resolveSection 精确匹配已知段", () => { + assert.deepEqual(resolveSection(["package"]), { kind: "known", group: "package" }); + assert.deepEqual(resolveSection(["build"]), { kind: "known", group: "build" }); + assert.deepEqual(resolveSection(["workspace", "dependencies"]), { kind: "known", group: "workspace.dependencies" }); + assert.deepEqual(resolveSection(["pack", "bundle-project"]), { kind: "known", group: "pack.bundle-project" }); + assert.deepEqual(resolveSection(["tools", "overrides"]), { kind: "known", group: "tools.overrides" }); + assert.deepEqual(resolveSection(["xlings", "envs"]), { kind: "known", group: "xlings.envs" }); +}); + +test("resolveSection 参数化段归入基组", () => { + assert.deepEqual(resolveSection(["targets", "myapp"]), { kind: "known", group: "targets" }); + assert.deepEqual(resolveSection(["profile", "release"]), { kind: "known", group: "profile" }); + assert.deepEqual(resolveSection(["feature-deps", "simd"]), { kind: "known", group: "feature-deps" }); + assert.deepEqual(resolveSection(["dependencies", "capi"]), { kind: "known", group: "dependencies" }); + assert.deepEqual(resolveSection(["dependencies", "capi", "lua"]), { kind: "known", group: "dependencies" }); + assert.deepEqual(resolveSection(["dev-dependencies", "ns"]), { kind: "known", group: "dev-dependencies" }); + assert.deepEqual(resolveSection(["workspace", "dependencies", "ns"]), { kind: "known", group: "workspace.dependencies" }); +}); + +test("resolveSection target 选择器与子表递归", () => { + assert.deepEqual(resolveSection(["target"]), { kind: "known", group: "target" }); + assert.deepEqual(resolveSection(["target", "x86_64-linux-gnu"]), { kind: "known", group: "target" }); + assert.deepEqual(resolveSection(["target", "cfg(windows)", "build"]), { kind: "known", group: "build" }); + assert.deepEqual(resolveSection(["target", "x", "feature-deps", "codegen"]), { kind: "known", group: "feature-deps" }); + assert.deepEqual(resolveSection(["target", "x", "dependencies", "capi"]), { kind: "known", group: "dependencies" }); +}); + +test("resolveSection runtime capability 子表", () => { + assert.deepEqual(resolveSection(["runtime"]), { kind: "known", group: "runtime" }); + assert.deepEqual(resolveSection(["runtime", "opengl.glx.driver"]), { kind: "known", group: "runtime.capability" }); +}); + +test("resolveSection 区分未知段与文档顶部", () => { + assert.deepEqual(resolveSection(["custom"]), { kind: "unknown", segments: ["custom"] }); + assert.deepEqual(resolveSection(["build", "flags"]), { kind: "unknown", segments: ["build", "flags"] }); + assert.deepEqual(resolveSection([]), { kind: "top" }); +}); + +// ---- contextAt 光标上下文 ---- + +test("contextAt 未闭合段头内:段头上下文与替换范围", () => { + const context = contextAt(["[tar"], 0, 4); + assert.equal(context.kind, "section-header"); + if (context.kind !== "section-header") return; + assert.deepEqual(context.segments, []); + assert.equal(context.isArray, false); + assert.deepEqual(context.replaceRange, { startCharacter: 1, endCharacter: 4 }); +}); + +test("contextAt 段头点号之后:已解析段作为前缀", () => { + const context = contextAt(["[targets."], 0, 9); + assert.equal(context.kind, "section-header"); + if (context.kind !== "section-header") return; + assert.deepEqual(context.segments, ["targets"]); + assert.deepEqual(context.replaceRange, { startCharacter: 9, endCharacter: 9 }); +}); + +test("contextAt 段头中间段的替换范围覆盖整个 token", () => { + const context = contextAt(["[targets.myapp]"], 0, 11); + assert.equal(context.kind, "section-header"); + if (context.kind !== "section-header") return; + assert.deepEqual(context.segments, ["targets"]); + assert.deepEqual(context.replaceRange, { startCharacter: 9, endCharacter: 14 }); +}); + +test("contextAt 数组表段头刚输入 [[ 时", () => { + const context = contextAt(["[["], 0, 2); + assert.equal(context.kind, "section-header"); + if (context.kind !== "section-header") return; + assert.equal(context.isArray, true); + assert.deepEqual(context.segments, []); +}); + +test("contextAt 已知段内空行:键位置并携带段归属", () => { + const context = contextAt(["[package]", ""], 1, 0); + assert.equal(context.kind, "key"); + if (context.kind !== "key") return; + assert.deepEqual(context.section, { kind: "known", group: "package" }); + assert.deepEqual(context.containerPath, []); + assert.deepEqual(context.keyPrefix, []); + assert.deepEqual(context.replaceRange, { startCharacter: 0, endCharacter: 0 }); +}); + +test("contextAt 区分文档顶部与未知段", () => { + const top = contextAt([""], 0, 0); + assert.equal(top.kind, "key"); + if (top.kind === "key") { + assert.deepEqual(top.section, { kind: "top" }); + } + + const unknown = contextAt(["[custom]", ""], 1, 0); + assert.equal(unknown.kind, "key"); + if (unknown.kind === "key") { + assert.deepEqual(unknown.section, { kind: "unknown", segments: ["custom"] }); + } +}); + +test("contextAt 点分键中间:keyPrefix 与替换范围", () => { + const context = contextAt(["capi.la"], 0, 7); + assert.equal(context.kind, "key"); + if (context.kind !== "key") return; + assert.deepEqual(context.containerPath, []); + assert.deepEqual(context.keyPrefix, ["capi"]); + assert.deepEqual(context.replaceRange, { startCharacter: 5, endCharacter: 7 }); +}); + +test("contextAt 字符串值内部:insideString 与替换范围", () => { + const context = contextAt(['kind = "bin"'], 0, 9); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.deepEqual(context.keyPath, ["kind"]); + assert.equal(context.valueKind, "string"); + assert.equal(context.insideString, true); + assert.equal(context.stringOpen, false); + assert.deepEqual(context.replaceRange, { startCharacter: 8, endCharacter: 11 }); +}); + +test("contextAt 未闭合字符串内:stringOpen 为 true", () => { + const context = contextAt(['name = "hel'], 0, 11); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.deepEqual(context.keyPath, ["name"]); + assert.equal(context.valueKind, "string"); + assert.equal(context.insideString, true); + assert.equal(context.stringOpen, true); + assert.deepEqual(context.replaceRange, { startCharacter: 8, endCharacter: 11 }); +}); + +test("contextAt 等号后值未开始:valueKind 为 undefined", () => { + const context = contextAt(["kind = "], 0, 7); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.deepEqual(context.keyPath, ["kind"]); + assert.equal(context.valueKind, undefined); + assert.equal(context.insideString, false); + assert.deepEqual(context.replaceRange, { startCharacter: 7, endCharacter: 7 }); +}); + +test("contextAt 数组元素槽位", () => { + const context = contextAt(["sources = [ "], 0, 12); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.deepEqual(context.keyPath, ["sources"]); + assert.equal(context.valueKind, undefined); + assert.deepEqual(context.replaceRange, { startCharacter: 12, endCharacter: 12 }); +}); + +test("contextAt 深度下钻:features 值 → flags 数组 → 内联表条目的键位置", () => { + const context = contextAt(["[features]", "simd = { flags = [ { "], 1, 21); + assert.equal(context.kind, "key"); + if (context.kind !== "key") return; + assert.deepEqual(context.section, { kind: "known", group: "features" }); + assert.deepEqual(context.containerPath, ["simd", "flags"]); + assert.deepEqual(context.keyPrefix, []); + assert.deepEqual(context.replaceRange, { startCharacter: 21, endCharacter: 21 }); +}); + +test("contextAt 内联表条目的字符串值:keyPath 含下钻路径", () => { + const context = contextAt(['dep = { version = "1.0" }'], 0, 20); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.deepEqual(context.keyPath, ["dep", "version"]); + assert.equal(context.valueKind, "string"); + assert.equal(context.insideString, true); + assert.deepEqual(context.replaceRange, { startCharacter: 19, endCharacter: 22 }); +}); + +test("contextAt 数组内内联表条目的值:keyPath 穿过数组", () => { + const context = contextAt(['flags = [ { glob = "src/**" } ]'], 0, 23); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.deepEqual(context.keyPath, ["flags", "glob"]); + assert.equal(context.insideString, true); + assert.deepEqual(context.replaceRange, { startCharacter: 20, endCharacter: 26 }); +}); + +test("contextAt 布尔 token 中间:valueKind 为 boolean 且替换范围覆盖 token", () => { + const context = contextAt(["debug = true"], 0, 9); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.equal(context.valueKind, "boolean"); + assert.deepEqual(context.replaceRange, { startCharacter: 8, endCharacter: 12 }); +}); + +test("contextAt 兼容 CRLF 行尾", () => { + const context = contextAt(["[package]\r", 'name = "demo"\r'], 1, 9); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.deepEqual(context.section, { kind: "known", group: "package" }); + assert.equal(context.insideString, true); + assert.deepEqual(context.replaceRange, { startCharacter: 8, endCharacter: 12 }); +}); + +test("contextAt 越界坐标被钳制而不抛错", () => { + const context = contextAt(["[package]"], 99, 99); + assert.equal(context.kind, "key"); + if (context.kind !== "key") return; + assert.deepEqual(context.section, { kind: "known", group: "package" }); +}); + +// ---- 回归:值槽位替换范围 / 引号停止符 / 裸值停止字符 ---- + +test("contextAt 光标在 = 与值 token 之间的空白上:替换整个 token", () => { + const context = contextAt(["cmdline = true"], 0, 9); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.deepEqual(context.keyPath, ["cmdline"]); + assert.equal(context.valueKind, "boolean"); + assert.deepEqual(context.replaceRange, { startCharacter: 10, endCharacter: 14 }); +}); + +test("contextAt 光标恰在值 token 首字符:替换范围覆盖整个 token", () => { + const context = contextAt(["cmdline = true"], 0, 10); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.equal(context.valueKind, "boolean"); + assert.deepEqual(context.replaceRange, { startCharacter: 10, endCharacter: 14 }); +}); + +test("contextAt 光标在字符串 token 前的空白上:替换范围覆盖整个带引号 token", () => { + const context = contextAt(['name = "demo"'], 0, 6); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.equal(context.valueKind, "string"); + assert.equal(context.insideString, false); + assert.deepEqual(context.replaceRange, { startCharacter: 7, endCharacter: 13 }); +}); + +test("contextAt 单引号字符串内的双引号不截断替换范围", () => { + const context = contextAt(["s = 'a\"b'"], 0, 7); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.equal(context.valueKind, "string"); + assert.equal(context.insideString, true); + assert.deepEqual(context.replaceRange, { startCharacter: 5, endCharacter: 8 }); +}); + +test("contextAt 双引号字符串内的单引号不截断替换范围", () => { + const context = contextAt(["s = \"a'b\""], 0, 7); + assert.equal(context.kind, "value"); + if (context.kind !== "value") return; + assert.equal(context.insideString, true); + assert.deepEqual(context.replaceRange, { startCharacter: 5, endCharacter: 8 }); +}); + +test("裸值不停在 [ / { 之外的后续结构:x = 1 [dep", () => { + const document = parseMcppToml(["x = 1 [dep"]); + const keyValue = document.nodes[0] as TomlKeyValueNode; + assert.equal(keyValue.type, "keyValue"); + assert.equal(keyValue.value?.kind, "integer"); + assert.equal(keyValue.value?.text, "1"); + assert.deepEqual(keyValue.value?.range, { startLine: 0, startCharacter: 4, endLine: 0, endCharacter: 5 }); + const section = document.nodes[1] as TomlSectionNode; + assert.equal(section.type, "section"); + assert.equal(section.open, true); + assert.deepEqual(section.segments.map((s) => s.name), ["dep"]); +}); + +test("裸值在 { 前停止:y = 2 {k = 1}", () => { + const keyValue = keyValueAt(["y = 2 {k = 1}"], 0); + assert.equal(keyValue.value?.kind, "integer"); + assert.equal(keyValue.value?.text, "2"); + assert.deepEqual(keyValue.value?.range, { startLine: 0, startCharacter: 4, endLine: 0, endCharacter: 5 }); +}); From 736cbaf000dcb6fca0a9dd1c3afb63dc5fd2cb2e Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 16:33:42 +0800 Subject: [PATCH 035/119] feat: add automatic module setup controller --- src/cliController.ts | 96 +++++++++++++++++++++++++++++++++++++++- src/moduleSetup.ts | 2 +- test/artifacts.test.ts | 13 ++++++ test/moduleSetup.test.ts | 2 +- 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/cliController.ts b/src/cliController.ts index 2e53172..ef77e6a 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -26,6 +26,12 @@ import { } from "./tasks"; import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; import { runNewProjectFlow, validateNewProjectName } from "./newProject"; +import { + mcppModuleSetupCommands, + type ModuleSetupCommand, + type ModuleSetupDecision, + type ModuleSetupStepResult, +} from "./moduleSetup"; export interface McppCliControllerOptions { output: vscode.OutputChannel; @@ -112,6 +118,48 @@ export class McppCliController { return this.operations.hasActive(); } + public async runAutomaticModuleSetup( + plan: Extract, + ): Promise { + const project = this.requireProject(); + const commands = mcppModuleSetupCommands(plan); + const firstCommand = commands[0]; + if (project === undefined || !this.requireTrusted() || firstCommand === undefined) { + return { + stage: firstCommand?.stage ?? "build", + state: "failed", + detail: "当前工作区无法执行自动模块配置。", + }; + } + + const token: OperationToken = {}; + if (this.operations.beginGlobal(token) !== undefined) { + return { + stage: firstCommand.stage, + state: "failed", + detail: "已有 mcpp 操作正在运行。", + }; + } + + const executable = this.mcppExecutable(project); + try { + for (const command of commands) { + const result = await this.executeAutomaticModuleSetupCommand( + project, + executable, + command, + ); + if (result.state !== "succeeded") { + return result; + } + } + return { stage: "build", state: "succeeded" }; + } finally { + // 整个 install/default/build 事务共用一个 token,异常时也必须释放。 + this.operations.finishGlobal(token); + } + } + public async runProjectTask(kind: ProjectTaskKind): Promise { const project = this.requireProject(); if (project === undefined || !this.requireTrusted()) { @@ -598,8 +646,8 @@ export class McppCliController { return folder !== undefined && folder.uri.fsPath !== project.root; } - private async readToolchainInventory( - project: McppProjectDiscovery | undefined, + public async readToolchainInventory( + project: McppProjectDiscovery | undefined = this.options.currentProject(), ): Promise { const executable = this.mcppExecutable(project); const args = mcppCommandArguments("toolchain", "list"); @@ -622,6 +670,50 @@ export class McppCliController { return inventory; } + private async executeAutomaticModuleSetupCommand( + project: McppProjectDiscovery, + executable: string, + command: ModuleSetupCommand, + ): Promise { + if (command.mode === "process") { + const result = await runProcess(executable, command.args, project.root); + this.appendShortCommand(`自动模块配置:${command.stage}`, executable, command.args, result); + return { + stage: command.stage, + state: result.exitCode === 0 ? "succeeded" : "failed", + exitCode: result.exitCode, + }; + } + + // build 直接复用任务执行器,避免在 global token 内再次申请 project token。 + let completion: TaskCompletion; + try { + completion = await this.executeTask( + project.root, + executable, + "mcpp: 自动模块配置构建", + command.args, + ); + } catch (error) { + return { + stage: command.stage, + state: "failed", + detail: error instanceof Error ? error.message : String(error), + }; + } + this.appendTaskCompletion( + project.root, + "mcpp: 自动模块配置构建", + command.args, + completion, + ); + return { + stage: command.stage, + state: completion.state, + exitCode: completion.exitCode, + }; + } + private async executeTask( root: string, executable: string, diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts index 92d2dde..4598101 100644 --- a/src/moduleSetup.ts +++ b/src/moduleSetup.ts @@ -64,7 +64,7 @@ export function mcppModuleSetupCommands(plan: ModuleSetupPlan): ModuleSetupComma if (plan.installLlvm) { commands.push({ stage: "install", - mode: "process", + mode: "task", args: ["toolchain", "install", "llvm"], }); } diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index e9f304b..a80e886 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -63,6 +63,19 @@ test("declares the official clangd dependency and mcpp commands", () => { }); }); +test("自动模块配置使用非交互复合操作和单一全局锁", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + assert.match(source, /public async readToolchainInventory\s*\(/); + const start = source.indexOf("public async runAutomaticModuleSetup"); + assert.notEqual(start, -1); + const end = source.indexOf("public async ", start + 10); + const method = source.slice(start, end === -1 ? source.length : end); + assert.match(method, /mcppModuleSetupCommands\(plan\)/); + assert.match(method, /beginGlobal\(token\)/); + assert.match(method, /finally\s*\{[\s\S]*finishGlobal\(token\)/); + assert.doesNotMatch(method, /pickInstallSpec|selectDefaultToolchainFromInventory|showQuickPick|showWarningMessage/); +}); + test("shows editor title buttons only inside mcpp projects", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; assert.deepEqual(manifest.contributes?.menus?.["editor/title"], [ diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts index 9fe9968..6b7630d 100644 --- a/test/moduleSetup.test.ts +++ b/test/moduleSetup.test.ts @@ -214,7 +214,7 @@ test("模块设置命令使用固定 argv 数组且始终构建", () => { assert.deepEqual( mcppModuleSetupCommands({ kind: "ready", installLlvm: true, switchDefault: true }), [ - { stage: "install", mode: "process", args: ["toolchain", "install", "llvm"] }, + { stage: "install", mode: "task", args: ["toolchain", "install", "llvm"] }, { stage: "default", mode: "process", args: ["toolchain", "default", "llvm"] }, { stage: "build", mode: "task", args: ["build"] }, ], From bc56d0b590cd397f2cc196b86347273ae49e6823 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 16:40:00 +0800 Subject: [PATCH 036/119] feat: make module setup one click --- README.md | 15 ++- src/extension.ts | 267 ++++++++++++++++++--------------------- src/moduleSetup.ts | 39 ++++-- test/artifacts.test.ts | 24 ++++ test/moduleSetup.test.ts | 19 +++ 5 files changed, 202 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index 4e0d263..326d8a7 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,11 @@ TextMate 语法规则提供。 检查可以区分模块可用、PCM 不匹配、模块产物不可用、语言模式错误和一般检查失败。 完整命令及输出会写入 `mcpp` 输出频道。 -当 clangd 与编译器不匹配时,**mcpp: 一键配置模块代码提示** 会通过 xlings 自动下载 -匹配版本的 llvm-tools(含 clangd),完成后配置 clangd 并刷新状态栏,无需 Reload -Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明,不做自动切换。 +**mcpp: 一键配置模块代码提示** 会先读取工具链状态,再显示一次模态确认。确认后, +它可以自动安装 host LLVM、设置 mcpp 全局默认、执行 `mcpp build`,并通过已安装的 +xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB、配置 clangd 和检查 +模块状态,无需 Reload Window。项目在 `mcpp.toml` 或 target 中显式固定 GCC/MSVC 时, +一键流程会停止并提示手动处理,绝不修改项目配置或伪装成全局切换成功。 ### 自动协调与多根工作区 @@ -142,8 +144,9 @@ Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明, 状态和可安装版本。 - 安装 mcpp 支持的工具链 spec,或从已安装/系统项中选择全局默认工具链。 -安装和修改默认值都需要确认。取消操作不会被当成成功,工具链安装和默认值修改共享 -全局互斥锁。 +安装和修改默认值都需要确认。取消操作不会被当成成功,工具链安装、默认值修改和构建 +共享一个全局互斥锁;一键配置只在开始处确认一次,之后不会弹出工具链选择或“是否刷新 +编译数据库”的二次询问。 ## 命令 @@ -160,7 +163,7 @@ Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明, | **mcpp: 配置 clangd** | 手动重新应用当前 LLVM 工程的 clangd 配置 | | **mcpp: 刷新编译数据库** | 执行 `mcpp build`,随后重新读取 CDB 并协调 clangd | | **mcpp: 检查模块支持** | 立即执行 clangd 直接检查并刷新模块状态 | -| **mcpp: 一键配置模块代码提示** | 自动检测匹配 clangd,缺失时通过 xlings 安装 llvm-tools 并配置 | +| **mcpp: 一键配置模块代码提示** | 一次确认后自动安装/切换 host LLVM、执行 `mcpp build`,通过 xlings 补齐匹配 llvm-tools,重载 CDB 并配置 clangd;项目显式固定非 LLVM 工具链时停止 | ## 设置 diff --git a/src/extension.ts b/src/extension.ts index 802b186..199dbcf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -45,6 +45,13 @@ import { } from "./workflow"; import { classifyTaskExit, type TaskCompletion } from "./tasks"; import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./inProject"; +import { + buildModuleSetupPlan, + executeModuleSetup, + moduleSetupConfirmation, + type ModuleSetupBlockedReason, + type ModuleSetupStepResult, +} from "./moduleSetup"; const COMMAND_CONFIGURE = "mcpp.configureClangd"; const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; @@ -134,6 +141,19 @@ function loadProjectContext(project: McppProjectDiscovery | undefined = findCurr } } +function moduleSetupBlockedMessage(reason: ModuleSetupBlockedReason): string { + switch (reason) { + case "project-toolchain-override": + return "当前项目显式固定了非 LLVM 工具链;一键配置不会修改 mcpp.toml,请先手动切换项目工具链。"; + case "untrusted": + return "当前工作区未受信任,不会执行外部程序。请先信任工作区。"; + case "busy": + return "已有 mcpp 操作正在运行,请等待完成后再试。"; + case "unrecognized-inventory": + return "无法识别 mcpp 工具链状态,请查看 mcpp 输出频道并检查 mcpp 版本。"; + } +} + function configurationTarget(uri: vscode.Uri): vscode.ConfigurationTarget { return vscode.workspace.getWorkspaceFolder(uri) === undefined ? vscode.ConfigurationTarget.Workspace @@ -685,171 +705,124 @@ async function autoConfigureModulesWizard( ): Promise { appendOutputLine(output, "[一键配置] 开始一键配置模块代码提示..."); - // Step 1: Guard checks - if (context.analysis.capability === "unavailable") { - await vscode.window.showErrorMessage(context.analysis.reason); + if (!vscode.workspace.isTrusted) { + await vscode.window.showWarningMessage(moduleSetupBlockedMessage("untrusted")); return; } - if (context.analysis.capability === "syntax-only") { - const kind = context.analysis.kind.toUpperCase(); - const install = "安装 LLVM 工具链"; - const select = "选择默认工具链"; - const choice = await vscode.window.showErrorMessage( - `当前工具链是 ${kind},其模块产物不能被 clangd 消费,模块代码提示功能不可用。\n\n如需使用模块代码提示,请先切换到 LLVM 工具链后重新构建项目。`, - install, - select, - "关闭", - ); - if (choice === install) { - await vscode.commands.executeCommand(CLI_COMMANDS.installToolchain); - } else if (choice === select) { - await vscode.commands.executeCommand(CLI_COMMANDS.selectDefaultToolchain); - } + const inventory = await cliController.readToolchainInventory(context.project); + if (inventory === undefined) { return; } - if (context.analysis.compilerPath === undefined) { - await vscode.window.showErrorMessage("compile_commands.json 中没有可用的编译器路径。"); + const decision = buildModuleSetupPlan( + inventory, + context.analysis.capability, + vscode.workspace.isTrusted, + cliController.isBusy(), + ); + if (decision.kind === "blocked") { + await vscode.window.showWarningMessage(moduleSetupBlockedMessage(decision.reason)); return; } - if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { - await vscode.window.showWarningMessage( - "当前工作区未受信任,不会执行外部程序。请先信任工作区。", - ); + + const confirmation = moduleSetupConfirmation(context.analysis.capability, decision); + const choice = await vscode.window.showWarningMessage( + confirmation.message, + { modal: true, detail: confirmation.detail }, + "确认一键配置", + ); + if (choice !== "确认一键配置") { return; } - // Step 2: Check current clangd state - const compilerVersion = await runToolVersion(context.analysis.compilerPath); - appendOutputLine( - output, - `[一键配置] 编译器版本:LLVM ${llvmToolsVersionSpec(compilerVersion.identity ?? { major: 0, minor: 0, patch: 0 })}`, - ); + let currentContext = context; + const outcome = await executeModuleSetup(decision, { + preparePlan: () => cliController.runAutomaticModuleSetup(decision), + reload: async (): Promise => { + const refreshed = loadProjectContext(context.project); + if (refreshed === undefined) { + return { stage: "reload", state: "failed", detail: "无法重新加载 mcpp 工程。" }; + } + currentContext = refreshed; + updateStatusBar(status, currentContext); + if (currentContext.analysis.capability !== "full" || currentContext.analysis.kind !== "llvm") { + return { + stage: "reload", + state: "failed", + detail: "构建后没有得到可供 clangd 使用的 LLVM 编译数据库。", + }; + } + return { stage: "reload", state: "succeeded" }; + }, + ensureClangd: async (): Promise => { + const clangd = await resolveClangd(currentContext); + if (clangd?.comparison.compatible) { + const configured = await configureClangd(currentContext, status, output, "automatic", true); + return configured + ? { stage: "clangd", state: "succeeded" } + : { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" }; + } - const clangd = await resolveClangd(context); - if (clangd?.comparison.compatible) { - appendOutputLine(output, "[一键配置] 已找到兼容的 clangd,跳过安装步骤。"); - // Skip to configuration - } else { - if (clangd !== undefined) { + const xlingsPath = findXlingsExecutable(); + const compilerPath = currentContext.analysis.compilerPath; + if (xlingsPath === undefined || compilerPath === undefined) { + return { + stage: "clangd", + state: "failed", + detail: "未找到 xlings 或 LLVM 编译器,无法安装匹配的 llvm-tools。", + }; + } + const compilerVersion = await runToolVersion(compilerPath); + const installArgs = xlingsInstallArgs( + compilerVersion.identity === undefined ? undefined : llvmToolsVersionSpec(compilerVersion.identity), + ); + const installed = await executeXlingsInstallTask(xlingsPath, installArgs, currentContext.project.root); appendOutputLine( output, - `[一键配置] 未找到兼容的 clangd。原因:${clangd.comparison.reason}`, + `[一键配置] xlings install 完成(退出码 ${installed.exitCode ?? "未知"})`, ); - } else { - appendOutputLine(output, "[一键配置] 未找到任何可用的 clangd。"); - } - - // Step 3: Check xlings - const xlingsPath = findXlingsExecutable(); - if (xlingsPath === undefined) { - const installXlings = "安装 xlings"; - const setPath = "设置自定义 clangd 路径"; - const choice = await vscode.window.showErrorMessage( - "未找到 xlings,无法自动下载匹配的 clangd。请先安装 xlings(https://xlings.org),\n或手动安装与编译器版本匹配的 llvm-tools,或在设置中指定 clangd 路径。", - installXlings, - setPath, - "跳过", - ); - if (choice === installXlings) { - await vscode.env.openExternal( - vscode.Uri.parse("https://xlings.org"), - ); - } else if (choice === setPath) { - await vscode.commands.executeCommand( - "workbench.action.openSettings", - "mcpp.clangd.path", - ); + if (installed.state !== "succeeded") { + return { + stage: "clangd", + state: installed.state, + exitCode: installed.exitCode, + detail: installed.state === "cancelled" ? "llvm-tools 安装已取消。" : "llvm-tools 安装失败。", + }; } - return; - } - // Step 4: Offer installation - const version = compilerVersion.identity !== undefined - ? llvmToolsVersionSpec(compilerVersion.identity) - : "latest"; - const install = "$(cloud-download) 安装 llvm-tools(推荐)"; - const setPath = "$(settings-gear) 设置自定义 clangd 路径"; - const choice = await vscode.window.showWarningMessage( - `未找到与 mcpp 编译器匹配的 clangd(LLVM ${version})。是否通过 xlings 安装 llvm-tools?`, - { modal: true, detail: "xlings 将自动下载并安装匹配版本的 clangd、clang-format 和 clang-tidy。" }, - install, - setPath, - "跳过", - ); - if (choice === setPath) { - await vscode.commands.executeCommand( - "workbench.action.openSettings", - "mcpp.clangd.path", - ); - return; - } - if (choice !== install) { - return; - } - - // Check concurrency - if (cliController.isBusy()) { - await vscode.window.showWarningMessage( - "已有 mcpp 操作正在运行,请等待完成后再试。", - ); - return; - } - - // Note: xlings install is not tracked by McppOperationRegistry because it - // operates on xlings-managed paths (not mcpp). The serial executor - // (executeWithWorkspaceClangd) prevents concurrent wizard runs, and the - // isBusy() check catches active mcpp operations before install starts. + const refreshed = loadProjectContext(currentContext.project); + if (refreshed === undefined) { + return { stage: "clangd", state: "failed", detail: "安装 llvm-tools 后无法重新加载工程。" }; + } + currentContext = refreshed; + updateStatusBar(status, currentContext); + const resolved = await resolveClangd(currentContext); + if (resolved === undefined || !resolved.comparison.compatible) { + return { stage: "clangd", state: "failed", detail: "未找到与 LLVM 编译器匹配的 clangd。" }; + } + const configured = await configureClangd(currentContext, status, output, "automatic", true); + return configured + ? { stage: "clangd", state: "succeeded" } + : { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" }; + }, + checkModules: async (): Promise => { + const moduleStatus = await runModuleSupportCheck(currentContext, status, output, "automatic"); + return moduleStatus?.state === "available" + ? { stage: "check", state: "succeeded" } + : { stage: "check", state: "failed", detail: "模块支持检查未通过。" }; + }, + }); - // Execute xlings install - appendOutputLine(output, `[一键配置] 正在通过 xlings 安装 llvm-tools(${xlingsPath})...`); - const installArgs = xlingsInstallArgs( - compilerVersion.identity !== undefined ? version : undefined, - ); - const xlingsResult = await executeXlingsInstallTask( - xlingsPath, - installArgs, - context.project.root, - ); - appendOutputLine( - output, - `[一键配置] xlings install 完成(退出码 ${xlingsResult.exitCode ?? "未知"})`, - ); - if (xlingsResult.state !== "succeeded") { - await vscode.window.showErrorMessage( - `llvm-tools 安装${xlingsResult.state === "cancelled" ? "已取消" : "失败"}。请查看任务终端获取详细信息。`, - ); - return; + if (outcome.state === "succeeded") { + if (outcome.degraded) { + await vscode.window.showWarningMessage("构建失败,语言服务已刷新。请查看任务终端获取构建错误。"); + } else { + await vscode.window.showInformationMessage("mcpp 模块代码提示一键配置完成。"); } + } else if (outcome.state === "cancelled") { + await vscode.window.showWarningMessage(`一键配置已取消(${outcome.stage})。`); + } else { + await vscode.window.showErrorMessage(`一键配置失败(${outcome.stage})。${outcome.steps.at(-1)?.detail ?? "请查看 mcpp 输出频道。"}`); } - - // Step 5: Re-resolve clangd - const resolvedClangd = await resolveClangd(context); - if (resolvedClangd === undefined || !resolvedClangd.comparison.compatible) { - await vscode.window.showErrorMessage( - "llvm-tools 安装完成,但未找到匹配的 clangd。请检查 xlings 安装日志或手动设置 mcpp.clangd.path。", - ); - return; - } - appendOutputLine(output, `[一键配置] 已找到 clangd:${resolvedClangd.path}`); - - // Step 6: Configure clangd - await configureClangd(context, status, output, "interactive", true); - - // Step 7: Offer rebuild - const rebuild = "$(sync) 刷新编译数据库"; - const done = "$(close) 完成"; - const postChoice = await vscode.window.showInformationMessage( - "clangd 配置完成。建议刷新编译数据库以生成模块 PCM。", - rebuild, - done, - ); - if (postChoice === rebuild) { - await cliController.runProjectTask("build"); - } - - // Step 8: Refresh module status and update status bar - await runModuleSupportCheck(context, status, output, "automatic"); - appendOutputLine(output, `[一键配置] 一键配置完成。clangd:${resolvedClangd.path}`); } export async function activate(extensionContext: vscode.ExtensionContext): Promise { diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts index 4598101..c689a1b 100644 --- a/src/moduleSetup.ts +++ b/src/moduleSetup.ts @@ -113,7 +113,8 @@ export interface ModuleSetupStepResult { } export interface ModuleSetupOperations { - prepare(command: ModuleSetupCommand): Promise; + prepare?: (command: ModuleSetupCommand) => Promise; + preparePlan?: () => Promise; reload(): Promise; ensureClangd(): Promise; checkModules(): Promise; @@ -151,17 +152,37 @@ export async function executeModuleSetup( const steps: ModuleSetupStepResult[] = []; let buildFailed = false; - for (const command of mcppModuleSetupCommands(plan)) { - const result = await operations.prepare(command); + const commands = mcppModuleSetupCommands(plan); + if (operations.preparePlan !== undefined) { + const result = await operations.preparePlan(); steps.push(result); - if (result.state === "succeeded") { - continue; + if (result.state !== "succeeded") { + if (result.stage === "build" && result.state === "failed") { + buildFailed = true; + } else { + return stoppedOutcome(result, steps); + } + } + } else { + if (operations.prepare === undefined) { + const firstCommand = commands[0]; + if (firstCommand === undefined) { + return stoppedOutcome({ stage: "build", state: "failed" }, steps); + } + return stoppedOutcome({ stage: firstCommand.stage, state: "failed" }, steps); } - if (command.stage === "build" && result.state === "failed") { - buildFailed = true; - break; + for (const command of commands) { + const result = await operations.prepare(command); + steps.push(result); + if (result.state === "succeeded") { + continue; + } + if (command.stage === "build" && result.state === "failed") { + buildFailed = true; + break; + } + return stoppedOutcome(result, steps); } - return stoppedOutcome(result, steps); } // 构建失败后仍尝试恢复现有 CDB;只有完整恢复链成功才算降级成功。 diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index a80e886..c439cbde 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -76,6 +76,30 @@ test("自动模块配置使用非交互复合操作和单一全局锁", () => { assert.doesNotMatch(method, /pickInstallSpec|selectDefaultToolchainFromInventory|showQuickPick|showWarningMessage/); }); +test("一键向导只有一次确认并在构建后重载上下文", () => { + const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); + const start = source.indexOf("async function autoConfigureModulesWizard"); + const end = source.indexOf("export async function activate", start); + assert.notEqual(start, -1); + assert.notEqual(end, -1); + const wizard = source.slice(start, end); + for (const expected of [ + "readToolchainInventory", + "buildModuleSetupPlan", + "moduleSetupConfirmation", + "runAutomaticModuleSetup", + "executeModuleSetup", + "loadProjectContext", + ]) { + assert.match(wizard, new RegExp(expected)); + } + assert.match(wizard, /modal:\s*true/); + assert.match(wizard, /let currentContext/); + assert.doesNotMatch(wizard, /CLI_COMMANDS\.(installToolchain|selectDefaultToolchain)/); + assert.doesNotMatch(wizard, /刷新编译数据库|showQuickPick|showInputBox|maybeDisableCppTools/); + assert.doesNotMatch(wizard, /configureClangd\([^\n]*"interactive"/); +}); + test("shows editor title buttons only inside mcpp projects", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; assert.deepEqual(manifest.contributes?.menus?.["editor/title"], [ diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts index 6b7630d..3597059 100644 --- a/test/moduleSetup.test.ts +++ b/test/moduleSetup.test.ts @@ -311,6 +311,25 @@ test("成功流程严格按安装、默认、构建、重载、clangd、检查 ); }); +test("批量准备操作只执行一次并继续恢复链", async () => { + const fixture = operationsWith(); + let preparePlanCalls = 0; + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: true, switchDefault: true }, + { + ...fixture.operations, + preparePlan: async () => { + preparePlanCalls += 1; + return result("build", "succeeded"); + }, + }, + ); + + assert.equal(preparePlanCalls, 1); + assert.deepEqual(fixture.calls, ["reload", "clangd", "check"]); + assert.equal(outcome.state, "succeeded"); +}); + test("安装或默认步骤取消、失败后立即停止", async () => { const cases: Array<{ stage: "install" | "default"; From 2cb6a5e3cb407da19a7c9fe29bbed07113372b18 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 16:59:01 +0800 Subject: [PATCH 037/119] test: add extension host smoke coverage --- package-lock.json | 1269 +++++++++++++++++++- package.json | 7 +- test/e2e/fixtures/clangd-stub/extension.js | 9 + test/e2e/fixtures/clangd-stub/package.json | 9 + test/e2e/fixtures/fake-mcpp.js | 9 + test/e2e/fixtures/project/main.cpp | 1 + test/e2e/fixtures/project/mcpp.toml | 3 + test/e2e/runTest.ts | 56 + test/e2e/suite/extension.test.ts | 34 + test/e2e/suite/index.ts | 16 + 10 files changed, 1399 insertions(+), 14 deletions(-) create mode 100644 test/e2e/fixtures/clangd-stub/extension.js create mode 100644 test/e2e/fixtures/clangd-stub/package.json create mode 100644 test/e2e/fixtures/fake-mcpp.js create mode 100644 test/e2e/fixtures/project/main.cpp create mode 100644 test/e2e/fixtures/project/mcpp.toml create mode 100644 test/e2e/runTest.ts create mode 100644 test/e2e/suite/extension.test.ts create mode 100644 test/e2e/suite/index.ts diff --git a/package-lock.json b/package-lock.json index 7cd3fcc..ae908dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "0.2.5", "license": "Apache-2.0", "devDependencies": { + "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", "@types/vscode": "^1.90.0", + "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "mocha": "^11.8.0", "typescript": "^5.9.0" }, "engines": { @@ -228,6 +231,80 @@ "node": ">=6.9.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -266,6 +343,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -537,6 +625,13 @@ "@textlint/ast-node-types": "15.7.1" } }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -583,6 +678,23 @@ "node": ">=22.0.0" } }, + "node_modules/@vscode/test-electron": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", + "integrity": "sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@vscode/vsce": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", @@ -983,6 +1095,13 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1073,6 +1192,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1134,6 +1266,22 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -1142,6 +1290,73 @@ "license": "ISC", "optional": true }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cockatiel": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", @@ -1195,6 +1410,28 @@ "node": ">=18" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -1243,6 +1480,19 @@ } } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1335,6 +1585,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -1409,6 +1669,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1543,6 +1810,29 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1618,6 +1908,50 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -1668,6 +2002,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1839,6 +2196,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -1958,6 +2325,13 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/index-to-position": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", @@ -1976,8 +2350,7 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", @@ -2055,6 +2428,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2065,22 +2451,69 @@ "node": ">=0.12.0" } }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, "engines": { - "node": ">=16" - }, + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/istextorbinary": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", @@ -2099,6 +2532,22 @@ "url": "https://bevry.me/fund" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2192,6 +2641,52 @@ "npm": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2238,6 +2733,16 @@ "node": ">=6" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/linkify-it": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", @@ -2258,6 +2763,22 @@ "uc.micro": "^2.0.0" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -2321,6 +2842,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2439,6 +2977,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -2498,6 +3049,151 @@ "license": "MIT", "optional": true }, + "node_modules/mocha": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", + "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2628,6 +3324,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -2647,6 +3359,143 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", @@ -2660,6 +3509,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parse-json": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", @@ -2751,6 +3614,26 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -2857,6 +3740,13 @@ "node": ">=10" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -2917,6 +3807,16 @@ ], "license": "MIT" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3009,6 +3909,30 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3019,6 +3943,23 @@ "node": ">=0.10.0" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -3140,6 +4081,46 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -3216,6 +4197,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3332,6 +4326,19 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3358,6 +4365,45 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3397,6 +4443,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -3719,8 +4789,7 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -3770,6 +4839,112 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3818,6 +4993,16 @@ "node": ">=4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -3825,6 +5010,51 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/yauzl": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", @@ -3847,6 +5077,19 @@ "dependencies": { "buffer-crc32": "~0.2.3" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 071823f..4ccbd35 100644 --- a/package.json +++ b/package.json @@ -210,12 +210,17 @@ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", "compile": "tsc -p tsconfig.json", "test": "npm run clean && npm run compile && node --test dist/test/*.test.js", - "package": "npm run clean && npm run compile && vsce package" + "package": "npm run clean && npm run compile && vsce package", + "test:e2e": "npm run compile && node dist/test/e2e/runTest.js", + "test:all": "npm test && npm run test:e2e" }, "devDependencies": { + "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", "@types/vscode": "^1.90.0", + "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "mocha": "^11.8.0", "typescript": "^5.9.0" } } diff --git a/test/e2e/fixtures/clangd-stub/extension.js b/test/e2e/fixtures/clangd-stub/extension.js new file mode 100644 index 0000000..97cc7ef --- /dev/null +++ b/test/e2e/fixtures/clangd-stub/extension.js @@ -0,0 +1,9 @@ +const vscode = require("vscode"); + +function activate(context) { + context.subscriptions.push( + vscode.commands.registerCommand("clangd.restart", () => undefined), + ); +} + +module.exports = { activate }; diff --git a/test/e2e/fixtures/clangd-stub/package.json b/test/e2e/fixtures/clangd-stub/package.json new file mode 100644 index 0000000..19a5588 --- /dev/null +++ b/test/e2e/fixtures/clangd-stub/package.json @@ -0,0 +1,9 @@ +{ + "name": "vscode-clangd", + "displayName": "clangd stub", + "version": "0.0.0", + "publisher": "llvm-vs-code-extensions", + "engines": { "vscode": "^1.90.0" }, + "main": "./extension.js", + "activationEvents": ["onCommand:clangd.restart"] +} diff --git a/test/e2e/fixtures/fake-mcpp.js b/test/e2e/fixtures/fake-mcpp.js new file mode 100644 index 0000000..76daadf --- /dev/null +++ b/test/e2e/fixtures/fake-mcpp.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +const fs = require("node:fs"); + +const logPath = process.env.MCPP_E2E_LOG; +if (typeof logPath === "string") { + fs.appendFileSync(logPath, `${process.argv.slice(2).join(" ")}\n`); +} +process.exit(process.argv.slice(2).join(" ") === "build" ? 0 : 1); diff --git a/test/e2e/fixtures/project/main.cpp b/test/e2e/fixtures/project/main.cpp new file mode 100644 index 0000000..76e8197 --- /dev/null +++ b/test/e2e/fixtures/project/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/e2e/fixtures/project/mcpp.toml b/test/e2e/fixtures/project/mcpp.toml new file mode 100644 index 0000000..055092f --- /dev/null +++ b/test/e2e/fixtures/project/mcpp.toml @@ -0,0 +1,3 @@ +[package] +name = "mcpp-vscode-e2e" +version = "0.0.0" diff --git a/test/e2e/runTest.ts b/test/e2e/runTest.ts new file mode 100644 index 0000000..01b6211 --- /dev/null +++ b/test/e2e/runTest.ts @@ -0,0 +1,56 @@ +import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +import { runTests } from "@vscode/test-electron"; + +async function main(): Promise { + const repositoryRoot = resolve(__dirname, "../../.."); + const fixtureRoot = join(repositoryRoot, "test/e2e/fixtures"); + const tempRoot = mkdtempSync(join(tmpdir(), "mcpp-vscode-e2e-")); + const userDataDir = join(tempRoot, "user-data"); + const extensionsDir = join(tempRoot, "extensions"); + const workspaceDir = join(tempRoot, "project"); + const fakeMcpp = join(tempRoot, "mcpp"); + const logPath = join(tempRoot, "mcpp.log"); + const clangdStub = join(extensionsDir, "llvm-vs-code-extensions.vscode-clangd-0.0.0"); + + mkdirSync(userDataDir, { recursive: true }); + mkdirSync(extensionsDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + mkdirSync(clangdStub, { recursive: true }); + copyFileSync(join(fixtureRoot, "fake-mcpp.js"), fakeMcpp); + chmodSync(fakeMcpp, 0o755); + copyFileSync(join(fixtureRoot, "clangd-stub/package.json"), join(clangdStub, "package.json")); + copyFileSync(join(fixtureRoot, "clangd-stub/extension.js"), join(clangdStub, "extension.js")); + copyFileSync(join(fixtureRoot, "project/mcpp.toml"), join(workspaceDir, "mcpp.toml")); + copyFileSync(join(fixtureRoot, "project/main.cpp"), join(workspaceDir, "main.cpp")); + + try { + await runTests({ + extensionDevelopmentPath: repositoryRoot, + extensionTestsPath: join(repositoryRoot, "dist/test/e2e/suite/index.js"), + launchArgs: [ + workspaceDir, + "--user-data-dir", + userDataDir, + "--extensions-dir", + extensionsDir, + "--disable-updates", + "--skip-welcome", + "--disable-workspace-trust", + ], + extensionTestsEnv: { + MCPP_E2E_FAKE_MCPP: fakeMcpp, + MCPP_E2E_LOG: logPath, + }, + }); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } +} + +void main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/test/e2e/suite/extension.test.ts b/test/e2e/suite/extension.test.ts new file mode 100644 index 0000000..4859237 --- /dev/null +++ b/test/e2e/suite/extension.test.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import * as path from "node:path"; +import * as vscode from "vscode"; + +suite("mcpp extension smoke", () => { + test("activates, registers commands and executes a configured build", async () => { + const extension = vscode.extensions.getExtension("mcpp-community.mcpp-vscode"); + assert.ok(extension, "mcpp extension should be installed in development host"); + await extension.activate(); + + const commands = await vscode.commands.getCommands(true); + assert.ok(commands.includes("mcpp.build")); + assert.ok(commands.includes("mcpp.autoConfigureModules")); + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, "fixture workspace should be open"); + const fakeMcpp = process.env.MCPP_E2E_FAKE_MCPP; + const logPath = process.env.MCPP_E2E_LOG; + assert.ok(fakeMcpp); + assert.ok(logPath); + await vscode.workspace.getConfiguration("mcpp", workspaceFolder.uri) + .update("path", fakeMcpp, vscode.ConfigurationTarget.Workspace); + + void vscode.commands.executeCommand("mcpp.build"); + const deadline = Date.now() + 15_000; + while (!existsSync(logPath) && Date.now() < deadline) { + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); + } + assert.ok(existsSync(logPath), "fake mcpp should have been invoked"); + assert.equal(readFileSync(logPath, "utf8").trim(), "build"); + assert.equal(path.basename(workspaceFolder.uri.fsPath), "project"); + }); +}); diff --git a/test/e2e/suite/index.ts b/test/e2e/suite/index.ts new file mode 100644 index 0000000..7181a3d --- /dev/null +++ b/test/e2e/suite/index.ts @@ -0,0 +1,16 @@ +import Mocha from "mocha"; +import path from "node:path"; + +export function run(): Promise { + const mocha = new Mocha({ ui: "tdd", color: true, timeout: 30_000 }); + mocha.addFile(path.resolve(__dirname, "extension.test.js")); + return new Promise((resolvePromise, reject) => { + mocha.run((failures) => { + if (failures === 0) { + resolvePromise(); + } else { + reject(new Error(`${failures} Extension Host smoke test(s) failed.`)); + } + }); + }); +} From f233584c3611e930312cb48f7a608bbe87407fa7 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 17:00:15 +0800 Subject: [PATCH 038/119] ci: add pull request extension checks --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++++++ test/artifacts.test.ts | 16 ++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..330891c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit-and-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm test + - run: npm run package + - name: Verify VSIX archive + run: unzip -t mcpp-vscode-*.vsix + + extension-host-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Run Extension Host smoke tests + run: xvfb-run -a npm run test:e2e diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index c439cbde..4ab77e4 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -334,3 +334,19 @@ test("tag release 工作流校验版本并发布 VSIX", () => { assert.match(workflow, /gh release create/); assert.match(workflow, /gh release upload.*--clobber/s); }); + +test("PR CI 分离单元打包和 Extension Host E2E", () => { + const workflow = readFileSync(path.join(root, ".github/workflows/ci.yml"), "utf8"); + assert.match(workflow, /pull_request:/); + assert.match(workflow, /push:\s*\n\s+branches:\s*\n\s+- main/); + assert.match(workflow, /permissions:\s*\n\s+contents: read/); + assert.match(workflow, /concurrency:[\s\S]*cancel-in-progress: true/); + assert.match(workflow, /node-version: 22/); + assert.match(workflow, /unit-and-package:/); + assert.match(workflow, /extension-host-e2e:/); + assert.match(workflow, /npm ci/); + assert.match(workflow, /npm test/); + assert.match(workflow, /npm run package/); + assert.match(workflow, /unzip -t/); + assert.match(workflow, /xvfb-run -a npm run test:e2e/); +}); From de7d250ae2b00451c76c32674a239f5f236f06a3 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 17:46:26 +0800 Subject: [PATCH 039/119] fix: force LLVM module database refresh --- src/moduleSetup.ts | 4 ++-- test/e2e/runTest.ts | 1 + test/moduleSetup.test.ts | 16 +++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts index c689a1b..14d962a 100644 --- a/src/moduleSetup.ts +++ b/src/moduleSetup.ts @@ -49,7 +49,7 @@ export function buildModuleSetupPlan( return { kind: "ready", installLlvm: !installedHostLlvm, - switchDefault: inventory.effective !== undefined && !effectiveIsLlvm, + switchDefault: !effectiveIsLlvm, }; } @@ -75,7 +75,7 @@ export function mcppModuleSetupCommands(plan: ModuleSetupPlan): ModuleSetupComma args: ["toolchain", "default", "llvm"], }); } - commands.push({ stage: "build", mode: "task", args: ["build"] }); + commands.push({ stage: "build", mode: "task", args: ["build", "--no-cache"] }); return commands; } diff --git a/test/e2e/runTest.ts b/test/e2e/runTest.ts index 01b6211..e7d5699 100644 --- a/test/e2e/runTest.ts +++ b/test/e2e/runTest.ts @@ -28,6 +28,7 @@ async function main(): Promise { try { await runTests({ + version: "1.90.2", extensionDevelopmentPath: repositoryRoot, extensionTestsPath: join(repositoryRoot, "dist/test/e2e/suite/index.js"), launchArgs: [ diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts index 3597059..c6fad77 100644 --- a/test/moduleSetup.test.ts +++ b/test/moduleSetup.test.ts @@ -97,7 +97,7 @@ test("LLVM 已生效时不重复安装或切换", () => { }); }); -test("没有 effective 工具链时不推断需要切换全局默认", () => { +test("没有 effective 工具链时安装 LLVM 后设置全局默认", () => { const plan = buildModuleSetupPlan( inventory(), "unavailable", @@ -108,7 +108,7 @@ test("没有 effective 工具链时不推断需要切换全局默认", () => { assert.deepEqual(plan, { kind: "ready", installLlvm: true, - switchDefault: false, + switchDefault: true, }); }); @@ -216,12 +216,12 @@ test("模块设置命令使用固定 argv 数组且始终构建", () => { [ { stage: "install", mode: "task", args: ["toolchain", "install", "llvm"] }, { stage: "default", mode: "process", args: ["toolchain", "default", "llvm"] }, - { stage: "build", mode: "task", args: ["build"] }, + { stage: "build", mode: "task", args: ["build", "--no-cache"] }, ], ); assert.deepEqual( mcppModuleSetupCommands({ kind: "ready", installLlvm: false, switchDefault: false }), - [{ stage: "build", mode: "task", args: ["build"] }], + [{ stage: "build", mode: "task", args: ["build", "--no-cache"] }], ); }); @@ -255,7 +255,13 @@ test("确认文案说明将复用已安装的 LLVM", () => { }); test("确认文案在无需切换时明确不修改全局默认值", () => { - const plan = ready(buildModuleSetupPlan(inventory(), "unavailable", true, false)); + const llvm = toolchain("llvm", true); + const plan = ready(buildModuleSetupPlan( + inventory({ installed: [llvm], effective: llvm }), + "full", + true, + false, + )); const confirmation = moduleSetupConfirmation( "unavailable", plan, From d5b953a525d55ff42c83c98075890e99b9a3e40f Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 23:29:52 +0800 Subject: [PATCH 040/119] =?UTF-8?q?fix:=20PR=20#9=20review=20=E6=89=93?= =?UTF-8?q?=E7=A3=A8=E2=80=94=E2=80=94=E8=A1=A5=20[build-dependencies]=20?= =?UTF-8?q?=E6=AE=B5=E5=A4=B4=E3=80=81[[...]]=20=E6=95=B0=E7=BB=84?= =?UTF-8?q?=E8=A1=A8=E4=B8=8D=E5=87=BA=E5=BB=BA=E8=AE=AE=E3=80=81README=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SECTION_HEADERS 增加 [build-dependencies](mcpp 真实依赖表,契约测试已覆盖) - [[...]] 数组表段头不提供建议,避免把数组表意图悄悄替换成普通段 [x] - 补行为测试(数组表 / build-dependencies 模板)与段头注册表关键组断言 - README:能力矩阵、已实现功能、设置三处补充结构补全说明 --- README.md | 18 +++++++++++++++++- src/mcppTomlCompletion.ts | 6 ++++++ test/mcppTomlCompletion.test.ts | 14 ++++++++++++++ test/mcppTomlContract.test.ts | 2 +- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 326d8a7..d2884f1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 | --- | --- | --- | --- | | 识别 mcpp 工程 | 支持 | 支持 | 支持 | | `mcpp.toml` TOML 语法高亮 | 支持 | 支持 | 支持 | +| `mcpp.toml` 结构补全(段头 + 写法模板) | 支持 | 支持 | 支持 | | `build.mcpp`、`.cppm`、`.ixx`、`.mpp`、`.ccm` 文件关联 | 支持 | 支持 | 支持 | | `module`、`export module`、`import` 语法高亮 | 支持 | 支持 | 支持 | | 构建、运行、测试、清理命令 | 支持 | 支持 | 支持 | @@ -97,6 +98,19 @@ TextMate 语法规则提供。 `import mcpp;` 不会再被误报为缺少 C++ 模块。真正的 mcpp API 补全需要 mcpp 核心 未来生成宿主 helper 的 CDB 和 PCM 映射。 +### mcpp.toml 结构补全 + +- 段头补全:26 个段(`[package]`、`[targets.]`、`[dependencies]`、`[build-dependencies]`、 + `[features]`、`[indices]`、`[pack]` 等),参数化段带可跳转占位符。 +- 写法模板:依赖段的依赖写法(版本 / 路径 / git / features / tools)、`[features]` 表形式、 + capabilities / xlings / tools.overrides / generated_files 等开放段的条目形态。 +- 每条建议携带显式替换范围,部分输入(`[dep`、`na`)不会残留无效文本;所有语义规则有真实 + mcpp 契约测试(44 例,无 mcpp 环境自动跳过)。 +- 范围边界:不提供静态字段键/枚举(等上游版本化 manifest schema),不提供依赖包名/版本候选 + (等上游批量 catalog 接口);未知自定义段与 `[[...]]` 数组表不提供建议。 +- 由 `mcpp.tomlCompletion` 设置控制(默认开启);未受信任工作区仅做纯文本分析,不执行任何 + 外部程序。 + ### LLVM 与 clangd 集成 扩展读取 mcpp 生成的 `compile_commands.json`,然后: @@ -172,7 +186,8 @@ xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB "mcpp.path": "/path/to/mcpp", "mcpp.clangd.path": "/path/to/matching/clangd", "mcpp.modulesSupport": "auto", - "mcpp.configureCppTools": true + "mcpp.configureCppTools": true, + "mcpp.tomlCompletion": true } ``` @@ -182,6 +197,7 @@ xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB | `mcpp.clangd.path` | 空 | 与 CDB 中 LLVM 编译器匹配的 clangd;空值表示自动发现 | | `mcpp.modulesSupport` | `auto` | `auto`、`on` 或 `off`,控制 clangd 实验模块参数 | | `mcpp.configureCppTools` | `true` | 手动配置 clangd 时,是否询问关闭当前工作区的 cpptools IntelliSense | +| `mcpp.tomlCompletion` | `true` | 为 `mcpp.toml` 提供结构补全:段头与写法模板(snippet);所有建议带显式替换范围,并经真实 mcpp 契约测试验证 | `mcpp.path` 只影响插件执行 mcpp CLI 命令。工程实际编译器来自 `compile_commands.json`,`mcpp.clangd.path` 只指定语言服务器,三者相互独立。 diff --git a/src/mcppTomlCompletion.ts b/src/mcppTomlCompletion.ts index 8694846..3cc0e16 100644 --- a/src/mcppTomlCompletion.ts +++ b/src/mcppTomlCompletion.ts @@ -43,6 +43,7 @@ export const SECTION_HEADERS: readonly SectionHeaderSpec[] = [ { group: "generated_files", label: "[generated_files]", header: "[generated_files]", detail: "生成文件(路径 → 内容)" }, { group: "dependencies", label: "[dependencies]", header: "[dependencies]", detail: "运行时依赖" }, { group: "dev-dependencies", label: "[dev-dependencies]", header: "[dev-dependencies]", detail: "开发/测试依赖" }, + { group: "build-dependencies", label: "[build-dependencies]", header: "[build-dependencies]", detail: "构建期依赖(仅构建期拉取,运行时不可见)" }, { group: "workspace", label: "[workspace]", header: "[workspace]", detail: "工作空间成员声明" }, { group: "workspace.dependencies", label: "[workspace.dependencies]", header: "[workspace.dependencies]", detail: "集中声明依赖版本,成员用 workspace = true 继承" }, { group: "features", label: "[features]", header: "[features]", detail: "feature 定义" }, @@ -189,6 +190,11 @@ export function computeMcppTomlCompletions( const context = contextAt(lines, line, character); if (context.kind === "section-header") { + // mcpp manifest 不使用 TOML 数组表([[...]]);[[ 内不提供建议, + // 避免把用户意图的数组表悄悄替换成普通段 [x](未知段会被 mcpp 静默忽略)。 + if (context.isArray) { + return []; + } // parser 的替换范围从段名 token 开始;段头建议插入的是完整 "[xxx]", // 需要把范围扩展到本行的 "[",避免留下 "[["。仅当 "[" 是行内首个 // 非空白字符时才扩展(section-header 上下文正常都满足,防御奇怪输入)。 diff --git a/test/mcppTomlCompletion.test.ts b/test/mcppTomlCompletion.test.ts index e8924eb..33983d7 100644 --- a/test/mcppTomlCompletion.test.ts +++ b/test/mcppTomlCompletion.test.ts @@ -15,6 +15,7 @@ test("suggests section headers on a partial bracket line", () => { assert.ok(suggestions.length > 0); assert.ok(suggestions.every((suggestion) => suggestion.kind === "section")); assert.ok(labels(suggestions).includes("[dependencies]")); + assert.ok(labels(suggestions).includes("[build-dependencies]")); assert.ok(labels(suggestions).includes("[workspace]")); assert.ok(labels(suggestions).includes("[indices]")); // 每条建议都带显式替换范围(覆盖已输入的 "[dep")。 @@ -26,6 +27,13 @@ test("suggests section headers on a partial bracket line", () => { assert.equal(targets?.insertSnippet, "[targets.${1:name}]"); }); +test("offers nothing inside [[...]] array-table headers", () => { + // mcpp manifest 不使用 TOML 数组表([[...]]):[[ 内不出建议, + // 避免把用户意图的数组表悄悄替换成普通段 [x]。 + assert.deepEqual(computeMcppTomlCompletions(["[[dep"], 0, 5), []); + assert.deepEqual(computeMcppTomlCompletions(["[[dependencies]"], 0, 3), []); +}); + test("suggests section headers at the top of the document", () => { const suggestions = computeMcppTomlCompletions([""], 0, 0); assert.ok(suggestions.length > 0); @@ -61,6 +69,12 @@ test("suggests dependency templates in conditional dependency sections", () => { assert.ok(labels(suggestions).includes('name = "version"')); }); +test("suggests dependency templates in build-dependencies", () => { + const suggestions = computeMcppTomlCompletions(["[build-dependencies]", ""], 1, 0); + assert.ok(labels(suggestions).includes('name = "version"')); + assert.ok(suggestions.every((suggestion) => suggestion.kind === "template")); +}); + test("suggests templates in free-key sections", () => { const features = computeMcppTomlCompletions(["[features]", ""], 1, 0); assert.ok(features.every((suggestion) => suggestion.kind === "template")); diff --git a/test/mcppTomlContract.test.ts b/test/mcppTomlContract.test.ts index dc1c4e3..a9506fb 100644 --- a/test/mcppTomlContract.test.ts +++ b/test/mcppTomlContract.test.ts @@ -121,7 +121,7 @@ function fixtureFor(entry: SectionHeaderSpec): { manifest: string; files: Record test("段头注册表形态:数量与关键段齐全(防止测试与实现脱节)", { skip: skipReason }, () => { assert.ok(SECTION_HEADERS.length >= 20, `段头注册表只有 ${SECTION_HEADERS.length} 条(期望 ≥ 20)`); const groups = new Set(SECTION_HEADERS.map((entry) => entry.group)); - for (const key of ["package", "dependencies", "workspace", "pack"]) { + for (const key of ["package", "dependencies", "build-dependencies", "workspace", "pack"]) { assert.ok(groups.has(key), `段头注册表缺少关键段 ${key}`); } const labels = SECTION_HEADERS.map((entry) => entry.label); From 2316fe79c542dea0e859ec7890994d19514cf4d4 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 23:37:11 +0800 Subject: [PATCH 041/119] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=200.2.5=20?= =?UTF-8?q?=E5=8F=98=E6=9B=B4=E6=97=A5=E5=BF=97=EF=BC=88=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=90=91=E5=AF=BC=20+=20mcpp.toml=20=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a51eb06..0b64012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 0.2.5 +- 新增 **mcpp: 一键配置模块代码提示** 向导:按「安装/切换工具链 → 构建 → 重载 → clangd + 配置 → 模块检查」链恢复 IDE,支持取消与失败降级;仅在受信任工作区执行(issues #6/#7)。 +- 新增 `mcpp.toml` 结构补全:段头 snippet(26 段)与依赖/feature 等写法模板,所有建议 + 携带显式替换范围并经真实 mcpp 契约测试;由 `mcpp.tomlCompletion` 设置控制(默认开启), + 未受信任工作区仅纯文本分析(PR #9)。 - 编辑器标题栏的 mcpp 运行/测试操作按当前活动文件所属的 mcpp member 作用域执行;工作区外 打开的文件不会显示这些按钮。 - 将状态栏快捷菜单名称明确为 `$(tools) mcpp: 快捷菜单`,与模块可用性状态按钮区分。 From 4367b956c2a5aee0e7d3d576d842cea187d88a28 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 8 Aug 2026 23:42:27 +0800 Subject: [PATCH 042/119] =?UTF-8?q?release:=20mcpp-vscode=200.2.6=EF=BC=88?= =?UTF-8?q?mcpp.toml=20=E7=BB=93=E6=9E=84=E8=A1=A5=E5=85=A8=20+=20?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E6=A8=A1=E5=9D=97=E9=85=8D=E7=BD=AE=E5=90=91?= =?UTF-8?q?=E5=AF=BC=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++-- package-lock.json | 4 ++-- package.json | 2 +- test/artifacts.test.ts | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b64012..0e4177e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,16 @@ # 更新日志 -## 0.2.5 +## 0.2.6 - 新增 **mcpp: 一键配置模块代码提示** 向导:按「安装/切换工具链 → 构建 → 重载 → clangd - 配置 → 模块检查」链恢复 IDE,支持取消与失败降级;仅在受信任工作区执行(issues #6/#7)。 + 配置 → 模块检查」链恢复 IDE,支持取消与失败降级;仅在受信任工作区执行(issues #6/#7, + PR #10)。 - 新增 `mcpp.toml` 结构补全:段头 snippet(26 段)与依赖/feature 等写法模板,所有建议 携带显式替换范围并经真实 mcpp 契约测试;由 `mcpp.tomlCompletion` 设置控制(默认开启), 未受信任工作区仅纯文本分析(PR #9)。 + +## 0.2.5 + - 编辑器标题栏的 mcpp 运行/测试操作按当前活动文件所属的 mcpp member 作用域执行;工作区外 打开的文件不会显示这些按钮。 - 将状态栏快捷菜单名称明确为 `$(tools) mcpp: 快捷菜单`,与模块可用性状态按钮区分。 diff --git a/package-lock.json b/package-lock.json index ae908dd..6488a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.2.5", + "version": "0.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.2.5", + "version": "0.2.6", "license": "Apache-2.0", "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/package.json b/package.json index ff193c0..fe479dc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.5", + "version": "0.2.6", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index ebcf44b..2add919 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -26,7 +26,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.5"); + assert.equal(manifest.version, "0.2.6"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); From 9e1740cfa02be5dcb53833f227d3c723348c5b9c Mon Sep 17 00:00:00 2001 From: lildengzi <2580862656@qq.com> Date: Sun, 9 Aug 2026 01:20:59 +0800 Subject: [PATCH 043/119] =?UTF-8?q?fix:=20findXlingsExecutable=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=8F=91=E7=8E=B0=20mcpp=20=E5=86=85=E7=BD=AE=20xling?= =?UTF-8?q?s=EF=BC=88$MCPP=5FHOME/registry/bin=20=E4=B8=8E=20MCPP=5FVENDOR?= =?UTF-8?q?ED=5FXLINGS=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llvmTools.ts | 40 ++++++++++++++++++++++++++++----- test/llvmTools.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/llvmTools.ts b/src/llvmTools.ts index be064f9..9d7785f 100644 --- a/src/llvmTools.ts +++ b/src/llvmTools.ts @@ -60,8 +60,16 @@ export function xlingsInstallArgs(version?: string): string[] { return ["update", "llvm-tools"]; } -export function findXlingsExecutable(): string | undefined { - const home = os.homedir(); +export interface FindXlingsOptions { + /** Override for tests: base home directory instead of os.homedir(). */ + home?: string; + /** Override for tests: environment instead of process.env. */ + env?: NodeJS.ProcessEnv; +} + +export function findXlingsExecutable(options?: FindXlingsOptions): string | undefined { + const home = options?.home ?? os.homedir(); + const env = options?.env ?? process.env; const knownPaths = [ path.join(home, ".xlings", "subos", "current", "bin", "xlings"), path.join(home, ".xlings", "bin", "xlings"), @@ -72,6 +80,26 @@ export function findXlingsExecutable(): string | undefined { ); } + // mcpp (install.sh / AUR / mcpp-m) bundles xlings inside its own registry + // sandbox instead of installing to ~/.xlings. The AUR launcher pins the + // path via MCPP_VENDORED_XLINGS; otherwise it lives at + // $MCPP_HOME/registry/bin/xlings. Without probing both, the one-click + // module setup can never auto-install llvm-tools after a standard install. + const vendored = env.MCPP_VENDORED_XLINGS?.trim(); + if (vendored !== undefined && vendored.length > 0) { + knownPaths.push(vendored); + } + const mcppHome = env.MCPP_HOME?.trim(); + const extension = process.platform === "win32" ? ".exe" : ""; + knownPaths.push( + path.join( + mcppHome !== undefined && mcppHome.length > 0 ? mcppHome : path.join(home, ".mcpp"), + "registry", + "bin", + `xlings${extension}`, + ), + ); + // Check known install paths first for (const candidate of knownPaths) { if (existsSync(candidate)) { @@ -82,15 +110,15 @@ export function findXlingsExecutable(): string | undefined { // Fall back to PATH, but only when "xlings" actually resolves there. Always // returning "xlings" hid the not-installed case, so callers could never show // the "xlings 未安装" guidance. - return xlingsResolvableOnPath() ? "xlings" : undefined; + return xlingsResolvableOnPath(env.PATH) ? "xlings" : undefined; } -function xlingsResolvableOnPath(): boolean { +function xlingsResolvableOnPath(pathValue?: string): boolean { const names = process.platform === "win32" ? ["xlings.exe", "xlings.cmd", "xlings.bat"] : ["xlings"]; - const pathValue = process.env.PATH ?? ""; - for (const dir of pathValue.split(path.delimiter)) { + const pathEnv = pathValue ?? ""; + for (const dir of pathEnv.split(path.delimiter)) { if (dir.length === 0) { continue; } diff --git a/test/llvmTools.test.ts b/test/llvmTools.test.ts index b6348f5..e87750b 100644 --- a/test/llvmTools.test.ts +++ b/test/llvmTools.test.ts @@ -1,4 +1,7 @@ import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import test from "node:test"; import { @@ -68,3 +71,50 @@ test("findXlingsExecutable returns a string or undefined", () => { // Returns string (PATH fallback or known path) or undefined if xlings not found assert.ok(result === undefined || typeof result === "string"); }); + +test("findXlingsExecutable finds the xlings bundled in $MCPP_HOME/registry/bin", () => { + const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-mcpp-home-")); + const registryBin = path.join(home, "registry", "bin"); + const xlingsPath = path.join(registryBin, "xlings"); + mkdirSync(registryBin, { recursive: true }); + writeFileSync(xlingsPath, "#!/bin/sh\n"); + try { + assert.equal( + findXlingsExecutable({ home, env: { MCPP_HOME: home } }), + xlingsPath, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } +}); + +test("findXlingsExecutable falls back to $HOME/.mcpp/registry/bin when MCPP_HOME is unset", () => { + const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-home-")); + const registryBin = path.join(home, ".mcpp", "registry", "bin"); + const xlingsPath = path.join(registryBin, "xlings"); + mkdirSync(registryBin, { recursive: true }); + writeFileSync(xlingsPath, "#!/bin/sh\n"); + try { + assert.equal( + findXlingsExecutable({ home, env: {} }), + xlingsPath, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } +}); + +test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-vendored-")); + const vendored = path.join(root, "opt-mcpp", "registry", "bin", "xlings"); + mkdirSync(path.dirname(vendored), { recursive: true }); + writeFileSync(vendored, "#!/bin/sh\n"); + try { + assert.equal( + findXlingsExecutable({ home: root, env: { MCPP_VENDORED_XLINGS: vendored } }), + vendored, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); From b11ce64550d7798dfce08e2ae11e7cc80d2faa58 Mon Sep 17 00:00:00 2001 From: lildengzi <2580862656@qq.com> Date: Sun, 9 Aug 2026 01:30:19 +0800 Subject: [PATCH 044/119] =?UTF-8?q?fix:=20xlings=20=E5=8F=91=E7=8E=B0?= =?UTF-8?q?=E4=BB=A5=20`mcpp=20self=20env`=20=E4=B8=BA=E6=9D=83=E5=A8=81?= =?UTF-8?q?=E6=9D=A5=E6=BA=90=EF=BC=88=E9=A1=B9=E7=9B=AE=E7=BA=A7=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=EF=BC=89=EF=BC=8C=E8=B7=AF=E5=BE=84=E6=8E=A2=E6=B5=8B?= =?UTF-8?q?=E4=BB=85=E4=BD=9C=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cliController.ts | 2 +- src/extension.ts | 6 ++++-- src/llvmTools.ts | 26 +++++++++++++++++++++++++- test/llvmTools.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/cliController.ts b/src/cliController.ts index ef77e6a..2416b72 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -630,7 +630,7 @@ export class McppCliController { return false; } - private mcppExecutable(project: McppProjectDiscovery | undefined): string { + public mcppExecutable(project: McppProjectDiscovery | undefined): string { const uri = project === undefined ? vscode.workspace.workspaceFolders?.[0]?.uri : vscode.Uri.file(project.root); diff --git a/src/extension.ts b/src/extension.ts index f16d760..23fb2de 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,7 +19,7 @@ import { type McppProjectDiscovery, } from "./discovery"; import { - findXlingsExecutable, + resolveXlingsExecutable, llvmToolsVersionSpec, xlingsInstallArgs, } from "./llvmTools"; @@ -763,7 +763,9 @@ async function autoConfigureModulesWizard( : { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" }; } - const xlingsPath = findXlingsExecutable(); + const xlingsPath = await resolveXlingsExecutable( + cliController.mcppExecutable(currentContext.project), + ); const compilerPath = currentContext.analysis.compilerPath; if (xlingsPath === undefined || compilerPath === undefined) { return { diff --git a/src/llvmTools.ts b/src/llvmTools.ts index 9d7785f..e268b5a 100644 --- a/src/llvmTools.ts +++ b/src/llvmTools.ts @@ -4,7 +4,11 @@ import path from "node:path"; import process from "node:process"; import type { ToolIdentity } from "./analysis"; -import { runProcess, type ProcessResult } from "./process"; +import { + runProcess, + type ProcessResult, + type ProcessRunner, +} from "./process"; export function llvmToolsVersionSpec(identity: ToolIdentity): string { return `${identity.major}.${identity.minor}.${identity.patch}`; @@ -131,6 +135,26 @@ function xlingsResolvableOnPath(pathValue?: string): boolean { return false; } +const XLINGS_BINARY_LINE = /^\s*xlings binary\s*=\s*(.+?)\s*$/im; + +// Source of truth is mcpp itself, not the filesystem or PATH: `mcpp self env` +// reports the exact xlings bundled with THIS mcpp (mcpp is a project-level +// environment; it owns its tool paths). Works for install.sh, AUR and any +// custom MCPP_PREFIX layout. Falls back to the historical path heuristics for +// standalone ~/.xlings installs and for mcpp versions without the line. +export async function resolveXlingsExecutable( + mcppExecutable: string, + runner: ProcessRunner = runProcess, +): Promise { + const result = await runner(mcppExecutable, ["self", "env"]); + const match = `${result.stdout}\n${result.stderr}`.match(XLINGS_BINARY_LINE); + const reported = match?.[1]?.trim(); + if (reported !== undefined && reported.length > 0 && existsSync(reported)) { + return reported; + } + return findXlingsExecutable(); +} + export async function runXlingsCommand( xlingsPath: string, args: string[], diff --git a/test/llvmTools.test.ts b/test/llvmTools.test.ts index e87750b..a7528a9 100644 --- a/test/llvmTools.test.ts +++ b/test/llvmTools.test.ts @@ -10,6 +10,7 @@ import { xlingsInstallArgs, deriveInstalledClangdPath, findXlingsExecutable, + resolveXlingsExecutable, } from "../src/llvmTools"; test("extracts version string from ToolIdentity", () => { @@ -118,3 +119,41 @@ test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("resolveXlingsExecutable reads the xlings binary from `mcpp self env`", async () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-selfenv-")); + const xlingsPath = path.join(root, "registry", "bin", "xlings"); + mkdirSync(path.dirname(xlingsPath), { recursive: true }); + writeFileSync(xlingsPath, "#!/bin/sh\n"); + const runner = async () => ({ + exitCode: 0, + stdout: `MCPP_HOME = ${root}\nxlings binary = ${xlingsPath}\nxlings pinned = 2026.8.8.1\n`, + stderr: "", + }); + try { + assert.equal( + await resolveXlingsExecutable("/tools/mcpp", runner), + xlingsPath, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("resolveXlingsExecutable falls back when the reported path does not exist", async () => { + const runner = async () => ({ + exitCode: 0, + stdout: "xlings binary = /no/such/xlings\n", + stderr: "", + }); + const result = await resolveXlingsExecutable("/tools/mcpp", runner); + // Fallback heuristics find nothing in this environment, so the result is + // undefined unless a standalone ~/.xlings or PATH xlings happens to exist. + assert.ok(result === undefined || typeof result === "string"); +}); + +test("resolveXlingsExecutable falls back when `mcpp self env` fails", async () => { + const runner = async () => ({ exitCode: 1, stdout: "", stderr: "boom\n" }); + const result = await resolveXlingsExecutable("/tools/mcpp", runner); + assert.ok(result === undefined || typeof result === "string"); +}); From c2f5507d79b30ae98cfd69da48b6c643aed3f215 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 9 Aug 2026 01:58:57 +0800 Subject: [PATCH 045/119] =?UTF-8?q?fix:=20PR=20#11=20=E6=89=93=E7=A3=A8?= =?UTF-8?q?=E2=80=94=E2=80=94`mcpp=20self=20env`=20=E5=8A=A0=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E4=BF=9D=E6=8A=A4=E3=80=81=E5=9B=9E=E9=80=80=E6=8E=A2?= =?UTF-8?q?=E6=B5=8B=E6=94=AF=E6=8C=81=E6=B3=A8=E5=85=A5=E3=80=81=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=B9=B3=E5=8F=B0=E5=8F=AF=E7=A7=BB=E6=A4=8D=E3=80=81?= =?UTF-8?q?=E8=A1=A5=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 +++++ src/llvmTools.ts | 13 ++++++++-- test/llvmTools.test.ts | 54 ++++++++++++++++++++++++++++++++---------- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4177e..0015cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +## 0.2.7 + +- 修复「一键配置模块代码提示」在标准 mcpp 安装(install.sh / AUR)下无法发现 mcpp 内置 + xlings 的问题:xlings 发现以 `mcpp self env` 为权威来源(项目级契约),路径探测仅作回退; + 为 `mcpp self env` 调用增加超时保护,并补齐测试(PR #11)。 + ## 0.2.6 - 新增 **mcpp: 一键配置模块代码提示** 向导:按「安装/切换工具链 → 构建 → 重载 → clangd diff --git a/src/llvmTools.ts b/src/llvmTools.ts index e268b5a..f573f05 100644 --- a/src/llvmTools.ts +++ b/src/llvmTools.ts @@ -142,17 +142,26 @@ const XLINGS_BINARY_LINE = /^\s*xlings binary\s*=\s*(.+?)\s*$/im; // environment; it owns its tool paths). Works for install.sh, AUR and any // custom MCPP_PREFIX layout. Falls back to the historical path heuristics for // standalone ~/.xlings installs and for mcpp versions without the line. +// +// The subprocess is bounded by MCPP_SELF_ENV_TIMEOUT_MS: the wizard reaches +// this step only after mcpp is initialized (toolchain list / build already +// ran), so 60s is generous while still guarding against an extreme hang. +const MCPP_SELF_ENV_TIMEOUT_MS = 60_000; + export async function resolveXlingsExecutable( mcppExecutable: string, runner: ProcessRunner = runProcess, + options?: FindXlingsOptions, ): Promise { - const result = await runner(mcppExecutable, ["self", "env"]); + const result = await runner(mcppExecutable, ["self", "env"], undefined, { + timeoutMs: MCPP_SELF_ENV_TIMEOUT_MS, + }); const match = `${result.stdout}\n${result.stderr}`.match(XLINGS_BINARY_LINE); const reported = match?.[1]?.trim(); if (reported !== undefined && reported.length > 0 && existsSync(reported)) { return reported; } - return findXlingsExecutable(); + return findXlingsExecutable(options); } export async function runXlingsCommand( diff --git a/test/llvmTools.test.ts b/test/llvmTools.test.ts index a7528a9..8e967d7 100644 --- a/test/llvmTools.test.ts +++ b/test/llvmTools.test.ts @@ -13,6 +13,9 @@ import { resolveXlingsExecutable, } from "../src/llvmTools"; +// `xlings` on POSIX, `xlings.exe` on Windows — mirrors mcpp's exe_suffix. +const xlingsBinaryName = process.platform === "win32" ? "xlings.exe" : "xlings"; + test("extracts version string from ToolIdentity", () => { assert.equal( llvmToolsVersionSpec({ major: 22, minor: 1, patch: 8, revision: "abc1234" }), @@ -76,7 +79,7 @@ test("findXlingsExecutable returns a string or undefined", () => { test("findXlingsExecutable finds the xlings bundled in $MCPP_HOME/registry/bin", () => { const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-mcpp-home-")); const registryBin = path.join(home, "registry", "bin"); - const xlingsPath = path.join(registryBin, "xlings"); + const xlingsPath = path.join(registryBin, xlingsBinaryName); mkdirSync(registryBin, { recursive: true }); writeFileSync(xlingsPath, "#!/bin/sh\n"); try { @@ -92,7 +95,7 @@ test("findXlingsExecutable finds the xlings bundled in $MCPP_HOME/registry/bin", test("findXlingsExecutable falls back to $HOME/.mcpp/registry/bin when MCPP_HOME is unset", () => { const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-home-")); const registryBin = path.join(home, ".mcpp", "registry", "bin"); - const xlingsPath = path.join(registryBin, "xlings"); + const xlingsPath = path.join(registryBin, xlingsBinaryName); mkdirSync(registryBin, { recursive: true }); writeFileSync(xlingsPath, "#!/bin/sh\n"); try { @@ -107,7 +110,7 @@ test("findXlingsExecutable falls back to $HOME/.mcpp/registry/bin when MCPP_HOME test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-vendored-")); - const vendored = path.join(root, "opt-mcpp", "registry", "bin", "xlings"); + const vendored = path.join(root, "opt-mcpp", "registry", "bin", xlingsBinaryName); mkdirSync(path.dirname(vendored), { recursive: true }); writeFileSync(vendored, "#!/bin/sh\n"); try { @@ -122,7 +125,7 @@ test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { test("resolveXlingsExecutable reads the xlings binary from `mcpp self env`", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-selfenv-")); - const xlingsPath = path.join(root, "registry", "bin", "xlings"); + const xlingsPath = path.join(root, "registry", "bin", xlingsBinaryName); mkdirSync(path.dirname(xlingsPath), { recursive: true }); writeFileSync(xlingsPath, "#!/bin/sh\n"); const runner = async () => ({ @@ -140,20 +143,47 @@ test("resolveXlingsExecutable reads the xlings binary from `mcpp self env`", asy } }); -test("resolveXlingsExecutable falls back when the reported path does not exist", async () => { +test("resolveXlingsExecutable passes a timeout to `mcpp self env`", async () => { + let captured: { timeoutMs?: number } | undefined; + const runner = async ( + _executable: string, + _args: string[], + _cwd?: string, + options?: { timeoutMs?: number }, + ) => { + captured = options; + return { exitCode: 0, stdout: "", stderr: "" }; + }; + await resolveXlingsExecutable("/tools/mcpp", runner); + assert.equal(captured?.timeoutMs, 60_000); +}); + +test("resolveXlingsExecutable falls back to path probing when the reported path does not exist", async () => { + const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-fallback-missing-")); const runner = async () => ({ exitCode: 0, stdout: "xlings binary = /no/such/xlings\n", stderr: "", }); - const result = await resolveXlingsExecutable("/tools/mcpp", runner); - // Fallback heuristics find nothing in this environment, so the result is - // undefined unless a standalone ~/.xlings or PATH xlings happens to exist. - assert.ok(result === undefined || typeof result === "string"); + try { + assert.equal( + await resolveXlingsExecutable("/tools/mcpp", runner, { home, env: {} }), + undefined, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } }); -test("resolveXlingsExecutable falls back when `mcpp self env` fails", async () => { +test("resolveXlingsExecutable falls back to path probing when `mcpp self env` fails", async () => { + const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-fallback-fail-")); const runner = async () => ({ exitCode: 1, stdout: "", stderr: "boom\n" }); - const result = await resolveXlingsExecutable("/tools/mcpp", runner); - assert.ok(result === undefined || typeof result === "string"); + try { + assert.equal( + await resolveXlingsExecutable("/tools/mcpp", runner, { home, env: {} }), + undefined, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } }); From 2fcaf26e8346b56a23f308458dffeeeb5ad50d8a Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 9 Aug 2026 02:06:49 +0800 Subject: [PATCH 046/119] =?UTF-8?q?release:=20mcpp-vscode=200.2.7=EF=BC=88?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=A0=87=E5=87=86=E5=AE=89=E8=A3=85=E4=B8=8B?= =?UTF-8?q?=20xlings=20=E5=8F=91=E7=8E=B0=EF=BC=8C=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=90=91=E5=AF=BC=E9=80=82=E9=85=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 4 ++-- package.json | 2 +- test/artifacts.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6488a5a..4d170ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.2.6", + "version": "0.2.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.2.6", + "version": "0.2.7", "license": "Apache-2.0", "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/package.json b/package.json index fe479dc..647824e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.6", + "version": "0.2.7", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 2add919..fc7be0c 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -26,7 +26,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.6"); + assert.equal(manifest.version, "0.2.7"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); From 2892481a0a33c9d6bb12c7f472974c42b6617dda Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 9 Aug 2026 02:08:27 +0800 Subject: [PATCH 047/119] =?UTF-8?q?docs:=20README=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E8=87=B3=200.2.7=EF=BC=88=E7=89=88=E6=9C=AC=E5=8F=B7=E3=80=81?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=9C=B0=E5=9D=80=E8=BF=81=E7=A7=BB=20mcpp-c?= =?UTF-8?q?ommunity=E3=80=81xlings=20=E5=AE=9A=E4=BD=8D=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d2884f1..3b0792d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -当前版本为 `0.2.5`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +当前版本为 `0.2.7`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 > 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 @@ -33,11 +33,11 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 ## 安装 -从 [GitHub Releases](https://github.com/wellwei/mcpp-vscode/releases) 下载对应版本的 +从 [GitHub Releases](https://github.com/mcpp-community/mcpp-vscode/releases) 下载对应版本的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.2.5.vsix +code --install-extension /path/to/mcpp-vscode-0.2.7.vsix ``` 安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 @@ -364,6 +364,8 @@ macOS 从图形界面启动 VS Code 时可能没有继承终端 `PATH`。将 `mc - 执行 **mcpp: 一键配置模块代码提示**,扩展会自动通过 xlings 安装与当前编译器 版本匹配的 llvm-tools(含 clangd)。 +- xlings 可执行文件的定位以 `mcpp self env` 报告为准(覆盖 install.sh、AUR 等 + mcpp 内置安装布局),找不到时再回退到独立 `~/.xlings` 与 `PATH`。 - `mcpp.clangd.path` 可以指向任意安装来源的真实 clangd,不要求与 mcpp 同目录。 - 自动发现会尝试编译器同目录、匹配的 xlings `xim-x-llvm-tools//bin` 和 `PATH`。 @@ -400,13 +402,13 @@ API、状态栏、任务和 clangd 集成。 版本完全一致的 tag: ```sh -git tag -a v0.2.5 -m "mcpp-vscode 0.2.5" -git push origin v0.2.5 +git tag -a v0.2.7 -m "mcpp-vscode 0.2.7" +git push origin v0.2.7 ``` `.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, 并创建或更新对应的 GitHub Release。工作流当前不会发布到 VS Marketplace。 -项目地址:[wellwei/mcpp-vscode](https://github.com/wellwei/mcpp-vscode) +项目地址:[mcpp-community/mcpp-vscode](https://github.com/mcpp-community/mcpp-vscode) -问题反馈:[GitHub Issues](https://github.com/wellwei/mcpp-vscode/issues) +问题反馈:[GitHub Issues](https://github.com/mcpp-community/mcpp-vscode/issues) From e1eb80e37add55f3d85ec0fd3d22a7847ba8bede Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 9 Aug 2026 02:10:17 +0800 Subject: [PATCH 048/119] =?UTF-8?q?meta:=20package.json=20=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=85=83=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB=E8=87=B3?= =?UTF-8?q?=20mcpp-community=EF=BC=88repository/homepage/bugs=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 647824e..d0232c2 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "icon": "images/logo.png", "repository": { "type": "git", - "url": "https://github.com/wellwei/mcpp-vscode.git" + "url": "https://github.com/mcpp-community/mcpp-vscode.git" }, - "homepage": "https://github.com/wellwei/mcpp-vscode#readme", + "homepage": "https://github.com/mcpp-community/mcpp-vscode#readme", "bugs": { - "url": "https://github.com/wellwei/mcpp-vscode/issues" + "url": "https://github.com/mcpp-community/mcpp-vscode/issues" }, "engines": { "vscode": "^1.90.0" From 736ecbb994a3113517292767ed1c0a15f3feeecb Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 9 Aug 2026 02:12:05 +0800 Subject: [PATCH 049/119] =?UTF-8?q?test:=20=E5=90=8C=E6=AD=A5=20artifacts?= =?UTF-8?q?=20=E6=96=AD=E8=A8=80=E4=BB=93=E5=BA=93=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E8=87=B3=20mcpp-community?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/artifacts.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index fc7be0c..72961f7 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -301,9 +301,9 @@ test("新建工程契约是创建并打开,不自动构建", () => { test("声明 GitHub 仓库和扩展图标", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; assert.equal(manifest.icon, "images/logo.png"); - assert.equal(manifest.repository?.url, "https://github.com/wellwei/mcpp-vscode.git"); - assert.equal(manifest.homepage, "https://github.com/wellwei/mcpp-vscode#readme"); - assert.equal(manifest.bugs?.url, "https://github.com/wellwei/mcpp-vscode/issues"); + assert.equal(manifest.repository?.url, "https://github.com/mcpp-community/mcpp-vscode.git"); + assert.equal(manifest.homepage, "https://github.com/mcpp-community/mcpp-vscode#readme"); + assert.equal(manifest.bugs?.url, "https://github.com/mcpp-community/mcpp-vscode/issues"); const icon = readFileSync(path.join(root, manifest.icon)); assert.deepEqual([...icon.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); From 51a5821df0738048804e68c08bdab1e3390435b0 Mon Sep 17 00:00:00 2001 From: wellwei Date: Thu, 6 Aug 2026 17:08:44 +0800 Subject: [PATCH 050/119] feat: bootstrap clangd from mcpp IDE configure --- package.json | 5 ++ src/extension.ts | 105 ++++++++++++++++++++++++++++++++++++++- src/ideProtocol.ts | 97 ++++++++++++++++++++++++++++++++++++ src/ideWorkflow.ts | 40 +++++++++++++++ test/artifacts.test.ts | 11 ++++ test/ideProtocol.test.ts | 72 +++++++++++++++++++++++++++ test/ideWorkflow.test.ts | 78 +++++++++++++++++++++++++++++ 7 files changed, 406 insertions(+), 2 deletions(-) create mode 100644 src/ideProtocol.ts create mode 100644 src/ideWorkflow.ts create mode 100644 test/ideProtocol.test.ts create mode 100644 test/ideWorkflow.test.ts diff --git a/package.json b/package.json index d0232c2..6692245 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "onCommand:mcpp.showToolchains", "onCommand:mcpp.installToolchain", "onCommand:mcpp.selectDefaultToolchain", + "onCommand:mcpp.configureIde", "onCommand:mcpp.autoConfigureModules" ], "categories": [ @@ -104,6 +105,10 @@ "command": "mcpp.selectDefaultToolchain", "title": "mcpp: 选择全局默认工具链" }, + { + "command": "mcpp.configureIde", + "title": "mcpp: 生成 IDE 编译数据库" + }, { "command": "mcpp.configureClangd", "title": "mcpp: 配置 clangd" diff --git a/src/extension.ts b/src/extension.ts index 23fb2de..d658e3a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,6 +26,8 @@ import { import { CLI_COMMANDS } from "./commands"; import { McppCliController } from "./cliController"; import { runClangdCheck, runToolVersion, type ToolVersionResult } from "./process"; +import { runIdeConfigure, type IdeSnapshotPublished } from "./ideProtocol"; +import { ensureIdeConfigured } from "./ideWorkflow"; import { configurationReadyAfterRestart, configurationAffectsModuleSupport, @@ -55,6 +57,7 @@ import { } from "./moduleSetup"; const COMMAND_CONFIGURE = "mcpp.configureClangd"; +const COMMAND_IDE_CONFIGURE = "mcpp.configureIde"; const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; const COMMAND_CHECK = "mcpp.checkModuleSupport"; @@ -86,6 +89,43 @@ const moduleStatusByProject = new Map(); const moduleCheckOperations = createLatestOperationTracker(); let lastReconciledProjectRoot: string | undefined; +function mcppExecutable(project: McppProjectDiscovery): string { + const configured = projectConfiguration(project).get("path", "").trim(); + return configured.length === 0 ? "mcpp" : configured; +} + +async function runIdeConfigureForProject( + project: McppProjectDiscovery, + output: vscode.OutputChannel, + interactive: boolean, +): Promise { + if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { + appendOutputLine(output, "[IDE 配置] 工作区未受信任,跳过 mcpp ide configure。"); + if (interactive) { + await vscode.window.showWarningMessage( + "当前工作区未受信任,mcpp 不会执行 IDE 配置。请先信任工作区。", + ); + } + return undefined; + } + + const executable = mcppExecutable(project); + const args = ["ide", "configure", "--format", "ndjson"]; + try { + const result = await runIdeConfigure(project.root, executable); + appendProcessOutput(output, "IDE 配置", executable, args, result); + appendOutputLine(output, `[IDE 配置] 已发布 CDB:${result.compileCommands}`); + return result; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[IDE 配置] ${message}`); + if (interactive) { + await vscode.window.showErrorMessage(`mcpp IDE 配置失败:${message}`); + } + return undefined; + } +} + function findCurrentProject(): McppProjectDiscovery | undefined { const activeEditor = vscode.window.activeTextEditor; if (activeEditor !== undefined) { @@ -902,9 +942,9 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi project: McppProjectDiscovery | undefined, forceRestart: boolean, ): Promise => { - const context = loadProjectContext(project); - updateStatusBar(status, context); + let context = loadProjectContext(project); if (context === undefined) { + updateStatusBar(status, context); return { context, databaseFound: false, @@ -912,6 +952,32 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi }; } + // 首次打开工程时,先发布配置阶段 CDB,再让 clangd 读取它;普通源码 + // 是否能编译成功不参与这个决定。已有 CDB 仍走兼容的旧流程。 + try { + const outcome = await ensureIdeConfigured({ + projectRoot: context.project.root, + compilationDatabasePath: context.project.compilationDatabasePath, + trusted: vscode.workspace.isTrusted, + databaseExists: () => existsSync(context?.project.compilationDatabasePath ?? ""), + configure: async () => { + const published = await runIdeConfigureForProject(context!.project, output, false); + if (published === undefined) { + throw new Error("mcpp ide configure 未发布快照"); + } + return published; + }, + }); + if (outcome.state === "configured") { + context = loadProjectContext(context.project) ?? context; + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[自动配置] ${message}`); + } + + updateStatusBar(status, context); + const configured = await configureClangd( context, status, @@ -1068,6 +1134,41 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await updateModuleSupportForContext(context, configured, status, output); }); })), + vscode.commands.registerCommand(COMMAND_IDE_CONFIGURE, runGuarded(async () => { + const project = findCurrentProject(); + if (project === undefined) { + await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); + return; + } + await executeWithWorkspaceClangd(async () => { + const context = loadProjectContext(project); + if (context === undefined) { + return; + } + try { + const outcome = await ensureIdeConfigured({ + projectRoot: project.root, + compilationDatabasePath: project.compilationDatabasePath, + trusted: vscode.workspace.isTrusted, + force: true, + databaseExists: () => existsSync(project.compilationDatabasePath), + configure: async () => { + const published = await runIdeConfigureForProject(project, output, true); + if (published === undefined) { + throw new Error("mcpp ide configure 未发布快照"); + } + return published; + }, + }); + if (outcome.state === "configured") { + await reconcileProjectContext(project, true); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[IDE 配置] ${message}`); + } + }); + })), vscode.commands.registerCommand(COMMAND_REFRESH, runGuarded(async () => { await cliController.runProjectTask("build"); })), diff --git a/src/ideProtocol.ts b/src/ideProtocol.ts new file mode 100644 index 0000000..5b9faad --- /dev/null +++ b/src/ideProtocol.ts @@ -0,0 +1,97 @@ +import { runProcess, type ProcessResult, type ProcessRunner } from "./process"; + +export interface IdeSnapshotPublished { + phase: "configured" | "ready"; + compileCommands: string; + compatibilityCompileCommands?: string; + snapshotId?: string; + configurationId?: string; +} + +interface IdeEvent { + seq: number; + type: string; + phase?: string; + compileCommands?: unknown; + compatibilityCompileCommands?: unknown; + snapshotId?: unknown; + configurationId?: unknown; + status?: unknown; +} + +function parseEvent(line: string): IdeEvent { + let value: unknown; + try { + value = JSON.parse(line); + } catch { + throw new Error("mcpp ide configure 输出包含无效 JSON"); + } + if (value === null || typeof value !== "object") { + throw new Error("mcpp ide configure 事件不是对象"); + } + const event = value as Partial; + if (typeof event.seq !== "number" || !Number.isSafeInteger(event.seq) + || event.seq <= 0 || typeof event.type !== "string") { + throw new Error("mcpp ide configure 事件缺少有效 seq/type"); + } + return event as IdeEvent; +} + +export function parseIdeConfigureOutput(output: string): IdeSnapshotPublished { + let previousSeq = 0; + let published: IdeSnapshotPublished | undefined; + let finished = false; + for (const line of output.split(/\r?\n/)) { + if (line.trim().length === 0) { + continue; + } + const event = parseEvent(line); + if (event.seq <= previousSeq) { + throw new Error("mcpp ide configure 事件序号倒退"); + } + previousSeq = event.seq; + if (event.type !== "snapshot-published") { + if (event.type === "operation-finished") { + if (event.status !== "success") { + throw new Error("mcpp ide configure 未成功完成"); + } + finished = true; + } + continue; + } + if ((event.phase !== "configured" && event.phase !== "ready") + || typeof event.compileCommands !== "string") { + throw new Error("mcpp ide configure 发布事件缺少 CDB"); + } + published = { + phase: event.phase, + compileCommands: event.compileCommands, + compatibilityCompileCommands: + typeof event.compatibilityCompileCommands === "string" + ? event.compatibilityCompileCommands + : undefined, + snapshotId: typeof event.snapshotId === "string" ? event.snapshotId : undefined, + configurationId: typeof event.configurationId === "string" ? event.configurationId : undefined, + }; + } + if (published === undefined) { + throw new Error("mcpp ide configure 没有发布快照"); + } + if (!finished) { + throw new Error("mcpp ide configure 未成功完成"); + } + return published; +} + +export async function runIdeConfigure( + projectRoot: string, + executable = "mcpp", + runner: ProcessRunner = runProcess, +): Promise { + const args = ["ide", "configure", "--format", "ndjson"]; + const result = await runner(executable, args, projectRoot); + if (result.exitCode !== 0) { + throw new Error(result.stderr.trim() || `mcpp ide configure 失败(退出码 ${result.exitCode})`); + } + return { ...result, ...parseIdeConfigureOutput(result.stdout) }; +} diff --git a/src/ideWorkflow.ts b/src/ideWorkflow.ts new file mode 100644 index 0000000..7763efb --- /dev/null +++ b/src/ideWorkflow.ts @@ -0,0 +1,40 @@ +import type { IdeSnapshotPublished } from "./ideProtocol"; + +export interface IdeConfigurationRequest { + projectRoot: string; + compilationDatabasePath: string; + trusted: boolean; + force?: boolean; + databaseExists: () => boolean; + configure: () => Promise; +} + +export type IdeConfigurationOutcome = + | { state: "configured"; compileCommands: string; snapshot: IdeSnapshotPublished } + | { state: "existing"; compileCommands: string } + | { state: "untrusted"; compileCommands?: string }; + +/** + * 决定是否需要执行 mcpp ide configure;不在这里读写 VS Code 设置, + * 便于 Extension Host 和纯 Node 测试共享同一套生命周期边界。 + */ +export async function ensureIdeConfigured( + request: IdeConfigurationRequest, +): Promise { + const exists = request.databaseExists(); + if (!request.trusted) { + return { + state: "untrusted", + ...(exists ? { compileCommands: request.compilationDatabasePath } : {}), + }; + } + if (exists && !request.force) { + return { state: "existing", compileCommands: request.compilationDatabasePath }; + } + const snapshot = await request.configure(); + return { + state: "configured", + compileCommands: snapshot.compatibilityCompileCommands ?? snapshot.compileCommands, + snapshot, + }; +} diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 72961f7..26d577d 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -47,6 +47,7 @@ test("declares the official clangd dependency and mcpp commands", () => { "mcpp.showToolchains", "mcpp.installToolchain", "mcpp.selectDefaultToolchain", + "mcpp.configureIde", "mcpp.configureClangd", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport", @@ -113,6 +114,16 @@ test("shows editor title buttons only inside mcpp projects", () => { assert.ok(!commands.some((command) => command.command === "mcpp.inProject")); }); +test("wires IDE configure before clangd reconciliation", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.configureIde")); + + const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); + assert.match(source, /ensureIdeConfigured/); + assert.match(source, /runIdeConfigure/); + assert.match(source, /registerCommand\(COMMAND_IDE_CONFIGURE/); +}); + test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; const associations = manifest.contributes?.configurationDefaults?.["files.associations"] as diff --git a/test/ideProtocol.test.ts b/test/ideProtocol.test.ts new file mode 100644 index 0000000..7fa39ca --- /dev/null +++ b/test/ideProtocol.test.ts @@ -0,0 +1,72 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { parseIdeConfigureOutput, runIdeConfigure } from "../src/ideProtocol"; + +test("parses configured snapshot without human-output regexes", () => { + const result = parseIdeConfigureOutput([ + JSON.stringify({ seq: 1, type: "operation-started", operation: "configure" }), + JSON.stringify({ + seq: 2, + type: "snapshot-published", + phase: "configured", + compileCommands: "/work/.mcpp/ide/replies/compile_commands.json", + }), + JSON.stringify({ seq: 3, type: "operation-finished", status: "success" }), + ].join("\n")); + + assert.equal(result.phase, "configured"); + assert.equal(result.compileCommands, "/work/.mcpp/ide/replies/compile_commands.json"); +}); + +test("passes configure through the configured executable and cwd", async () => { + const calls: Array<{ executable: string; args: string[]; cwd?: string }> = []; + const result = await runIdeConfigure( + "/work/app", + "/tools/mcpp", + async (executable, args, cwd) => { + calls.push({ executable, args, cwd }); + return { + exitCode: 0, + stdout: [ + JSON.stringify({ seq: 1, type: "operation-started" }), + JSON.stringify({ + seq: 2, + type: "snapshot-published", + phase: "configured", + compileCommands: "/work/app/compile_commands.json", + }), + JSON.stringify({ seq: 3, type: "operation-finished", status: "success" }), + ].join("\n"), + stderr: "", + }; + }, + ); + + assert.deepEqual(calls, [{ + executable: "/tools/mcpp", + args: ["ide", "configure", "--format", "ndjson"], + cwd: "/work/app", + }]); + assert.equal(result.compileCommands, "/work/app/compile_commands.json"); +}); + +test("rejects a published snapshot without a successful finish event", () => { + assert.throws(() => parseIdeConfigureOutput([ + JSON.stringify({ seq: 1, type: "operation-started", operation: "configure" }), + JSON.stringify({ + seq: 2, + type: "snapshot-published", + phase: "configured", + compileCommands: "/work/app/compile_commands.json", + }), + JSON.stringify({ seq: 3, type: "operation-finished", status: "failed" }), + ].join("\n")), /未成功完成/); +}); + +test("rejects human-readable output mixed into NDJSON", () => { + assert.throws(() => parseIdeConfigureOutput([ + JSON.stringify({ seq: 1, type: "operation-started" }), + "c++fly on clang: enabled", + ].join("\n")), /无效 JSON/); +}); diff --git a/test/ideWorkflow.test.ts b/test/ideWorkflow.test.ts new file mode 100644 index 0000000..80386af --- /dev/null +++ b/test/ideWorkflow.test.ts @@ -0,0 +1,78 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { ensureIdeConfigured } from "../src/ideWorkflow"; + +test("configures a trusted project before build when the CDB is missing", async () => { + let calls = 0; + const outcome = await ensureIdeConfigured({ + projectRoot: "/work/app", + compilationDatabasePath: "/work/app/compile_commands.json", + trusted: true, + databaseExists: () => false, + configure: async () => { + calls += 1; + return { + phase: "configured", + compileCommands: "/work/app/.mcpp/ide/replies/compile_commands-cfg.json", + compatibilityCompileCommands: "/work/app/compile_commands.json", + }; + }, + }); + + assert.equal(calls, 1); + assert.equal(outcome.state, "configured"); + assert.equal(outcome.compileCommands, "/work/app/compile_commands.json"); +}); + +test("keeps an existing CDB without invoking mcpp", async () => { + let called = false; + const outcome = await ensureIdeConfigured({ + projectRoot: "/work/app", + compilationDatabasePath: "/work/app/compile_commands.json", + trusted: true, + databaseExists: () => true, + configure: async () => { + called = true; + throw new Error("must not run"); + }, + }); + + assert.equal(called, false); + assert.equal(outcome.state, "existing"); +}); + +test("does not execute mcpp in an untrusted workspace", async () => { + let called = false; + const outcome = await ensureIdeConfigured({ + projectRoot: "/work/app", + compilationDatabasePath: "/work/app/compile_commands.json", + trusted: false, + databaseExists: () => false, + configure: async () => { + called = true; + throw new Error("must not run"); + }, + }); + + assert.equal(called, false); + assert.equal(outcome.state, "untrusted"); +}); + +test("force refresh invokes configure even when a CDB already exists", async () => { + let calls = 0; + const outcome = await ensureIdeConfigured({ + projectRoot: "/work/app", + compilationDatabasePath: "/work/app/compile_commands.json", + trusted: true, + force: true, + databaseExists: () => true, + configure: async () => { + calls += 1; + return { phase: "configured", compileCommands: "/work/app/compile_commands.json" }; + }, + }); + + assert.equal(calls, 1); + assert.equal(outcome.state, "configured"); +}); From ff891643842dec8763a40d4b212bba8159e9c7f9 Mon Sep 17 00:00:00 2001 From: wellwei Date: Thu, 6 Aug 2026 17:16:52 +0800 Subject: [PATCH 051/119] fix: refresh IDE snapshot after project changes --- src/extension.ts | 25 ++++++++++++++++++++++++- test/artifacts.test.ts | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index d658e3a..f3d126c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -87,6 +87,9 @@ type ConfigureMode = "automatic" | "interactive"; const moduleStatusByProject = new Map(); const moduleCheckOperations = createLatestOperationTracker(); +// 记录 manifest/settings 变化触发的 IDE 配置请求;CDB watcher 只重读 +// 已发布快照,避免写 CDB 后再次启动 mcpp 形成重入。 +const forceIdeConfigureByProject = new Set(); let lastReconciledProjectRoot: string | undefined; function mcppExecutable(project: McppProjectDiscovery): string { @@ -912,6 +915,7 @@ const mcppTomlCompletionProvider: vscode.CompletionItemProvider = { export async function activate(extensionContext: vscode.ExtensionContext): Promise { moduleStatusByProject.clear(); moduleCheckOperations.clear(); + forceIdeConfigureByProject.clear(); const output = vscode.window.createOutputChannel("mcpp"); const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); @@ -941,6 +945,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi const reconcileProjectContext = async ( project: McppProjectDiscovery | undefined, forceRestart: boolean, + forceIdeConfigure: boolean = false, ): Promise => { let context = loadProjectContext(project); if (context === undefined) { @@ -959,6 +964,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi projectRoot: context.project.root, compilationDatabasePath: context.project.compilationDatabasePath, trusted: vscode.workspace.isTrusted, + force: forceIdeConfigure, databaseExists: () => existsSync(context?.project.compilationDatabasePath ?? ""), configure: async () => { const published = await runIdeConfigureForProject(context!.project, output, false); @@ -1005,7 +1011,11 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi configured: false, }; } - return reconcileProjectContext(project, forceRestart); + return reconcileProjectContext( + project, + forceRestart, + forceIdeConfigureByProject.delete(projectRoot), + ); }, ), ); @@ -1053,10 +1063,13 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (configurationAffectsModuleSupport( (section) => event.affectsConfiguration(section, uri), )) { + forceIdeConfigureByProject.add(project.root); requestCurrentProjectReconciliation(true); } }); const trustWatcher = vscode.workspace.onDidGrantWorkspaceTrust(() => { + const project = findCurrentProject(); + if (project !== undefined) forceIdeConfigureByProject.add(project.root); requestCurrentProjectReconciliation(true); }); @@ -1315,14 +1328,23 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi manifestWatcher.onDidCreate(() => { refreshStatus(); cliController.refreshStatus(); + const project = findCurrentProject(); + if (project !== undefined) forceIdeConfigureByProject.add(project.root); + requestCurrentProjectReconciliation(true); }), manifestWatcher.onDidChange(() => { refreshStatus(); cliController.refreshStatus(); + const project = findCurrentProject(); + if (project !== undefined) forceIdeConfigureByProject.add(project.root); + requestCurrentProjectReconciliation(true); }), manifestWatcher.onDidDelete(() => { refreshStatus(); cliController.refreshStatus(); + const project = findCurrentProject(); + if (project !== undefined) forceIdeConfigureByProject.add(project.root); + requestCurrentProjectReconciliation(true); }), ...registerCompilationDatabaseReconciliation( compilationDatabaseWatcher, @@ -1346,5 +1368,6 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi export function deactivate(): void { moduleStatusByProject.clear(); moduleCheckOperations.clear(); + forceIdeConfigureByProject.clear(); // VS Code 会释放 activate() 注册的所有订阅。 } diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 26d577d..b3622af 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -122,6 +122,7 @@ test("wires IDE configure before clangd reconciliation", () => { assert.match(source, /ensureIdeConfigured/); assert.match(source, /runIdeConfigure/); assert.match(source, /registerCommand\(COMMAND_IDE_CONFIGURE/); + assert.match(source, /manifestWatcher\.onDidChange\(\(\) => \{[\s\S]*requestCurrentProjectReconciliation\(true\)/); }); test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => { From ce56724a83aad6b29cbbc403a4f5704f28898767 Mon Sep 17 00:00:00 2001 From: wellwei Date: Tue, 11 Aug 2026 11:26:38 +0800 Subject: [PATCH 052/119] feat: consume configure-only compile database workflow --- CHANGELOG.md | 11 + README.md | 77 ++++--- package.json | 5 - src/cliController.ts | 27 ++- src/configureOnly.ts | 11 + src/discovery.ts | 77 ++++++- src/extension.ts | 343 ++++++++++++++++++++----------- src/ideProtocol.ts | 97 --------- src/ideWorkflow.ts | 29 +-- src/newProject.ts | 4 +- src/workflow.ts | 46 +++++ test/artifacts.test.ts | 31 ++- test/configureOnly.test.ts | 52 +++++ test/discovery.test.ts | 95 ++++++++- test/e2e/fixtures/fake-mcpp.js | 18 +- test/e2e/suite/extension.test.ts | 27 ++- test/ideProtocol.test.ts | 72 ------- test/ideWorkflow.test.ts | 76 +++++-- test/workflow.test.ts | 59 ++++-- 19 files changed, 764 insertions(+), 393 deletions(-) create mode 100644 src/configureOnly.ts delete mode 100644 src/ideProtocol.ts create mode 100644 test/configureOnly.test.ts delete mode 100644 test/ideProtocol.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0015cde..ed5cdd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 更新日志 +## 未发布 + +- 将编译数据库刷新迁移到 `mcpp build --configure-only`:不解析 stdout 人类文本,以退出码 + 和可解析 CDB 作为成功条件,失败时保留 last-known-good CDB。 +- 删除旧 `mcpp ide configure --format ndjson` 解析层和重复的 `mcpp.configureIde` 命令; + configure-only 与 build/run/test 共用项目操作锁,manifest 与 `mcpp.path` 变化按工程协调。 +- CDB watcher 只重读已发布数据库,不反向触发 configure-only;多根工作区按事件 URI 路由, + 显式刷新会分别报告 CDB 生成与 clangd 协调结果。 +- 对齐 mcpp #387 的最终 workspace 契约:virtual workspace 根不作为单一 clangd 工程, + 扩展消费当前活动 member 根的 CDB;rooted workspace 仍按根 package 处理。 + ## 0.2.7 - 修复「一键配置模块代码提示」在标准 mcpp 安装(install.sh / AUR)下无法发现 mcpp 内置 diff --git a/README.md b/README.md index 3b0792d..68e94e4 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,9 @@ code --install-extension /path/to/mcpp-vscode-0.2.7.vsix - VS Code 1.90 或更高版本。 - 包含 `mcpp.toml` 的 mcpp 工程。 -- 可执行的 mcpp。它可以由 xlings、官方独立安装脚本或其他受支持方式安装。 +- 包含 mcpp #387(合并提交 `3f237ed`)的可执行文件;在该能力进入正式版本前, + 可使用对应主线构建。旧版 mcpp 不支持 configure-only 时,扩展会保留原有 CDB 并提示升级。 + 它可以由 xlings、官方独立安装脚本或其他受支持方式安装。 - 官方 `llvm-vs-code-extensions.vscode-clangd` 扩展。它是本扩展的功能依赖。 - 要启用模块语义,需要 LLVM mcpp 工具链、工程编译数据库,以及与该工具链来自 同一 LLVM revision 的 clangd。 @@ -57,16 +59,21 @@ xlings 安装。 ## 快速开始 -1. 使用 LLVM 工具链构建一次项目,使 mcpp 在工程根目录生成 - `compile_commands.json` 和所需 PCM。 -2. 使用 VS Code 打开包含 `mcpp.toml` 的目录。 -3. 扩展会自动分析编译数据库、选择匹配的 clangd、写入工作区配置并检查模块支持。 +1. 使用 VS Code 打开包含 `mcpp.toml` 的目录,并按需信任工作区。 +2. 工程没有有效 `compile_commands.json` 时,扩展会运行 + `mcpp build --configure-only` 生成配置阶段数据,不编译普通对象或链接产物。 +3. 扩展会分析编译数据库、选择匹配的 clangd、写入工作区配置并检查模块支持;缺少 + 项目 PCM 或需要验证正式产物时,仍应执行完整构建或使用一键配置向导。 4. 查看状态栏中的 `mcpp: 模块可用`、`mcpp: 模块不可用` 或 `mcpp: 缺少模块 CDB` 状态。 工程还没有编译数据库时,可以点击“缺少模块 CDB”,或执行 -**mcpp: 刷新编译数据库**。这个命令当前实际执行一次 `mcpp build`,并在任务结束后 -重新协调 CDB 和 clangd。 +**mcpp: 刷新编译数据库**。这个命令执行 `mcpp build --configure-only`,只在退出码为 +0 且新 CDB 可解析时将其视为成功;失败时继续保留并使用原有可用 CDB。 + +virtual workspace 根不对应单一 CDB:mcpp 会把数据库分别发布到各 member 根。扩展 +因此只接管当前活动 member;请先打开该 member 的源码或 `mcpp.toml`,不会把多个 member +的 CDB 合并到虚拟根。 ## 已实现功能 @@ -78,7 +85,8 @@ xlings 安装。 - 打开已有有效 CDB 的 LLVM 工程后自动配置并检查,不需要先点击状态栏或 Reload Window。 -扩展激活本身不会静默运行 `mcpp build`,也不会自动下载、安装或切换工具链。 +扩展激活不会静默运行完整 `mcpp build`,也不会自动下载、安装或切换工具链;受信任 +工程缺少有效 CDB 时会自动运行配置阶段的 `mcpp build --configure-only`。 ### 语法高亮 @@ -141,8 +149,9 @@ xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB - 只接受当前工程最新一次模块检查的结果,迟到结果不会覆盖新状态。 - 多根工作区中只有活动 mcpp 工程接管窗口级 clangd 配置;后台工程只失效缓存,切换 过去时再协调。 -- 修改 `mcpp.clangd.path` 或 `mcpp.modulesSupport` 后自动重新配置和检查。 -- 构建即使因普通源码错误退出,只要留下可用 CDB,扩展仍会尝试恢复 IDE 状态,并把 +- 修改资源域 `mcpp.path` 时按工程标记重新生成 CDB;修改 `mcpp.clangd.path` 或 + `mcpp.modulesSupport` 时只重新配置和检查,不重复运行 mcpp。 +- 完整构建即使因普通源码错误退出,只要留下可用 CDB,扩展仍会尝试恢复 IDE 状态,并把 “构建失败”和“IDE 数据可用”分别报告。 ### mcpp CLI 与工具链管理 @@ -175,7 +184,7 @@ xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB | **mcpp: 安装工具链** | 确认后执行 `mcpp toolchain install ` | | **mcpp: 选择全局默认工具链** | 确认后执行 `mcpp toolchain default ` | | **mcpp: 配置 clangd** | 手动重新应用当前 LLVM 工程的 clangd 配置 | -| **mcpp: 刷新编译数据库** | 执行 `mcpp build`,随后重新读取 CDB 并协调 clangd | +| **mcpp: 刷新编译数据库** | 执行 `mcpp build --configure-only`,验证 CDB 后协调 clangd;失败时保留原有可用 CDB | | **mcpp: 检查模块支持** | 立即执行 clangd 直接检查并刷新模块状态 | | **mcpp: 一键配置模块代码提示** | 一次确认后自动安装/切换 host LLVM、执行 `mcpp build`,通过 xlings 补齐匹配 llvm-tools,重载 CDB 并配置 clangd;项目显式固定非 LLVM 工具链时停止 | @@ -233,11 +242,12 @@ GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它 不能直接消费;当前版本没有 GCC 原生模块语言服务器、cpptools 模块后端或 LLVM 影子 分析副本。因此 GCC/MSVC 工程只保证模块语法高亮和 mcpp 操作。 -### 首次语义配置仍需要构建数据 +### 首次语义配置仍需要 mcpp 生成的数据 当前插件只读取 `compile_commands.json`,不解析 `mcpp.toml`,也不自行推导依赖和 -模块图。工程没有 CDB 和 PCM 时,必须先显式执行构建或“刷新编译数据库”。插件不会 -因为打开工程就自动构建,也不会自行生成、改写或修复 mcpp 没有输出的 CDB。 +模块图。受信任工程没有有效 CDB 时,插件会调用 `mcpp build --configure-only`,但不会 +自行生成或改写 mcpp 没有输出的 CDB。完整模块语义仍取决于 CDB 引用的兼容 PCM;正式 +构建、链接和测试仍由普通 `mcpp build`、`run` 或 `test` 负责。 ### clangd 模块支持仍是实验能力 @@ -253,7 +263,9 @@ GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它 未受信任工作区只启用文件关联和语法高亮,不执行 CDB 中的编译器、mcpp、clangd 或 工程指定程序,也不接管 clangd 配置。授予信任后,扩展会自动重新协调当前工程;仍不 -会在没有用户操作时下载工具链或发起构建。 +会在没有用户操作时下载或切换工具链,也不会发起完整构建;缺少有效 CDB 时可以执行 +配置阶段的 configure-only。该命令不是只读操作:mcpp 仍可能执行 `build.mcpp`、解析或 +安装缺失依赖和工具链,并写入 lock、构建目录与相关元数据。 ## 当前版本能达到的效果 @@ -265,8 +277,9 @@ GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它 - 在重新构建或 CDB 变化后自动刷新,不要求 Reload Window; - 通过 VS Code 任务完成常用 mcpp 项目和工具链操作。 -这些能力受 clangd 实验模块实现和 PCM 兼容性约束。工程没有 CDB 时,当前版本只能 -提供语法高亮、mcpp 命令和引导操作,不能仅凭插件恢复完整模块语义。 +这些能力受 clangd 实验模块实现和 PCM 兼容性约束。工程没有 CDB 时,扩展可以请求 +mcpp 生成配置阶段 CDB;若 mcpp 版本不支持该命令、配置失败或仍缺少兼容 PCM,则只能 +提供语法高亮、mcpp 命令和引导操作。 ## 理想最终效果 @@ -288,12 +301,13 @@ GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它 ## 达到理想效果所需支持 -下面按完成程度区分必要工作。接口名称只是设计示例,不是当前 mcpp 已提供的命令。 +下面按完成程度区分必要工作。`mcpp build --configure-only` 是当前扩展采用的已实现接口; +其余机器可读工程模型和结构化诊断仍是后续协议工作。 | 支持项 | 当前程度 | 理想完成标准 | 优先级 | | --- | --- | --- | --- | | 机器可读工程模型 | 插件只能读取 CDB,mcpp 主要输出面向人的文本 | mcpp 输出带版本 schema 的成员、目标、源码、模块、依赖、工具链、缓存与诊断 | 必需 | -| IDE 配置阶段 | 必须通过正式构建间接生成 CDB | 提供类似 `mcpp resolve --message-format=json --emit-cdb` 或 `mcpp configure --ide` 的可取消命令 | 必需 | +| IDE 配置阶段 | 已使用 `mcpp build --configure-only` 生成并验证 CDB | 增加结构化进度、取消和能力协商,不把人类 stdout 当协议解析 | 部分完成 | | IDE 专用模块产物 | PCM 是正式构建的副产物 | 只构建 `std`、依赖和项目模块 PCM,不编译普通对象或链接,并按工具链身份隔离缓存 | 必需 | | 完整 CDB 与模块图 | LLVM CDB 已有部分显式 PCM 参数 | producer 和 consumer 都使用确定的绝对 PCM 路径,覆盖分区、依赖包、成员和测试目标 | 必需 | | 结构化诊断 | 未知导入等信息主要来自构建或 clangd 文本 | mcpp 输出文件、行、列、严重级别和稳定错误码,插件可精确定位配置期错误 | 必需 | @@ -311,21 +325,24 @@ GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它 ### 阶段 1:当前版本 -- 使用用户显式构建后产生的 LLVM CDB 和 PCM。 +- 缺少有效 CDB 时,在受信任工作区自动执行 `mcpp build --configure-only`;失败时保留 + last-known-good CDB。 +- 使用 configure-only 或用户完整构建产生的 LLVM CDB 和已有 PCM。 - 自动配置 clangd、检查模块、监听变化并提供 CLI/工具链菜单。 - 一键自动安装匹配的 llvm-tools 并配置模块代码提示。 - GCC/MSVC 保持语法高亮和 mcpp 构建操作,不增加语义后端。 -### 阶段 2:mcpp IDE 协议 +### 阶段 2:机器可读工程协议增强 -- 在 mcpp 核心增加带版本的机器可读工程模型和 IDE 配置命令。 -- 生成完整模块图、结构化诊断、IDE CDB 和专用 PCM。 +- 在 mcpp 核心增加带版本的机器可读工程模型,不改变 configure-only 的退出码 + CDB + 客户端契约。 +- 补充完整模块图、结构化诊断、IDE 专用 PCM 和能力协商。 - 明确定义工作区成员、配置身份、缓存路径、进度和取消协议。 -### 阶段 3:打开即用的 LLVM 体验 +### 阶段 3:完善打开即用的 LLVM 体验 -- 插件在工作区信任后自动调用 IDE 配置阶段,而不是完整构建。 -- 支持离线缓存、last-known-good、增量刷新、过期状态和多根隔离。 +- 在已有自动 configure-only 基础上增加可取消进度、离线缓存、增量刷新和过期状态。 +- 完善按工程隔离的 last-known-good 数据与多根集成验证。 - 以真实 VS Code 端到端测试作为模块诊断和跳转能力的发布门槛。 ### 未来重新评估 GCC/MSVC @@ -355,10 +372,10 @@ macOS 从图形界面启动 VS Code 时可能没有继承终端 `PATH`。将 `mc ### 缺少模块 CDB -当前版本不会在打开工程时静默构建。执行 **mcpp: 刷新编译数据库**,或在终端运行 -`mcpp build`。无论构建成功还是失败,只要工程根目录仍没有 -`compile_commands.json`,插件都会保持“缺少模块 CDB”状态;请查看 mcpp 输出和任务 -终端,确认当前构建是否实际生成了 CDB。 +受信任工程缺少有效 CDB 时会自动尝试 `mcpp build --configure-only`,也可以手动执行 +**mcpp: 刷新编译数据库**。若 mcpp 版本不支持该参数、命令失败或生成的 CDB 无法解析, +插件会保持“缺少模块 CDB”状态,并保留原有可用 CDB;请查看 `mcpp` 输出频道。需要 +编译普通对象、生成完整项目产物或验证链接时,再在终端运行 `mcpp build`。 ### clangd 未安装或无法匹配 diff --git a/package.json b/package.json index 6692245..d0232c2 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "onCommand:mcpp.showToolchains", "onCommand:mcpp.installToolchain", "onCommand:mcpp.selectDefaultToolchain", - "onCommand:mcpp.configureIde", "onCommand:mcpp.autoConfigureModules" ], "categories": [ @@ -105,10 +104,6 @@ "command": "mcpp.selectDefaultToolchain", "title": "mcpp: 选择全局默认工具链" }, - { - "command": "mcpp.configureIde", - "title": "mcpp: 生成 IDE 编译数据库" - }, { "command": "mcpp.configureClangd", "title": "mcpp: 配置 clangd" diff --git a/src/cliController.ts b/src/cliController.ts index 2416b72..12cbaf0 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -15,7 +15,8 @@ import { type ToolchainItem, } from "./cli"; import type { McppProjectDiscovery } from "./discovery"; -import { runProcess } from "./process"; +import { runConfigureOnly as runConfigureOnlyProcess } from "./configureOnly"; +import { runProcess, type ProcessResult } from "./process"; import { McppOperationRegistry, classifyTaskExit, @@ -118,6 +119,30 @@ export class McppCliController { return this.operations.hasActive(); } + public async runConfigureOnly( + project: McppProjectDiscovery, + ): Promise { + if (!this.options.isTrusted()) { + return undefined; + } + + const token: OperationToken = {}; + if (this.operations.beginProject(project.root, token) !== undefined) { + return undefined; + } + + const executable = this.mcppExecutable(project); + const args = ["build", "--configure-only"]; + try { + const result = await runConfigureOnlyProcess(project.root, executable); + this.appendShortCommand("刷新编译数据库", executable, args, result); + return result; + } finally { + // configure-only 与 build/run/test 共用项目锁,异常时也必须释放。 + this.operations.finishProject(project.root, token); + } + } + public async runAutomaticModuleSetup( plan: Extract, ): Promise { diff --git a/src/configureOnly.ts b/src/configureOnly.ts new file mode 100644 index 0000000..a677a74 --- /dev/null +++ b/src/configureOnly.ts @@ -0,0 +1,11 @@ +import { runProcess, type ProcessResult, type ProcessRunner } from "./process"; + +export const configureOnlyArguments = ["build", "--configure-only"] as const; + +export function runConfigureOnly( + projectRoot: string, + executable = "mcpp", + runner: ProcessRunner = runProcess, +): Promise { + return runner(executable, [...configureOnlyArguments], projectRoot); +} diff --git a/src/discovery.ts b/src/discovery.ts index e5d0efc..64c461c 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -1,15 +1,30 @@ import { existsSync, + readFileSync, statSync, } from "node:fs"; import path from "node:path"; +import { parseMcppToml } from "./mcppTomlParser"; + export interface McppProjectDiscovery { root: string; manifestPath: string; compilationDatabasePath: string; } +export function manifestProjectRoot(manifestPath: string): string { + return path.dirname(path.resolve(manifestPath)); +} + +export function shouldReconcileDeletedManifest( + currentProjectRoot: string | undefined, + manifestPath: string, +): boolean { + return currentProjectRoot !== undefined + && path.resolve(currentProjectRoot) === manifestProjectRoot(manifestPath); +} + function isWindowsPath(value: string): boolean { return /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\"); } @@ -18,7 +33,7 @@ function unique(values: string[]): string[] { return values.filter((value, index) => values.indexOf(value) === index); } -function isPathWithin(candidate: string, root: string): boolean { +export function isPathWithinProject(candidate: string, root: string): boolean { const relative = path.relative(root, candidate); return relative === "" || ( relative !== ".." @@ -27,6 +42,58 @@ function isPathWithin(candidate: string, root: string): boolean { ); } +interface ManifestShape { + hasPackage: boolean; + hasWorkspace: boolean; +} + +function readManifestShape(manifestPath: string): ManifestShape { + try { + const document = parseMcppToml(readFileSync(manifestPath, "utf8").split(/\r?\n/)); + let hasPackage = false; + let hasWorkspace = false; + let insideSection = false; + for (const node of document.nodes) { + if (node.type === "section") { + insideSection = true; + if (node.segments.length === 1) { + hasPackage ||= node.segments[0].name === "package"; + hasWorkspace ||= node.segments[0].name === "workspace"; + } + continue; + } + // mcpp 同样接受顶层 dotted key 和 inline table 写法。 + if (!insideSection) { + hasPackage ||= node.keyPath[0]?.name === "package"; + hasWorkspace ||= node.keyPath[0]?.name === "workspace"; + } + } + return { hasPackage, hasWorkspace }; + } catch { + // 无法读取时保留原发现结果,让 mcpp 自己给出清单诊断。 + return { hasPackage: false, hasWorkspace: false }; + } +} + +export function projectAffectedByManifest( + manifestPath: string, + currentProject: McppProjectDiscovery | undefined, + manifestProject: McppProjectDiscovery | undefined, +): McppProjectDiscovery | undefined { + const manifestRoot = manifestProjectRoot(manifestPath); + const shape = readManifestShape(manifestPath); + if ( + shape.hasWorkspace + && currentProject !== undefined + && currentProject.root !== manifestRoot + && isPathWithinProject(currentProject.root, manifestRoot) + ) { + // member 会继承 workspace 根配置,根清单变化必须刷新当前 member 的 CDB。 + return currentProject; + } + return manifestProject; +} + export function findNearestMcppProject( startPath: string, workspaceRoot?: string, @@ -42,13 +109,19 @@ export function findNearestMcppProject( // 新创建的 VS Code 工作区路径可能尚不存在,此时按目录处理。 } - if (boundary !== undefined && !isPathWithin(current, boundary)) { + if (boundary !== undefined && !isPathWithinProject(current, boundary)) { return undefined; } while (true) { const manifestPath = path.join(current, "mcpp.toml"); if (existsSync(manifestPath)) { + // #387 的 virtual workspace fan-out 只在各 member 根发布 CDB; + // 虚拟根不是 clangd 可消费的单一 package 工程。 + const shape = readManifestShape(manifestPath); + if (shape.hasWorkspace && !shape.hasPackage) { + return undefined; + } return { root: current, manifestPath, diff --git a/src/extension.ts b/src/extension.ts index f3d126c..fdb7476 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,10 @@ import { import { deriveClangdCandidates, findNearestMcppProject, + isPathWithinProject, + manifestProjectRoot, + projectAffectedByManifest, + shouldReconcileDeletedManifest, type McppProjectDiscovery, } from "./discovery"; import { @@ -25,15 +29,16 @@ import { } from "./llvmTools"; import { CLI_COMMANDS } from "./commands"; import { McppCliController } from "./cliController"; -import { runClangdCheck, runToolVersion, type ToolVersionResult } from "./process"; -import { runIdeConfigure, type IdeSnapshotPublished } from "./ideProtocol"; +import { runClangdCheck, runToolVersion, type ProcessResult, type ToolVersionResult } from "./process"; import { ensureIdeConfigured } from "./ideWorkflow"; import { configurationReadyAfterRestart, + configurationAffectsMcppExecution, configurationAffectsModuleSupport, createKeyedSingleFlightReconciler, createLatestOperationTracker, createSerialExecutor, + describeConfigureOnlyOutcome, describeRefreshOutcome, moduleSupportState, registerCompilationDatabaseReconciliation, @@ -57,7 +62,6 @@ import { } from "./moduleSetup"; const COMMAND_CONFIGURE = "mcpp.configureClangd"; -const COMMAND_IDE_CONFIGURE = "mcpp.configureIde"; const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; const COMMAND_CHECK = "mcpp.checkModuleSupport"; @@ -89,46 +93,9 @@ const moduleStatusByProject = new Map(); const moduleCheckOperations = createLatestOperationTracker(); // 记录 manifest/settings 变化触发的 IDE 配置请求;CDB watcher 只重读 // 已发布快照,避免写 CDB 后再次启动 mcpp 形成重入。 -const forceIdeConfigureByProject = new Set(); +const forceConfigureOnlyByProject = new Set(); let lastReconciledProjectRoot: string | undefined; -function mcppExecutable(project: McppProjectDiscovery): string { - const configured = projectConfiguration(project).get("path", "").trim(); - return configured.length === 0 ? "mcpp" : configured; -} - -async function runIdeConfigureForProject( - project: McppProjectDiscovery, - output: vscode.OutputChannel, - interactive: boolean, -): Promise { - if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { - appendOutputLine(output, "[IDE 配置] 工作区未受信任,跳过 mcpp ide configure。"); - if (interactive) { - await vscode.window.showWarningMessage( - "当前工作区未受信任,mcpp 不会执行 IDE 配置。请先信任工作区。", - ); - } - return undefined; - } - - const executable = mcppExecutable(project); - const args = ["ide", "configure", "--format", "ndjson"]; - try { - const result = await runIdeConfigure(project.root, executable); - appendProcessOutput(output, "IDE 配置", executable, args, result); - appendOutputLine(output, `[IDE 配置] 已发布 CDB:${result.compileCommands}`); - return result; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[IDE 配置] ${message}`); - if (interactive) { - await vscode.window.showErrorMessage(`mcpp IDE 配置失败:${message}`); - } - return undefined; - } -} - function findCurrentProject(): McppProjectDiscovery | undefined { const activeEditor = vscode.window.activeTextEditor; if (activeEditor !== undefined) { @@ -144,7 +111,10 @@ function findCurrentProject(): McppProjectDiscovery | undefined { } for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { - const project = findNearestMcppProject(workspaceFolder.uri.fsPath); + const project = findNearestMcppProject( + workspaceFolder.uri.fsPath, + workspaceFolder.uri.fsPath, + ); if (project !== undefined) { return project; } @@ -152,6 +122,33 @@ function findCurrentProject(): McppProjectDiscovery | undefined { return undefined; } +function findProjectForUri(uri: vscode.Uri): McppProjectDiscovery | undefined { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + if (workspaceFolder === undefined) { + return undefined; + } + return findNearestMcppProject(uri.fsPath, workspaceFolder.uri.fsPath); +} + +function findWorkspaceProjects( + currentProject: McppProjectDiscovery | undefined, +): McppProjectDiscovery[] { + const projects = new Map(); + if (currentProject !== undefined) { + projects.set(currentProject.root, currentProject); + } + for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { + const project = findNearestMcppProject( + workspaceFolder.uri.fsPath, + workspaceFolder.uri.fsPath, + ); + if (project !== undefined) { + projects.set(project.root, project); + } + } + return [...projects.values()]; +} + function loadProjectContext(project: McppProjectDiscovery | undefined = findCurrentProject()): ProjectContext | undefined { if (project === undefined) { return undefined; @@ -185,6 +182,10 @@ function loadProjectContext(project: McppProjectDiscovery | undefined = findCurr } } +function hasUsableCompilationDatabase(project: McppProjectDiscovery): boolean { + return loadProjectContext(project)?.analysis.capability !== "unavailable"; +} + function moduleSetupBlockedMessage(reason: ModuleSetupBlockedReason): string { switch (reason) { case "project-toolchain-override": @@ -915,7 +916,8 @@ const mcppTomlCompletionProvider: vscode.CompletionItemProvider = { export async function activate(extensionContext: vscode.ExtensionContext): Promise { moduleStatusByProject.clear(); moduleCheckOperations.clear(); - forceIdeConfigureByProject.clear(); + forceConfigureOnlyByProject.clear(); + lastReconciledProjectRoot = undefined; const output = vscode.window.createOutputChannel("mcpp"); const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); @@ -929,6 +931,31 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi void vscode.window.showErrorMessage(`mcpp:${message}`); } }; + let cliController: McppCliController; + const runConfigureOnlyForProject = async ( + project: McppProjectDiscovery, + interactive: boolean, + ): Promise => { + if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { + appendOutputLine(output, "[CDB 配置] 工作区未受信任,跳过 mcpp build --configure-only。"); + if (interactive) { + await vscode.window.showWarningMessage( + "当前工作区未受信任,不会刷新编译数据库。请先信任工作区。", + ); + } + return undefined; + } + const result = await cliController.runConfigureOnly(project); + if (result === undefined) { + appendOutputLine(output, "[CDB 配置] 已有 mcpp 操作正在运行,本次刷新已跳过。"); + if (interactive) { + await vscode.window.showWarningMessage( + "已有 mcpp 操作正在运行,暂不能刷新编译数据库。请等待当前操作完成后重试。", + ); + } + } + return result; + }; const manifestWatcher = vscode.workspace.createFileSystemWatcher(MCPP_MANIFEST_GLOB); const compilationDatabaseWatcher = vscode.workspace.createFileSystemWatcher("**/compile_commands.json"); const inProjectContext = registerInProjectContext({ @@ -945,7 +972,8 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi const reconcileProjectContext = async ( project: McppProjectDiscovery | undefined, forceRestart: boolean, - forceIdeConfigure: boolean = false, + forceConfigureOnly: boolean = false, + allowConfigureOnly: boolean = true, ): Promise => { let context = loadProjectContext(project); if (context === undefined) { @@ -957,29 +985,35 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi }; } - // 首次打开工程时,先发布配置阶段 CDB,再让 clangd 读取它;普通源码 - // 是否能编译成功不参与这个决定。已有 CDB 仍走兼容的旧流程。 - try { - const outcome = await ensureIdeConfigured({ - projectRoot: context.project.root, - compilationDatabasePath: context.project.compilationDatabasePath, - trusted: vscode.workspace.isTrusted, - force: forceIdeConfigure, - databaseExists: () => existsSync(context?.project.compilationDatabasePath ?? ""), - configure: async () => { - const published = await runIdeConfigureForProject(context!.project, output, false); - if (published === undefined) { - throw new Error("mcpp ide configure 未发布快照"); - } - return published; - }, - }); - if (outcome.state === "configured") { - context = loadProjectContext(context.project) ?? context; + // 缺少可用 CDB 时只运行配置阶段;完整 build/test 后则只重读已有 CDB。 + if (allowConfigureOnly) { + try { + const outcome = await ensureIdeConfigured({ + projectRoot: context.project.root, + compilationDatabasePath: context.project.compilationDatabasePath, + trusted: vscode.workspace.isTrusted, + force: forceConfigureOnly, + databaseValid: () => hasUsableCompilationDatabase(context!.project), + configure: async () => { + const result = await runConfigureOnlyForProject(context!.project, false); + if (result === undefined) { + throw new Error("已有 mcpp 操作正在运行"); + } + if (forceConfigureOnly) { + forceConfigureOnlyByProject.delete(context!.project.root); + } + return result; + }, + }); + if (outcome.state === "configured") { + context = loadProjectContext(context.project) ?? context; + } else if (outcome.state === "failed") { + appendOutputLine(output, `[自动配置] mcpp configure-only 失败(退出码 ${outcome.exitCode})。`); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[自动配置] ${message}`); } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[自动配置] ${message}`); } updateStatusBar(status, context); @@ -1014,11 +1048,26 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi return reconcileProjectContext( project, forceRestart, - forceIdeConfigureByProject.delete(projectRoot), + forceConfigureOnlyByProject.has(projectRoot), ); }, ), ); + const reconcilePublishedCdbByRoot = createKeyedSingleFlightReconciler( + (projectRoot: string, forceRestart) => executeWithWorkspaceClangd( + async () => { + const project = findNearestMcppProject(projectRoot, projectRoot); + if (project === undefined) { + return { + context: undefined, + databaseFound: false, + configured: false, + }; + } + return reconcileProjectContext(project, forceRestart, false, false); + }, + ), + ); const reconcileProject = ( project: McppProjectDiscovery, forceRestart: boolean = false, @@ -1030,7 +1079,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi compilationDatabase: vscode.Uri, forceRestart: boolean, ): void => { - const project = findNearestMcppProject(compilationDatabase.fsPath); + const project = findProjectForUri(compilationDatabase); if (project === undefined) { return; } @@ -1038,7 +1087,8 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { return; } - void reconcileProject(project, forceRestart).catch((error: unknown) => { + // CDB watcher 只重读已经发布的数据库,避免配置阶段写 CDB 后再次启动 mcpp。 + void reconcilePublishedCdbByRoot(project.root, forceRestart).catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); appendOutputLine(output, `[自动配置] ${message}`); }); @@ -1046,30 +1096,88 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi const requestCurrentProjectReconciliation = (forceRestart: boolean): void => { const project = findCurrentProject(); if (project === undefined) { + lastReconciledProjectRoot = undefined; refreshStatus(); return; } + lastReconciledProjectRoot = project.root; void reconcileProject(project, forceRestart).catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); appendOutputLine(output, `[自动配置] ${message}`); }); }; - const configurationWatcher = vscode.workspace.onDidChangeConfiguration((event) => { - const project = findCurrentProject(); + const requestManifestReconciliation = (manifestUri: vscode.Uri): void => { + refreshStatus(); + cliController.refreshStatus(); + const currentProject = findCurrentProject(); + const project = projectAffectedByManifest( + manifestUri.fsPath, + currentProject, + findProjectForUri(manifestUri), + ); if (project === undefined) { return; } - const uri = vscode.Uri.file(project.root); + forceConfigureOnlyByProject.add(project.root); + if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { + return; + } + lastReconciledProjectRoot = project.root; + void reconcileProject(project, true).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + appendOutputLine(output, `[自动配置] ${message}`); + }); + }; + const requestDeletedManifestReconciliation = (manifestUri: vscode.Uri): void => { + refreshStatus(); + cliController.refreshStatus(); + const deletedProjectRoot = manifestProjectRoot(manifestUri.fsPath); + forceConfigureOnlyByProject.delete(deletedProjectRoot); + invalidateModuleStatus(deletedProjectRoot); + if (!shouldReconcileDeletedManifest(lastReconciledProjectRoot, manifestUri.fsPath)) { + return; + } + + lastReconciledProjectRoot = undefined; + const fallbackProject = findCurrentProject(); + if ( + fallbackProject !== undefined + && isPathWithinProject(manifestProjectRoot(manifestUri.fsPath), fallbackProject.root) + ) { + forceConfigureOnlyByProject.add(fallbackProject.root); + } + requestCurrentProjectReconciliation(true); + }; + const configurationWatcher = vscode.workspace.onDidChangeConfiguration((event) => { + const currentProject = findCurrentProject(); + let currentExecutionChanged = false; + for (const project of findWorkspaceProjects(currentProject)) { + const uri = vscode.Uri.file(project.root); + if (!configurationAffectsMcppExecution( + (section) => event.affectsConfiguration(section, uri), + )) { + continue; + } + forceConfigureOnlyByProject.add(project.root); + currentExecutionChanged ||= project.root === currentProject?.root; + } + if (currentExecutionChanged) { + requestCurrentProjectReconciliation(true); + return; + } + if (currentProject === undefined) { + return; + } + const uri = vscode.Uri.file(currentProject.root); if (configurationAffectsModuleSupport( (section) => event.affectsConfiguration(section, uri), )) { - forceIdeConfigureByProject.add(project.root); requestCurrentProjectReconciliation(true); } }); const trustWatcher = vscode.workspace.onDidGrantWorkspaceTrust(() => { const project = findCurrentProject(); - if (project !== undefined) forceIdeConfigureByProject.add(project.root); + if (project !== undefined) forceConfigureOnlyByProject.add(project.root); requestCurrentProjectReconciliation(true); }); @@ -1084,7 +1192,15 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi return; } - const reconciled = await reconcileProject(project, true); + const configureOnlyPending = forceConfigureOnlyByProject.has(project.root); + const reconciled = await executeWithWorkspaceClangd( + () => reconcileProjectContext( + project, + true, + configureOnlyPending, + configureOnlyPending, + ), + ); if (kind === "build") { if (reconciled.context?.analysis.capability === "syntax-only") { const buildMessage = completion.state === "succeeded" ? "mcpp 构建完成" : "mcpp 构建失败"; @@ -1113,7 +1229,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await vscode.window.showInformationMessage(`mcpp ${label}完成;clangd/CDB 状态已重新检查。`); } }; - const cliController = new McppCliController({ + cliController = new McppCliController({ output, currentProject: findCurrentProject, afterProjectTask, @@ -1147,44 +1263,35 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await updateModuleSupportForContext(context, configured, status, output); }); })), - vscode.commands.registerCommand(COMMAND_IDE_CONFIGURE, runGuarded(async () => { + vscode.commands.registerCommand(COMMAND_REFRESH, runGuarded(async () => { const project = findCurrentProject(); if (project === undefined) { await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); return; } await executeWithWorkspaceClangd(async () => { - const context = loadProjectContext(project); - if (context === undefined) { + const hadUsableDatabase = hasUsableCompilationDatabase(project); + const result = await runConfigureOnlyForProject(project, true); + if (result === undefined) { return; } - try { - const outcome = await ensureIdeConfigured({ - projectRoot: project.root, - compilationDatabasePath: project.compilationDatabasePath, - trusted: vscode.workspace.isTrusted, - force: true, - databaseExists: () => existsSync(project.compilationDatabasePath), - configure: async () => { - const published = await runIdeConfigureForProject(project, output, true); - if (published === undefined) { - throw new Error("mcpp ide configure 未发布快照"); - } - return published; - }, - }); - if (outcome.state === "configured") { - await reconcileProjectContext(project, true); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[IDE 配置] ${message}`); + const reconciled = await reconcileProjectContext(project, true, false, false); + const databaseValid = reconciled.context?.analysis.capability !== "unavailable"; + const outcome = describeConfigureOnlyOutcome( + result.exitCode, + databaseValid, + hadUsableDatabase && result.exitCode !== 0, + reconciled.configured, + ); + if (outcome.level === "information") { + await vscode.window.showInformationMessage(outcome.message); + } else if (outcome.level === "warning") { + await vscode.window.showWarningMessage(outcome.message); + } else { + await vscode.window.showErrorMessage(outcome.message); } }); })), - vscode.commands.registerCommand(COMMAND_REFRESH, runGuarded(async () => { - await cliController.runProjectTask("build"); - })), vscode.commands.registerCommand(COMMAND_CHECK, runGuarded(async () => { const project = findCurrentProject(); if (project === undefined) { @@ -1237,7 +1344,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await runModuleSupportCheck(ctx, status, output, "interactive"); return; case "rebuild": - await cliController.runProjectTask("build"); + await vscode.commands.executeCommand(COMMAND_REFRESH); return; } } @@ -1325,27 +1432,11 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi ); extensionContext.subscriptions.push( - manifestWatcher.onDidCreate(() => { - refreshStatus(); - cliController.refreshStatus(); - const project = findCurrentProject(); - if (project !== undefined) forceIdeConfigureByProject.add(project.root); - requestCurrentProjectReconciliation(true); - }), - manifestWatcher.onDidChange(() => { - refreshStatus(); - cliController.refreshStatus(); - const project = findCurrentProject(); - if (project !== undefined) forceIdeConfigureByProject.add(project.root); - requestCurrentProjectReconciliation(true); - }), - manifestWatcher.onDidDelete(() => { - refreshStatus(); - cliController.refreshStatus(); - const project = findCurrentProject(); - if (project !== undefined) forceIdeConfigureByProject.add(project.root); - requestCurrentProjectReconciliation(true); + manifestWatcher.onDidCreate((manifestUri) => requestManifestReconciliation(manifestUri)), + manifestWatcher.onDidChange((manifestUri) => { + requestManifestReconciliation(manifestUri); }), + manifestWatcher.onDidDelete((manifestUri) => requestDeletedManifestReconciliation(manifestUri)), ...registerCompilationDatabaseReconciliation( compilationDatabaseWatcher, requestAutomaticReconciliation, @@ -1357,6 +1448,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (project === undefined) { await reconcileProjectContext(undefined, false); } else { + lastReconciledProjectRoot = project.root; await reconcileProject(project, false); } } catch (error) { @@ -1368,6 +1460,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi export function deactivate(): void { moduleStatusByProject.clear(); moduleCheckOperations.clear(); - forceIdeConfigureByProject.clear(); + forceConfigureOnlyByProject.clear(); + lastReconciledProjectRoot = undefined; // VS Code 会释放 activate() 注册的所有订阅。 } diff --git a/src/ideProtocol.ts b/src/ideProtocol.ts deleted file mode 100644 index 5b9faad..0000000 --- a/src/ideProtocol.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { runProcess, type ProcessResult, type ProcessRunner } from "./process"; - -export interface IdeSnapshotPublished { - phase: "configured" | "ready"; - compileCommands: string; - compatibilityCompileCommands?: string; - snapshotId?: string; - configurationId?: string; -} - -interface IdeEvent { - seq: number; - type: string; - phase?: string; - compileCommands?: unknown; - compatibilityCompileCommands?: unknown; - snapshotId?: unknown; - configurationId?: unknown; - status?: unknown; -} - -function parseEvent(line: string): IdeEvent { - let value: unknown; - try { - value = JSON.parse(line); - } catch { - throw new Error("mcpp ide configure 输出包含无效 JSON"); - } - if (value === null || typeof value !== "object") { - throw new Error("mcpp ide configure 事件不是对象"); - } - const event = value as Partial; - if (typeof event.seq !== "number" || !Number.isSafeInteger(event.seq) - || event.seq <= 0 || typeof event.type !== "string") { - throw new Error("mcpp ide configure 事件缺少有效 seq/type"); - } - return event as IdeEvent; -} - -export function parseIdeConfigureOutput(output: string): IdeSnapshotPublished { - let previousSeq = 0; - let published: IdeSnapshotPublished | undefined; - let finished = false; - for (const line of output.split(/\r?\n/)) { - if (line.trim().length === 0) { - continue; - } - const event = parseEvent(line); - if (event.seq <= previousSeq) { - throw new Error("mcpp ide configure 事件序号倒退"); - } - previousSeq = event.seq; - if (event.type !== "snapshot-published") { - if (event.type === "operation-finished") { - if (event.status !== "success") { - throw new Error("mcpp ide configure 未成功完成"); - } - finished = true; - } - continue; - } - if ((event.phase !== "configured" && event.phase !== "ready") - || typeof event.compileCommands !== "string") { - throw new Error("mcpp ide configure 发布事件缺少 CDB"); - } - published = { - phase: event.phase, - compileCommands: event.compileCommands, - compatibilityCompileCommands: - typeof event.compatibilityCompileCommands === "string" - ? event.compatibilityCompileCommands - : undefined, - snapshotId: typeof event.snapshotId === "string" ? event.snapshotId : undefined, - configurationId: typeof event.configurationId === "string" ? event.configurationId : undefined, - }; - } - if (published === undefined) { - throw new Error("mcpp ide configure 没有发布快照"); - } - if (!finished) { - throw new Error("mcpp ide configure 未成功完成"); - } - return published; -} - -export async function runIdeConfigure( - projectRoot: string, - executable = "mcpp", - runner: ProcessRunner = runProcess, -): Promise { - const args = ["ide", "configure", "--format", "ndjson"]; - const result = await runner(executable, args, projectRoot); - if (result.exitCode !== 0) { - throw new Error(result.stderr.trim() || `mcpp ide configure 失败(退出码 ${result.exitCode})`); - } - return { ...result, ...parseIdeConfigureOutput(result.stdout) }; -} diff --git a/src/ideWorkflow.ts b/src/ideWorkflow.ts index 7763efb..954ae55 100644 --- a/src/ideWorkflow.ts +++ b/src/ideWorkflow.ts @@ -1,40 +1,45 @@ -import type { IdeSnapshotPublished } from "./ideProtocol"; +import type { ProcessResult } from "./process"; export interface IdeConfigurationRequest { projectRoot: string; compilationDatabasePath: string; trusted: boolean; force?: boolean; - databaseExists: () => boolean; - configure: () => Promise; + databaseValid: () => boolean; + configure: () => Promise; } export type IdeConfigurationOutcome = - | { state: "configured"; compileCommands: string; snapshot: IdeSnapshotPublished } + | { state: "configured"; compileCommands: string } | { state: "existing"; compileCommands: string } + | { state: "failed"; compileCommands?: string; exitCode: number } | { state: "untrusted"; compileCommands?: string }; /** - * 决定是否需要执行 mcpp ide configure;不在这里读写 VS Code 设置, + * 决定是否需要执行 mcpp build --configure-only;不在这里读写 VS Code 设置, * 便于 Extension Host 和纯 Node 测试共享同一套生命周期边界。 */ export async function ensureIdeConfigured( request: IdeConfigurationRequest, ): Promise { - const exists = request.databaseExists(); + const validBefore = request.databaseValid(); if (!request.trusted) { return { state: "untrusted", - ...(exists ? { compileCommands: request.compilationDatabasePath } : {}), + ...(validBefore ? { compileCommands: request.compilationDatabasePath } : {}), }; } - if (exists && !request.force) { + if (validBefore && !request.force) { return { state: "existing", compileCommands: request.compilationDatabasePath }; } - const snapshot = await request.configure(); + const result = await request.configure(); + const validAfter = request.databaseValid(); + if (result.exitCode === 0 && validAfter) { + return { state: "configured", compileCommands: request.compilationDatabasePath }; + } return { - state: "configured", - compileCommands: snapshot.compatibilityCompileCommands ?? snapshot.compileCommands, - snapshot, + state: "failed", + ...(validAfter ? { compileCommands: request.compilationDatabasePath } : {}), + exitCode: result.exitCode, }; } diff --git a/src/newProject.ts b/src/newProject.ts index 07cdead..e7b6f0c 100644 --- a/src/newProject.ts +++ b/src/newProject.ts @@ -10,8 +10,8 @@ export type NewProjectOutcome = "exists" | "declined" | "failed" | "opened"; /** * 新建工程的核心流程,依赖全部注入以便单测。契约:创建并打开工程—— - * 打开后的构建交给用户手动触发(或后续 #5 的 IDE configure 流程), - * 避免与缺少 CDB 时的 configure 重复执行。 + * 打开后的完整构建交给用户手动触发;新窗口按自身信任状态与 CDB 状态决定是否运行 + * `mcpp build --configure-only`,避免创建流程跨窗口重复启动配置。 */ export async function runNewProjectFlow( projectName: string, diff --git a/src/workflow.ts b/src/workflow.ts index 3def1fb..2d80817 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -182,6 +182,12 @@ export function configurationAffectsModuleSupport( return ["mcpp.clangd.path", "mcpp.modulesSupport"].some(affectsConfiguration); } +export function configurationAffectsMcppExecution( + affectsConfiguration: (section: string) => boolean, +): boolean { + return affectsConfiguration("mcpp.path"); +} + export function workspaceAllowsToolExecution(trusted: boolean): boolean { return trusted; } @@ -230,6 +236,46 @@ export function describeRefreshOutcome( }; } +export function describeConfigureOnlyOutcome( + exitCode: number, + databaseValid: boolean, + retainedExisting: boolean, + clangdConfigured: boolean, +): RefreshOutcome { + if (exitCode === 0 && databaseValid) { + return clangdConfigured + ? { + level: "information", + message: "编译数据库已刷新,clangd 配置已重新加载。", + } + : { + level: "warning", + message: "编译数据库已刷新,但 clangd 配置未完成。请查看 mcpp 输出频道。", + }; + } + if (exitCode === 0) { + return { + level: "error", + message: "mcpp configure-only 已完成,但没有生成可用的 compile_commands.json。", + }; + } + if (databaseValid) { + return retainedExisting + ? { + level: "warning", + message: "mcpp configure-only 失败;继续使用原有编译数据库。请查看 mcpp 输出频道。", + } + : { + level: "warning", + message: "mcpp configure-only 失败,但检测到可用的编译数据库。请查看 mcpp 输出频道。", + }; + } + return { + level: "error", + message: "mcpp configure-only 失败,且没有可用的编译数据库。请确认 mcpp 版本支持 build --configure-only。", + }; +} + export function statusCommandForCapability(capability: ModuleCapability): string { return capability === "unavailable" ? "mcpp.refreshCompilationDatabase" diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index b3622af..f09a8f4 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -47,7 +47,6 @@ test("declares the official clangd dependency and mcpp commands", () => { "mcpp.showToolchains", "mcpp.installToolchain", "mcpp.selectDefaultToolchain", - "mcpp.configureIde", "mcpp.configureClangd", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport", @@ -114,15 +113,35 @@ test("shows editor title buttons only inside mcpp projects", () => { assert.ok(!commands.some((command) => command.command === "mcpp.inProject")); }); -test("wires IDE configure before clangd reconciliation", () => { +test("wires configure-only through the existing refresh command", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.configureIde")); + assert.ok(!manifest.activationEvents?.includes("onCommand:mcpp.configureIde")); + assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.refreshCompilationDatabase")); const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); assert.match(source, /ensureIdeConfigured/); - assert.match(source, /runIdeConfigure/); - assert.match(source, /registerCommand\(COMMAND_IDE_CONFIGURE/); - assert.match(source, /manifestWatcher\.onDidChange\(\(\) => \{[\s\S]*requestCurrentProjectReconciliation\(true\)/); + assert.match(source, /runConfigureOnly/); + assert.doesNotMatch(source, /runIdeConfigure|COMMAND_IDE_CONFIGURE|ide["'],\s*["']configure/); + assert.match(source, /findProjectForUri[\s\S]*findNearestMcppProject\(uri\.fsPath, workspaceFolder\.uri\.fsPath\)/); + assert.match(source, /requestManifestReconciliation\s*=\s*\(manifestUri:[^)]+\)[\s\S]*findProjectForUri\(manifestUri\)/); + assert.match(source, /requestManifestReconciliation[\s\S]*projectAffectedByManifest\([\s\S]*manifestUri\.fsPath/); + assert.match(source, /manifestWatcher\.onDidChange\(\(manifestUri\) => \{[\s\S]*requestManifestReconciliation\(manifestUri\)/); + assert.match(source, /manifestWatcher\.onDidDelete\(\(manifestUri\) => requestDeletedManifestReconciliation\(manifestUri\)\)/); + assert.match(source, /configurationAffectsMcppExecution[\s\S]*forceConfigureOnlyByProject\.add/); + assert.match(source, /reconcilePublishedCdbByRoot[\s\S]*reconcileProjectContext\(project, forceRestart, false, false\)/); + assert.match(source, /requestAutomaticReconciliation[\s\S]*reconcilePublishedCdbByRoot\(project\.root, forceRestart\)/); + assert.match(source, /forceConfigureOnlyByProject\.has\(projectRoot\)/); +}); + +test("configure-only shares the project operation lock", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const start = source.indexOf("public async runConfigureOnly"); + assert.notEqual(start, -1); + const end = source.indexOf("public async ", start + 10); + const method = source.slice(start, end === -1 ? source.length : end); + assert.match(method, /beginProject\(project\.root, token\)/); + assert.match(method, /runConfigureOnlyProcess/); + assert.match(method, /finally\s*\{[\s\S]*finishProject\(project\.root, token\)/); }); test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => { diff --git a/test/configureOnly.test.ts b/test/configureOnly.test.ts new file mode 100644 index 0000000..c12c9dd --- /dev/null +++ b/test/configureOnly.test.ts @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { configureOnlyArguments, runConfigureOnly } from "../src/configureOnly"; + +test("runs mcpp build --configure-only with the configured executable and cwd", async () => { + const calls: Array<{ executable: string; args: string[]; cwd?: string }> = []; + const result = await runConfigureOnly( + "/work/app", + "/tools/mcpp", + async (executable, args, cwd) => { + calls.push({ executable, args, cwd }); + return { exitCode: 0, stdout: "Configured 2 compile commands", stderr: "" }; + }, + ); + + assert.deepEqual(calls, [{ + executable: "/tools/mcpp", + args: [...configureOnlyArguments], + cwd: "/work/app", + }]); + assert.equal(result.exitCode, 0); +}); + +test("does not interpret human-readable configure output", async () => { + const result = await runConfigureOnly( + "/work/app", + "/tools/mcpp", + async () => ({ + exitCode: 0, + stdout: "Configured 2 compile commands\nFinished dev in 0.02s", + stderr: "", + }), + ); + + assert.equal(result.stdout, "Configured 2 compile commands\nFinished dev in 0.02s"); +}); + +test("returns a non-zero configure-only exit code without parsing stdout", async () => { + const result = await runConfigureOnly( + "/work/app", + "/tools/mcpp", + async () => ({ + exitCode: 2, + stdout: "error: unknown option '--configure-only'", + stderr: "", + }), + ); + + assert.equal(result.exitCode, 2); + assert.equal(result.stdout, "error: unknown option '--configure-only'"); +}); diff --git a/test/discovery.test.ts b/test/discovery.test.ts index 2a44bae..807c126 100644 --- a/test/discovery.test.ts +++ b/test/discovery.test.ts @@ -4,7 +4,14 @@ import os from "node:os"; import path from "node:path"; import test from "node:test"; -import { deriveClangdCandidates, findNearestMcppProject } from "../src/discovery"; +import { + deriveClangdCandidates, + findNearestMcppProject, + isPathWithinProject, + manifestProjectRoot, + projectAffectedByManifest, + shouldReconcileDeletedManifest, +} from "../src/discovery"; test("finds the nearest mcpp manifest and root compilation database", () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-discovery-")); @@ -24,6 +31,14 @@ test("finds the nearest mcpp manifest and root compilation database", () => { } }); +test("routes a deleted manifest only to the project that owned it", () => { + assert.equal(manifestProjectRoot("/work/member/mcpp.toml"), path.join("/work", "member")); + assert.equal(shouldReconcileDeletedManifest("/work/member", "/work/member/mcpp.toml"), true); + assert.equal(shouldReconcileDeletedManifest("/work/other", "/work/member/mcpp.toml"), false); + assert.equal(isPathWithinProject("/work/member", "/work"), true); + assert.equal(isPathWithinProject("/work-other/member", "/work"), false); +}); + test("selects the nearest member inside a multi-member workspace", () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-members-")); try { @@ -44,6 +59,84 @@ test("selects the nearest member inside a multi-member workspace", () => { } }); +test("does not treat a virtual workspace root as a package project", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-virtual-workspace-")); + try { + writeFileSync(path.join(root, "mcpp.toml"), "[workspace]\nmembers = ['A', 'B']\n"); + + // #387 在 virtual workspace 根执行 configure-only 时只发布 member CDB, + // 根目录本身没有可供 clangd 消费的 compile_commands.json。 + assert.equal(findNearestMcppProject(root, root), undefined); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("keeps a rooted workspace as a package project", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-rooted-workspace-")); + try { + writeFileSync( + path.join(root, "mcpp.toml"), + "[package]\nname = 'root'\n[workspace]\nmembers = ['A']\n", + ); + + assert.equal(findNearestMcppProject(root, root)?.root, root); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("recognizes dotted virtual and inline rooted workspace manifests", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-workspace-toml-forms-")); + try { + writeFileSync(path.join(root, "mcpp.toml"), "workspace.members = ['A']\n"); + assert.equal(findNearestMcppProject(root, root), undefined); + + writeFileSync( + path.join(root, "mcpp.toml"), + "package = { name = 'root', version = '0.1.0' }\n[workspace]\nmembers = ['A']\n", + ); + assert.equal(findNearestMcppProject(root, root)?.root, root); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("routes workspace root manifest changes to the active member", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-workspace-change-")); + try { + const memberRoot = path.join(root, "A"); + const member = { + root: memberRoot, + manifestPath: path.join(memberRoot, "mcpp.toml"), + compilationDatabasePath: path.join(memberRoot, "compile_commands.json"), + }; + const rootProject = { + root, + manifestPath: path.join(root, "mcpp.toml"), + compilationDatabasePath: path.join(root, "compile_commands.json"), + }; + mkdirSync(memberRoot, { recursive: true }); + + writeFileSync(rootProject.manifestPath, "[workspace]\nmembers = ['A']\n"); + assert.deepEqual( + projectAffectedByManifest(rootProject.manifestPath, member, undefined), + member, + ); + + writeFileSync( + rootProject.manifestPath, + "[package]\nname = 'root'\n[workspace]\nmembers = ['A']\n", + ); + assert.deepEqual( + projectAffectedByManifest(rootProject.manifestPath, member, rootProject), + member, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("does not discover an mcpp project outside the opened workspace folder", () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-workspace-boundary-")); try { diff --git a/test/e2e/fixtures/fake-mcpp.js b/test/e2e/fixtures/fake-mcpp.js index 76daadf..b7e4b14 100644 --- a/test/e2e/fixtures/fake-mcpp.js +++ b/test/e2e/fixtures/fake-mcpp.js @@ -1,9 +1,23 @@ #!/usr/bin/env node const fs = require("node:fs"); +const path = require("node:path"); const logPath = process.env.MCPP_E2E_LOG; +const command = process.argv.slice(2).join(" "); if (typeof logPath === "string") { - fs.appendFileSync(logPath, `${process.argv.slice(2).join(" ")}\n`); + fs.appendFileSync(logPath, `${command}\n`); } -process.exit(process.argv.slice(2).join(" ") === "build" ? 0 : 1); + +if (command === "build --configure-only") { + // 使用 syntax-only CDB,避免 smoke test 依赖宿主 clangd 或真实编译器。 + const cwd = process.cwd(); + fs.writeFileSync(path.join(cwd, "compile_commands.json"), JSON.stringify([{ + directory: cwd, + file: path.join(cwd, "main.cpp"), + arguments: ["gcc", "-std=c++23", "-c", path.join(cwd, "main.cpp")], + }])); + process.exit(0); +} + +process.exit(command === "build" ? 0 : 1); diff --git a/test/e2e/suite/extension.test.ts b/test/e2e/suite/extension.test.ts index 4859237..d1d088d 100644 --- a/test/e2e/suite/extension.test.ts +++ b/test/e2e/suite/extension.test.ts @@ -4,7 +4,7 @@ import * as path from "node:path"; import * as vscode from "vscode"; suite("mcpp extension smoke", () => { - test("activates, registers commands and executes a configured build", async () => { + test("configures a missing CDB once before executing a full build", async () => { const extension = vscode.extensions.getExtension("mcpp-community.mcpp-vscode"); assert.ok(extension, "mcpp extension should be installed in development host"); await extension.activate(); @@ -22,13 +22,30 @@ suite("mcpp extension smoke", () => { await vscode.workspace.getConfiguration("mcpp", workspaceFolder.uri) .update("path", fakeMcpp, vscode.ConfigurationTarget.Workspace); - void vscode.commands.executeCommand("mcpp.build"); - const deadline = Date.now() + 15_000; - while (!existsSync(logPath) && Date.now() < deadline) { + const configureDeadline = Date.now() + 15_000; + let invocations: string[] = []; + while (Date.now() < configureDeadline) { + if (existsSync(logPath)) { + invocations = readFileSync(logPath, "utf8").trim().split("\n"); + if (invocations.includes("build --configure-only")) { + break; + } + } await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); } assert.ok(existsSync(logPath), "fake mcpp should have been invoked"); - assert.equal(readFileSync(logPath, "utf8").trim(), "build"); + + void vscode.commands.executeCommand("mcpp.build"); + const buildDeadline = Date.now() + 15_000; + while (Date.now() < buildDeadline) { + invocations = readFileSync(logPath, "utf8").trim().split("\n"); + if (invocations.includes("build")) { + break; + } + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); + } + assert.deepEqual(invocations, ["build --configure-only", "build"]); + assert.ok(existsSync(path.join(workspaceFolder.uri.fsPath, "compile_commands.json"))); assert.equal(path.basename(workspaceFolder.uri.fsPath), "project"); }); }); diff --git a/test/ideProtocol.test.ts b/test/ideProtocol.test.ts deleted file mode 100644 index 7fa39ca..0000000 --- a/test/ideProtocol.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { parseIdeConfigureOutput, runIdeConfigure } from "../src/ideProtocol"; - -test("parses configured snapshot without human-output regexes", () => { - const result = parseIdeConfigureOutput([ - JSON.stringify({ seq: 1, type: "operation-started", operation: "configure" }), - JSON.stringify({ - seq: 2, - type: "snapshot-published", - phase: "configured", - compileCommands: "/work/.mcpp/ide/replies/compile_commands.json", - }), - JSON.stringify({ seq: 3, type: "operation-finished", status: "success" }), - ].join("\n")); - - assert.equal(result.phase, "configured"); - assert.equal(result.compileCommands, "/work/.mcpp/ide/replies/compile_commands.json"); -}); - -test("passes configure through the configured executable and cwd", async () => { - const calls: Array<{ executable: string; args: string[]; cwd?: string }> = []; - const result = await runIdeConfigure( - "/work/app", - "/tools/mcpp", - async (executable, args, cwd) => { - calls.push({ executable, args, cwd }); - return { - exitCode: 0, - stdout: [ - JSON.stringify({ seq: 1, type: "operation-started" }), - JSON.stringify({ - seq: 2, - type: "snapshot-published", - phase: "configured", - compileCommands: "/work/app/compile_commands.json", - }), - JSON.stringify({ seq: 3, type: "operation-finished", status: "success" }), - ].join("\n"), - stderr: "", - }; - }, - ); - - assert.deepEqual(calls, [{ - executable: "/tools/mcpp", - args: ["ide", "configure", "--format", "ndjson"], - cwd: "/work/app", - }]); - assert.equal(result.compileCommands, "/work/app/compile_commands.json"); -}); - -test("rejects a published snapshot without a successful finish event", () => { - assert.throws(() => parseIdeConfigureOutput([ - JSON.stringify({ seq: 1, type: "operation-started", operation: "configure" }), - JSON.stringify({ - seq: 2, - type: "snapshot-published", - phase: "configured", - compileCommands: "/work/app/compile_commands.json", - }), - JSON.stringify({ seq: 3, type: "operation-finished", status: "failed" }), - ].join("\n")), /未成功完成/); -}); - -test("rejects human-readable output mixed into NDJSON", () => { - assert.throws(() => parseIdeConfigureOutput([ - JSON.stringify({ seq: 1, type: "operation-started" }), - "c++fly on clang: enabled", - ].join("\n")), /无效 JSON/); -}); diff --git a/test/ideWorkflow.test.ts b/test/ideWorkflow.test.ts index 80386af..0f1f415 100644 --- a/test/ideWorkflow.test.ts +++ b/test/ideWorkflow.test.ts @@ -3,35 +3,35 @@ import test from "node:test"; import { ensureIdeConfigured } from "../src/ideWorkflow"; -test("configures a trusted project before build when the CDB is missing", async () => { +const success = { exitCode: 0, stdout: "Configured 1 compile command", stderr: "" }; + +test("configures a trusted project before clangd when the CDB is missing", async () => { let calls = 0; const outcome = await ensureIdeConfigured({ projectRoot: "/work/app", compilationDatabasePath: "/work/app/compile_commands.json", trusted: true, - databaseExists: () => false, + databaseValid: () => calls > 0, configure: async () => { calls += 1; - return { - phase: "configured", - compileCommands: "/work/app/.mcpp/ide/replies/compile_commands-cfg.json", - compatibilityCompileCommands: "/work/app/compile_commands.json", - }; + return success; }, }); assert.equal(calls, 1); - assert.equal(outcome.state, "configured"); - assert.equal(outcome.compileCommands, "/work/app/compile_commands.json"); + assert.deepEqual(outcome, { + state: "configured", + compileCommands: "/work/app/compile_commands.json", + }); }); -test("keeps an existing CDB without invoking mcpp", async () => { +test("keeps a valid existing CDB without invoking mcpp", async () => { let called = false; const outcome = await ensureIdeConfigured({ projectRoot: "/work/app", compilationDatabasePath: "/work/app/compile_commands.json", trusted: true, - databaseExists: () => true, + databaseValid: () => true, configure: async () => { called = true; throw new Error("must not run"); @@ -39,7 +39,10 @@ test("keeps an existing CDB without invoking mcpp", async () => { }); assert.equal(called, false); - assert.equal(outcome.state, "existing"); + assert.deepEqual(outcome, { + state: "existing", + compileCommands: "/work/app/compile_commands.json", + }); }); test("does not execute mcpp in an untrusted workspace", async () => { @@ -48,7 +51,7 @@ test("does not execute mcpp in an untrusted workspace", async () => { projectRoot: "/work/app", compilationDatabasePath: "/work/app/compile_commands.json", trusted: false, - databaseExists: () => false, + databaseValid: () => false, configure: async () => { called = true; throw new Error("must not run"); @@ -56,23 +59,60 @@ test("does not execute mcpp in an untrusted workspace", async () => { }); assert.equal(called, false); - assert.equal(outcome.state, "untrusted"); + assert.deepEqual(outcome, { state: "untrusted" }); +}); + +test("retains an existing CDB when forced configure-only fails", async () => { + let called = false; + const outcome = await ensureIdeConfigured({ + projectRoot: "/work/app", + compilationDatabasePath: "/work/app/compile_commands.json", + trusted: true, + force: true, + databaseValid: () => true, + configure: async () => { + called = true; + return { exitCode: 1, stdout: "compile failed", stderr: "" }; + }, + }); + + assert.equal(called, true); + assert.deepEqual(outcome, { + state: "failed", + compileCommands: "/work/app/compile_commands.json", + exitCode: 1, + }); }); -test("force refresh invokes configure even when a CDB already exists", async () => { +test("requires a valid CDB after configure-only succeeds", async () => { + const outcome = await ensureIdeConfigured({ + projectRoot: "/work/app", + compilationDatabasePath: "/work/app/compile_commands.json", + trusted: true, + databaseValid: () => false, + configure: async () => success, + }); + + assert.deepEqual(outcome, { state: "failed", exitCode: 0 }); +}); + +test("force refresh invokes configure-only when a CDB already exists", async () => { let calls = 0; const outcome = await ensureIdeConfigured({ projectRoot: "/work/app", compilationDatabasePath: "/work/app/compile_commands.json", trusted: true, force: true, - databaseExists: () => true, + databaseValid: () => true, configure: async () => { calls += 1; - return { phase: "configured", compileCommands: "/work/app/compile_commands.json" }; + return success; }, }); assert.equal(calls, 1); - assert.equal(outcome.state, "configured"); + assert.deepEqual(outcome, { + state: "configured", + compileCommands: "/work/app/compile_commands.json", + }); }); diff --git a/test/workflow.test.ts b/test/workflow.test.ts index e56019b..ce84c52 100644 --- a/test/workflow.test.ts +++ b/test/workflow.test.ts @@ -5,6 +5,9 @@ import * as workflow from "../src/workflow"; import { createSerialExecutor, createSingleFlightReconciler, + configurationAffectsMcppExecution, + configurationAffectsModuleSupport, + describeConfigureOnlyOutcome, describeRefreshOutcome, statusCommandForCapability, } from "../src/workflow"; @@ -28,6 +31,33 @@ test("keeps IDE configuration when a source build fails after producing a databa assert.equal(describeRefreshOutcome(1, false, false).level, "error"); }); +test("describes configure-only results without treating an old CDB as newly generated", () => { + assert.deepEqual(describeConfigureOnlyOutcome(0, true, false, true), { + level: "information", + message: "编译数据库已刷新,clangd 配置已重新加载。", + }); + assert.deepEqual(describeConfigureOnlyOutcome(0, true, false, false), { + level: "warning", + message: "编译数据库已刷新,但 clangd 配置未完成。请查看 mcpp 输出频道。", + }); + assert.deepEqual(describeConfigureOnlyOutcome(0, false, false, false), { + level: "error", + message: "mcpp configure-only 已完成,但没有生成可用的 compile_commands.json。", + }); + assert.deepEqual(describeConfigureOnlyOutcome(2, true, true, false), { + level: "warning", + message: "mcpp configure-only 失败;继续使用原有编译数据库。请查看 mcpp 输出频道。", + }); + assert.deepEqual(describeConfigureOnlyOutcome(2, true, false, false), { + level: "warning", + message: "mcpp configure-only 失败,但检测到可用的编译数据库。请查看 mcpp 输出频道。", + }); + assert.deepEqual(describeConfigureOnlyOutcome(2, false, false, false), { + level: "error", + message: "mcpp configure-only 失败,且没有可用的编译数据库。请确认 mcpp 版本支持 build --configure-only。", + }); +}); + test("serializes reconciliation and merges pending restart requests", async () => { let releaseFirst: (() => void) | undefined; const firstGate = new Promise((resolve) => { @@ -369,30 +399,29 @@ test("accepts module status only from the latest check for the current project", assert.equal(shouldRender(undefined, "/work/a"), false); }); -test("reconciles when a module-related mcpp setting changes", () => { - const affectsModuleConfiguration = ( - workflow as typeof workflow & { - configurationAffectsModuleSupport?: ( - affectsConfiguration: (section: string) => boolean, - ) => boolean; - } - ).configurationAffectsModuleSupport; - assert.equal(typeof affectsModuleConfiguration, "function"); - assert.ok(affectsModuleConfiguration); - +test("separates mcpp executable changes from clangd-only settings", () => { assert.equal( - affectsModuleConfiguration((section) => section === "mcpp.clangd.path"), + configurationAffectsModuleSupport((section) => section === "mcpp.clangd.path"), true, ); assert.equal( - affectsModuleConfiguration((section) => section === "mcpp.modulesSupport"), + configurationAffectsModuleSupport((section) => section === "mcpp.modulesSupport"), + true, + ); + assert.equal( + configurationAffectsModuleSupport((section) => section === "mcpp.path"), + false, + ); + assert.equal( + configurationAffectsMcppExecution((section) => section === "mcpp.path"), true, ); assert.equal( - affectsModuleConfiguration((section) => section === "mcpp.path"), + configurationAffectsMcppExecution((section) => section === "mcpp.clangd.path"), false, ); - assert.equal(affectsModuleConfiguration(() => false), false); + assert.equal(configurationAffectsModuleSupport(() => false), false); + assert.equal(configurationAffectsMcppExecution(() => false), false); }); test("only executes workspace-selected tools in trusted workspaces", () => { From 83f4cc88028b3e51f5fe075df28d3f3644aee681 Mon Sep 17 00:00:00 2001 From: wellwei Date: Tue, 11 Aug 2026 11:33:45 +0800 Subject: [PATCH 053/119] release: mcpp-vscode 0.3.0 --- CHANGELOG.md | 2 +- README.md | 8 ++++---- package-lock.json | 4 ++-- package.json | 2 +- test/artifacts.test.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5cdd9..f72d00d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # 更新日志 -## 未发布 +## 0.3.0 - 2026-08-11 - 将编译数据库刷新迁移到 `mcpp build --configure-only`:不解析 stdout 人类文本,以退出码 和可解析 CDB 作为成功条件,失败时保留 last-known-good CDB。 diff --git a/README.md b/README.md index 68e94e4..0e2a655 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -当前版本为 `0.2.7`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +当前版本为 `0.3.0`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 > 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 @@ -37,7 +37,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.2.7.vsix +code --install-extension /path/to/mcpp-vscode-0.3.0.vsix ``` 安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 @@ -419,8 +419,8 @@ API、状态栏、任务和 clangd 集成。 版本完全一致的 tag: ```sh -git tag -a v0.2.7 -m "mcpp-vscode 0.2.7" -git push origin v0.2.7 +git tag -a v0.3.0 -m "mcpp-vscode 0.3.0" +git push origin v0.3.0 ``` `.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, diff --git a/package-lock.json b/package-lock.json index 4d170ad..0033314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.2.7", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.2.7", + "version": "0.3.0", "license": "Apache-2.0", "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/package.json b/package.json index d0232c2..1cb55bc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.7", + "version": "0.3.0", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index f09a8f4..486e4b2 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -26,7 +26,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.7"); + assert.equal(manifest.version, "0.3.0"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); From 499ba2f7538caec70fd6bfd8862fbf890a086123 Mon Sep 17 00:00:00 2001 From: wellwei Date: Tue, 11 Aug 2026 15:31:41 +0800 Subject: [PATCH 054/119] fix: keep IDE commands responsive after CDB removal --- CHANGELOG.md | 5 ++ src/cliController.ts | 6 +-- src/configureOnly.ts | 9 +++- src/extension.ts | 82 ++++++++++++++++++++------------ src/workflow.ts | 20 ++++++++ test/artifacts.test.ts | 10 ++++ test/configureOnly.test.ts | 12 +++-- test/e2e/suite/extension.test.ts | 30 +++++++++++- test/workflow.test.ts | 10 ++++ 9 files changed, 144 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f72d00d..e83eafc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +## 未发布 + +- 修复缺少 CDB 时 IDE 命令看似无响应的问题:配置 clangd、刷新编译数据库、检查模块支持和一键配置现在会立即显示进度并打开 `mcpp` 输出频道;clangd 重启与 `mcpp build --configure-only` 增加超时,避免异步操作永久占用 IDE 队列。 +- 增加删除 `compile_commands.json` 后刷新 CDB、配置 clangd、检查模块支持和一键配置的 Extension Host 回归测试。 + ## 0.3.0 - 2026-08-11 - 将编译数据库刷新迁移到 `mcpp build --configure-only`:不解析 stdout 人类文本,以退出码 diff --git a/src/cliController.ts b/src/cliController.ts index 12cbaf0..0cec3d9 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -632,7 +632,7 @@ export class McppCliController { } catch { // 输出频道可能已经在窗口重载时释放。 } - await vscode.window.showErrorMessage(`mcpp:${message}`); + void vscode.window.showErrorMessage(`mcpp:${message}`); } }; } @@ -679,7 +679,7 @@ export class McppCliController { const result = await runProcess(executable, args, workingDirectory(project)); this.appendShortCommand("查看工具链", executable, args, result); if (result.exitCode !== 0) { - await vscode.window.showErrorMessage( + void vscode.window.showErrorMessage( `mcpp toolchain list 失败(退出码 ${result.exitCode})。请查看 mcpp 输出频道。`, ); return undefined; @@ -687,7 +687,7 @@ export class McppCliController { const inventory = parseToolchainList(`${result.stdout}${result.stderr.length > 0 ? `\n${result.stderr}` : ""}`); if (!inventory.recognized) { - await vscode.window.showErrorMessage( + void vscode.window.showErrorMessage( "无法识别当前 mcpp toolchain list 输出;原始输出已保留在 mcpp 输出频道,请检查 mcpp 版本。", ); return undefined; diff --git a/src/configureOnly.ts b/src/configureOnly.ts index a677a74..cf233ae 100644 --- a/src/configureOnly.ts +++ b/src/configureOnly.ts @@ -1,11 +1,18 @@ import { runProcess, type ProcessResult, type ProcessRunner } from "./process"; export const configureOnlyArguments = ["build", "--configure-only"] as const; +// 配置阶段可能解析工具链和依赖,但不能无限期占住 IDE 操作队列。 +export const configureOnlyTimeoutMs = 5 * 60_000; export function runConfigureOnly( projectRoot: string, executable = "mcpp", runner: ProcessRunner = runProcess, ): Promise { - return runner(executable, [...configureOnlyArguments], projectRoot); + return runner( + executable, + [...configureOnlyArguments], + projectRoot, + { timeoutMs: configureOnlyTimeoutMs }, + ); } diff --git a/src/extension.ts b/src/extension.ts index fdb7476..0d26e3d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -47,6 +47,7 @@ import { shouldRenderProjectStatus, shouldUseWorkspaceClangd, statusCommandForCapability, + withTimeout, workspaceAllowsToolExecution, type ModuleSupportState, } from "./workflow"; @@ -306,12 +307,23 @@ async function maybeDisableCppTools(project: McppProjectDiscovery): Promise { +const CLANGD_RESTART_TIMEOUT_MS = 15_000; + +async function restartClangd(output: vscode.OutputChannel): Promise { if (vscode.extensions.getExtension("llvm-vs-code-extensions.vscode-clangd") === undefined) { return false; } try { - await vscode.commands.executeCommand("clangd.restart"); + const timedOut = {}; + const result = await withTimeout( + vscode.commands.executeCommand("clangd.restart"), + CLANGD_RESTART_TIMEOUT_MS, + timedOut, + ); + if (result === timedOut) { + appendOutputLine(output, `[clangd] 重启命令超过 ${CLANGD_RESTART_TIMEOUT_MS / 1000} 秒未完成,已释放 IDE 操作队列。`); + return false; + } return true; } catch { // clangd 扩展可能尚未激活,但配置仍然已经写入。 @@ -329,7 +341,7 @@ async function configureClangd( const interactive = mode === "interactive"; if (context.analysis.capability === "syntax-only") { if (interactive) { - await vscode.window.showWarningMessage( + void vscode.window.showWarningMessage( `${context.analysis.kind.toUpperCase()} 模块产物无法由 clangd 读取。语法高亮仍然可用,但模块语义诊断需要 LLVM mcpp 工具链。`, ); } @@ -338,7 +350,7 @@ async function configureClangd( } if (context.analysis.capability !== "full" || context.analysis.compilerPath === undefined) { if (interactive) { - await vscode.window.showWarningMessage( + void vscode.window.showWarningMessage( `${context.analysis.reason} 请先运行“mcpp: 刷新编译数据库”。`, ); } @@ -347,7 +359,7 @@ async function configureClangd( if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { const message = "当前工作区未受信任,mcpp 不会执行 CDB 中的编译器或 clangd,也不会接管 clangd 配置。"; if (interactive) { - await vscode.window.showWarningMessage(message); + void vscode.window.showWarningMessage(message); } else { appendOutputLine(output, `[自动配置] ${message}`); } @@ -358,7 +370,7 @@ async function configureClangd( if (clangd === undefined) { const message = "没有找到可用的 clangd。请安装与 mcpp LLVM 编译器来自同一 revision 的 clangd,或设置 mcpp.clangd.path;clangd 可以来自 xlings llvm-tools,也可以独立安装。"; if (interactive) { - await vscode.window.showErrorMessage(message); + void vscode.window.showErrorMessage(message); } else { appendOutputLine(output, `[自动配置] ${message}`); } @@ -399,11 +411,11 @@ async function configureClangd( await maybeDisableCppTools(context.project); } const restartRequired = shouldRestartClangd(plan.changed, interactive, forceRestart); - const restartSucceeded = restartRequired ? await restartClangd() : false; + const restartSucceeded = restartRequired ? await restartClangd(output) : false; if (!configurationReadyAfterRestart(restartRequired, restartSucceeded)) { const message = "clangd 配置已写入,但无法重启语言服务器。请查看 mcpp 输出频道,或手动执行 clangd 重启命令。"; if (interactive) { - await vscode.window.showErrorMessage(message); + void vscode.window.showErrorMessage(message); } else { appendOutputLine(output, `[自动配置] ${message}`); } @@ -412,11 +424,11 @@ async function configureClangd( updateStatusBar(status, context); if (!clangd.comparison.compatible) { - await vscode.window.showWarningMessage( + void vscode.window.showWarningMessage( `clangd 已配置,但 LLVM 身份与 mcpp 编译器不匹配:${clangd.comparison.reason}。`, ); } else if (interactive) { - await vscode.window.showInformationMessage("mcpp 已为当前工作区配置匹配的 clangd。"); + void vscode.window.showInformationMessage("mcpp 已为当前工作区配置匹配的 clangd。"); } else if (plan.changed) { appendOutputLine(output, `[自动配置] clangd.path = ${plan.path}`); } @@ -546,7 +558,7 @@ async function runModuleSupportCheck( storeModuleStatus(status, context, { state: "unavailable", message }); } if (interactive) { - await vscode.window.showWarningMessage(message); + void vscode.window.showWarningMessage(message); } return context.analysis.capability === "full" ? { state: "unavailable", message } @@ -559,7 +571,7 @@ async function runModuleSupportCheck( const moduleStatus = { state: "unavailable", message } as const; storeModuleStatus(status, context, moduleStatus); if (interactive) { - await vscode.window.showWarningMessage(message); + void vscode.window.showWarningMessage(message); } else { appendOutputLine(output, `[自动检查] ${message}`); } @@ -583,7 +595,7 @@ async function runModuleSupportCheck( return undefined; } if (interactive) { - await vscode.window.showErrorMessage(message); + void vscode.window.showErrorMessage(message); } else { appendOutputLine(output, `[自动检查] ${message}`); } @@ -630,9 +642,9 @@ async function runModuleSupportCheck( } if (interactive) { if (moduleStatus.state === "available") { - await vscode.window.showInformationMessage(message); + void vscode.window.showInformationMessage(message); } else { - await vscode.window.showErrorMessage(message); + void vscode.window.showErrorMessage(message); } } else { appendOutputLine(output, `[自动检查] ${message}`); @@ -751,7 +763,7 @@ async function autoConfigureModulesWizard( appendOutputLine(output, "[一键配置] 开始一键配置模块代码提示..."); if (!vscode.workspace.isTrusted) { - await vscode.window.showWarningMessage(moduleSetupBlockedMessage("untrusted")); + void vscode.window.showWarningMessage(moduleSetupBlockedMessage("untrusted")); return; } const inventory = await cliController.readToolchainInventory(context.project); @@ -765,7 +777,7 @@ async function autoConfigureModulesWizard( cliController.isBusy(), ); if (decision.kind === "blocked") { - await vscode.window.showWarningMessage(moduleSetupBlockedMessage(decision.reason)); + void vscode.window.showWarningMessage(moduleSetupBlockedMessage(decision.reason)); return; } @@ -861,14 +873,14 @@ async function autoConfigureModulesWizard( if (outcome.state === "succeeded") { if (outcome.degraded) { - await vscode.window.showWarningMessage("构建失败,语言服务已刷新。请查看任务终端获取构建错误。"); + void vscode.window.showWarningMessage("构建失败,语言服务已刷新。请查看任务终端获取构建错误。"); } else { - await vscode.window.showInformationMessage("mcpp 模块代码提示一键配置完成。"); + void vscode.window.showInformationMessage("mcpp 模块代码提示一键配置完成。"); } } else if (outcome.state === "cancelled") { - await vscode.window.showWarningMessage(`一键配置已取消(${outcome.stage})。`); + void vscode.window.showWarningMessage(`一键配置已取消(${outcome.stage})。`); } else { - await vscode.window.showErrorMessage(`一键配置失败(${outcome.stage})。${outcome.steps.at(-1)?.detail ?? "请查看 mcpp 输出频道。"}`); + void vscode.window.showErrorMessage(`一键配置失败(${outcome.stage})。${outcome.steps.at(-1)?.detail ?? "请查看 mcpp 输出频道。"}`); } } @@ -931,6 +943,10 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi void vscode.window.showErrorMessage(`mcpp:${message}`); } }; + const showInteractiveIdeStart = (title: string): void => { + appendOutputLine(output, `\n[IDE] ${title}:已接收,正在等待 IDE 操作队列。`); + output.show(true); + }; let cliController: McppCliController; const runConfigureOnlyForProject = async ( project: McppProjectDiscovery, @@ -939,7 +955,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { appendOutputLine(output, "[CDB 配置] 工作区未受信任,跳过 mcpp build --configure-only。"); if (interactive) { - await vscode.window.showWarningMessage( + void vscode.window.showWarningMessage( "当前工作区未受信任,不会刷新编译数据库。请先信任工作区。", ); } @@ -949,7 +965,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (result === undefined) { appendOutputLine(output, "[CDB 配置] 已有 mcpp 操作正在运行,本次刷新已跳过。"); if (interactive) { - await vscode.window.showWarningMessage( + void vscode.window.showWarningMessage( "已有 mcpp 操作正在运行,暂不能刷新编译数据库。请等待当前操作完成后重试。", ); } @@ -1204,7 +1220,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (kind === "build") { if (reconciled.context?.analysis.capability === "syntax-only") { const buildMessage = completion.state === "succeeded" ? "mcpp 构建完成" : "mcpp 构建失败"; - await vscode.window.showWarningMessage( + void vscode.window.showWarningMessage( `${buildMessage};${reconciled.context.analysis.reason}。模块语法高亮仍然可用。`, ); return; @@ -1215,18 +1231,18 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi reconciled.configured, ); if (outcome.level === "information") { - await vscode.window.showInformationMessage(outcome.message); + void vscode.window.showInformationMessage(outcome.message); } else if (outcome.level === "warning") { - await vscode.window.showWarningMessage(outcome.message); + void vscode.window.showWarningMessage(outcome.message); } else { - await vscode.window.showErrorMessage(outcome.message); + void vscode.window.showErrorMessage(outcome.message); } return; } if (completion.state === "succeeded") { const label = kind === "run" ? "运行" : kind === "test" ? "测试" : "清理"; - await vscode.window.showInformationMessage(`mcpp ${label}完成;clangd/CDB 状态已重新检查。`); + void vscode.window.showInformationMessage(`mcpp ${label}完成;clangd/CDB 状态已重新检查。`); } }; cliController = new McppCliController({ @@ -1251,6 +1267,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); return; } + showInteractiveIdeStart("配置 clangd"); await executeWithWorkspaceClangd(async () => { if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { return; @@ -1269,6 +1286,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); return; } + showInteractiveIdeStart("刷新编译数据库"); await executeWithWorkspaceClangd(async () => { const hadUsableDatabase = hasUsableCompilationDatabase(project); const result = await runConfigureOnlyForProject(project, true); @@ -1284,11 +1302,11 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi reconciled.configured, ); if (outcome.level === "information") { - await vscode.window.showInformationMessage(outcome.message); + void vscode.window.showInformationMessage(outcome.message); } else if (outcome.level === "warning") { - await vscode.window.showWarningMessage(outcome.message); + void vscode.window.showWarningMessage(outcome.message); } else { - await vscode.window.showErrorMessage(outcome.message); + void vscode.window.showErrorMessage(outcome.message); } }); })), @@ -1298,6 +1316,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); return; } + showInteractiveIdeStart("检查模块支持"); await executeWithWorkspaceClangd(async () => { if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { return; @@ -1401,6 +1420,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); return; } + showInteractiveIdeStart("一键配置模块代码提示"); await executeWithWorkspaceClangd(async () => { if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { return; diff --git a/src/workflow.ts b/src/workflow.ts index 2d80817..371b501 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -16,6 +16,26 @@ export interface CompilationDatabaseWatcher { onDidDelete(listener: (event: E) => void): T; } +export async function withTimeout( + operation: PromiseLike, + timeoutMs: number, + fallback: T, +): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + Promise.resolve(operation), + new Promise((resolve) => { + timer = setTimeout(() => resolve(fallback), timeoutMs); + }), + ]); + } finally { + if (timer !== undefined) { + clearTimeout(timer); + } + } +} + export function registerCompilationDatabaseReconciliation( watcher: CompilationDatabaseWatcher, requestReconciliation: (event: E, forceRestart: boolean) => void, diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 486e4b2..59fabb7 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -144,6 +144,16 @@ test("configure-only shares the project operation lock", () => { assert.match(method, /finally\s*\{[\s\S]*finishProject\(project\.root, token\)/); }); +test("IDE commands expose progress and bound clangd restart waits", () => { + const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); + assert.match(source, /const CLANGD_RESTART_TIMEOUT_MS = 15_000/); + assert.match(source, /withTimeout\([\s\S]*CLANGD_RESTART_TIMEOUT_MS/); + for (const title of ["配置 clangd", "刷新编译数据库", "检查模块支持", "一键配置模块代码提示"]) { + assert.match(source, new RegExp(`showInteractiveIdeStart\\("${title}"\\)`)); + } + assert.match(source, /output\.show\(true\)/); +}); + test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; const associations = manifest.contributes?.configurationDefaults?.["files.associations"] as diff --git a/test/configureOnly.test.ts b/test/configureOnly.test.ts index c12c9dd..890abb9 100644 --- a/test/configureOnly.test.ts +++ b/test/configureOnly.test.ts @@ -4,12 +4,17 @@ import test from "node:test"; import { configureOnlyArguments, runConfigureOnly } from "../src/configureOnly"; test("runs mcpp build --configure-only with the configured executable and cwd", async () => { - const calls: Array<{ executable: string; args: string[]; cwd?: string }> = []; + const calls: Array<{ + executable: string; + args: string[]; + cwd?: string; + timeoutMs?: number; + }> = []; const result = await runConfigureOnly( "/work/app", "/tools/mcpp", - async (executable, args, cwd) => { - calls.push({ executable, args, cwd }); + async (executable, args, cwd, options) => { + calls.push({ executable, args, cwd, timeoutMs: options?.timeoutMs }); return { exitCode: 0, stdout: "Configured 2 compile commands", stderr: "" }; }, ); @@ -18,6 +23,7 @@ test("runs mcpp build --configure-only with the configured executable and cwd", executable: "/tools/mcpp", args: [...configureOnlyArguments], cwd: "/work/app", + timeoutMs: 5 * 60_000, }]); assert.equal(result.exitCode, 0); }); diff --git a/test/e2e/suite/extension.test.ts b/test/e2e/suite/extension.test.ts index d1d088d..3f211cb 100644 --- a/test/e2e/suite/extension.test.ts +++ b/test/e2e/suite/extension.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, rmSync } from "node:fs"; import * as path from "node:path"; import * as vscode from "vscode"; @@ -35,7 +35,7 @@ suite("mcpp extension smoke", () => { } assert.ok(existsSync(logPath), "fake mcpp should have been invoked"); - void vscode.commands.executeCommand("mcpp.build"); + await vscode.commands.executeCommand("mcpp.build"); const buildDeadline = Date.now() + 15_000; while (Date.now() < buildDeadline) { invocations = readFileSync(logPath, "utf8").trim().split("\n"); @@ -47,5 +47,31 @@ suite("mcpp extension smoke", () => { assert.deepEqual(invocations, ["build --configure-only", "build"]); assert.ok(existsSync(path.join(workspaceFolder.uri.fsPath, "compile_commands.json"))); assert.equal(path.basename(workspaceFolder.uri.fsPath), "project"); + + const compilationDatabase = path.join(workspaceFolder.uri.fsPath, "compile_commands.json"); + const executeWithinDeadline = async (command: string): Promise => { + let timer: NodeJS.Timeout | undefined; + try { + await Promise.race([ + vscode.commands.executeCommand(command), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${command} did not finish`)), 5_000); + }), + ]); + } finally { + if (timer !== undefined) { + clearTimeout(timer); + } + } + }; + + rmSync(compilationDatabase); + await executeWithinDeadline("mcpp.refreshCompilationDatabase"); + assert.ok(existsSync(compilationDatabase), "refresh should recreate a missing CDB"); + + rmSync(compilationDatabase); + await executeWithinDeadline("mcpp.configureClangd"); + await executeWithinDeadline("mcpp.checkModuleSupport"); + await executeWithinDeadline("mcpp.autoConfigureModules"); }); }); diff --git a/test/workflow.test.ts b/test/workflow.test.ts index ce84c52..7c3b1c3 100644 --- a/test/workflow.test.ts +++ b/test/workflow.test.ts @@ -58,6 +58,16 @@ test("describes configure-only results without treating an old CDB as newly gene }); }); +test("releases an IDE operation queue when an external operation exceeds its deadline", async () => { + const stalled = new Promise(() => { + // 模拟 clangd.restart 或 mcpp configure-only 永久等待。 + }); + + const result = await workflow.withTimeout(stalled, 10, "timed-out"); + + assert.equal(result, "timed-out"); +}); + test("serializes reconciliation and merges pending restart requests", async () => { let releaseFirst: (() => void) | undefined; const firstGate = new Promise((resolve) => { From db80d420640e41091f7f81d47547ecda45f6a222 Mon Sep 17 00:00:00 2001 From: wellwei Date: Tue, 11 Aug 2026 15:34:06 +0800 Subject: [PATCH 055/119] release: mcpp-vscode 0.3.1 --- CHANGELOG.md | 2 +- README.md | 8 ++++---- package-lock.json | 4 ++-- package.json | 2 +- test/artifacts.test.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e83eafc..831d24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # 更新日志 -## 未发布 +## 0.3.1 - 2026-08-11 - 修复缺少 CDB 时 IDE 命令看似无响应的问题:配置 clangd、刷新编译数据库、检查模块支持和一键配置现在会立即显示进度并打开 `mcpp` 输出频道;clangd 重启与 `mcpp build --configure-only` 增加超时,避免异步操作永久占用 IDE 队列。 - 增加删除 `compile_commands.json` 后刷新 CDB、配置 clangd、检查模块支持和一键配置的 Extension Host 回归测试。 diff --git a/README.md b/README.md index 0e2a655..b96ae2a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -当前版本为 `0.3.0`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +当前版本为 `0.3.1`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 > 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 @@ -37,7 +37,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.3.0.vsix +code --install-extension /path/to/mcpp-vscode-0.3.1.vsix ``` 安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 @@ -419,8 +419,8 @@ API、状态栏、任务和 clangd 集成。 版本完全一致的 tag: ```sh -git tag -a v0.3.0 -m "mcpp-vscode 0.3.0" -git push origin v0.3.0 +git tag -a v0.3.1 -m "mcpp-vscode 0.3.1" +git push origin v0.3.1 ``` `.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, diff --git a/package-lock.json b/package-lock.json index 0033314..3feca73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.3.0", + "version": "0.3.1", "license": "Apache-2.0", "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/package.json b/package.json index 1cb55bc..7c681d5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.3.0", + "version": "0.3.1", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 59fabb7..bf5baba 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -26,7 +26,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.3.0"); + assert.equal(manifest.version, "0.3.1"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); From 304463d29596a9533ce5ed8e194e6dae1a68155b Mon Sep 17 00:00:00 2001 From: HikariTish <809456206@qq.com> Date: Wed, 12 Aug 2026 00:02:15 +0800 Subject: [PATCH 056/119] fix: preserve PCM when LLVM setup is unchanged --- src/moduleSetup.ts | 5 ++++- test/moduleSetup.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts index 14d962a..6c6b8a0 100644 --- a/src/moduleSetup.ts +++ b/src/moduleSetup.ts @@ -75,7 +75,10 @@ export function mcppModuleSetupCommands(plan: ModuleSetupPlan): ModuleSetupComma args: ["toolchain", "default", "llvm"], }); } - commands.push({ stage: "build", mode: "task", args: ["build", "--no-cache"] }); + const buildArgs = plan.installLlvm || plan.switchDefault + ? ["build", "--no-cache"] + : ["build"]; + commands.push({ stage: "build", mode: "task", args: buildArgs }); return commands; } diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts index c6fad77..2796c97 100644 --- a/test/moduleSetup.test.ts +++ b/test/moduleSetup.test.ts @@ -210,7 +210,7 @@ test("阻止原因按未信任、未知清单、忙碌、项目覆盖的顺序 ); }); -test("模块设置命令使用固定 argv 数组且始终构建", () => { +test("模块设置命令仅在工具链变化时清缓存", () => { assert.deepEqual( mcppModuleSetupCommands({ kind: "ready", installLlvm: true, switchDefault: true }), [ @@ -221,7 +221,7 @@ test("模块设置命令使用固定 argv 数组且始终构建", () => { ); assert.deepEqual( mcppModuleSetupCommands({ kind: "ready", installLlvm: false, switchDefault: false }), - [{ stage: "build", mode: "task", args: ["build", "--no-cache"] }], + [{ stage: "build", mode: "task", args: ["build"] }], ); }); From 504eb110e13a706e1b008baa2244b8268492f996 Mon Sep 17 00:00:00 2001 From: HikariTish <809456206@qq.com> Date: Wed, 12 Aug 2026 01:25:41 +0800 Subject: [PATCH 057/119] chore: use workspace TypeScript SDK --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6b21fe3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.tsdk.path": "node_modules/typescript/lib" +} From 1bfd6c83cf4c27c88da39754b36b272e71e6cbc7 Mon Sep 17 00:00:00 2001 From: wellwei Date: Fri, 25 Sep 2026 21:58:58 +0800 Subject: [PATCH 058/119] =?UTF-8?q?feat:=20=E8=AF=AD=E8=A8=80=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=BE=9D=E8=B5=96=E5=88=87=E6=8D=A2=E4=B8=BA=20mcpp-l?= =?UTF-8?q?anguage-server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 C++ 模块语言服务职责从官方 clangd 扩展迁移到 sunrisepeak.mcpp-language-server(mcppls),本扩展只保留 mcpp 的构建、 工具链、任务与 mcpp.toml 编辑支持。 - package.json:extensionDependencies 换为 sunrisepeak.mcpp-language-server, VS Code 下限提升到 ^1.91.0,版本定为 0.4.0 - 新增 src/languageServer.ts:通过 mcppls 公开命令(restartServer / selectContext / showModuleGraph / showLogs)桥接,依赖缺失与命令失败 返回稳定结果,不引入 vscode-languageclient,不 spawn mcppls serve - src/extension.ts 收缩到 CLI + mcpp.toml 补全 + 语言服务桥接 - 删除 clangd 专属实现:analysis.ts、llvmTools.ts、workflow.ts 及其测试 - process.ts 只保留 runProcess;discovery.ts 去掉 CDB 路径与 clangd 候选 - 一键流程改为 mcpp build + mcppls 刷新,不再安装 llvm-tools 或切换 LLVM - 旧 mcpp.configureClangd 保留为弃用 alias,转发到新实现 顺带修复 mcpp.toml 补全与新 mcpp 契约的漂移:移除已被 mcpp 删除的 [xlings.envs] 段,[xlings.workspace] 示例版本改用当前索引可安装的 node。 npm test:179/179 通过。 --- package-lock.json | 8 +- package.json | 46 +- src/analysis.ts | 457 ---------- src/cliController.ts | 152 +--- src/commands.ts | 19 +- src/discovery.ts | 128 +-- src/extension.ts | 1428 +++---------------------------- src/languageServer.ts | 80 ++ src/llvmTools.ts | 173 ---- src/mcppTomlCompletion.ts | 8 +- src/moduleSetup.ts | 159 +--- src/process.ts | 57 -- src/tasks.ts | 6 +- src/workflow.ts | 315 ------- test/analysis.test.ts | 412 --------- test/artifacts.test.ts | 114 +-- test/commands.test.ts | 25 +- test/discovery.test.ts | 124 +-- test/languageServer.test.ts | 107 +++ test/llvmTools.test.ts | 189 ---- test/mcppTomlCompletion.test.ts | 6 + test/mcppTomlContract.test.ts | 8 +- test/moduleSetup.test.ts | 512 +++-------- test/process.test.ts | 70 +- test/tasks.test.ts | 18 +- test/workflow.test.ts | 499 ----------- 26 files changed, 594 insertions(+), 4526 deletions(-) delete mode 100644 src/analysis.ts create mode 100644 src/languageServer.ts delete mode 100644 src/llvmTools.ts delete mode 100644 src/workflow.ts delete mode 100644 test/analysis.test.ts create mode 100644 test/languageServer.test.ts delete mode 100644 test/llvmTools.test.ts delete mode 100644 test/workflow.test.ts diff --git a/package-lock.json b/package-lock.json index 3feca73..fb78b66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,24 @@ { "name": "mcpp-vscode", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.3.1", + "version": "0.4.0", "license": "Apache-2.0", "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", - "@types/vscode": "^1.90.0", + "@types/vscode": "^1.91.0", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", "mocha": "^11.8.0", "typescript": "^5.9.0" }, "engines": { - "vscode": "^1.90.0" + "vscode": "^1.91.0" } }, "node_modules/@azu/format-text": { diff --git a/package.json b/package.json index 7c681d5..5b43e50 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "mcpp-vscode", "displayName": "mcpp", - "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.3.1", + "description": "mcpp 工程构建、工具链与 C++ Modules 语言服务集成", + "version": "0.4.0", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", @@ -15,16 +15,17 @@ "url": "https://github.com/mcpp-community/mcpp-vscode/issues" }, "engines": { - "vscode": "^1.90.0" + "vscode": "^1.91.0" }, "extensionDependencies": [ - "llvm-vs-code-extensions.vscode-clangd" + "sunrisepeak.mcpp-language-server" ], "activationEvents": [ "workspaceContains:mcpp.toml", "onLanguage:cpp", "onLanguage:mcpp-build", "onLanguage:mcpp-toml", + "onCommand:mcpp.configureLanguageServer", "onCommand:mcpp.configureClangd", "onCommand:mcpp.refreshCompilationDatabase", "onCommand:mcpp.checkModuleSupport", @@ -37,7 +38,9 @@ "onCommand:mcpp.showToolchains", "onCommand:mcpp.installToolchain", "onCommand:mcpp.selectDefaultToolchain", - "onCommand:mcpp.autoConfigureModules" + "onCommand:mcpp.autoConfigureModules", + "onCommand:mcpp.showModuleGraph", + "onCommand:mcpp.showLanguageServerLogs" ], "categories": [ "Programming Languages", @@ -46,7 +49,7 @@ "capabilities": { "untrustedWorkspaces": { "supported": "limited", - "description": "未受信任工作区仅启用模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。" + "description": "未受信任工作区仅启用本扩展的模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 mcpp CLI 或接管语言服务配置。" } }, "main": "./dist/src/extension.js", @@ -104,21 +107,33 @@ "command": "mcpp.selectDefaultToolchain", "title": "mcpp: 选择全局默认工具链" }, + { + "command": "mcpp.configureLanguageServer", + "title": "mcpp: 选择 C++ 模块分析上下文" + }, { "command": "mcpp.configureClangd", - "title": "mcpp: 配置 clangd" + "title": "mcpp: [已弃用] 配置 C++ 模块语言服务" }, { "command": "mcpp.refreshCompilationDatabase", - "title": "mcpp: 刷新编译数据库" + "title": "mcpp: 刷新模块构建描述" }, { "command": "mcpp.checkModuleSupport", - "title": "mcpp: 检查模块支持" + "title": "mcpp: 重启 C++ Modules 语言服务" }, { "command": "mcpp.autoConfigureModules", - "title": "mcpp: 一键配置模块代码提示" + "title": "mcpp: 一键构建并刷新模块语言服务" + }, + { + "command": "mcpp.showModuleGraph", + "title": "mcpp: 查看模块图" + }, + { + "command": "mcpp.showLanguageServerLogs", + "title": "mcpp: 打开 C++ Modules 日志" } ], "configuration": { @@ -134,7 +149,8 @@ "type": "string", "default": "", "scope": "resource", - "description": "与 CDB 中 LLVM 编译器来自同一 revision 的 clangd 可执行文件路径;不要求与 mcpp 安装在同一位置。" + "description": "已弃用:C++ 模块语言服务已迁移到 sunrisepeak.mcpp-language-server,此设置不再生效。", + "deprecationMessage": "C++ 模块语言服务已迁移到 sunrisepeak.mcpp-language-server;此设置不再被 mcpp-vscode 使用。" }, "mcpp.modulesSupport": { "type": "string", @@ -145,13 +161,15 @@ ], "default": "auto", "scope": "resource", - "description": "控制 clangd 的 C++ 模块实验支持参数。" + "description": "已弃用:C++ 模块语言服务已迁移到 sunrisepeak.mcpp-language-server,此设置不再生效。", + "deprecationMessage": "C++ 模块语言服务已迁移到 sunrisepeak.mcpp-language-server;此设置不再被 mcpp-vscode 使用。" }, "mcpp.configureCppTools": { "type": "boolean", "default": true, "scope": "resource", - "description": "配置 clangd 时,是否询问关闭 Microsoft C/C++ IntelliSense。" + "description": "已弃用:语言服务冲突现由 sunrisepeak.mcpp-language-server 管理,此设置不再生效。", + "deprecationMessage": "C++ 模块语言服务已迁移到 sunrisepeak.mcpp-language-server;此设置不再被 mcpp-vscode 使用。" }, "mcpp.tomlCompletion": { "type": "boolean", @@ -224,7 +242,7 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", - "@types/vscode": "^1.90.0", + "@types/vscode": "^1.91.0", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", "mocha": "^11.8.0", diff --git a/src/analysis.ts b/src/analysis.ts deleted file mode 100644 index 921beb3..0000000 --- a/src/analysis.ts +++ /dev/null @@ -1,457 +0,0 @@ -import path from "node:path"; - -export type CompilerKind = "llvm" | "gcc" | "msvc" | "unknown"; -export type ModuleCapability = "full" | "syntax-only" | "unavailable"; -export type ModulesSupportMode = "auto" | "on" | "off"; -export type CheckResult = - | "ready" - | "pcm-mismatch" - | "module-unavailable" - | "wrong-language-mode" - | "check-failed"; - -export interface CompilationDatabaseAnalysis { - kind: CompilerKind; - capability: ModuleCapability; - compilerPath?: string; - sourceFile?: string; - directory?: string; - arguments?: string[]; - hasPrebuiltModules?: boolean; - reason: string; -} - -export interface ToolIdentity { - major: number; - minor: number; - patch: number; - revision?: string; -} - -export interface ToolIdentityComparison { - compatible: boolean; - reason: string; -} - -export interface ClangdArgumentOptions { - compilerPath: string; - compilationArguments?: readonly string[]; - modulesSupport: ModulesSupportMode; - clangdIdentity?: ToolIdentity; - platform: NodeJS.Platform; - hasPrebuiltModules?: boolean; - workspaceFolder?: string; -} - -export interface ClangdConfigurationPlan { - path: string; - arguments: string[]; - changed: boolean; -} - -interface CompilationCommand { - directory?: unknown; - file?: unknown; - arguments?: unknown; - command?: unknown; -} - -function unavailable(reason: string): CompilationDatabaseAnalysis { - return { - kind: "unknown", - capability: "unavailable", - reason, - }; -} - -function splitCommand(command: string): string[] { - const result: string[] = []; - let token = ""; - let quote: "'" | '"' | undefined; - - for (let index = 0; index < command.length; index += 1) { - const character = command[index]; - - if (quote !== "'" && character === "\\" && index + 1 < command.length) { - const next = command[index + 1]; - if (next === "\\" || next === "'" || next === '"' || /\s/.test(next)) { - token += next; - index += 1; - continue; - } - token += character; - continue; - } - - if (character === "'" || character === '"') { - if (quote === character) { - quote = undefined; - } else if (quote === undefined) { - quote = character; - } else { - token += character; - } - continue; - } - - if (/\s/.test(character) && quote === undefined) { - if (token.length > 0) { - result.push(token); - token = ""; - } - continue; - } - - token += character; - } - - if (token.length > 0) { - result.push(token); - } - - return result; -} - -function commandArguments(command: CompilationCommand): string[] | undefined { - if ( - Array.isArray(command.arguments) - && command.arguments.length > 0 - && command.arguments.every((argument) => typeof argument === "string") - ) { - return command.arguments; - } - - if (typeof command.command === "string") { - const argumentsFromCommand = splitCommand(command.command); - return argumentsFromCommand.length > 0 ? argumentsFromCommand : undefined; - } - - return undefined; -} - -function compilerKind(compilerPath: string): CompilerKind { - const executable = path.win32.basename(compilerPath).toLowerCase(); - const name = executable.endsWith(".exe") ? executable.slice(0, -4) : executable; - - if (name === "clang" || name === "clang++" || name === "clang-cl") { - return "llvm"; - } - if (name === "gcc" || name === "g++" || name === "c++") { - return "gcc"; - } - if (name === "cl") { - return "msvc"; - } - return "unknown"; -} - -export function analyzeCompilationDatabase(contents: string): CompilationDatabaseAnalysis { - let parsed: unknown; - try { - parsed = JSON.parse(contents); - } catch { - return unavailable("compile_commands.json 不是有效的 JSON"); - } - - if (!Array.isArray(parsed) || parsed.length === 0) { - return unavailable("compile_commands.json 至少需要包含一条编译命令"); - } - - const candidates: CompilationDatabaseAnalysis[] = []; - - for (const value of parsed) { - if (value === null || typeof value !== "object") { - continue; - } - - const command = value as CompilationCommand; - const args = commandArguments(command); - if (args === undefined) { - continue; - } - - const kind = compilerKind(args[0]); - if (kind === "unknown") { - continue; - } - - const hasPrebuiltModules = args.some((argument) => ( - argument.startsWith("-fmodule-file=") - || argument.startsWith("-fprebuilt-module-path=") - )); - const directory = typeof command.directory === "string" ? command.directory : undefined; - const sourceFile = typeof command.file === "string" - ? resolveCompilationSourceFile(directory, command.file) - : undefined; - candidates.push({ - kind, - capability: kind === "llvm" ? "full" : "syntax-only", - compilerPath: args[0], - sourceFile, - directory, - arguments: args, - hasPrebuiltModules, - reason: kind === "llvm" - ? "Clang 编译命令可以由 clangd 使用" - : `${kind.toUpperCase()} 模块产物不能由 clangd 使用`, - }); - } - - if (candidates.length === 0) { - return unavailable("compile_commands.json 不包含受支持的编译器命令"); - } - - const score = (candidate: CompilationDatabaseAnalysis): number => { - const sourceFile = candidate.sourceFile ?? ""; - const moduleInterface = /\.(?:cppm|ixx|mpp|ccm)$/i.test(sourceFile); - const inProject = candidate.directory !== undefined && isWithinDirectory(candidate.directory, sourceFile); - return (inProject ? 200 : 0) - + (candidate.directory !== undefined && isProjectSource(candidate.directory, sourceFile) ? 200 : 0) - + (moduleInterface ? 100 : 0) - + (candidate.hasPrebuiltModules ? 10 : 0); - }; - - return candidates.reduce((best, candidate) => (score(candidate) > score(best) ? candidate : best)); -} - -function isWithinDirectory(directory: string, file: string): boolean { - const windows = /^[A-Za-z]:[\\/]/.test(directory) || directory.includes("\\") - || /^[A-Za-z]:[\\/]/.test(file) || file.includes("\\"); - const pathApi = windows ? path.win32 : path.posix; - const relative = pathApi.relative(pathApi.resolve(directory), pathApi.resolve(file)); - return relative === "" - || (relative !== ".." && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative)); -} - -function resolveCompilationSourceFile(directory: string | undefined, file: string): string { - if (directory === undefined) { - return file; - } - const windows = /^[A-Za-z]:[\\/]/.test(directory) || directory.includes("\\") - || /^[A-Za-z]:[\\/]/.test(file) || file.includes("\\"); - const pathApi = windows ? path.win32 : path.posix; - return pathApi.isAbsolute(file) ? file : pathApi.resolve(directory, file); -} - -function isProjectSource(directory: string, file: string): boolean { - if (!isWithinDirectory(directory, file)) { - return false; - } - const windows = /^[A-Za-z]:[\\/]/.test(directory) || directory.includes("\\") - || /^[A-Za-z]:[\\/]/.test(file) || file.includes("\\"); - const pathApi = windows ? path.win32 : path.posix; - const relative = pathApi.relative(pathApi.resolve(directory), pathApi.resolve(file)); - const firstComponent = relative.split(pathApi.sep)[0]; - return firstComponent !== ".mcpp" && firstComponent !== "target"; -} - -function removeManagedArguments(arguments_: readonly string[]): string[] { - const result: string[] = []; - - for (let index = 0; index < arguments_.length; index += 1) { - const argument = arguments_[index]; - if (argument === "--experimental-modules-support") { - continue; - } - if (argument === "--query-driver") { - index += 1; - continue; - } - if (argument.startsWith("--query-driver=")) { - continue; - } - result.push(argument); - } - - return result; -} - -function hasExplicitLibcxxPath(arguments_: readonly string[]): boolean { - return arguments_.some((argument, index) => { - const value = argument === "-isystem" - ? arguments_[index + 1] ?? "" - : argument.startsWith("-isystem") - ? argument.slice("-isystem".length) - : ""; - return /[\\/]include[\\/]c\+\+[\\/]v1(?:[\\/]|$)/.test(value); - }); -} - -function isHermeticClangCommand(arguments_: readonly string[] | undefined): boolean { - if (arguments_ === undefined) { - return false; - } - return arguments_.includes("--no-default-config") - && arguments_.includes("-nostdinc++") - && hasExplicitLibcxxPath(arguments_); -} - -function expandWorkspaceVariables(argument: string, workspaceFolder?: string): string { - if (workspaceFolder === undefined) { - return argument; - } - - return argument.replace(/\$\{workspace(?:Folder|Root)\}/g, () => workspaceFolder); -} - -function shouldEnableExperimentalModules(options: ClangdArgumentOptions): boolean { - if (options.modulesSupport === "on") { - return true; - } - if (options.modulesSupport === "off") { - return false; - } - if (options.hasPrebuiltModules) { - return false; - } - - const identity = options.clangdIdentity; - if (identity === undefined || identity.major < 21) { - return false; - } - - return !( - options.platform === "win32" - && identity.major === 20 - && identity.minor === 1 - && identity.patch === 7 - ); -} - -export function buildClangdArguments( - existingArguments: readonly string[], - options: ClangdArgumentOptions, -): string[] { - const result = removeManagedArguments(existingArguments) - .map((argument) => expandWorkspaceVariables(argument, options.workspaceFolder)); - if (!isHermeticClangCommand(options.compilationArguments)) { - result.push(`--query-driver=${options.compilerPath}`); - } - - if (shouldEnableExperimentalModules(options)) { - result.push("--experimental-modules-support"); - } - - return result; -} - -export function buildClangdConfigurationPlan( - currentPath: string, - currentArguments: readonly string[], - resolvedPath: string, - options: ClangdArgumentOptions, -): ClangdConfigurationPlan { - const arguments_ = buildClangdArguments(currentArguments, options); - return { - path: resolvedPath, - arguments: arguments_, - changed: currentPath !== resolvedPath - || arguments_.length !== currentArguments.length - || arguments_.some((argument, index) => argument !== currentArguments[index]), - }; -} - -export function parseToolIdentity(versionOutput: string): ToolIdentity | undefined { - const version = versionOutput.match(/\b(?:clangd|clang)(?:\s+version)?\s+(\d+)\.(\d+)(?:\.(\d+))?/i) - ?? versionOutput.match(/\b(\d+)\.(\d+)(?:\.(\d+))?\b/); - if (version === null) { - return undefined; - } - - const revision = versionOutput.match(/\b[0-9a-f]{7,40}\b/gi)?.at(-1); - return { - major: Number(version[1]), - minor: Number(version[2]), - patch: Number(version[3] ?? 0), - ...(revision === undefined ? {} : { revision: revision.toLowerCase() }), - }; -} - -export function compareToolIdentities( - compiler: ToolIdentity | undefined, - clangd: ToolIdentity | undefined, -): ToolIdentityComparison { - if (compiler === undefined || clangd === undefined) { - return { - compatible: false, - reason: "无法确定两套 LLVM 工具的身份", - }; - } - - if ( - compiler.major !== clangd.major - || compiler.minor !== clangd.minor - || compiler.patch !== clangd.patch - ) { - return { - compatible: false, - reason: "编译器与 clangd 的 LLVM 版本不同", - }; - } - - if (compiler.revision === undefined || clangd.revision === undefined) { - return { - compatible: false, - reason: "LLVM 版本相同,但无法获得精确的 revision", - }; - } - - if ( - !compiler.revision.startsWith(clangd.revision) - && !clangd.revision.startsWith(compiler.revision) - ) { - return { - compatible: false, - reason: "编译器与 clangd 的 LLVM revision 不同", - }; - } - - return { - compatible: true, - reason: "LLVM 版本和 revision 均匹配", - }; -} - -export function classifyCheckResult(exitCode: number, output: string): CheckResult { - if (exitCode === 0) { - return "ready"; - } - - const normalized = output.toLowerCase(); - if ( - normalized.includes("ast_file_different_branch") - || normalized.includes("different branch") - || normalized.includes("pch file uses an older pch format") - || normalized.includes("ast_file_version_too_new") - || normalized.includes("newer format that cannot be read") - || normalized.includes("ast_file_version_too_old") - || normalized.includes("older format that is no longer supported") - ) { - return "pcm-mismatch"; - } - if ( - normalized.includes("don't get the module unit") - || normalized.includes("failed to build module") - || normalized.includes("module file not found") - || /module ['\"].+['\"] not found/.test(normalized) - ) { - return "module-unavailable"; - } - if ( - normalized.includes("unknown type name 'import'") - || normalized.includes('unknown type name "import"') - || normalized.includes("expected unqualified-id") && normalized.includes("import") - ) { - return "wrong-language-mode"; - } - if (normalized.includes("all checks completed")) { - const diagnostics = output - .split(/\r?\n/) - .filter((line) => /^\s*e\[/i.test(line)); - if (diagnostics.length === 0 || diagnostics.every((line) => /\btweak:/i.test(line))) { - return "ready"; - } - } - - return "check-failed"; -} diff --git a/src/cliController.ts b/src/cliController.ts index 0cec3d9..79e9fc9 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -15,24 +15,17 @@ import { type ToolchainItem, } from "./cli"; import type { McppProjectDiscovery } from "./discovery"; -import { runConfigureOnly as runConfigureOnlyProcess } from "./configureOnly"; -import { runProcess, type ProcessResult } from "./process"; +import { runProcess } from "./process"; import { McppOperationRegistry, classifyTaskExit, projectTaskPlan, - shouldReconcileAfterTask, + shouldRefreshLanguageServerAfterTask, type ProjectTaskKind, type TaskCompletion, } from "./tasks"; import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; import { runNewProjectFlow, validateNewProjectName } from "./newProject"; -import { - mcppModuleSetupCommands, - type ModuleSetupCommand, - type ModuleSetupDecision, - type ModuleSetupStepResult, -} from "./moduleSetup"; export interface McppCliControllerOptions { output: vscode.OutputChannel; @@ -53,6 +46,11 @@ interface ToolchainPickItem extends vscode.QuickPickItem { type OperationToken = object; +export interface ProjectTaskRunOptions { + /** Set to false when the caller performs its own post-task action. */ + notify?: boolean; +} + const INSTALL_CUSTOM_LABEL = "$(edit) 输入其他兼容工具链 spec…"; const CONFIRM_INSTALL = "安装"; const CONFIRM_DETECT = "检测"; @@ -119,76 +117,13 @@ export class McppCliController { return this.operations.hasActive(); } - public async runConfigureOnly( - project: McppProjectDiscovery, - ): Promise { - if (!this.options.isTrusted()) { - return undefined; - } - - const token: OperationToken = {}; - if (this.operations.beginProject(project.root, token) !== undefined) { - return undefined; - } - - const executable = this.mcppExecutable(project); - const args = ["build", "--configure-only"]; - try { - const result = await runConfigureOnlyProcess(project.root, executable); - this.appendShortCommand("刷新编译数据库", executable, args, result); - return result; - } finally { - // configure-only 与 build/run/test 共用项目锁,异常时也必须释放。 - this.operations.finishProject(project.root, token); - } - } - - public async runAutomaticModuleSetup( - plan: Extract, - ): Promise { - const project = this.requireProject(); - const commands = mcppModuleSetupCommands(plan); - const firstCommand = commands[0]; - if (project === undefined || !this.requireTrusted() || firstCommand === undefined) { - return { - stage: firstCommand?.stage ?? "build", - state: "failed", - detail: "当前工作区无法执行自动模块配置。", - }; - } - - const token: OperationToken = {}; - if (this.operations.beginGlobal(token) !== undefined) { - return { - stage: firstCommand.stage, - state: "failed", - detail: "已有 mcpp 操作正在运行。", - }; - } - - const executable = this.mcppExecutable(project); - try { - for (const command of commands) { - const result = await this.executeAutomaticModuleSetupCommand( - project, - executable, - command, - ); - if (result.state !== "succeeded") { - return result; - } - } - return { stage: "build", state: "succeeded" }; - } finally { - // 整个 install/default/build 事务共用一个 token,异常时也必须释放。 - this.operations.finishGlobal(token); - } - } - - public async runProjectTask(kind: ProjectTaskKind): Promise { + public async runProjectTask( + kind: ProjectTaskKind, + options: ProjectTaskRunOptions = {}, + ): Promise { const project = this.requireProject(); if (project === undefined || !this.requireTrusted()) { - return; + return undefined; } if (kind === "clean") { @@ -198,7 +133,7 @@ export class McppCliController { CONFIRM_CLEAN, ); if (choice !== CONFIRM_CLEAN) { - return; + return undefined; } } @@ -212,7 +147,7 @@ export class McppCliController { if (choice === SHOW_TASKS) { await vscode.commands.executeCommand("workbench.action.tasks.showTasks"); } - return; + return undefined; } let completion: TaskCompletion | undefined; @@ -229,9 +164,12 @@ export class McppCliController { this.operations.finishProject(project.root, token); } - if (completion !== undefined && shouldReconcileAfterTask(kind, completion)) { + if (options.notify !== false + && completion !== undefined + && shouldRefreshLanguageServerAfterTask(kind, completion)) { await this.options.afterProjectTask(project, kind, completion); } + return completion; } public async showToolchains(): Promise { @@ -557,7 +495,7 @@ export class McppCliController { label: item.label, description: item.group === "project" ? "当前 mcpp 工程" - : item.group === "toolchain" ? "mcpp 工具链管理" : "clangd 与编译数据库", + : item.group === "toolchain" ? "mcpp 工具链管理" : "C++ Modules 语言服务", command: item.command, })); const picked = await vscode.window.showQuickPick(items, { @@ -621,7 +559,7 @@ export class McppCliController { }); } - private guarded(operation: () => Promise): () => Promise { + private guarded(operation: () => Promise): () => Promise { return async () => { try { await operation(); @@ -632,7 +570,7 @@ export class McppCliController { } catch { // 输出频道可能已经在窗口重载时释放。 } - void vscode.window.showErrorMessage(`mcpp:${message}`); + await vscode.window.showErrorMessage(`mcpp:${message}`); } }; } @@ -679,7 +617,7 @@ export class McppCliController { const result = await runProcess(executable, args, workingDirectory(project)); this.appendShortCommand("查看工具链", executable, args, result); if (result.exitCode !== 0) { - void vscode.window.showErrorMessage( + await vscode.window.showErrorMessage( `mcpp toolchain list 失败(退出码 ${result.exitCode})。请查看 mcpp 输出频道。`, ); return undefined; @@ -687,7 +625,7 @@ export class McppCliController { const inventory = parseToolchainList(`${result.stdout}${result.stderr.length > 0 ? `\n${result.stderr}` : ""}`); if (!inventory.recognized) { - void vscode.window.showErrorMessage( + await vscode.window.showErrorMessage( "无法识别当前 mcpp toolchain list 输出;原始输出已保留在 mcpp 输出频道,请检查 mcpp 版本。", ); return undefined; @@ -695,50 +633,6 @@ export class McppCliController { return inventory; } - private async executeAutomaticModuleSetupCommand( - project: McppProjectDiscovery, - executable: string, - command: ModuleSetupCommand, - ): Promise { - if (command.mode === "process") { - const result = await runProcess(executable, command.args, project.root); - this.appendShortCommand(`自动模块配置:${command.stage}`, executable, command.args, result); - return { - stage: command.stage, - state: result.exitCode === 0 ? "succeeded" : "failed", - exitCode: result.exitCode, - }; - } - - // build 直接复用任务执行器,避免在 global token 内再次申请 project token。 - let completion: TaskCompletion; - try { - completion = await this.executeTask( - project.root, - executable, - "mcpp: 自动模块配置构建", - command.args, - ); - } catch (error) { - return { - stage: command.stage, - state: "failed", - detail: error instanceof Error ? error.message : String(error), - }; - } - this.appendTaskCompletion( - project.root, - "mcpp: 自动模块配置构建", - command.args, - completion, - ); - return { - stage: command.stage, - state: completion.state, - exitCode: completion.exitCode, - }; - } - private async executeTask( root: string, executable: string, diff --git a/src/commands.ts b/src/commands.ts index a28796c..eb60afe 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -8,7 +8,16 @@ export const CLI_COMMANDS = { showToolchains: "mcpp.showToolchains", installToolchain: "mcpp.installToolchain", selectDefaultToolchain: "mcpp.selectDefaultToolchain", + configureLanguageServer: "mcpp.configureLanguageServer", + refreshCompilationDatabase: "mcpp.refreshCompilationDatabase", + checkModuleSupport: "mcpp.checkModuleSupport", autoConfigureModules: "mcpp.autoConfigureModules", + showModuleGraph: "mcpp.showModuleGraph", + showLanguageServerLogs: "mcpp.showLanguageServerLogs", +} as const; + +export const DEPRECATED_COMMANDS = { + configureClangd: "mcpp.configureClangd", } as const; export const quickMenuStatusText = "$(tools) mcpp: 快捷菜单"; @@ -27,8 +36,10 @@ export const quickMenuItems: readonly QuickMenuItem[] = [ { label: "$(list-unordered) 查看工具链", command: CLI_COMMANDS.showToolchains, group: "toolchain" }, { label: "$(cloud-download) 安装工具链", command: CLI_COMMANDS.installToolchain, group: "toolchain" }, { label: "$(settings-gear) 选择全局默认工具链", command: CLI_COMMANDS.selectDefaultToolchain, group: "toolchain" }, - { label: "$(symbol-interface) 配置 clangd", command: "mcpp.configureClangd", group: "ide" }, - { label: "$(database) 刷新编译数据库", command: "mcpp.refreshCompilationDatabase", group: "ide" }, - { label: "$(check) 检查模块支持", command: "mcpp.checkModuleSupport", group: "ide" }, - { label: "$(rocket) 一键配置模块代码提示", command: CLI_COMMANDS.autoConfigureModules, group: "ide" }, + { label: "$(symbol-interface) 选择 C++ 模块分析上下文", command: CLI_COMMANDS.configureLanguageServer, group: "ide" }, + { label: "$(database) 刷新模块构建描述", command: CLI_COMMANDS.refreshCompilationDatabase, group: "ide" }, + { label: "$(check) 重启 C++ Modules 语言服务", command: CLI_COMMANDS.checkModuleSupport, group: "ide" }, + { label: "$(type-hierarchy) 查看模块图", command: CLI_COMMANDS.showModuleGraph, group: "ide" }, + { label: "$(output) 打开 C++ Modules 日志", command: CLI_COMMANDS.showLanguageServerLogs, group: "ide" }, + { label: "$(rocket) 一键构建并刷新模块语言服务", command: CLI_COMMANDS.autoConfigureModules, group: "ide" }, ]; diff --git a/src/discovery.ts b/src/discovery.ts index 64c461c..c7a08ff 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -1,39 +1,12 @@ -import { - existsSync, - readFileSync, - statSync, -} from "node:fs"; +import { existsSync, statSync } from "node:fs"; import path from "node:path"; -import { parseMcppToml } from "./mcppTomlParser"; - export interface McppProjectDiscovery { root: string; manifestPath: string; - compilationDatabasePath: string; -} - -export function manifestProjectRoot(manifestPath: string): string { - return path.dirname(path.resolve(manifestPath)); -} - -export function shouldReconcileDeletedManifest( - currentProjectRoot: string | undefined, - manifestPath: string, -): boolean { - return currentProjectRoot !== undefined - && path.resolve(currentProjectRoot) === manifestProjectRoot(manifestPath); -} - -function isWindowsPath(value: string): boolean { - return /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\"); } -function unique(values: string[]): string[] { - return values.filter((value, index) => values.indexOf(value) === index); -} - -export function isPathWithinProject(candidate: string, root: string): boolean { +function isPathWithin(candidate: string, root: string): boolean { const relative = path.relative(root, candidate); return relative === "" || ( relative !== ".." @@ -42,58 +15,6 @@ export function isPathWithinProject(candidate: string, root: string): boolean { ); } -interface ManifestShape { - hasPackage: boolean; - hasWorkspace: boolean; -} - -function readManifestShape(manifestPath: string): ManifestShape { - try { - const document = parseMcppToml(readFileSync(manifestPath, "utf8").split(/\r?\n/)); - let hasPackage = false; - let hasWorkspace = false; - let insideSection = false; - for (const node of document.nodes) { - if (node.type === "section") { - insideSection = true; - if (node.segments.length === 1) { - hasPackage ||= node.segments[0].name === "package"; - hasWorkspace ||= node.segments[0].name === "workspace"; - } - continue; - } - // mcpp 同样接受顶层 dotted key 和 inline table 写法。 - if (!insideSection) { - hasPackage ||= node.keyPath[0]?.name === "package"; - hasWorkspace ||= node.keyPath[0]?.name === "workspace"; - } - } - return { hasPackage, hasWorkspace }; - } catch { - // 无法读取时保留原发现结果,让 mcpp 自己给出清单诊断。 - return { hasPackage: false, hasWorkspace: false }; - } -} - -export function projectAffectedByManifest( - manifestPath: string, - currentProject: McppProjectDiscovery | undefined, - manifestProject: McppProjectDiscovery | undefined, -): McppProjectDiscovery | undefined { - const manifestRoot = manifestProjectRoot(manifestPath); - const shape = readManifestShape(manifestPath); - if ( - shape.hasWorkspace - && currentProject !== undefined - && currentProject.root !== manifestRoot - && isPathWithinProject(currentProject.root, manifestRoot) - ) { - // member 会继承 workspace 根配置,根清单变化必须刷新当前 member 的 CDB。 - return currentProject; - } - return manifestProject; -} - export function findNearestMcppProject( startPath: string, workspaceRoot?: string, @@ -106,29 +27,18 @@ export function findNearestMcppProject( current = path.dirname(current); } } catch { - // 新创建的 VS Code 工作区路径可能尚不存在,此时按目录处理。 + // A newly-created workspace path may not exist yet; treat it as a directory. } - if (boundary !== undefined && !isPathWithinProject(current, boundary)) { + if (boundary !== undefined && !isPathWithin(current, boundary)) { return undefined; } while (true) { const manifestPath = path.join(current, "mcpp.toml"); if (existsSync(manifestPath)) { - // #387 的 virtual workspace fan-out 只在各 member 根发布 CDB; - // 虚拟根不是 clangd 可消费的单一 package 工程。 - const shape = readManifestShape(manifestPath); - if (shape.hasWorkspace && !shape.hasPackage) { - return undefined; - } - return { - root: current, - manifestPath, - compilationDatabasePath: path.join(current, "compile_commands.json"), - }; + return { root: current, manifestPath }; } - if (boundary !== undefined && current === boundary) { return undefined; } @@ -139,31 +49,3 @@ export function findNearestMcppProject( current = parent; } } - -export function deriveClangdCandidates(compilerPath: string): string[] { - const windows = isWindowsPath(compilerPath); - const pathApi = windows ? path.win32 : path.posix; - const compilerName = pathApi.basename(compilerPath); - const extension = /\.exe$/i.test(compilerName) ? ".exe" : ""; - const sibling = pathApi.join(pathApi.dirname(compilerPath), `clangd${extension}`); - const xlingsMatch = compilerPath.match( - /^(.*?)([\\/])xim-x-llvm[\\/](\d+(?:\.\d+){1,3})[\\/]bin[\\/]clang(?:\+\+|-cl)(\.exe)?$/i, - ); - - const xlingsCandidate = xlingsMatch === null - ? undefined - : `${xlingsMatch[1]}${xlingsMatch[2]}xim-x-llvm-tools${xlingsMatch[2]}${xlingsMatch[3]}${xlingsMatch[2]}bin${xlingsMatch[2]}clangd${xlingsMatch[4] ?? ""}`; - const userXlingsMatch = compilerPath.match( - /^(.*?)([\\/])\.mcpp[\\/]registry[\\/]data[\\/]xpkgs[\\/]xim-x-llvm[\\/](\d+(?:\.\d+){1,3})[\\/]bin[\\/]clang(?:\+\+|-cl)(\.exe)?$/i, - ); - const userXlingsCandidate = userXlingsMatch === null - ? undefined - : `${userXlingsMatch[1]}${userXlingsMatch[2]}.xlings${userXlingsMatch[2]}data${userXlingsMatch[2]}xpkgs${userXlingsMatch[2]}xim-x-llvm-tools${userXlingsMatch[2]}${userXlingsMatch[3]}${userXlingsMatch[2]}bin${userXlingsMatch[2]}clangd${userXlingsMatch[4] ?? ""}`; - - return unique([ - sibling, - ...(xlingsCandidate === undefined ? [] : [xlingsCandidate]), - ...(userXlingsCandidate === undefined ? [] : [userXlingsCandidate]), - "clangd", - ]); -} diff --git a/src/extension.ts b/src/extension.ts index 0d26e3d..b4d875e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,101 +1,23 @@ -import { existsSync, readFileSync } from "node:fs"; -import process from "node:process"; - import * as vscode from "vscode"; -import { - analyzeCompilationDatabase, - buildClangdArguments, - buildClangdConfigurationPlan, - compareToolIdentities, - type CheckResult, - type CompilationDatabaseAnalysis, - type ModulesSupportMode, - type ToolIdentityComparison, -} from "./analysis"; -import { - deriveClangdCandidates, - findNearestMcppProject, - isPathWithinProject, - manifestProjectRoot, - projectAffectedByManifest, - shouldReconcileDeletedManifest, - type McppProjectDiscovery, -} from "./discovery"; -import { - resolveXlingsExecutable, - llvmToolsVersionSpec, - xlingsInstallArgs, -} from "./llvmTools"; -import { CLI_COMMANDS } from "./commands"; import { McppCliController } from "./cliController"; -import { runClangdCheck, runToolVersion, type ProcessResult, type ToolVersionResult } from "./process"; -import { ensureIdeConfigured } from "./ideWorkflow"; -import { - configurationReadyAfterRestart, - configurationAffectsMcppExecution, - configurationAffectsModuleSupport, - createKeyedSingleFlightReconciler, - createLatestOperationTracker, - createSerialExecutor, - describeConfigureOnlyOutcome, - describeRefreshOutcome, - moduleSupportState, - registerCompilationDatabaseReconciliation, - shouldRestartClangd, - shouldCheckModuleSupport, - shouldRenderProjectStatus, - shouldUseWorkspaceClangd, - statusCommandForCapability, - withTimeout, - workspaceAllowsToolExecution, - type ModuleSupportState, -} from "./workflow"; -import { classifyTaskExit, type TaskCompletion } from "./tasks"; +import { CLI_COMMANDS, DEPRECATED_COMMANDS } from "./commands"; +import { findNearestMcppProject, type McppProjectDiscovery } from "./discovery"; import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./inProject"; +import { + createLanguageServerBridge, + type LanguageServerBridge, + type LanguageServerCommandResult, +} from "./languageServer"; import { computeMcppTomlCompletions } from "./mcppTomlCompletion"; import { buildModuleSetupPlan, executeModuleSetup, moduleSetupConfirmation, - type ModuleSetupBlockedReason, + type ModuleSetupDecision, type ModuleSetupStepResult, } from "./moduleSetup"; - -const COMMAND_CONFIGURE = "mcpp.configureClangd"; -const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; -const COMMAND_CHECK = "mcpp.checkModuleSupport"; - -interface ProjectContext { - project: McppProjectDiscovery; - analysis: CompilationDatabaseAnalysis; -} - -interface ClangdResolution { - path: string; - version: ToolVersionResult; - comparison: ToolIdentityComparison; -} - -interface ProjectReconciliation { - context: ProjectContext | undefined; - databaseFound: boolean; - configured: boolean; -} - -interface ModuleStatus { - state: ModuleSupportState; - message: string; -} - -type ConfigureMode = "automatic" | "interactive"; - -const moduleStatusByProject = new Map(); -const moduleCheckOperations = createLatestOperationTracker(); -// 记录 manifest/settings 变化触发的 IDE 配置请求;CDB watcher 只重读 -// 已发布快照,避免写 CDB 后再次启动 mcpp 形成重入。 -const forceConfigureOnlyByProject = new Set(); -let lastReconciledProjectRoot: string | undefined; +import type { TaskCompletion } from "./tasks"; function findCurrentProject(): McppProjectDiscovery | undefined { const activeEditor = vscode.window.activeTextEditor; @@ -112,10 +34,7 @@ function findCurrentProject(): McppProjectDiscovery | undefined { } for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { - const project = findNearestMcppProject( - workspaceFolder.uri.fsPath, - workspaceFolder.uri.fsPath, - ); + const project = findNearestMcppProject(workspaceFolder.uri.fsPath); if (project !== undefined) { return project; } @@ -123,665 +42,66 @@ function findCurrentProject(): McppProjectDiscovery | undefined { return undefined; } -function findProjectForUri(uri: vscode.Uri): McppProjectDiscovery | undefined { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); - if (workspaceFolder === undefined) { - return undefined; - } - return findNearestMcppProject(uri.fsPath, workspaceFolder.uri.fsPath); -} - -function findWorkspaceProjects( - currentProject: McppProjectDiscovery | undefined, -): McppProjectDiscovery[] { - const projects = new Map(); - if (currentProject !== undefined) { - projects.set(currentProject.root, currentProject); - } - for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { - const project = findNearestMcppProject( - workspaceFolder.uri.fsPath, - workspaceFolder.uri.fsPath, - ); - if (project !== undefined) { - projects.set(project.root, project); - } - } - return [...projects.values()]; -} - -function loadProjectContext(project: McppProjectDiscovery | undefined = findCurrentProject()): ProjectContext | undefined { - if (project === undefined) { - return undefined; - } - - if (!existsSync(project.compilationDatabasePath)) { - return { - project, - analysis: { - kind: "unknown", - capability: "unavailable", - reason: `找不到编译数据库:${project.compilationDatabasePath}`, - }, - }; - } - +function outputText(output: vscode.OutputChannel, line: string): void { try { - return { - project, - analysis: analyzeCompilationDatabase(readFileSync(project.compilationDatabasePath, "utf8")), - }; - } catch (error) { - return { - project, - analysis: { - kind: "unknown", - capability: "unavailable", - reason: error instanceof Error ? error.message : String(error), - }, - }; + output.appendLine(line); + } catch { + // The output channel can close before an async task finishes during shutdown. } } -function hasUsableCompilationDatabase(project: McppProjectDiscovery): boolean { - return loadProjectContext(project)?.analysis.capability !== "unavailable"; +function resultText(output: vscode.OutputChannel, result: LanguageServerCommandResult): void { + outputText(output, `[C++ Modules] ${result.message}`); + if (result.state === "unavailable") { + void vscode.window.showWarningMessage(result.message, "安装扩展").then((choice) => { + if (choice === "安装扩展") { + void vscode.commands.executeCommand("workbench.extensions.search", "@id:sunrisepeak.mcpp-language-server"); + } + }); + } else if (result.state === "failed") { + void vscode.window.showErrorMessage(result.message); + } } -function moduleSetupBlockedMessage(reason: ModuleSetupBlockedReason): string { +function moduleSetupBlockedMessage(reason: Extract["reason"]): string { switch (reason) { - case "project-toolchain-override": - return "当前项目显式固定了非 LLVM 工具链;一键配置不会修改 mcpp.toml,请先手动切换项目工具链。"; case "untrusted": - return "当前工作区未受信任,不会执行外部程序。请先信任工作区。"; + return "当前工作区未受信任,不会执行 mcpp 或刷新 C++ 模块语言服务。请先信任工作区。"; case "busy": return "已有 mcpp 操作正在运行,请等待完成后再试。"; - case "unrecognized-inventory": - return "无法识别 mcpp 工具链状态,请查看 mcpp 输出频道并检查 mcpp 版本。"; - } -} - -function configurationTarget(uri: vscode.Uri): vscode.ConfigurationTarget { - return vscode.workspace.getWorkspaceFolder(uri) === undefined - ? vscode.ConfigurationTarget.Workspace - : vscode.ConfigurationTarget.WorkspaceFolder; -} - -function projectConfiguration(project: McppProjectDiscovery): vscode.WorkspaceConfiguration { - return vscode.workspace.getConfiguration("mcpp", vscode.Uri.file(project.root)); -} - -function officialClangdConfiguration(project: McppProjectDiscovery): vscode.WorkspaceConfiguration { - return vscode.workspace.getConfiguration("clangd", vscode.Uri.file(project.root)); -} - -function appendProcessOutput( - output: vscode.OutputChannel, - title: string, - executable: string, - args: string[], - result: { exitCode: number; stdout: string; stderr: string }, -): void { - try { - output.appendLine(`\n[${new Date().toISOString()}] ${title}`); - output.appendLine(`$ ${executable} ${args.join(" ")}`); - if (result.stdout.length > 0) { - output.appendLine(result.stdout.trimEnd()); - } - if (result.stderr.length > 0) { - output.appendLine(result.stderr.trimEnd()); - } - output.appendLine(`[exit ${result.exitCode}]`); - } catch { - // 重载窗口时输出频道可能先于异步检查关闭。 - } -} - -function appendOutputLine(output: vscode.OutputChannel, value: string): void { - try { - output.appendLine(value); - } catch { - // 重载窗口时输出频道可能先于异步操作关闭。 - } -} - -async function resolveClangd(context: ProjectContext): Promise { - const compilerPath = context.analysis.compilerPath; - if (compilerPath === undefined) { - return undefined; - } - - const compilerVersion = await runToolVersion(compilerPath); - const configuredPath = projectConfiguration(context.project).get("clangd.path", "").trim(); - const candidates = configuredPath.length > 0 - ? [configuredPath] - : deriveClangdCandidates(compilerPath); - let fallback: ClangdResolution | undefined; - - for (const candidate of candidates) { - if (candidate !== "clangd" && !existsSync(candidate)) { - continue; - } - - const version = await runToolVersion(candidate); - if (version.exitCode !== 0 || version.identity === undefined) { - continue; - } - - const comparison = compareToolIdentities(compilerVersion.identity, version.identity); - const resolution = { path: candidate, version, comparison }; - if (comparison.compatible) { - return resolution; - } - fallback ??= resolution; - } - - return fallback; -} - -async function maybeDisableCppTools(project: McppProjectDiscovery): Promise { - if (vscode.extensions.getExtension("ms-vscode.cpptools") === undefined) { - return; - } - if (!projectConfiguration(project).get("configureCppTools", true)) { - return; - } - - const uri = vscode.Uri.file(project.root); - const configuration = vscode.workspace.getConfiguration("C_Cpp", uri); - if (configuration.get("intelliSenseEngine") === "disabled") { - return; - } - - const disable = "关闭 IntelliSense"; - const choice = await vscode.window.showWarningMessage( - "clangd 和 Microsoft C/C++ IntelliSense 都可能报告诊断。是否只关闭当前工作区的 cpptools IntelliSense?", - disable, - "保留 IntelliSense", - ); - if (choice === disable) { - await configuration.update( - "intelliSenseEngine", - "disabled", - configurationTarget(uri), - ); - } -} - -const CLANGD_RESTART_TIMEOUT_MS = 15_000; - -async function restartClangd(output: vscode.OutputChannel): Promise { - if (vscode.extensions.getExtension("llvm-vs-code-extensions.vscode-clangd") === undefined) { - return false; - } - try { - const timedOut = {}; - const result = await withTimeout( - vscode.commands.executeCommand("clangd.restart"), - CLANGD_RESTART_TIMEOUT_MS, - timedOut, - ); - if (result === timedOut) { - appendOutputLine(output, `[clangd] 重启命令超过 ${CLANGD_RESTART_TIMEOUT_MS / 1000} 秒未完成,已释放 IDE 操作队列。`); - return false; - } - return true; - } catch { - // clangd 扩展可能尚未激活,但配置仍然已经写入。 - return false; - } -} - -async function configureClangd( - context: ProjectContext, - status: vscode.StatusBarItem, - output: vscode.OutputChannel, - mode: ConfigureMode, - forceRestart: boolean = false, -): Promise { - const interactive = mode === "interactive"; - if (context.analysis.capability === "syntax-only") { - if (interactive) { - void vscode.window.showWarningMessage( - `${context.analysis.kind.toUpperCase()} 模块产物无法由 clangd 读取。语法高亮仍然可用,但模块语义诊断需要 LLVM mcpp 工具链。`, - ); - } - updateStatusBar(status, context); - return false; - } - if (context.analysis.capability !== "full" || context.analysis.compilerPath === undefined) { - if (interactive) { - void vscode.window.showWarningMessage( - `${context.analysis.reason} 请先运行“mcpp: 刷新编译数据库”。`, - ); - } - return false; - } - if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { - const message = "当前工作区未受信任,mcpp 不会执行 CDB 中的编译器或 clangd,也不会接管 clangd 配置。"; - if (interactive) { - void vscode.window.showWarningMessage(message); - } else { - appendOutputLine(output, `[自动配置] ${message}`); - } - return false; - } - - const clangd = await resolveClangd(context); - if (clangd === undefined) { - const message = "没有找到可用的 clangd。请安装与 mcpp LLVM 编译器来自同一 revision 的 clangd,或设置 mcpp.clangd.path;clangd 可以来自 xlings llvm-tools,也可以独立安装。"; - if (interactive) { - void vscode.window.showErrorMessage(message); - } else { - appendOutputLine(output, `[自动配置] ${message}`); - } - return false; - } - if (!clangd.comparison.compatible && !interactive) { - appendOutputLine( - output, - `[自动配置] 跳过不匹配的 clangd ${clangd.path}:${clangd.comparison.reason}。`, - ); - return false; - } - - const clangdConfiguration = officialClangdConfiguration(context.project); - const modulesSupport = projectConfiguration(context.project) - .get("modulesSupport", "auto"); - const plan = buildClangdConfigurationPlan( - clangdConfiguration.get("path", "clangd"), - clangdConfiguration.get("arguments", []), - clangd.path, - { - compilerPath: context.analysis.compilerPath, - compilationArguments: context.analysis.arguments, - modulesSupport, - clangdIdentity: clangd.version.identity, - platform: process.platform, - hasPrebuiltModules: context.analysis.hasPrebuiltModules, - workspaceFolder: context.project.root, - }, - ); - - if (plan.changed) { - // clangd.arguments 不是资源域设置,必须与 clangd.path 一起写到工作区层级。 - await clangdConfiguration.update("path", plan.path, vscode.ConfigurationTarget.Workspace); - await clangdConfiguration.update("arguments", plan.arguments, vscode.ConfigurationTarget.Workspace); - } - if (interactive) { - await maybeDisableCppTools(context.project); - } - const restartRequired = shouldRestartClangd(plan.changed, interactive, forceRestart); - const restartSucceeded = restartRequired ? await restartClangd(output) : false; - if (!configurationReadyAfterRestart(restartRequired, restartSucceeded)) { - const message = "clangd 配置已写入,但无法重启语言服务器。请查看 mcpp 输出频道,或手动执行 clangd 重启命令。"; - if (interactive) { - void vscode.window.showErrorMessage(message); - } else { - appendOutputLine(output, `[自动配置] ${message}`); - } - return false; - } - updateStatusBar(status, context); - - if (!clangd.comparison.compatible) { - void vscode.window.showWarningMessage( - `clangd 已配置,但 LLVM 身份与 mcpp 编译器不匹配:${clangd.comparison.reason}。`, - ); - } else if (interactive) { - void vscode.window.showInformationMessage("mcpp 已为当前工作区配置匹配的 clangd。"); - } else if (plan.changed) { - appendOutputLine(output, `[自动配置] clangd.path = ${plan.path}`); - } - - return true; -} - -function renderModuleStatus(status: vscode.StatusBarItem, moduleStatus: ModuleStatus): void { - status.command = COMMAND_CHECK; - status.show(); - status.text = moduleStatus.state === "available" - ? "$(pass) mcpp: 模块可用" - : "$(warning) mcpp: 模块不可用"; - status.tooltip = moduleStatus.message; -} - -function invalidateModuleStatus(projectRoot: string): void { - moduleCheckOperations.invalidate(projectRoot); - moduleStatusByProject.delete(projectRoot); -} - -function storeModuleStatus( - status: vscode.StatusBarItem, - context: ProjectContext, - moduleStatus: ModuleStatus, - checkToken?: number, -): boolean { - if ( - checkToken !== undefined - && !moduleCheckOperations.isCurrent(context.project.root, checkToken) - ) { - return false; - } - moduleStatusByProject.set(context.project.root, moduleStatus); - if (shouldRenderProjectStatus(findCurrentProject()?.root, context.project.root)) { - renderModuleStatus(status, moduleStatus); - } - return true; -} - -function updateStatusBar(status: vscode.StatusBarItem, context?: ProjectContext): void { - const currentProjectRoot = findCurrentProject()?.root; - if (context === undefined) { - if (currentProjectRoot === undefined) { - status.hide(); - } - return; - } - - if (context.analysis.capability !== "full") { - invalidateModuleStatus(context.project.root); - } - if (!shouldRenderProjectStatus(currentProjectRoot, context.project.root)) { - return; - } - - status.command = statusCommandForCapability(context.analysis.capability); - status.show(); - if (context.analysis.capability === "full") { - const moduleStatus = moduleStatusByProject.get(context.project.root); - if (moduleStatus !== undefined) { - renderModuleStatus(status, moduleStatus); - return; - } - status.text = "$(symbol-interface) mcpp: LLVM 模块"; - status.tooltip = "正在等待 clangd 检查模块编译命令和 PCM。"; - return; - } - if (context.analysis.capability === "syntax-only") { - status.text = `$(info) mcpp: 仅${context.analysis.kind.toUpperCase()}语法`; - status.tooltip = `${context.analysis.reason} 模块语法高亮仍然可用。`; - return; - } - - status.text = "$(warning) mcpp: 缺少模块 CDB"; - status.tooltip = context.analysis.reason; -} - -function checkResultMessage(classification: CheckResult): string { - switch (classification) { - case "ready": - return "clangd 已加载 mcpp 编译命令和模块产物。"; - case "pcm-mismatch": - return "clangd 与 mcpp 编译器或 PCM 来自不同的 LLVM 构建。建议运行“一键配置模块代码提示”安装匹配的 llvm-tools,或设置 mcpp.clangd.path。"; - case "module-unavailable": - return "clangd 无法加载所需模块产物,请运行“mcpp: 刷新编译数据库”,并检查 CDB 中的 PCM 路径。"; - case "wrong-language-mode": - return "clangd 没有为当前文件读取 C++20 或更高版本的编译命令。"; - default: - return "clangd 模块检查失败,完整诊断请查看 mcpp 输出频道。"; - } -} - -async function runModuleSupportCheck( - context: ProjectContext, - status: vscode.StatusBarItem, - output: vscode.OutputChannel, - mode: ConfigureMode, -): Promise { - const interactive = mode === "interactive"; - if (context.analysis.capability === "syntax-only") { - const message = `${context.analysis.kind.toUpperCase()} 模块产物不能由 clangd 消费,模块代码提示不可用。语法高亮仍然可用。`; - updateStatusBar(status, context); - if (interactive) { - const configure = "一键配置"; - const choice = await vscode.window.showWarningMessage( - `${message}\n\n如需启用模块代码提示,请切换到 LLVM 工具链后重新构建。`, - configure, - "关闭", - ); - if (choice === configure) { - await vscode.commands.executeCommand(CLI_COMMANDS.autoConfigureModules); - } - } - return undefined; - } - if ( - context.analysis.capability !== "full" - || context.analysis.compilerPath === undefined - || context.analysis.sourceFile === undefined - ) { - invalidateModuleStatus(context.project.root); - const message = context.analysis.capability === "full" - ? "compile_commands.json 没有可供 clangd 检查的源文件。" - : context.analysis.reason; - if (context.analysis.capability === "full") { - storeModuleStatus(status, context, { state: "unavailable", message }); - } - if (interactive) { - void vscode.window.showWarningMessage(message); - } - return context.analysis.capability === "full" - ? { state: "unavailable", message } - : undefined; - } - - if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { - invalidateModuleStatus(context.project.root); - const message = "当前工作区未受信任,不会执行 clangd 模块检查。信任工作区后扩展会自动重新检查。"; - const moduleStatus = { state: "unavailable", message } as const; - storeModuleStatus(status, context, moduleStatus); - if (interactive) { - void vscode.window.showWarningMessage(message); - } else { - appendOutputLine(output, `[自动检查] ${message}`); - } - return moduleStatus; - } - - const checkToken = moduleCheckOperations.begin(context.project.root); - moduleStatusByProject.delete(context.project.root); - if (shouldRenderProjectStatus(findCurrentProject()?.root, context.project.root)) { - status.command = COMMAND_CHECK; - status.text = "$(sync~spin) mcpp: 正在检查模块"; - status.tooltip = "正在使用 clangd 检查模块编译命令和 PCM。"; - status.show(); - } - - const clangd = await resolveClangd(context); - if (clangd === undefined) { - const message = "当前 LLVM 工具链没有找到可用的 clangd。请设置 mcpp.clangd.path。"; - const moduleStatus = { state: "unavailable", message } as const; - if (!storeModuleStatus(status, context, moduleStatus, checkToken)) { - return undefined; - } - if (interactive) { - void vscode.window.showErrorMessage(message); - } else { - appendOutputLine(output, `[自动检查] ${message}`); - } - return moduleStatus; - } - - const arguments_ = buildClangdArguments( - officialClangdConfiguration(context.project).get("arguments", []), - { - compilerPath: context.analysis.compilerPath, - compilationArguments: context.analysis.arguments, - modulesSupport: projectConfiguration(context.project) - .get("modulesSupport", "auto"), - clangdIdentity: clangd.version.identity, - platform: process.platform, - hasPrebuiltModules: context.analysis.hasPrebuiltModules, - workspaceFolder: context.project.root, - }, - ); - const result = await runClangdCheck( - clangd.path, - context.analysis.sourceFile, - context.project.root, - arguments_, - ); - appendProcessOutput( - output, - interactive ? "检查模块支持" : "自动检查模块支持", - clangd.path, - [`--check=${context.analysis.sourceFile}`, ...arguments_], - result, - ); - if (interactive) { - output.show(true); } - - const message = checkResultMessage(result.classification); - const moduleStatus = { - state: moduleSupportState(result.classification), - message, - }; - if (!storeModuleStatus(status, context, moduleStatus, checkToken)) { - return undefined; - } - if (interactive) { - if (moduleStatus.state === "available") { - void vscode.window.showInformationMessage(message); - } else { - void vscode.window.showErrorMessage(message); - } - } else { - appendOutputLine(output, `[自动检查] ${message}`); - } - return moduleStatus; -} - -async function updateModuleSupportForContext( - context: ProjectContext, - configured: boolean, - status: vscode.StatusBarItem, - output: vscode.OutputChannel, -): Promise { - if (shouldCheckModuleSupport( - context.analysis.capability, - configured, - context.analysis.sourceFile, - )) { - const moduleStatus = await runModuleSupportCheck(context, status, output, "automatic"); - return moduleStatus?.state === "available"; - } - - if (context.analysis.capability === "full") { - invalidateModuleStatus(context.project.root); - const message = configured - ? "compile_commands.json 没有可供 clangd 检查的源文件。" - : "clangd 未完成配置,模块语义当前不可用。详情请查看 mcpp 输出频道。"; - storeModuleStatus(status, context, { state: "unavailable", message }); - } - return false; } -async function executeXlingsInstallTask( - xlingsPath: string, - args: string[], - cwd: string, -): Promise { - const task = new vscode.Task( - { type: "mcpp-xlings", command: args[0] ?? "xlings" }, - vscode.TaskScope.Workspace, - "mcpp: 安装 llvm-tools", - "mcpp", - new vscode.ProcessExecution(xlingsPath, args, { cwd }), - ); - task.presentationOptions = { - reveal: vscode.TaskRevealKind.Always, - panel: vscode.TaskPanelKind.Dedicated, - focus: true, - clear: true, - showReuseMessage: false, - }; - - let execution: vscode.TaskExecution | undefined; - let earlyCompletion: TaskCompletion | undefined; - let settled = false; - let processEndSubscription: vscode.Disposable | undefined; - let taskEndSubscription: vscode.Disposable | undefined; - const disposeListeners = (): void => { - processEndSubscription?.dispose(); - taskEndSubscription?.dispose(); - }; - const finish = (completion: TaskCompletion): void => { - if (settled) { - return; - } - settled = true; - disposeListeners(); - resolveCompletion?.(completion); - }; - let resolveCompletion: ((completion: TaskCompletion) => void) | undefined; - - const completion = new Promise((resolve) => { - resolveCompletion = resolve; - processEndSubscription = vscode.tasks.onDidEndTaskProcess((event) => { - if (event.execution.task !== task) { - return; - } - const classified = classifyTaskExit(event.exitCode); - if (execution === undefined) { - earlyCompletion ??= classified; - return; - } - finish(classified); - }); - taskEndSubscription = vscode.tasks.onDidEndTask((event) => { - if (event.execution.task !== task) { - return; - } - const classified = classifyTaskExit(undefined); - if (execution === undefined) { - earlyCompletion ??= classified; - return; - } - finish(classified); - }); - }); - - try { - execution = await vscode.tasks.executeTask(task); - } catch (error) { - disposeListeners(); - throw error; +function taskCompletionText(completion: TaskCompletion): string { + if (completion.state === "cancelled") { + return "构建已取消;C++ 模块语言服务未刷新。"; } - if (earlyCompletion !== undefined) { - finish(earlyCompletion); + if (completion.state === "failed") { + return `mcpp 构建失败(退出码 ${completion.exitCode ?? "未知"});C++ 模块语言服务已重新读取现有构建描述。`; } - return completion; + return "mcpp 构建完成,C++ 模块语言服务已刷新。"; } async function autoConfigureModulesWizard( - context: ProjectContext, - status: vscode.StatusBarItem, - output: vscode.OutputChannel, + bridge: LanguageServerBridge, cliController: McppCliController, + output: vscode.OutputChannel, ): Promise { - appendOutputLine(output, "[一键配置] 开始一键配置模块代码提示..."); - if (!vscode.workspace.isTrusted) { - void vscode.window.showWarningMessage(moduleSetupBlockedMessage("untrusted")); + await vscode.window.showWarningMessage(moduleSetupBlockedMessage("untrusted")); return; } - const inventory = await cliController.readToolchainInventory(context.project); - if (inventory === undefined) { + if (cliController.isBusy()) { + await vscode.window.showWarningMessage(moduleSetupBlockedMessage("busy")); return; } - const decision = buildModuleSetupPlan( - inventory, - context.analysis.capability, - vscode.workspace.isTrusted, - cliController.isBusy(), - ); + + const decision = buildModuleSetupPlan(true, false); if (decision.kind === "blocked") { - void vscode.window.showWarningMessage(moduleSetupBlockedMessage(decision.reason)); + await vscode.window.showWarningMessage(moduleSetupBlockedMessage(decision.reason)); return; } - - const confirmation = moduleSetupConfirmation(context.analysis.capability, decision); + const confirmation = moduleSetupConfirmation(); const choice = await vscode.window.showWarningMessage( confirmation.message, { modal: true, detail: confirmation.detail }, @@ -791,102 +111,47 @@ async function autoConfigureModulesWizard( return; } - let currentContext = context; const outcome = await executeModuleSetup(decision, { - preparePlan: () => cliController.runAutomaticModuleSetup(decision), - reload: async (): Promise => { - const refreshed = loadProjectContext(context.project); - if (refreshed === undefined) { - return { stage: "reload", state: "failed", detail: "无法重新加载 mcpp 工程。" }; - } - currentContext = refreshed; - updateStatusBar(status, currentContext); - if (currentContext.analysis.capability !== "full" || currentContext.analysis.kind !== "llvm") { - return { - stage: "reload", - state: "failed", - detail: "构建后没有得到可供 clangd 使用的 LLVM 编译数据库。", - }; - } - return { stage: "reload", state: "succeeded" }; - }, - ensureClangd: async (): Promise => { - const clangd = await resolveClangd(currentContext); - if (clangd?.comparison.compatible) { - const configured = await configureClangd(currentContext, status, output, "automatic", true); - return configured - ? { stage: "clangd", state: "succeeded" } - : { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" }; - } - - const xlingsPath = await resolveXlingsExecutable( - cliController.mcppExecutable(currentContext.project), - ); - const compilerPath = currentContext.analysis.compilerPath; - if (xlingsPath === undefined || compilerPath === undefined) { + build: async (): Promise => { + const completion = await cliController.runProjectTask("build", { notify: false }); + if (completion === undefined) { return { - stage: "clangd", - state: "failed", - detail: "未找到 xlings 或 LLVM 编译器,无法安装匹配的 llvm-tools。", + stage: "build", + state: "not-started", + detail: "未启动 mcpp 构建。", }; } - const compilerVersion = await runToolVersion(compilerPath); - const installArgs = xlingsInstallArgs( - compilerVersion.identity === undefined ? undefined : llvmToolsVersionSpec(compilerVersion.identity), - ); - const installed = await executeXlingsInstallTask(xlingsPath, installArgs, currentContext.project.root); - appendOutputLine( - output, - `[一键配置] xlings install 完成(退出码 ${installed.exitCode ?? "未知"})`, - ); - if (installed.state !== "succeeded") { - return { - stage: "clangd", - state: installed.state, - exitCode: installed.exitCode, - detail: installed.state === "cancelled" ? "llvm-tools 安装已取消。" : "llvm-tools 安装失败。", - }; - } - - const refreshed = loadProjectContext(currentContext.project); - if (refreshed === undefined) { - return { stage: "clangd", state: "failed", detail: "安装 llvm-tools 后无法重新加载工程。" }; - } - currentContext = refreshed; - updateStatusBar(status, currentContext); - const resolved = await resolveClangd(currentContext); - if (resolved === undefined || !resolved.comparison.compatible) { - return { stage: "clangd", state: "failed", detail: "未找到与 LLVM 编译器匹配的 clangd。" }; - } - const configured = await configureClangd(currentContext, status, output, "automatic", true); - return configured - ? { stage: "clangd", state: "succeeded" } - : { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" }; + return { + stage: "build", + state: completion.state, + exitCode: completion.exitCode, + }; }, - checkModules: async (): Promise => { - const moduleStatus = await runModuleSupportCheck(currentContext, status, output, "automatic"); - return moduleStatus?.state === "available" - ? { stage: "check", state: "succeeded" } - : { stage: "check", state: "failed", detail: "模块支持检查未通过。" }; + refreshLanguageServer: async (): Promise => { + const result = await bridge.refreshLanguageServerAfterBuild(); + resultText(output, result); + return { + stage: "language-server", + state: result.state === "completed" ? "succeeded" : "failed", + detail: result.state === "completed" ? undefined : result.message, + }; }, }); + const buildStep = outcome.steps[0]; if (outcome.state === "succeeded") { - if (outcome.degraded) { - void vscode.window.showWarningMessage("构建失败,语言服务已刷新。请查看任务终端获取构建错误。"); - } else { - void vscode.window.showInformationMessage("mcpp 模块代码提示一键配置完成。"); - } + await vscode.window.showInformationMessage( + taskCompletionText(buildStep?.state === "failed" ? { state: "failed", exitCode: buildStep.exitCode } : { state: "succeeded" }), + ); } else if (outcome.state === "cancelled") { - void vscode.window.showWarningMessage(`一键配置已取消(${outcome.stage})。`); + await vscode.window.showWarningMessage(taskCompletionText({ state: "cancelled" })); } else { - void vscode.window.showErrorMessage(`一键配置失败(${outcome.stage})。${outcome.steps.at(-1)?.detail ?? "请查看 mcpp 输出频道。"}`); + await vscode.window.showErrorMessage(outcome.steps.at(-1)?.detail ?? "C++ 模块语言服务配置失败。"); } } -// mcpp.toml 结构补全:建议由纯函数 computeMcppTomlCompletions 计算,这里只做 -// vscode 类型映射。范围:段头 snippet + 开放词汇段的写法模板;不含字段键/枚举 -// 与依赖数据(分别等上游版本化 schema 与批量 catalog 接口)。 +// mcpp.toml structural completion is deliberately local text analysis. It never executes mcpp or +// the language server, so it remains available in restricted workspaces. const mcppTomlCompletionKinds = { section: vscode.CompletionItemKind.Folder, template: vscode.CompletionItemKind.Snippet, @@ -894,7 +159,6 @@ const mcppTomlCompletionKinds = { const mcppTomlCompletionProvider: vscode.CompletionItemProvider = { provideCompletionItems(document, position) { - // mcpp.toml 结构补全由 mcpp.tomlCompletion 控制,按文档作用域读取。 if (!vscode.workspace.getConfiguration("mcpp", document.uri).get("tomlCompletion", true)) { return undefined; } @@ -926,54 +190,30 @@ const mcppTomlCompletionProvider: vscode.CompletionItemProvider = { }; export async function activate(extensionContext: vscode.ExtensionContext): Promise { - moduleStatusByProject.clear(); - moduleCheckOperations.clear(); - forceConfigureOnlyByProject.clear(); - lastReconciledProjectRoot = undefined; const output = vscode.window.createOutputChannel("mcpp"); - const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); - - const refreshStatus = (): void => updateStatusBar(status, loadProjectContext()); + const bridge = createLanguageServerBridge({ + extensionInstalled: (id) => vscode.extensions.getExtension(id) !== undefined, + activateExtension: async (id) => { + await vscode.extensions.getExtension(id)?.activate(); + }, + executeCommand: (command: string, ...args: unknown[]) => vscode.commands.executeCommand(command, ...args), + }); const runGuarded = (operation: () => Promise): (() => Promise) => async () => { try { await operation(); } catch (error) { const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `发生未预期错误:${message}`); + outputText(output, `发生未预期错误:${message}`); void vscode.window.showErrorMessage(`mcpp:${message}`); } }; - const showInteractiveIdeStart = (title: string): void => { - appendOutputLine(output, `\n[IDE] ${title}:已接收,正在等待 IDE 操作队列。`); - output.show(true); - }; - let cliController: McppCliController; - const runConfigureOnlyForProject = async ( - project: McppProjectDiscovery, - interactive: boolean, - ): Promise => { - if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { - appendOutputLine(output, "[CDB 配置] 工作区未受信任,跳过 mcpp build --configure-only。"); - if (interactive) { - void vscode.window.showWarningMessage( - "当前工作区未受信任,不会刷新编译数据库。请先信任工作区。", - ); - } - return undefined; - } - const result = await cliController.runConfigureOnly(project); - if (result === undefined) { - appendOutputLine(output, "[CDB 配置] 已有 mcpp 操作正在运行,本次刷新已跳过。"); - if (interactive) { - void vscode.window.showWarningMessage( - "已有 mcpp 操作正在运行,暂不能刷新编译数据库。请等待当前操作完成后重试。", - ); - } - } - return result; - }; + const invokeLanguageServer = ( + operation: () => Promise, + ): (() => Promise) => runGuarded(async () => { + resultText(output, await operation()); + }); + const manifestWatcher = vscode.workspace.createFileSystemWatcher(MCPP_MANIFEST_GLOB); - const compilationDatabaseWatcher = vscode.workspace.createFileSystemWatcher("**/compile_commands.json"); const inProjectContext = registerInProjectContext({ currentProject: findCurrentProject, setContextValue: (key, value) => vscode.commands.executeCommand("setContext", key, value), @@ -984,268 +224,25 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi manifestWatcher.onDidDelete(listener), ], }); - const executeWithWorkspaceClangd = createSerialExecutor(); - const reconcileProjectContext = async ( - project: McppProjectDiscovery | undefined, - forceRestart: boolean, - forceConfigureOnly: boolean = false, - allowConfigureOnly: boolean = true, - ): Promise => { - let context = loadProjectContext(project); - if (context === undefined) { - updateStatusBar(status, context); - return { - context, - databaseFound: false, - configured: false, - }; - } - - // 缺少可用 CDB 时只运行配置阶段;完整 build/test 后则只重读已有 CDB。 - if (allowConfigureOnly) { - try { - const outcome = await ensureIdeConfigured({ - projectRoot: context.project.root, - compilationDatabasePath: context.project.compilationDatabasePath, - trusted: vscode.workspace.isTrusted, - force: forceConfigureOnly, - databaseValid: () => hasUsableCompilationDatabase(context!.project), - configure: async () => { - const result = await runConfigureOnlyForProject(context!.project, false); - if (result === undefined) { - throw new Error("已有 mcpp 操作正在运行"); - } - if (forceConfigureOnly) { - forceConfigureOnlyByProject.delete(context!.project.root); - } - return result; - }, - }); - if (outcome.state === "configured") { - context = loadProjectContext(context.project) ?? context; - } else if (outcome.state === "failed") { - appendOutputLine(output, `[自动配置] mcpp configure-only 失败(退出码 ${outcome.exitCode})。`); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[自动配置] ${message}`); - } - } - - updateStatusBar(status, context); - - const configured = await configureClangd( - context, - status, - output, - "automatic", - forceRestart, - ); - await updateModuleSupportForContext(context, configured, status, output); - return { - context, - databaseFound: existsSync(context.project.compilationDatabasePath), - configured, - }; - }; - const reconcileProjectByRoot = createKeyedSingleFlightReconciler( - (projectRoot: string, forceRestart) => executeWithWorkspaceClangd( - async () => { - const project = findNearestMcppProject(projectRoot); - if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, projectRoot)) { - const context = loadProjectContext(project); - return { - context, - databaseFound: context !== undefined - && existsSync(context.project.compilationDatabasePath), - configured: false, - }; - } - return reconcileProjectContext( - project, - forceRestart, - forceConfigureOnlyByProject.has(projectRoot), - ); - }, - ), - ); - const reconcilePublishedCdbByRoot = createKeyedSingleFlightReconciler( - (projectRoot: string, forceRestart) => executeWithWorkspaceClangd( - async () => { - const project = findNearestMcppProject(projectRoot, projectRoot); - if (project === undefined) { - return { - context: undefined, - databaseFound: false, - configured: false, - }; - } - return reconcileProjectContext(project, forceRestart, false, false); - }, - ), - ); - const reconcileProject = ( - project: McppProjectDiscovery, - forceRestart: boolean = false, - ): Promise => { - invalidateModuleStatus(project.root); - return reconcileProjectByRoot(project.root, forceRestart); - }; - const requestAutomaticReconciliation = ( - compilationDatabase: vscode.Uri, - forceRestart: boolean, - ): void => { - const project = findProjectForUri(compilationDatabase); - if (project === undefined) { - return; - } - invalidateModuleStatus(project.root); - if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { - return; - } - // CDB watcher 只重读已经发布的数据库,避免配置阶段写 CDB 后再次启动 mcpp。 - void reconcilePublishedCdbByRoot(project.root, forceRestart).catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[自动配置] ${message}`); - }); - }; - const requestCurrentProjectReconciliation = (forceRestart: boolean): void => { - const project = findCurrentProject(); - if (project === undefined) { - lastReconciledProjectRoot = undefined; - refreshStatus(); - return; - } - lastReconciledProjectRoot = project.root; - void reconcileProject(project, forceRestart).catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[自动配置] ${message}`); - }); - }; - const requestManifestReconciliation = (manifestUri: vscode.Uri): void => { - refreshStatus(); - cliController.refreshStatus(); - const currentProject = findCurrentProject(); - const project = projectAffectedByManifest( - manifestUri.fsPath, - currentProject, - findProjectForUri(manifestUri), - ); - if (project === undefined) { - return; - } - forceConfigureOnlyByProject.add(project.root); - if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { - return; - } - lastReconciledProjectRoot = project.root; - void reconcileProject(project, true).catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[自动配置] ${message}`); - }); - }; - const requestDeletedManifestReconciliation = (manifestUri: vscode.Uri): void => { - refreshStatus(); - cliController.refreshStatus(); - const deletedProjectRoot = manifestProjectRoot(manifestUri.fsPath); - forceConfigureOnlyByProject.delete(deletedProjectRoot); - invalidateModuleStatus(deletedProjectRoot); - if (!shouldReconcileDeletedManifest(lastReconciledProjectRoot, manifestUri.fsPath)) { - return; - } - - lastReconciledProjectRoot = undefined; - const fallbackProject = findCurrentProject(); - if ( - fallbackProject !== undefined - && isPathWithinProject(manifestProjectRoot(manifestUri.fsPath), fallbackProject.root) - ) { - forceConfigureOnlyByProject.add(fallbackProject.root); - } - requestCurrentProjectReconciliation(true); - }; - const configurationWatcher = vscode.workspace.onDidChangeConfiguration((event) => { - const currentProject = findCurrentProject(); - let currentExecutionChanged = false; - for (const project of findWorkspaceProjects(currentProject)) { - const uri = vscode.Uri.file(project.root); - if (!configurationAffectsMcppExecution( - (section) => event.affectsConfiguration(section, uri), - )) { - continue; - } - forceConfigureOnlyByProject.add(project.root); - currentExecutionChanged ||= project.root === currentProject?.root; - } - if (currentExecutionChanged) { - requestCurrentProjectReconciliation(true); - return; - } - if (currentProject === undefined) { - return; - } - const uri = vscode.Uri.file(currentProject.root); - if (configurationAffectsModuleSupport( - (section) => event.affectsConfiguration(section, uri), - )) { - requestCurrentProjectReconciliation(true); - } - }); - const trustWatcher = vscode.workspace.onDidGrantWorkspaceTrust(() => { - const project = findCurrentProject(); - if (project !== undefined) forceConfigureOnlyByProject.add(project.root); - requestCurrentProjectReconciliation(true); - }); const afterProjectTask = async ( - project: McppProjectDiscovery, - kind: "build" | "run" | "test" | "clean", - completion: { state: "succeeded" | "failed" | "cancelled"; exitCode?: number }, + _project: McppProjectDiscovery, + _kind: "build" | "run" | "test" | "clean", + completion: TaskCompletion, ): Promise => { - if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { - invalidateModuleStatus(project.root); - refreshStatus(); + if (completion.state === "cancelled") { return; } - - const configureOnlyPending = forceConfigureOnlyByProject.has(project.root); - const reconciled = await executeWithWorkspaceClangd( - () => reconcileProjectContext( - project, - true, - configureOnlyPending, - configureOnlyPending, - ), - ); - if (kind === "build") { - if (reconciled.context?.analysis.capability === "syntax-only") { - const buildMessage = completion.state === "succeeded" ? "mcpp 构建完成" : "mcpp 构建失败"; - void vscode.window.showWarningMessage( - `${buildMessage};${reconciled.context.analysis.reason}。模块语法高亮仍然可用。`, - ); - return; - } - const outcome = describeRefreshOutcome( - completion.exitCode ?? 1, - reconciled.databaseFound, - reconciled.configured, - ); - if (outcome.level === "information") { - void vscode.window.showInformationMessage(outcome.message); - } else if (outcome.level === "warning") { - void vscode.window.showWarningMessage(outcome.message); - } else { - void vscode.window.showErrorMessage(outcome.message); - } - return; - } - - if (completion.state === "succeeded") { - const label = kind === "run" ? "运行" : kind === "test" ? "测试" : "清理"; - void vscode.window.showInformationMessage(`mcpp ${label}完成;clangd/CDB 状态已重新检查。`); + const result = await bridge.refreshLanguageServerAfterBuild(); + resultText(output, result); + if (completion.state === "succeeded" && result.state === "completed") { + await vscode.window.showInformationMessage(taskCompletionText(completion)); + } else if (completion.state === "failed" && result.state === "completed") { + await vscode.window.showWarningMessage(taskCompletionText(completion)); } }; - cliController = new McppCliController({ + + const cliController = new McppCliController({ output, currentProject: findCurrentProject, afterProjectTask, @@ -1254,233 +251,50 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi extensionContext.subscriptions.push( output, - status, + manifestWatcher, + inProjectContext, ...cliController.register(), vscode.languages.registerCompletionItemProvider( { language: "mcpp-toml" }, mcppTomlCompletionProvider, "[", ), - vscode.commands.registerCommand(COMMAND_CONFIGURE, runGuarded(async () => { - const project = findCurrentProject(); - if (project === undefined) { - await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); - return; - } - showInteractiveIdeStart("配置 clangd"); - await executeWithWorkspaceClangd(async () => { - if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { - return; - } - const context = loadProjectContext(project); - if (context === undefined) { - return; - } - const configured = await configureClangd(context, status, output, "interactive"); - await updateModuleSupportForContext(context, configured, status, output); - }); - })), - vscode.commands.registerCommand(COMMAND_REFRESH, runGuarded(async () => { - const project = findCurrentProject(); - if (project === undefined) { - await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); - return; - } - showInteractiveIdeStart("刷新编译数据库"); - await executeWithWorkspaceClangd(async () => { - const hadUsableDatabase = hasUsableCompilationDatabase(project); - const result = await runConfigureOnlyForProject(project, true); - if (result === undefined) { - return; - } - const reconciled = await reconcileProjectContext(project, true, false, false); - const databaseValid = reconciled.context?.analysis.capability !== "unavailable"; - const outcome = describeConfigureOnlyOutcome( - result.exitCode, - databaseValid, - hadUsableDatabase && result.exitCode !== 0, - reconciled.configured, - ); - if (outcome.level === "information") { - void vscode.window.showInformationMessage(outcome.message); - } else if (outcome.level === "warning") { - void vscode.window.showWarningMessage(outcome.message); - } else { - void vscode.window.showErrorMessage(outcome.message); - } - }); + vscode.commands.registerCommand(CLI_COMMANDS.configureLanguageServer, invokeLanguageServer( + () => bridge.selectContext(), + )), + vscode.commands.registerCommand(DEPRECATED_COMMANDS.configureClangd, invokeLanguageServer( + () => bridge.selectContext(), + )), + vscode.commands.registerCommand(CLI_COMMANDS.refreshCompilationDatabase, runGuarded(async () => { + await cliController.runProjectTask("build"); })), - vscode.commands.registerCommand(COMMAND_CHECK, runGuarded(async () => { + vscode.commands.registerCommand(CLI_COMMANDS.checkModuleSupport, invokeLanguageServer( + () => bridge.restartLanguageServer(), + )), + vscode.commands.registerCommand(CLI_COMMANDS.showModuleGraph, invokeLanguageServer( + () => bridge.showModuleGraph(), + )), + vscode.commands.registerCommand(CLI_COMMANDS.showLanguageServerLogs, invokeLanguageServer( + () => bridge.showLanguageServerLogs(), + )), + vscode.commands.registerCommand(CLI_COMMANDS.autoConfigureModules, runGuarded(async () => { const project = findCurrentProject(); if (project === undefined) { await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); return; } - showInteractiveIdeStart("检查模块支持"); - await executeWithWorkspaceClangd(async () => { - if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { - return; - } - const ctx = loadProjectContext(project); - if (ctx === undefined) { - return; - } - - // When module support is already known to be unavailable, offer actionable choices - const currentStatus = moduleStatusByProject.get(ctx.project.root); - if (currentStatus?.state === "unavailable" && ctx.analysis.capability === "full") { - interface StatusActionItem extends vscode.QuickPickItem { - action: "auto-configure" | "recheck" | "rebuild"; - } - const choice = await vscode.window.showQuickPick([ - { - label: "$(rocket) 一键配置模块代码提示", - description: "自动安装匹配的 llvm-tools 并配置 clangd", - action: "auto-configure", - }, - { - label: "$(check) 重新检查模块支持", - description: "再次运行 clangd 模块检查", - action: "recheck", - }, - { - label: "$(database) 刷新编译数据库", - description: "重新生成 compile_commands.json", - action: "rebuild", - }, - ], { - title: "mcpp 模块代码提示不可用", - placeHolder: "选择操作以修复模块代码提示", - }); - if (choice === undefined) { - return; - } - switch (choice.action) { - case "auto-configure": - await autoConfigureModulesWizard(ctx, status, output, cliController); - return; - case "recheck": - await runModuleSupportCheck(ctx, status, output, "interactive"); - return; - case "rebuild": - await vscode.commands.executeCommand(COMMAND_REFRESH); - return; - } - } - - // QuickPick for syntax-only (GCC/MSVC) projects — no module code intelligence available - if (ctx.analysis.capability === "syntax-only") { - interface SyntaxOnlyActionItem extends vscode.QuickPickItem { - action: "auto-configure" | "install-toolchain" | "select-default"; - } - const kindLabel = ctx.analysis.kind.toUpperCase(); - const choice = await vscode.window.showQuickPick([ - { - label: "$(rocket) 一键配置模块代码提示", - description: `当前为 ${kindLabel} 工具链,查看如何切换到 LLVM`, - action: "auto-configure", - }, - { - label: "$(cloud-download) 安装 LLVM 工具链", - description: "通过 mcpp 下载并安装 LLVM 工具链", - action: "install-toolchain", - }, - { - label: "$(settings-gear) 选择全局默认工具链", - description: "从已安装的工具链中选择默认", - action: "select-default", - }, - ], { - title: `mcpp: 当前为 ${kindLabel} 工具链,模块代码提示不可用`, - placeHolder: "切换到 LLVM 工具链以启用模块代码提示", - }); - if (choice === undefined) { - return; - } - switch (choice.action) { - case "auto-configure": - await autoConfigureModulesWizard(ctx, status, output, cliController); - return; - case "install-toolchain": - await vscode.commands.executeCommand(CLI_COMMANDS.installToolchain); - return; - case "select-default": - await vscode.commands.executeCommand(CLI_COMMANDS.selectDefaultToolchain); - return; - } - } - - // Original flow: run module check - await runModuleSupportCheck(ctx, status, output, "interactive"); - }); + await autoConfigureModulesWizard(bridge, cliController, output); })), - vscode.commands.registerCommand("mcpp.autoConfigureModules", runGuarded(async () => { - const project = findCurrentProject(); - if (project === undefined) { - await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); - return; - } - showInteractiveIdeStart("一键配置模块代码提示"); - await executeWithWorkspaceClangd(async () => { - if (!shouldUseWorkspaceClangd(findCurrentProject()?.root, project.root)) { - return; - } - const ctx = loadProjectContext(project); - if (ctx !== undefined) { - await autoConfigureModulesWizard(ctx, status, output, cliController); - } - }); - })), - configurationWatcher, - trustWatcher, - inProjectContext, - vscode.window.onDidChangeActiveTextEditor(() => { - refreshStatus(); - const current = findCurrentProject(); - if (current !== undefined && current.root !== lastReconciledProjectRoot) { - lastReconciledProjectRoot = current.root; - requestCurrentProjectReconciliation(false); - } - }), - vscode.workspace.onDidChangeWorkspaceFolders(() => { - refreshStatus(); - lastReconciledProjectRoot = undefined; - requestCurrentProjectReconciliation(true); - }), - manifestWatcher, - compilationDatabaseWatcher, + vscode.workspace.onDidGrantWorkspaceTrust(() => cliController.refreshStatus()), ); extensionContext.subscriptions.push( - manifestWatcher.onDidCreate((manifestUri) => requestManifestReconciliation(manifestUri)), - manifestWatcher.onDidChange((manifestUri) => { - requestManifestReconciliation(manifestUri); - }), - manifestWatcher.onDidDelete((manifestUri) => requestDeletedManifestReconciliation(manifestUri)), - ...registerCompilationDatabaseReconciliation( - compilationDatabaseWatcher, - requestAutomaticReconciliation, - ), + manifestWatcher.onDidCreate(() => cliController.refreshStatus()), + manifestWatcher.onDidChange(() => cliController.refreshStatus()), + manifestWatcher.onDidDelete(() => cliController.refreshStatus()), ); - - try { - const project = findCurrentProject(); - if (project === undefined) { - await reconcileProjectContext(undefined, false); - } else { - lastReconciledProjectRoot = project.root; - await reconcileProject(project, false); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - appendOutputLine(output, `[自动配置] ${message}`); - } } export function deactivate(): void { - moduleStatusByProject.clear(); - moduleCheckOperations.clear(); - forceConfigureOnlyByProject.clear(); - lastReconciledProjectRoot = undefined; - // VS Code 会释放 activate() 注册的所有订阅。 + // VS Code disposes everything registered in activate(). } diff --git a/src/languageServer.ts b/src/languageServer.ts new file mode 100644 index 0000000..509cde8 --- /dev/null +++ b/src/languageServer.ts @@ -0,0 +1,80 @@ +export const MCPPLS_EXTENSION_ID = "sunrisepeak.mcpp-language-server"; + +export const MCPPLS_COMMANDS = { + restart: "mcppls.restartServer", + selectContext: "mcppls.selectContext", + graph: "mcppls.showModuleGraph", + logs: "mcppls.showLogs", +} as const; + +export type LanguageServerCommandState = "completed" | "unavailable" | "failed"; + +export interface LanguageServerCommandResult { + state: LanguageServerCommandState; + message: string; +} + +export interface LanguageServerCommandExecutor { + extensionInstalled(id: string): boolean; + /** Activate the dependency before forwarding a command when VS Code has not activated it yet. */ + activateExtension?(id: string): Thenable; + executeCommand(command: string, ...args: unknown[]): Thenable; +} + +export interface LanguageServerBridge { + restartLanguageServer(): Promise; + refreshLanguageServerAfterBuild(): Promise; + selectContext(): Promise; + showModuleGraph(): Promise; + showLanguageServerLogs(): Promise; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export function createLanguageServerBridge( + executor: LanguageServerCommandExecutor, +): LanguageServerBridge { + let refreshInFlight: Promise | undefined; + + async function invoke(command: string, successMessage: string): Promise { + if (!executor.extensionInstalled(MCPPLS_EXTENSION_ID)) { + return { + state: "unavailable", + message: `C++ 模块语言服务依赖未安装或已禁用:${MCPPLS_EXTENSION_ID}。`, + }; + } + try { + await executor.activateExtension?.(MCPPLS_EXTENSION_ID); + await executor.executeCommand(command); + return { state: "completed", message: successMessage }; + } catch (error) { + return { + state: "failed", + message: `C++ Modules 命令执行失败:${errorMessage(error)}`, + }; + } + } + + async function restartLanguageServer(): Promise { + return invoke(MCPPLS_COMMANDS.restart, "C++ 模块语言服务已重启。"); + } + + function refreshLanguageServerAfterBuild(): Promise { + if (refreshInFlight !== undefined) { + return refreshInFlight; + } + refreshInFlight = invoke(MCPPLS_COMMANDS.restart, "C++ 模块语言服务已刷新。") + .finally(() => { refreshInFlight = undefined; }); + return refreshInFlight; + } + + return { + restartLanguageServer, + refreshLanguageServerAfterBuild, + selectContext: () => invoke(MCPPLS_COMMANDS.selectContext, "已打开 C++ 模块上下文选择。"), + showModuleGraph: () => invoke(MCPPLS_COMMANDS.graph, "已打开 C++ 模块图。"), + showLanguageServerLogs: () => invoke(MCPPLS_COMMANDS.logs, "已打开 C++ Modules 日志。"), + }; +} diff --git a/src/llvmTools.ts b/src/llvmTools.ts deleted file mode 100644 index f573f05..0000000 --- a/src/llvmTools.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { existsSync } from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import process from "node:process"; - -import type { ToolIdentity } from "./analysis"; -import { - runProcess, - type ProcessResult, - type ProcessRunner, -} from "./process"; - -export function llvmToolsVersionSpec(identity: ToolIdentity): string { - return `${identity.major}.${identity.minor}.${identity.patch}`; -} - -const COMPILER_PATH_VERSION_REGEX = /[\\/]xim-x-llvm[\\/](\d+(?:\.\d+){1,3})[\\/]bin[\\/]clang(?:\+\+|-cl)?(?:\.exe)?$/i; - -export function extractVersionFromCompilerPath(compilerPath: string): string | undefined { - const match = compilerPath.match(COMPILER_PATH_VERSION_REGEX); - return match?.[1]; -} - -function isWindowsPath(value: string): boolean { - return /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\"); -} - -export function deriveInstalledClangdPath( - compilerPath: string, - version: string, -): string[] { - const windows = isWindowsPath(compilerPath); - const pathApi = windows ? path.win32 : path.posix; - const candidateMatch = compilerPath.match( - /^(.*?[\\/])xim-x-llvm[\\/]\d+(?:\.\d+){1,3}[\\/]bin[\\/]clang(?:\+\+|-cl)?(?:\.exe)?$/i, - ); - const basePath = candidateMatch?.[1]; - const extension = /\.exe$/i.test(compilerPath) ? ".exe" : ""; - - const candidates: string[] = []; - if (basePath !== undefined) { - candidates.push( - pathApi.join(basePath, "xim-x-llvm-tools", version, "bin", `clangd${extension}`), - ); - } - - // Also try user xlings registry path if compiler is in mcpp registry - const userMatch = compilerPath.match( - /^(.*?)([\\/])\.mcpp[\\/]registry[\\/]data[\\/]xpkgs[\\/]xim-x-llvm[\\/]\d+(?:\.\d+){1,3}[\\/]bin[\\/]clang(?:\+\+|-cl)?(?:\.exe)?$/i, - ); - if (userMatch !== null) { - candidates.push( - `${userMatch[1]}${userMatch[2]}.xlings${userMatch[2]}data${userMatch[2]}xpkgs${userMatch[2]}xim-x-llvm-tools${userMatch[2]}${version}${userMatch[2]}bin${userMatch[2]}clangd${extension}`, - ); - } - - return candidates; -} - -export function xlingsInstallArgs(version?: string): string[] { - if (version !== undefined) { - return ["install", `xim:llvm-tools@${version}`]; - } - return ["update", "llvm-tools"]; -} - -export interface FindXlingsOptions { - /** Override for tests: base home directory instead of os.homedir(). */ - home?: string; - /** Override for tests: environment instead of process.env. */ - env?: NodeJS.ProcessEnv; -} - -export function findXlingsExecutable(options?: FindXlingsOptions): string | undefined { - const home = options?.home ?? os.homedir(); - const env = options?.env ?? process.env; - const knownPaths = [ - path.join(home, ".xlings", "subos", "current", "bin", "xlings"), - path.join(home, ".xlings", "bin", "xlings"), - ]; - if (process.platform === "win32") { - knownPaths.push( - path.join(home, ".xlings", "subos", "current", "bin", "xlings.exe"), - ); - } - - // mcpp (install.sh / AUR / mcpp-m) bundles xlings inside its own registry - // sandbox instead of installing to ~/.xlings. The AUR launcher pins the - // path via MCPP_VENDORED_XLINGS; otherwise it lives at - // $MCPP_HOME/registry/bin/xlings. Without probing both, the one-click - // module setup can never auto-install llvm-tools after a standard install. - const vendored = env.MCPP_VENDORED_XLINGS?.trim(); - if (vendored !== undefined && vendored.length > 0) { - knownPaths.push(vendored); - } - const mcppHome = env.MCPP_HOME?.trim(); - const extension = process.platform === "win32" ? ".exe" : ""; - knownPaths.push( - path.join( - mcppHome !== undefined && mcppHome.length > 0 ? mcppHome : path.join(home, ".mcpp"), - "registry", - "bin", - `xlings${extension}`, - ), - ); - - // Check known install paths first - for (const candidate of knownPaths) { - if (existsSync(candidate)) { - return candidate; - } - } - - // Fall back to PATH, but only when "xlings" actually resolves there. Always - // returning "xlings" hid the not-installed case, so callers could never show - // the "xlings 未安装" guidance. - return xlingsResolvableOnPath(env.PATH) ? "xlings" : undefined; -} - -function xlingsResolvableOnPath(pathValue?: string): boolean { - const names = process.platform === "win32" - ? ["xlings.exe", "xlings.cmd", "xlings.bat"] - : ["xlings"]; - const pathEnv = pathValue ?? ""; - for (const dir of pathEnv.split(path.delimiter)) { - if (dir.length === 0) { - continue; - } - for (const name of names) { - if (existsSync(path.join(dir, name))) { - return true; - } - } - } - return false; -} - -const XLINGS_BINARY_LINE = /^\s*xlings binary\s*=\s*(.+?)\s*$/im; - -// Source of truth is mcpp itself, not the filesystem or PATH: `mcpp self env` -// reports the exact xlings bundled with THIS mcpp (mcpp is a project-level -// environment; it owns its tool paths). Works for install.sh, AUR and any -// custom MCPP_PREFIX layout. Falls back to the historical path heuristics for -// standalone ~/.xlings installs and for mcpp versions without the line. -// -// The subprocess is bounded by MCPP_SELF_ENV_TIMEOUT_MS: the wizard reaches -// this step only after mcpp is initialized (toolchain list / build already -// ran), so 60s is generous while still guarding against an extreme hang. -const MCPP_SELF_ENV_TIMEOUT_MS = 60_000; - -export async function resolveXlingsExecutable( - mcppExecutable: string, - runner: ProcessRunner = runProcess, - options?: FindXlingsOptions, -): Promise { - const result = await runner(mcppExecutable, ["self", "env"], undefined, { - timeoutMs: MCPP_SELF_ENV_TIMEOUT_MS, - }); - const match = `${result.stdout}\n${result.stderr}`.match(XLINGS_BINARY_LINE); - const reported = match?.[1]?.trim(); - if (reported !== undefined && reported.length > 0 && existsSync(reported)) { - return reported; - } - return findXlingsExecutable(options); -} - -export async function runXlingsCommand( - xlingsPath: string, - args: string[], - cwd?: string, -): Promise { - return runProcess(xlingsPath, args, cwd); -} diff --git a/src/mcppTomlCompletion.ts b/src/mcppTomlCompletion.ts index 3cc0e16..016af8f 100644 --- a/src/mcppTomlCompletion.ts +++ b/src/mcppTomlCompletion.ts @@ -56,7 +56,6 @@ export const SECTION_HEADERS: readonly SectionHeaderSpec[] = [ { group: "toolchain", label: "[toolchain]", header: "[toolchain]", detail: "编译器工具链简写" }, { group: "xlings", label: "[xlings]", header: "[xlings]", detail: "构建环境(xlings 供给)" }, { group: "xlings.workspace", label: "[xlings.workspace]", header: "[xlings.workspace]", detail: "固定工具版本" }, - { group: "xlings.envs", label: "[xlings.envs]", header: "[xlings.envs]", detail: "工具环境的环境变量" }, { group: "target", label: "[target.]", header: "[target.${1:x86_64-linux-gnu}]", detail: "按目标三元组的配置" }, { group: "pack", label: "[pack]", header: "[pack]", detail: "mcpp pack 打包配置" }, { group: "pack.bundle-project", label: "[pack.bundle-project]", header: "[pack.bundle-project]", detail: "vendored 过滤策略微调" }, @@ -134,11 +133,7 @@ const CAPABILITY_TEMPLATES: readonly TemplateSpec[] = [ ]; const XLINGS_WORKSPACE_TEMPLATES: readonly TemplateSpec[] = [ - { label: 'tool = "version"', detail: "固定工具版本", insertSnippet: '${1:clang} = "${2:20.1.7}"' }, -]; - -const XLINGS_ENVS_TEMPLATES: readonly TemplateSpec[] = [ - { label: 'NAME = "value"', detail: "应用到工具环境的环境变量", insertSnippet: '${1:NAME} = "${2:value}"' }, + { label: 'tool = "version"', detail: "固定 xlings 工具版本", insertSnippet: '${1:node} = "${2:24.19.0}"' }, ]; const TOOLS_OVERRIDES_TEMPLATES: readonly TemplateSpec[] = [ @@ -154,7 +149,6 @@ const TEMPLATES_BY_GROUP: Record = { "generated_files": GENERATED_FILE_TEMPLATES, "capabilities": CAPABILITY_TEMPLATES, "xlings.workspace": XLINGS_WORKSPACE_TEMPLATES, - "xlings.envs": XLINGS_ENVS_TEMPLATES, "tools.overrides": TOOLS_OVERRIDES_TEMPLATES, }; diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts index 6c6b8a0..cc0557f 100644 --- a/src/moduleSetup.ts +++ b/src/moduleSetup.ts @@ -1,85 +1,22 @@ -import { hostDefaultToolchains, type ToolchainInventory } from "./cli"; - -export type ModuleSetupStage = "install" | "default" | "build" | "reload" | "clangd" | "check"; -export type ModuleSetupStepState = "succeeded" | "failed" | "cancelled"; -export type ModuleSetupBlockedReason = - | "untrusted" - | "busy" - | "project-toolchain-override" - | "unrecognized-inventory"; +export type ModuleSetupStage = "build" | "language-server"; +export type ModuleSetupStepState = "succeeded" | "failed" | "cancelled" | "not-started"; +export type ModuleSetupBlockedReason = "untrusted" | "busy"; export type ModuleSetupDecision = - | { - kind: "ready"; - installLlvm: boolean; - switchDefault: boolean; - } - | { - kind: "blocked"; - reason: ModuleSetupBlockedReason; - }; - -type ModuleSetupPlan = Extract; -type ModuleCapability = "full" | "syntax-only" | "unavailable"; + | { kind: "ready" } + | { kind: "blocked"; reason: ModuleSetupBlockedReason }; export function buildModuleSetupPlan( - inventory: ToolchainInventory, - _capability: ModuleCapability, trusted: boolean, busy: boolean, ): ModuleSetupDecision { if (!trusted) { return { kind: "blocked", reason: "untrusted" }; } - if (!inventory.recognized) { - return { kind: "blocked", reason: "unrecognized-inventory" }; - } if (busy) { return { kind: "blocked", reason: "busy" }; } - - const effectiveIsLlvm = inventory.effective?.family.toLowerCase() === "llvm"; - if (inventory.projectOverridesGlobal && inventory.effective !== undefined && !effectiveIsLlvm) { - return { kind: "blocked", reason: "project-toolchain-override" }; - } - - // target-only payload 不能成为 host 默认工具链,因此必须复用现有筛选规则。 - const installedHostLlvm = hostDefaultToolchains(inventory) - .some((toolchain) => toolchain.family.toLowerCase() === "llvm"); - return { - kind: "ready", - installLlvm: !installedHostLlvm, - switchDefault: !effectiveIsLlvm, - }; -} - -export interface ModuleSetupCommand { - stage: "install" | "default" | "build"; - mode: "task" | "process"; - args: string[]; -} - -export function mcppModuleSetupCommands(plan: ModuleSetupPlan): ModuleSetupCommand[] { - const commands: ModuleSetupCommand[] = []; - if (plan.installLlvm) { - commands.push({ - stage: "install", - mode: "task", - args: ["toolchain", "install", "llvm"], - }); - } - if (plan.switchDefault) { - commands.push({ - stage: "default", - mode: "process", - args: ["toolchain", "default", "llvm"], - }); - } - const buildArgs = plan.installLlvm || plan.switchDefault - ? ["build", "--no-cache"] - : ["build"]; - commands.push({ stage: "build", mode: "task", args: buildArgs }); - return commands; + return { kind: "ready" }; } export interface ModuleSetupConfirmation { @@ -87,24 +24,10 @@ export interface ModuleSetupConfirmation { detail: string; } -export function moduleSetupConfirmation( - capability: ModuleCapability, - plan: ModuleSetupPlan, -): ModuleSetupConfirmation { - const message = capability === "full" - ? "当前已具备 LLVM 模块能力,是否重新构建并配置模块代码提示?" - : capability === "syntax-only" - ? "当前仅提供模块语法高亮,是否切换到 LLVM 并配置模块代码提示?" - : "当前缺少可用的模块编译数据库,是否配置 LLVM 并构建项目?"; - const installDetail = plan.installLlvm - ? "流程可能下载 LLVM/llvm-tools" - : "将复用已安装的 LLVM;补齐配套组件时仍可能下载 LLVM/llvm-tools"; - const defaultDetail = plan.switchDefault - ? "并把 LLVM 设为全局默认工具链" - : "不会修改全局默认工具链"; +export function moduleSetupConfirmation(): ModuleSetupConfirmation { return { - message, - detail: `${installDetail},${defaultDetail};不会编辑项目清单 mcpp.toml。`, + message: "是否构建当前 mcpp 工程并刷新 C++ 模块语言服务?", + detail: "将执行 mcpp build,并由 C++ Modules 扩展重新读取构建描述;不会修改工具链、mcpp.toml 或任何语言服务设置。", }; } @@ -116,11 +39,8 @@ export interface ModuleSetupStepResult { } export interface ModuleSetupOperations { - prepare?: (command: ModuleSetupCommand) => Promise; - preparePlan?: () => Promise; - reload(): Promise; - ensureClangd(): Promise; - checkModules(): Promise; + build(): Promise; + refreshLanguageServer(): Promise; } export type ModuleSetupOutcome = @@ -149,62 +69,25 @@ function stoppedOutcome( } export async function executeModuleSetup( - plan: ModuleSetupPlan, + _decision: Extract, operations: ModuleSetupOperations, ): Promise { const steps: ModuleSetupStepResult[] = []; - let buildFailed = false; - - const commands = mcppModuleSetupCommands(plan); - if (operations.preparePlan !== undefined) { - const result = await operations.preparePlan(); - steps.push(result); - if (result.state !== "succeeded") { - if (result.stage === "build" && result.state === "failed") { - buildFailed = true; - } else { - return stoppedOutcome(result, steps); - } - } - } else { - if (operations.prepare === undefined) { - const firstCommand = commands[0]; - if (firstCommand === undefined) { - return stoppedOutcome({ stage: "build", state: "failed" }, steps); - } - return stoppedOutcome({ stage: firstCommand.stage, state: "failed" }, steps); - } - for (const command of commands) { - const result = await operations.prepare(command); - steps.push(result); - if (result.state === "succeeded") { - continue; - } - if (command.stage === "build" && result.state === "failed") { - buildFailed = true; - break; - } - return stoppedOutcome(result, steps); - } + const buildResult = await operations.build(); + steps.push(buildResult); + if (buildResult.state === "cancelled" || buildResult.state === "not-started") { + return stoppedOutcome(buildResult, steps); } - // 构建失败后仍尝试恢复现有 CDB;只有完整恢复链成功才算降级成功。 - const recoveryOperations: Array<() => Promise> = [ - () => operations.reload(), - () => operations.ensureClangd(), - () => operations.checkModules(), - ]; - for (const operation of recoveryOperations) { - const result = await operation(); - steps.push(result); - if (result.state !== "succeeded") { - return stoppedOutcome(result, steps); - } + const languageServerResult = await operations.refreshLanguageServer(); + steps.push(languageServerResult); + if (languageServerResult.state !== "succeeded") { + return stoppedOutcome(languageServerResult, steps); } return { state: "succeeded", - degraded: buildFailed, + degraded: buildResult.state === "failed", steps, }; } diff --git a/src/process.ts b/src/process.ts index c772b7a..e6740cf 100644 --- a/src/process.ts +++ b/src/process.ts @@ -1,13 +1,6 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; -import { - classifyCheckResult, - parseToolIdentity, - type CheckResult, - type ToolIdentity, -} from "./analysis"; - const execFileAsync = promisify(execFile); export interface ProcessResult { @@ -27,15 +20,6 @@ export type ProcessRunner = ( options?: ProcessRunOptions, ) => Promise; -export interface ToolVersionResult extends ProcessResult { - identity: ToolIdentity | undefined; -} - -export interface ClangdCheckResult extends ProcessResult { - output: string; - classification: CheckResult; -} - export async function runProcess( executable: string, args: string[], @@ -67,44 +51,3 @@ export async function runProcess( }; } } - -export async function runToolVersion( - executable: string, - versionArguments: string[] = ["--version"], - runner: ProcessRunner = runProcess, -): Promise { - const result = await runner(executable, versionArguments); - return { - ...result, - identity: parseToolIdentity(`${result.stdout}\n${result.stderr}`), - }; -} - -export async function runClangdCheck( - clangdPath: string, - sourceFile: string, - cwd: string, - clangdArguments: string[] = [], - runner: ProcessRunner = runProcess, -): Promise { - const result = await runner( - clangdPath, - [`--check=${sourceFile}`, ...clangdArguments], - cwd, - { timeoutMs: 60_000 }, - ); - const output = `${result.stdout}\n${result.stderr}`; - return { - ...result, - output, - classification: classifyCheckResult(result.exitCode, output), - }; -} - -export function runMcppBuild( - cwd: string, - executable: string = "mcpp", - runner: ProcessRunner = runProcess, -): Promise { - return runner(executable, ["build"], cwd); -} diff --git a/src/tasks.ts b/src/tasks.ts index ffa148c..912e692 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -27,11 +27,11 @@ export function projectTaskPlan(kind: ProjectTaskKind): ProjectTaskPlan { }; } -export function shouldReconcileAfterTask( - _kind: ProjectTaskKind, +export function shouldRefreshLanguageServerAfterTask( + kind: ProjectTaskKind, completion?: TaskCompletion, ): boolean { - return completion?.state !== "cancelled"; + return kind === "build" && completion !== undefined && completion.state !== "cancelled"; } export function classifyTaskExit(exitCode: number | undefined): TaskCompletion { diff --git a/src/workflow.ts b/src/workflow.ts deleted file mode 100644 index 371b501..0000000 --- a/src/workflow.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks"; - -import type { CheckResult, ModuleCapability } from "./analysis"; - -export type RefreshOutcomeLevel = "information" | "warning" | "error"; -export type ModuleSupportState = "available" | "unavailable"; - -export interface RefreshOutcome { - level: RefreshOutcomeLevel; - message: string; -} - -export interface CompilationDatabaseWatcher { - onDidCreate(listener: (event: E) => void): T; - onDidChange(listener: (event: E) => void): T; - onDidDelete(listener: (event: E) => void): T; -} - -export async function withTimeout( - operation: PromiseLike, - timeoutMs: number, - fallback: T, -): Promise { - let timer: ReturnType | undefined; - try { - return await Promise.race([ - Promise.resolve(operation), - new Promise((resolve) => { - timer = setTimeout(() => resolve(fallback), timeoutMs); - }), - ]); - } finally { - if (timer !== undefined) { - clearTimeout(timer); - } - } -} - -export function registerCompilationDatabaseReconciliation( - watcher: CompilationDatabaseWatcher, - requestReconciliation: (event: E, forceRestart: boolean) => void, -): T[] { - return [ - watcher.onDidCreate((event) => requestReconciliation(event, true)), - watcher.onDidChange((event) => requestReconciliation(event, true)), - watcher.onDidDelete((event) => requestReconciliation(event, false)), - ]; -} - -export function shouldRestartClangd( - configurationChanged: boolean, - interactive: boolean, - forceRestart: boolean, -): boolean { - return configurationChanged || interactive || forceRestart; -} - -export function configurationReadyAfterRestart( - restartRequired: boolean, - restartSucceeded: boolean, -): boolean { - return !restartRequired || restartSucceeded; -} - -export function createSingleFlightReconciler( - operation: (forceRestart: boolean) => Promise, -): (forceRestart?: boolean) => Promise { - let active: Promise | undefined; - let requested = false; - let pendingForceRestart = false; - - return (forceRestart = false): Promise => { - requested = true; - pendingForceRestart ||= forceRestart; - if (active !== undefined) { - return active; - } - - active = (async () => { - let result: T | undefined; - let completed = false; - do { - const currentForceRestart = pendingForceRestart; - requested = false; - pendingForceRestart = false; - try { - result = await operation(currentForceRestart); - completed = true; - } catch (error) { - if (!requested) { - throw error; - } - } - } while (requested); - if (!completed) { - throw new Error("重协调没有产生结果"); - } - return result as T; - })().finally(() => { - active = undefined; - requested = false; - pendingForceRestart = false; - }); - return active as Promise; - }; -} - -export function createKeyedSingleFlightReconciler( - operation: (key: K, forceRestart: boolean) => Promise, -): (key: K, forceRestart?: boolean) => Promise { - const reconcilerByKey = new Map Promise>(); - - return (key: K, forceRestart = false): Promise => { - let reconciler = reconcilerByKey.get(key); - if (reconciler === undefined) { - reconciler = createSingleFlightReconciler( - (pendingForceRestart) => operation(key, pendingForceRestart), - ); - reconcilerByKey.set(key, reconciler); - } - return reconciler(forceRestart); - }; -} - -export function createSerialExecutor(): (operation: () => Promise) => Promise { - let tail: Promise = Promise.resolve(); - // `running` is scoped to the async context of the in-flight operation via - // AsyncLocalStorage instead of a global counter, so only genuinely reentrant - // calls bypass the queue: - // - a call made from inside an operation's own async chain (e.g. - // wizard → build → reconcile → executor) sees `running` and executes - // immediately — the outer operation is awaiting it, so queuing would hang; - // - an unrelated concurrent caller (a separate command/event that has its - // own async context) does not see it and queues behind the in-flight - // operation, which the previous `depth > 0` check would wrongly let run - // concurrently. - const state = { running: false }; - const storage = new AsyncLocalStorage<{ running: boolean }>(); - - return (operation: () => Promise): Promise => { - if (storage.getStore() === state && state.running) { - return operation(); - } - - const wrapped = async (): Promise => { - state.running = true; - try { - return await storage.run(state, operation); - } finally { - state.running = false; - } - }; - - const result = tail.then(wrapped, wrapped); - tail = result.then( - () => undefined, - () => undefined, - ); - return result; - }; -} - -export interface LatestOperationTracker { - begin(key: K): number; - invalidate(key: K): void; - isCurrent(key: K, token: number): boolean; - clear(): void; -} - -export function createLatestOperationTracker(): LatestOperationTracker { - const latestTokenByKey = new Map(); - let nextToken = 0; - - return { - begin(key: K): number { - nextToken += 1; - latestTokenByKey.set(key, nextToken); - return nextToken; - }, - invalidate(key: K): void { - latestTokenByKey.delete(key); - }, - isCurrent(key: K, token: number): boolean { - return latestTokenByKey.get(key) === token; - }, - clear(): void { - latestTokenByKey.clear(); - }, - }; -} - -export function shouldRenderProjectStatus( - currentProjectRoot: string | undefined, - statusProjectRoot: string, -): boolean { - return currentProjectRoot === statusProjectRoot; -} - -export function configurationAffectsModuleSupport( - affectsConfiguration: (section: string) => boolean, -): boolean { - return ["mcpp.clangd.path", "mcpp.modulesSupport"].some(affectsConfiguration); -} - -export function configurationAffectsMcppExecution( - affectsConfiguration: (section: string) => boolean, -): boolean { - return affectsConfiguration("mcpp.path"); -} - -export function workspaceAllowsToolExecution(trusted: boolean): boolean { - return trusted; -} - -export function shouldUseWorkspaceClangd( - currentProjectRoot: string | undefined, - projectRoot: string, -): boolean { - return currentProjectRoot === projectRoot; -} - -export function describeRefreshOutcome( - buildExitCode: number, - databaseFound: boolean, - configured: boolean, -): RefreshOutcome { - if (configured) { - return buildExitCode === 0 - ? { - level: "information", - message: "mcpp 构建完成,clangd 配置已刷新。", - } - : { - level: "warning", - message: "mcpp 构建失败,但检测到可用的编译数据库,clangd 配置已刷新。请继续修复源码构建错误。", - }; - } - - if (!databaseFound) { - return buildExitCode === 0 - ? { - level: "error", - message: "mcpp 构建完成,但没有生成 compile_commands.json。", - } - : { - level: "error", - message: "mcpp 构建失败,且没有生成 compile_commands.json。请查看 mcpp 输出频道。", - }; - } - - return { - level: "error", - message: buildExitCode === 0 - ? "已检测到 compile_commands.json,但 clangd 配置未完成。请查看 mcpp 输出频道。" - : "mcpp 构建失败;已检测到 compile_commands.json,但 clangd 配置未完成。请查看 mcpp 输出频道。", - }; -} - -export function describeConfigureOnlyOutcome( - exitCode: number, - databaseValid: boolean, - retainedExisting: boolean, - clangdConfigured: boolean, -): RefreshOutcome { - if (exitCode === 0 && databaseValid) { - return clangdConfigured - ? { - level: "information", - message: "编译数据库已刷新,clangd 配置已重新加载。", - } - : { - level: "warning", - message: "编译数据库已刷新,但 clangd 配置未完成。请查看 mcpp 输出频道。", - }; - } - if (exitCode === 0) { - return { - level: "error", - message: "mcpp configure-only 已完成,但没有生成可用的 compile_commands.json。", - }; - } - if (databaseValid) { - return retainedExisting - ? { - level: "warning", - message: "mcpp configure-only 失败;继续使用原有编译数据库。请查看 mcpp 输出频道。", - } - : { - level: "warning", - message: "mcpp configure-only 失败,但检测到可用的编译数据库。请查看 mcpp 输出频道。", - }; - } - return { - level: "error", - message: "mcpp configure-only 失败,且没有可用的编译数据库。请确认 mcpp 版本支持 build --configure-only。", - }; -} - -export function statusCommandForCapability(capability: ModuleCapability): string { - return capability === "unavailable" - ? "mcpp.refreshCompilationDatabase" - : "mcpp.checkModuleSupport"; -} - -export function shouldCheckModuleSupport( - capability: ModuleCapability, - configured: boolean, - sourceFile?: string, -): boolean { - return capability === "full" && configured && sourceFile !== undefined && sourceFile.length > 0; -} - -export function moduleSupportState(classification: CheckResult): ModuleSupportState { - return classification === "ready" ? "available" : "unavailable"; -} diff --git a/test/analysis.test.ts b/test/analysis.test.ts deleted file mode 100644 index 521b429..0000000 --- a/test/analysis.test.ts +++ /dev/null @@ -1,412 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - analyzeCompilationDatabase, - buildClangdArguments, - classifyCheckResult, - compareToolIdentities, - parseToolIdentity, -} from "../src/analysis"; -import * as analysisModule from "../src/analysis"; - -function cdb(compiler: string, flags: string[]): string { - return JSON.stringify([ - { - directory: "/work/app", - file: "/work/app/src/main.cpp", - arguments: [compiler, "-std=c++23", ...flags, "-c", "/work/app/src/main.cpp"], - }, - ]); -} - -test("classifies an mcpp LLVM CDB as full module support", () => { - const result = analyzeCompilationDatabase(cdb("/tools/xim-x-llvm/22.1.8/bin/clang++", [ - "-fmodule-file=std=/work/app/target/pcm.cache/std.pcm", - "-fprebuilt-module-path=/work/app/target/pcm.cache", - ])); - - assert.equal(result.kind, "llvm"); - assert.equal(result.capability, "full"); - assert.equal(result.compilerPath, "/tools/xim-x-llvm/22.1.8/bin/clang++"); - assert.equal(result.sourceFile, "/work/app/src/main.cpp"); - assert.equal(result.hasPrebuiltModules, true); -}); - -test("prefers a project module interface over an earlier external command", () => { - const result = analyzeCompilationDatabase(JSON.stringify([ - { - directory: "/work/app", - file: "/deps/gtest.cc", - arguments: ["/tools/clang++", "-std=c++23", "-c", "/deps/gtest.cc"], - }, - { - directory: "/work/app", - file: "/work/app/src/demo.cppm", - arguments: [ - "/tools/clang++", - "-std=c++23", - "-fmodule-file=std=/work/app/target/pcm.cache/std.pcm", - "-c", - "/work/app/src/demo.cppm", - ], - }, - ])); - - assert.equal(result.sourceFile, "/work/app/src/demo.cppm"); - assert.equal(result.hasPrebuiltModules, true); -}); - -test("prefers a project source over an external module interface", () => { - const result = analyzeCompilationDatabase(JSON.stringify([ - { - directory: "/work/app", - file: "/deps/fmt_module.cppm", - arguments: [ - "/tools/clang++", - "-std=c++23", - "-fmodule-file=std=/work/app/target/pcm.cache/std.pcm", - "-c", - "/deps/fmt_module.cppm", - ], - }, - { - directory: "/work/app", - file: "/work/app/src/main.cpp", - arguments: [ - "/tools/clang++", - "-std=c++23", - "-fmodule-file=std=/work/app/target/pcm.cache/std.pcm", - "-c", - "/work/app/src/main.cpp", - ], - }, - ])); - - assert.equal(result.sourceFile, "/work/app/src/main.cpp"); -}); - -test("prefers a project source over an mcpp dependency cache source", () => { - const result = analyzeCompilationDatabase(JSON.stringify([ - { - directory: "/work/app", - file: "/work/app/.mcpp/packages/redis/src/transaction.cpp", - arguments: [ - "/tools/clang++", - "-std=c++23", - "-fprebuilt-module-path=/work/app/target/pcm.cache", - "-c", - "/work/app/.mcpp/packages/redis/src/transaction.cpp", - ], - }, - { - directory: "/work/app", - file: "/work/app/src/main.cpp", - arguments: [ - "/tools/clang++", - "-std=c++23", - "-fprebuilt-module-path=/work/app/target/pcm.cache", - "-c", - "/work/app/src/main.cpp", - ], - }, - ])); - - assert.equal(result.sourceFile, "/work/app/src/main.cpp"); -}); - -test("resolves relative CDB source files from their command directory", () => { - const result = analyzeCompilationDatabase(JSON.stringify([ - { - directory: "/work/app", - file: "/work/app/.mcpp/packages/redis/src/transaction.cpp", - arguments: [ - "/tools/clang++", - "-std=c++23", - "-fprebuilt-module-path=/work/app/target/pcm.cache", - "-c", - "/work/app/.mcpp/packages/redis/src/transaction.cpp", - ], - }, - { - directory: "/work/app", - file: "src/main.cpp", - arguments: [ - "/tools/clang++", - "-std=c++23", - "-fprebuilt-module-path=/work/app/target/pcm.cache", - "-c", - "src/main.cpp", - ], - }, - ])); - - assert.equal(result.sourceFile, "/work/app/src/main.cpp"); -}); - -test("keeps Windows separators when reading a command-form CDB entry", () => { - const result = analyzeCompilationDatabase(JSON.stringify([ - { - directory: "C:\\work\\app", - file: "C:\\work\\app\\main.cpp", - command: "C:\\LLVM\\bin\\clang++.exe -std=c++23 -c C:\\work\\app\\main.cpp", - }, - ])); - - assert.equal(result.kind, "llvm"); - assert.equal(result.compilerPath, "C:\\LLVM\\bin\\clang++.exe"); -}); - -test("degrades GCC module databases to syntax-only", () => { - const result = analyzeCompilationDatabase(cdb("/tools/g++", ["-fmodules"])); - - assert.equal(result.kind, "gcc"); - assert.equal(result.capability, "syntax-only"); -}); - -test("degrades MSVC module databases to syntax-only", () => { - const result = analyzeCompilationDatabase(cdb("C:\\LLVM\\cl.exe", [ - "/reference", - "std=C:\\work\\ifc.cache\\std.ifc", - ])); - - assert.equal(result.kind, "msvc"); - assert.equal(result.capability, "syntax-only"); -}); - -test("reports malformed compilation databases without throwing", () => { - const result = analyzeCompilationDatabase("not-json"); - - assert.equal(result.kind, "unknown"); - assert.equal(result.capability, "unavailable"); - assert.match(result.reason, /JSON/i); -}); - -test("auto module arguments enable experimental support for clangd 22", () => { - const args = buildClangdArguments( - ["--background-index", "--query-driver=/old/compiler"], - { - compilerPath: "/tools/clang++", - modulesSupport: "auto", - clangdIdentity: { major: 22, minor: 1, patch: 8 }, - platform: "darwin", - }, - ); - - assert.deepEqual(args, [ - "--background-index", - "--query-driver=/tools/clang++", - "--experimental-modules-support", - ]); -}); - -test("auto module arguments avoid the known Windows clangd 20 crash", () => { - const args = buildClangdArguments([], { - compilerPath: "C:\\tools\\clang++.exe", - modulesSupport: "auto", - clangdIdentity: { major: 20, minor: 1, patch: 7 }, - platform: "win32", - }); - - assert.deepEqual(args, ["--query-driver=C:\\tools\\clang++.exe"]); -}); - -test("auto consumes explicit mcpp PCMs without invoking clangd's module builder", () => { - const args = buildClangdArguments([], { - compilerPath: "/tools/clang++", - modulesSupport: "auto", - clangdIdentity: { major: 22, minor: 1, patch: 8 }, - platform: "darwin", - hasPrebuiltModules: true, - }); - - assert.deepEqual(args, ["--query-driver=/tools/clang++"]); -}); - -test("hermetic mcpp commands do not use query-driver discovery", () => { - const args = buildClangdArguments( - ["--background-index", "--query-driver=/old/compiler"], - { - compilerPath: "/tools/clang++", - compilationArguments: [ - "/tools/clang++", - "--no-default-config", - "-nostdinc++", - "-isystem", - "/tools/llvm/include/c++/v1", - "-isystem/tools/llvm/include/x86_64-unknown-linux-gnu", - "-isystem/tools/llvm/include", - "-c", - "/work/app/src/main.cpp", - ], - modulesSupport: "off", - platform: "darwin", - }, - ); - - assert.deepEqual(args, ["--background-index"]); -}); - -test("non-hermetic commands retain query-driver discovery", () => { - const args = buildClangdArguments([], { - compilerPath: "/tools/clang++", - compilationArguments: [ - "/tools/clang++", - "-std=c++23", - "-c", - "/work/app/src/main.cpp", - ], - modulesSupport: "off", - platform: "darwin", - }); - - assert.deepEqual(args, ["--query-driver=/tools/clang++"]); -}); - -test("expands workspace folder variables before invoking clangd", () => { - const options = { - compilerPath: "/tools/clang++", - modulesSupport: "off" as const, - platform: "darwin" as const, - workspaceFolder: "/work/app", - }; - const args = buildClangdArguments([ - "--compile-commands-dir=${workspaceFolder}", - "--background-index", - ], options); - - assert.deepEqual(args, [ - "--compile-commands-dir=/work/app", - "--background-index", - "--query-driver=/tools/clang++", - ]); -}); - -test("off removes an existing experimental module flag", () => { - const args = buildClangdArguments(["--experimental-modules-support"], { - compilerPath: "/tools/clang++", - modulesSupport: "off", - clangdIdentity: { major: 22, minor: 1, patch: 8 }, - platform: "linux", - }); - - assert.deepEqual(args, ["--query-driver=/tools/clang++"]); -}); - -test("plans replacing the default clangd shim with the matched executable", () => { - const planner = (analysisModule as typeof analysisModule & { - buildClangdConfigurationPlan?: ( - currentPath: string, - currentArguments: string[], - resolvedPath: string, - options: { - compilerPath: string; - modulesSupport: "auto"; - clangdIdentity: { major: number; minor: number; patch: number }; - platform: NodeJS.Platform; - hasPrebuiltModules: boolean; - workspaceFolder: string; - }, - ) => { path: string; arguments: string[]; changed: boolean }; - }).buildClangdConfigurationPlan; - - assert.equal(typeof planner, "function"); - const plan = planner?.( - "clangd", - ["--compile-commands-dir=${workspaceFolder}", "--background-index"], - "/tools/xim-x-llvm-tools/22.1.8/bin/clangd", - { - compilerPath: "/tools/xim-x-llvm/22.1.8/bin/clang++", - modulesSupport: "auto", - clangdIdentity: { major: 22, minor: 1, patch: 8 }, - platform: "darwin", - hasPrebuiltModules: true, - workspaceFolder: "/work/app", - }, - ); - - assert.deepEqual(plan, { - path: "/tools/xim-x-llvm-tools/22.1.8/bin/clangd", - arguments: [ - "--compile-commands-dir=/work/app", - "--background-index", - "--query-driver=/tools/xim-x-llvm/22.1.8/bin/clang++", - ], - changed: true, - }); -}); - -test("parses LLVM version and revision identities", () => { - const identity = parseToolIdentity( - "clangd version 22.1.8 (https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174dcb5aa28b59d1)", - ); - - assert.deepEqual(identity, { - major: 22, - minor: 1, - patch: 8, - revision: "ca7933e47d3a3451d81e72ac174dcb5aa28b59d1", - }); -}); - -test("rejects matching semantic versions from different LLVM revisions", () => { - const result = compareToolIdentities( - { major: 22, minor: 1, patch: 8, revision: "aaaa" }, - { major: 22, minor: 1, patch: 8, revision: "bbbb" }, - ); - - assert.equal(result.compatible, false); - assert.match(result.reason, /revision/i); -}); - -test("does not claim exact identity when an LLVM revision is unavailable", () => { - const result = compareToolIdentities( - { major: 22, minor: 1, patch: 8 }, - { major: 22, minor: 1, patch: 8 }, - ); - - assert.equal(result.compatible, false); - assert.match(result.reason, /revision/i); -}); - -test("classifies clangd check failures into actionable states", () => { - assert.equal( - classifyCheckResult(1, "error: ast_file_different_branch"), - "pcm-mismatch", - ); - assert.equal( - classifyCheckResult(1, "error: module file '.../std.pcm' uses a newer format that cannot be read"), - "pcm-mismatch", - ); - assert.equal( - classifyCheckResult(1, "Module file uses a newer format that cannot be read ast_file_version_too_new"), - "pcm-mismatch", - ); - assert.equal( - classifyCheckResult(1, "error: module file uses an older format that is no longer supported"), - "pcm-mismatch", - ); - assert.equal( - classifyCheckResult(1, "Failed to build module std; Don't get the module unit"), - "module-unavailable", - ); - assert.equal( - classifyCheckResult(3, "unknown type name 'import'"), - "wrong-language-mode", - ); - assert.equal( - classifyCheckResult(0, "All checks completed, 0 errors"), - "ready", - ); - assert.equal( - classifyCheckResult(3, "tweak: ExtractFunction ==> FAIL\nAll checks completed, 4 errors"), - "ready", - ); - assert.equal( - classifyCheckResult( - 3, - "E[12:00:00] [undeclared_var_use] Line 4: use of undeclared identifier 'missing'\nAll checks completed, 1 errors", - ), - "check-failed", - ); -}); diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index bf5baba..c3ca208 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -5,10 +5,14 @@ import test from "node:test"; interface PackageManifest { version?: string; + description?: string; icon?: string; + dependencies?: Record; + devDependencies?: Record; repository?: { url?: string }; homepage?: string; bugs?: { url?: string }; + engines?: { vscode?: string }; extensionDependencies?: string[]; activationEvents?: string[]; capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string } }; @@ -24,16 +28,21 @@ interface PackageManifest { const root = path.resolve(process.cwd()); -test("declares the official clangd dependency and mcpp commands", () => { +test("declares mcpp-language-server as the C++ modules language service", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.3.1"); - assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); + assert.equal(manifest.version, "0.4.0"); + assert.equal(manifest.description, "mcpp 工程构建、工具链与 C++ Modules 语言服务集成"); + assert.equal(manifest.engines?.vscode, "^1.91.0"); + assert.deepEqual(manifest.extensionDependencies, ["sunrisepeak.mcpp-language-server"]); + assert.ok(!manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); + assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.configureLanguageServer")); + assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.configureClangd")); // deprecated alias assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); assert.equal( manifest.capabilities?.untrustedWorkspaces?.description, - "未受信任工作区仅启用模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。", + "未受信任工作区仅启用本扩展的模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 mcpp CLI 或接管语言服务配置。", ); assert.deepEqual( manifest.contributes?.commands?.map((command) => command.command), @@ -47,14 +56,19 @@ test("declares the official clangd dependency and mcpp commands", () => { "mcpp.showToolchains", "mcpp.installToolchain", "mcpp.selectDefaultToolchain", + "mcpp.configureLanguageServer", "mcpp.configureClangd", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport", "mcpp.autoConfigureModules", + "mcpp.showModuleGraph", + "mcpp.showLanguageServerLogs", ], ); assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.path"]); - assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.modulesSupport"]); + assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.tomlCompletion"]); + assert.equal(manifest.dependencies?.["vscode-languageclient"], undefined); + assert.equal(manifest.devDependencies?.["vscode-languageclient"], undefined); assert.deepEqual(manifest.contributes?.configurationDefaults?.["files.associations"], { "*.ccm": "cpp", "*.cppm": "cpp", @@ -63,41 +77,27 @@ test("declares the official clangd dependency and mcpp commands", () => { }); }); -test("自动模块配置使用非交互复合操作和单一全局锁", () => { - const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); - assert.match(source, /public async readToolchainInventory\s*\(/); - const start = source.indexOf("public async runAutomaticModuleSetup"); - assert.notEqual(start, -1); - const end = source.indexOf("public async ", start + 10); - const method = source.slice(start, end === -1 ? source.length : end); - assert.match(method, /mcppModuleSetupCommands\(plan\)/); - assert.match(method, /beginGlobal\(token\)/); - assert.match(method, /finally\s*\{[\s\S]*finishGlobal\(token\)/); - assert.doesNotMatch(method, /pickInstallSpec|selectDefaultToolchainFromInventory|showQuickPick|showWarningMessage/); -}); - -test("一键向导只有一次确认并在构建后重载上下文", () => { +test("一键向导只执行普通 build 并在之后刷新 C++ 模块语言服务", () => { + const controller = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); const start = source.indexOf("async function autoConfigureModulesWizard"); - const end = source.indexOf("export async function activate", start); + const end = source.indexOf("const mcppTomlCompletionKinds", start); assert.notEqual(start, -1); assert.notEqual(end, -1); const wizard = source.slice(start, end); for (const expected of [ - "readToolchainInventory", "buildModuleSetupPlan", "moduleSetupConfirmation", - "runAutomaticModuleSetup", "executeModuleSetup", - "loadProjectContext", + 'runProjectTask("build", { notify: false })', + "refreshLanguageServerAfterBuild", ]) { - assert.match(wizard, new RegExp(expected)); + assert.ok(wizard.includes(expected), `wizard missing ${expected}`); } assert.match(wizard, /modal:\s*true/); - assert.match(wizard, /let currentContext/); assert.doesNotMatch(wizard, /CLI_COMMANDS\.(installToolchain|selectDefaultToolchain)/); - assert.doesNotMatch(wizard, /刷新编译数据库|showQuickPick|showInputBox|maybeDisableCppTools/); - assert.doesNotMatch(wizard, /configureClangd\([^\n]*"interactive"/); + assert.doesNotMatch(wizard, /readToolchainInventory|runAutomaticModuleSetup|ensureClangd|loadProjectContext/); + assert.doesNotMatch(controller, /runAutomaticModuleSetup|executeAutomaticModuleSetupCommand/); }); test("shows editor title buttons only inside mcpp projects", () => { @@ -113,47 +113,6 @@ test("shows editor title buttons only inside mcpp projects", () => { assert.ok(!commands.some((command) => command.command === "mcpp.inProject")); }); -test("wires configure-only through the existing refresh command", () => { - const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.ok(!manifest.activationEvents?.includes("onCommand:mcpp.configureIde")); - assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.refreshCompilationDatabase")); - - const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); - assert.match(source, /ensureIdeConfigured/); - assert.match(source, /runConfigureOnly/); - assert.doesNotMatch(source, /runIdeConfigure|COMMAND_IDE_CONFIGURE|ide["'],\s*["']configure/); - assert.match(source, /findProjectForUri[\s\S]*findNearestMcppProject\(uri\.fsPath, workspaceFolder\.uri\.fsPath\)/); - assert.match(source, /requestManifestReconciliation\s*=\s*\(manifestUri:[^)]+\)[\s\S]*findProjectForUri\(manifestUri\)/); - assert.match(source, /requestManifestReconciliation[\s\S]*projectAffectedByManifest\([\s\S]*manifestUri\.fsPath/); - assert.match(source, /manifestWatcher\.onDidChange\(\(manifestUri\) => \{[\s\S]*requestManifestReconciliation\(manifestUri\)/); - assert.match(source, /manifestWatcher\.onDidDelete\(\(manifestUri\) => requestDeletedManifestReconciliation\(manifestUri\)\)/); - assert.match(source, /configurationAffectsMcppExecution[\s\S]*forceConfigureOnlyByProject\.add/); - assert.match(source, /reconcilePublishedCdbByRoot[\s\S]*reconcileProjectContext\(project, forceRestart, false, false\)/); - assert.match(source, /requestAutomaticReconciliation[\s\S]*reconcilePublishedCdbByRoot\(project\.root, forceRestart\)/); - assert.match(source, /forceConfigureOnlyByProject\.has\(projectRoot\)/); -}); - -test("configure-only shares the project operation lock", () => { - const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); - const start = source.indexOf("public async runConfigureOnly"); - assert.notEqual(start, -1); - const end = source.indexOf("public async ", start + 10); - const method = source.slice(start, end === -1 ? source.length : end); - assert.match(method, /beginProject\(project\.root, token\)/); - assert.match(method, /runConfigureOnlyProcess/); - assert.match(method, /finally\s*\{[\s\S]*finishProject\(project\.root, token\)/); -}); - -test("IDE commands expose progress and bound clangd restart waits", () => { - const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); - assert.match(source, /const CLANGD_RESTART_TIMEOUT_MS = 15_000/); - assert.match(source, /withTimeout\([\s\S]*CLANGD_RESTART_TIMEOUT_MS/); - for (const title of ["配置 clangd", "刷新编译数据库", "检查模块支持", "一键配置模块代码提示"]) { - assert.match(source, new RegExp(`showInteractiveIdeStart\\("${title}"\\)`)); - } - assert.match(source, /output\.show\(true\)/); -}); - test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; const associations = manifest.contributes?.configurationDefaults?.["files.associations"] as @@ -273,7 +232,7 @@ test("设置全局默认后先释放工具链锁再提供立即构建", () => { assert.ok(unlock >= 0 && unlock < immediateBuild); }); -test("项目任务结束后先释放项目锁再重新协调 IDE", () => { +test("项目任务结束后先释放项目锁再刷新 C++ 模块语言服务", () => { const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); const start = source.indexOf("public async runProjectTask"); const end = source.indexOf("public async showToolchains", start); @@ -282,8 +241,8 @@ test("项目任务结束后先释放项目锁再重新协调 IDE", () => { const method = source.slice(start, end); const unlock = method.indexOf("this.operations.finishProject(project.root, token)"); - const reconcile = method.indexOf("this.options.afterProjectTask(project, kind, completion)"); - assert.ok(unlock >= 0 && unlock < reconcile); + const refresh = method.indexOf("this.options.afterProjectTask(project, kind, completion)"); + assert.ok(unlock >= 0 && unlock < refresh); }); test("安装流程把系统工具链和 target 兼容 spec 交给 mcpp 解析", () => { @@ -350,11 +309,16 @@ test("声明 GitHub 仓库和扩展图标", () => { assert.deepEqual([...icon.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); }); -test("README 区分 mcpp 的 MSVC 构建能力和 clangd 的 IFC 限制", () => { +test("README 说明 mcpp 与 mcppls 的职责边界和升级限制", () => { const readme = readFileSync(path.join(root, "README.md"), "utf8"); - assert.match(readme, /0\.0\.90 起 native `cl\.exe` 后端支持/); - assert.match(readme, /mcpp 可以正常构建它们,但 clangd\s+不能直接消费/); - assert.doesNotMatch(readme, /native MSVC\s+构建仍提示/); + assert.match(readme, /sunrisepeak\.mcpp-language-server/); + assert.match(readme, /不启动第二个\s+LSP 客户端/); + assert.match(readme, /不表示本扩展读取用户的 `clangd\.\*` 设置/); + assert.match(readme, /mcpp\.path.*只控制/s); + assert.match(readme, /darwin-x64/); + assert.match(readme, /不再读取或写入它们/); + assert.doesNotMatch(readme, /mcpp\.clangd\.path.*匹配 LLVM/); + assert.doesNotMatch(readme, /当前完整的模块语义能力只支持 LLVM/); }); test("嵌套工程提示不猜测它一定是 mcpp 工作区成员", () => { diff --git a/test/commands.test.ts b/test/commands.test.ts index 843a2fc..f9025b6 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -1,13 +1,13 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "../src/commands"; +import { CLI_COMMANDS, DEPRECATED_COMMANDS, quickMenuItems, quickMenuStatusText } from "../src/commands"; -test("状态栏快捷菜单名称与模块状态易于区分", () => { +test("状态栏快捷菜单名称与 mcpp 项目状态易于区分", () => { assert.equal(quickMenuStatusText, "$(tools) mcpp: 快捷菜单"); }); -test("CLI 命令覆盖项目、工具链和 IDE", () => { +test("CLI 命令覆盖项目、工具链和 C++ Modules 语言服务", () => { assert.deepEqual(Object.values(CLI_COMMANDS), [ "mcpp.showMenu", "mcpp.newProject", @@ -18,7 +18,12 @@ test("CLI 命令覆盖项目、工具链和 IDE", () => { "mcpp.showToolchains", "mcpp.installToolchain", "mcpp.selectDefaultToolchain", + "mcpp.configureLanguageServer", + "mcpp.refreshCompilationDatabase", + "mcpp.checkModuleSupport", "mcpp.autoConfigureModules", + "mcpp.showModuleGraph", + "mcpp.showLanguageServerLogs", ]); assert.deepEqual( quickMenuItems.map((item) => item.command), @@ -30,11 +35,23 @@ test("CLI 命令覆盖项目、工具链和 IDE", () => { "mcpp.showToolchains", "mcpp.installToolchain", "mcpp.selectDefaultToolchain", - "mcpp.configureClangd", + "mcpp.configureLanguageServer", "mcpp.refreshCompilationDatabase", "mcpp.checkModuleSupport", + "mcpp.showModuleGraph", + "mcpp.showLanguageServerLogs", "mcpp.autoConfigureModules", ], ); assert.ok(quickMenuItems.every((item) => item.label.length > 0)); + assert.ok(quickMenuItems.some((item) => item.label.includes("模块语言服务"))); + assert.ok(quickMenuItems.some((item) => item.label.includes("模块图"))); + assert.ok(quickMenuItems.some((item) => item.label.includes("C++ Modules 日志"))); + assert.ok(quickMenuItems.every((item) => !item.label.includes("clangd"))); +}); + +test("保留 configureClangd 作为仅转发到 mcppls 的弃用别名", () => { + assert.equal(DEPRECATED_COMMANDS.configureClangd, "mcpp.configureClangd"); + assert.ok(!(Object.values(CLI_COMMANDS) as readonly string[]).includes(DEPRECATED_COMMANDS.configureClangd)); + assert.ok(quickMenuItems.every((item) => item.command !== DEPRECATED_COMMANDS.configureClangd)); }); diff --git a/test/discovery.test.ts b/test/discovery.test.ts index 807c126..534c834 100644 --- a/test/discovery.test.ts +++ b/test/discovery.test.ts @@ -4,41 +4,24 @@ import os from "node:os"; import path from "node:path"; import test from "node:test"; -import { - deriveClangdCandidates, - findNearestMcppProject, - isPathWithinProject, - manifestProjectRoot, - projectAffectedByManifest, - shouldReconcileDeletedManifest, -} from "../src/discovery"; +import { findNearestMcppProject } from "../src/discovery"; -test("finds the nearest mcpp manifest and root compilation database", () => { +test("finds the nearest mcpp manifest and project root", () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-discovery-")); try { const sourceDirectory = path.join(root, "src", "nested"); mkdirSync(sourceDirectory, { recursive: true }); writeFileSync(path.join(root, "mcpp.toml"), "[package]\nname = 'demo'\n"); - writeFileSync(path.join(root, "compile_commands.json"), "[]\n"); assert.deepEqual(findNearestMcppProject(sourceDirectory), { root, manifestPath: path.join(root, "mcpp.toml"), - compilationDatabasePath: path.join(root, "compile_commands.json"), }); } finally { rmSync(root, { recursive: true, force: true }); } }); -test("routes a deleted manifest only to the project that owned it", () => { - assert.equal(manifestProjectRoot("/work/member/mcpp.toml"), path.join("/work", "member")); - assert.equal(shouldReconcileDeletedManifest("/work/member", "/work/member/mcpp.toml"), true); - assert.equal(shouldReconcileDeletedManifest("/work/other", "/work/member/mcpp.toml"), false); - assert.equal(isPathWithinProject("/work/member", "/work"), true); - assert.equal(isPathWithinProject("/work-other/member", "/work"), false); -}); - test("selects the nearest member inside a multi-member workspace", () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-members-")); try { @@ -59,84 +42,6 @@ test("selects the nearest member inside a multi-member workspace", () => { } }); -test("does not treat a virtual workspace root as a package project", () => { - const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-virtual-workspace-")); - try { - writeFileSync(path.join(root, "mcpp.toml"), "[workspace]\nmembers = ['A', 'B']\n"); - - // #387 在 virtual workspace 根执行 configure-only 时只发布 member CDB, - // 根目录本身没有可供 clangd 消费的 compile_commands.json。 - assert.equal(findNearestMcppProject(root, root), undefined); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - -test("keeps a rooted workspace as a package project", () => { - const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-rooted-workspace-")); - try { - writeFileSync( - path.join(root, "mcpp.toml"), - "[package]\nname = 'root'\n[workspace]\nmembers = ['A']\n", - ); - - assert.equal(findNearestMcppProject(root, root)?.root, root); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - -test("recognizes dotted virtual and inline rooted workspace manifests", () => { - const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-workspace-toml-forms-")); - try { - writeFileSync(path.join(root, "mcpp.toml"), "workspace.members = ['A']\n"); - assert.equal(findNearestMcppProject(root, root), undefined); - - writeFileSync( - path.join(root, "mcpp.toml"), - "package = { name = 'root', version = '0.1.0' }\n[workspace]\nmembers = ['A']\n", - ); - assert.equal(findNearestMcppProject(root, root)?.root, root); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - -test("routes workspace root manifest changes to the active member", () => { - const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-workspace-change-")); - try { - const memberRoot = path.join(root, "A"); - const member = { - root: memberRoot, - manifestPath: path.join(memberRoot, "mcpp.toml"), - compilationDatabasePath: path.join(memberRoot, "compile_commands.json"), - }; - const rootProject = { - root, - manifestPath: path.join(root, "mcpp.toml"), - compilationDatabasePath: path.join(root, "compile_commands.json"), - }; - mkdirSync(memberRoot, { recursive: true }); - - writeFileSync(rootProject.manifestPath, "[workspace]\nmembers = ['A']\n"); - assert.deepEqual( - projectAffectedByManifest(rootProject.manifestPath, member, undefined), - member, - ); - - writeFileSync( - rootProject.manifestPath, - "[package]\nname = 'root'\n[workspace]\nmembers = ['A']\n", - ); - assert.deepEqual( - projectAffectedByManifest(rootProject.manifestPath, member, rootProject), - member, - ); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - test("does not discover an mcpp project outside the opened workspace folder", () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-workspace-boundary-")); try { @@ -152,28 +57,3 @@ test("does not discover an mcpp project outside the opened workspace folder", () rmSync(root, { recursive: true, force: true }); } }); - -test("derives sibling, xlings and PATH clangd candidates", () => { - assert.deepEqual( - deriveClangdCandidates("/tools/xim-x-llvm/22.1.8/bin/clang++"), - [ - "/tools/xim-x-llvm/22.1.8/bin/clangd", - "/tools/xim-x-llvm-tools/22.1.8/bin/clangd", - "clangd", - ], - ); -}); - -test("derives the user xlings tools path from an mcpp registry compiler", () => { - assert.deepEqual( - deriveClangdCandidates( - "/Users/demo/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", - ), - [ - "/Users/demo/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clangd", - "/Users/demo/.mcpp/registry/data/xpkgs/xim-x-llvm-tools/22.1.8/bin/clangd", - "/Users/demo/.xlings/data/xpkgs/xim-x-llvm-tools/22.1.8/bin/clangd", - "clangd", - ], - ); -}); diff --git a/test/languageServer.test.ts b/test/languageServer.test.ts new file mode 100644 index 0000000..373e5f7 --- /dev/null +++ b/test/languageServer.test.ts @@ -0,0 +1,107 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + createLanguageServerBridge, + MCPPLS_COMMANDS, + MCPPLS_EXTENSION_ID, +} from "../src/languageServer"; + +function harness(installed = true) { + const calls: Array<{ command: string; args: unknown[] }> = []; + const activations: string[] = []; + const bridge = createLanguageServerBridge({ + extensionInstalled: (id) => installed && id === MCPPLS_EXTENSION_ID, + activateExtension: async (id) => { activations.push(id); }, + executeCommand: async (command: string, ...args: unknown[]): Promise => { + calls.push({ command, args }); + return undefined as T; + }, + }); + return { bridge, calls, activations }; +} + +test("uses the published mcppls extension and command identifiers", () => { + assert.equal(MCPPLS_EXTENSION_ID, "sunrisepeak.mcpp-language-server"); + assert.deepEqual(MCPPLS_COMMANDS, { + restart: "mcppls.restartServer", + selectContext: "mcppls.selectContext", + graph: "mcppls.showModuleGraph", + logs: "mcppls.showLogs", + }); +}); + +test("forwards the public mcppls UI commands", async () => { + const { bridge, calls, activations } = harness(); + + assert.deepEqual(await bridge.restartLanguageServer(), { + state: "completed", + message: "C++ 模块语言服务已重启。", + }); + await bridge.selectContext(); + await bridge.showModuleGraph(); + await bridge.showLanguageServerLogs(); + + assert.deepEqual(activations, [MCPPLS_EXTENSION_ID, MCPPLS_EXTENSION_ID, MCPPLS_EXTENSION_ID, MCPPLS_EXTENSION_ID]); + assert.deepEqual(calls, [ + { command: MCPPLS_COMMANDS.restart, args: [] }, + { command: MCPPLS_COMMANDS.selectContext, args: [] }, + { command: MCPPLS_COMMANDS.graph, args: [] }, + { command: MCPPLS_COMMANDS.logs, args: [] }, + ]); +}); + +test("returns a stable unavailable result instead of throwing", async () => { + const { bridge, calls } = harness(false); + + assert.deepEqual(await bridge.restartLanguageServer(), { + state: "unavailable", + message: "C++ 模块语言服务依赖未安装或已禁用:sunrisepeak.mcpp-language-server。", + }); + assert.deepEqual(calls, []); +}); + +test("returns a stable failed result when mcppls rejects a command", async () => { + const calls: string[] = []; + const bridge = createLanguageServerBridge({ + extensionInstalled: (id) => id === MCPPLS_EXTENSION_ID, + executeCommand: async (command: string): Promise => { + calls.push(command); + throw new Error("server is not running"); + }, + }); + + assert.deepEqual(await bridge.restartLanguageServer(), { + state: "failed", + message: "C++ Modules 命令执行失败:server is not running", + }); + assert.deepEqual(calls, [MCPPLS_COMMANDS.restart]); +}); + +test("coalesces concurrent build refresh requests into one restart", async () => { + let release: (() => void) | undefined; + const calls: string[] = []; + const bridge = createLanguageServerBridge({ + extensionInstalled: (id) => id === MCPPLS_EXTENSION_ID, + executeCommand: async (command: string): Promise => { + calls.push(command); + await new Promise((resolve) => { release = resolve; }); + return undefined as T; + }, + }); + + const first = bridge.refreshLanguageServerAfterBuild(); + const second = bridge.refreshLanguageServerAfterBuild(); + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(calls, [MCPPLS_COMMANDS.restart]); + release?.(); + assert.deepEqual(await first, { + state: "completed", + message: "C++ 模块语言服务已刷新。", + }); + assert.deepEqual(await second, { + state: "completed", + message: "C++ 模块语言服务已刷新。", + }); + assert.deepEqual(calls, [MCPPLS_COMMANDS.restart]); +}); diff --git a/test/llvmTools.test.ts b/test/llvmTools.test.ts deleted file mode 100644 index 8e967d7..0000000 --- a/test/llvmTools.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; - -import { - llvmToolsVersionSpec, - extractVersionFromCompilerPath, - xlingsInstallArgs, - deriveInstalledClangdPath, - findXlingsExecutable, - resolveXlingsExecutable, -} from "../src/llvmTools"; - -// `xlings` on POSIX, `xlings.exe` on Windows — mirrors mcpp's exe_suffix. -const xlingsBinaryName = process.platform === "win32" ? "xlings.exe" : "xlings"; - -test("extracts version string from ToolIdentity", () => { - assert.equal( - llvmToolsVersionSpec({ major: 22, minor: 1, patch: 8, revision: "abc1234" }), - "22.1.8", - ); - assert.equal( - llvmToolsVersionSpec({ major: 20, minor: 1, patch: 7 }), - "20.1.7", - ); -}); - -test("extracts version from mcpp compiler path", () => { - assert.equal( - extractVersionFromCompilerPath("/home/user/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++"), - "22.1.8", - ); - assert.equal( - extractVersionFromCompilerPath("C:\\Users\\test\\.mcpp\\registry\\data\\xpkgs\\xim-x-llvm\\20.1.7\\bin\\clang.exe"), - "20.1.7", - ); -}); - -test("returns undefined for non-xlings compiler paths", () => { - assert.equal(extractVersionFromCompilerPath("/usr/bin/clang++"), undefined); - assert.equal(extractVersionFromCompilerPath("/opt/homebrew/opt/llvm/bin/clang"), undefined); -}); - -test("builds xlings update args without version (auto-detect)", () => { - assert.deepEqual(xlingsInstallArgs(), ["update", "llvm-tools"]); -}); - -test("builds xlings install args with explicit version", () => { - assert.deepEqual(xlingsInstallArgs("22.1.8"), ["install", "xim:llvm-tools@22.1.8"]); -}); - -test("derives clangd path from mcpp registry compiler path", () => { - const paths = deriveInstalledClangdPath( - "/Users/test/.mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++", - "22.1.8", - ); - assert.ok(paths.some((p) => p.includes("xim-x-llvm-tools"))); - assert.ok(paths.some((p) => p.includes("22.1.8"))); - assert.ok(paths.some((p) => p.endsWith("clangd"))); -}); - -test("derives clangd path with .exe extension on Windows", () => { - const paths = deriveInstalledClangdPath( - "C:\\Users\\test\\.mcpp\\registry\\data\\xpkgs\\xim-x-llvm\\20.1.7\\bin\\clang.exe", - "20.1.7", - ); - assert.ok(paths.every((p) => p.endsWith(".exe"))); - assert.ok(paths.some((p) => p.includes(".xlings") && p.includes("clangd.exe"))); -}); - -test("findXlingsExecutable returns a string or undefined", () => { - const result = findXlingsExecutable(); - // Returns string (PATH fallback or known path) or undefined if xlings not found - assert.ok(result === undefined || typeof result === "string"); -}); - -test("findXlingsExecutable finds the xlings bundled in $MCPP_HOME/registry/bin", () => { - const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-mcpp-home-")); - const registryBin = path.join(home, "registry", "bin"); - const xlingsPath = path.join(registryBin, xlingsBinaryName); - mkdirSync(registryBin, { recursive: true }); - writeFileSync(xlingsPath, "#!/bin/sh\n"); - try { - assert.equal( - findXlingsExecutable({ home, env: { MCPP_HOME: home } }), - xlingsPath, - ); - } finally { - rmSync(home, { recursive: true, force: true }); - } -}); - -test("findXlingsExecutable falls back to $HOME/.mcpp/registry/bin when MCPP_HOME is unset", () => { - const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-home-")); - const registryBin = path.join(home, ".mcpp", "registry", "bin"); - const xlingsPath = path.join(registryBin, xlingsBinaryName); - mkdirSync(registryBin, { recursive: true }); - writeFileSync(xlingsPath, "#!/bin/sh\n"); - try { - assert.equal( - findXlingsExecutable({ home, env: {} }), - xlingsPath, - ); - } finally { - rmSync(home, { recursive: true, force: true }); - } -}); - -test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { - const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-vendored-")); - const vendored = path.join(root, "opt-mcpp", "registry", "bin", xlingsBinaryName); - mkdirSync(path.dirname(vendored), { recursive: true }); - writeFileSync(vendored, "#!/bin/sh\n"); - try { - assert.equal( - findXlingsExecutable({ home: root, env: { MCPP_VENDORED_XLINGS: vendored } }), - vendored, - ); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - -test("resolveXlingsExecutable reads the xlings binary from `mcpp self env`", async () => { - const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-selfenv-")); - const xlingsPath = path.join(root, "registry", "bin", xlingsBinaryName); - mkdirSync(path.dirname(xlingsPath), { recursive: true }); - writeFileSync(xlingsPath, "#!/bin/sh\n"); - const runner = async () => ({ - exitCode: 0, - stdout: `MCPP_HOME = ${root}\nxlings binary = ${xlingsPath}\nxlings pinned = 2026.8.8.1\n`, - stderr: "", - }); - try { - assert.equal( - await resolveXlingsExecutable("/tools/mcpp", runner), - xlingsPath, - ); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - -test("resolveXlingsExecutable passes a timeout to `mcpp self env`", async () => { - let captured: { timeoutMs?: number } | undefined; - const runner = async ( - _executable: string, - _args: string[], - _cwd?: string, - options?: { timeoutMs?: number }, - ) => { - captured = options; - return { exitCode: 0, stdout: "", stderr: "" }; - }; - await resolveXlingsExecutable("/tools/mcpp", runner); - assert.equal(captured?.timeoutMs, 60_000); -}); - -test("resolveXlingsExecutable falls back to path probing when the reported path does not exist", async () => { - const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-fallback-missing-")); - const runner = async () => ({ - exitCode: 0, - stdout: "xlings binary = /no/such/xlings\n", - stderr: "", - }); - try { - assert.equal( - await resolveXlingsExecutable("/tools/mcpp", runner, { home, env: {} }), - undefined, - ); - } finally { - rmSync(home, { recursive: true, force: true }); - } -}); - -test("resolveXlingsExecutable falls back to path probing when `mcpp self env` fails", async () => { - const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-fallback-fail-")); - const runner = async () => ({ exitCode: 1, stdout: "", stderr: "boom\n" }); - try { - assert.equal( - await resolveXlingsExecutable("/tools/mcpp", runner, { home, env: {} }), - undefined, - ); - } finally { - rmSync(home, { recursive: true, force: true }); - } -}); diff --git a/test/mcppTomlCompletion.test.ts b/test/mcppTomlCompletion.test.ts index 33983d7..2d2e132 100644 --- a/test/mcppTomlCompletion.test.ts +++ b/test/mcppTomlCompletion.test.ts @@ -27,6 +27,12 @@ test("suggests section headers on a partial bracket line", () => { assert.equal(targets?.insertSnippet, "[targets.${1:name}]"); }); +test("does not suggest the removed xlings.envs section", () => { + const suggestions = computeMcppTomlCompletions(["[xl"], 0, 3); + assert.ok(!labels(suggestions).includes("[xlings.envs]")); + assert.ok(labels(suggestions).includes("[xlings.workspace]")); +}); + test("offers nothing inside [[...]] array-table headers", () => { // mcpp manifest 不使用 TOML 数组表([[...]]):[[ 内不出建议, // 避免把用户意图的数组表悄悄替换成普通段 [x]。 diff --git a/test/mcppTomlContract.test.ts b/test/mcppTomlContract.test.ts index a9506fb..cba1865 100644 --- a/test/mcppTomlContract.test.ts +++ b/test/mcppTomlContract.test.ts @@ -187,17 +187,15 @@ test("模板键契约:[features] 表形式与数组简写被接受", { skip: s // provider 绑定在「无包 require 该能力」时不解析,静默通过;generated_files // 的条目会在源 glob 展开前写入工程树,这里同时验证生成机制生效(编译成功 // 即说明模块文件被正常纳入构建)。 -test("模板实例契约:capabilities / xlings / tools.overrides / generated_files", { skip: skipReason, timeout: 120_000 }, async () => { +test("模板实例契约:capabilities / xlings.workspace / tools.overrides / generated_files", { skip: skipReason, timeout: 120_000 }, async () => { const run = await buildWith( [ "[capabilities]", 'blas = "compat.openblas"', "", "[xlings.workspace]", - 'clang = "20.1.7"', + 'node = "24.19.0"', "", - "[xlings.envs]", - 'FOO = "1"', "", "[tools.overrides]", '"compat.protobuf:protoc" = "/usr/bin/protoc"', @@ -209,7 +207,7 @@ test("模板实例契约:capabilities / xlings / tools.overrides / generated_f "", ].join("\n"), ); - assertClean(run, "模板实例(capabilities/xlings/tools.overrides/generated_files)"); + assertClean(run, "模板实例(capabilities/xlings.workspace/tools.overrides/generated_files)"); }); test("条件段规则:[target.'cfg(windows)'.build] 接受 build inputs", { skip: skipReason, timeout: 120_000 }, async () => { diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts index 2796c97..489f260 100644 --- a/test/moduleSetup.test.ts +++ b/test/moduleSetup.test.ts @@ -1,441 +1,133 @@ import assert from "node:assert/strict"; import test from "node:test"; -import type { ToolchainInventory, ToolchainItem } from "../src/cli"; import { buildModuleSetupPlan, executeModuleSetup, - mcppModuleSetupCommands, moduleSetupConfirmation, - type ModuleSetupDecision, type ModuleSetupOperations, - type ModuleSetupStage, type ModuleSetupStepResult, - type ModuleSetupStepState, } from "../src/moduleSetup"; -function toolchain(family: string, effective = false): ToolchainItem { - return { - family, - version: "1.0.0", - spec: `${family}@1.0.0`, - source: "managed", - effective, - }; -} - -function inventory( - overrides: Partial = {}, -): ToolchainInventory { - return { - installed: [], - available: [], - targets: [], - effective: undefined, - effectiveTarget: undefined, - globalDefaultSpec: undefined, - projectOverridesGlobal: false, - recognized: true, - rawOutput: "", - ...overrides, - }; -} - -function ready(decision: ModuleSetupDecision): Extract { - if (decision.kind !== "ready") { - assert.fail(`expected ready plan, got ${decision.reason}`); - } - return decision; -} - -test("GCC 生效且没有已安装的 host LLVM 时计划安装并切换默认工具链", () => { - const gcc = toolchain("gcc", true); - const plan = buildModuleSetupPlan( - inventory({ installed: [gcc], effective: gcc }), - "unavailable", - true, - false, - ); - - assert.deepEqual(plan, { - kind: "ready", - installLlvm: true, - switchDefault: true, - }); +test("只信任且不忙时可开始一键配置", () => { + assert.deepEqual(buildModuleSetupPlan(true, false), { kind: "ready" }); + assert.deepEqual(buildModuleSetupPlan(false, false), { kind: "blocked", reason: "untrusted" }); + assert.deepEqual(buildModuleSetupPlan(true, true), { kind: "blocked", reason: "busy" }); }); -test("已安装 host LLVM 但 GCC 生效时只切换默认工具链", () => { - const gcc = toolchain("gcc", true); - const llvm = toolchain("llvm"); - const plan = buildModuleSetupPlan( - inventory({ installed: [gcc, llvm], effective: gcc }), - "syntax-only", - true, - false, - ); - - assert.deepEqual(plan, { - kind: "ready", - installLlvm: false, - switchDefault: true, - }); +test("确认文案不再要求切换 LLVM、下载 llvm-tools 或配置 clangd", () => { + const confirmation = moduleSetupConfirmation(); + assert.match(confirmation.message, /构建.*刷新 C\+\+ 模块语言服务/); + assert.match(confirmation.detail, /mcpp build/); + assert.match(confirmation.detail, /C\+\+ Modules/); + assert.doesNotMatch(confirmation.detail, /LLVM|llvm-tools/i); + assert.doesNotMatch(confirmation.detail, /配置 clangd/i); }); -test("LLVM 已生效时不重复安装或切换", () => { - const llvm = toolchain("llvm", true); - const plan = buildModuleSetupPlan( - inventory({ installed: [llvm], effective: llvm }), - "full", - true, - false, - ); - - assert.deepEqual(plan, { - kind: "ready", - installLlvm: false, - switchDefault: false, - }); -}); - -test("没有 effective 工具链时安装 LLVM 后设置全局默认", () => { - const plan = buildModuleSetupPlan( - inventory(), - "unavailable", - true, - false, - ); - - assert.deepEqual(plan, { - kind: "ready", - installLlvm: true, - switchDefault: true, - }); -}); - -test("available LLVM 不算已安装的 host 工具链", () => { - const gcc = toolchain("gcc", true); - const availableLlvm = toolchain("llvm"); - const plan = buildModuleSetupPlan( - inventory({ installed: [gcc], available: [availableLlvm], effective: gcc }), - "syntax-only", - true, - false, - ); - - assert.equal(ready(plan).installLlvm, true); -}); - -test("只有 target payload 的 LLVM 不算可设为默认的 host 工具链", () => { - const gcc = toolchain("gcc", true); - const llvm = toolchain("llvm"); - const plan = buildModuleSetupPlan( - inventory({ - installed: [gcc, llvm], - effective: gcc, - targets: [{ - target: "x86_64-linux-musl", - note: "static", - toolchainSpec: llvm.spec, - status: "installed", - effective: false, - }], - }), - "syntax-only", - true, - false, - ); - - assert.equal(ready(plan).installLlvm, true); -}); - -test("项目显式固定非 LLVM 时阻止修改全局默认", () => { - const gcc = toolchain("gcc", true); - const plan = buildModuleSetupPlan( - inventory({ - installed: [gcc], - effective: gcc, - projectOverridesGlobal: true, - }), - "syntax-only", - true, - false, - ); - - assert.deepEqual(plan, { - kind: "blocked", - reason: "project-toolchain-override", - }); -}); - -test("项目显式固定 LLVM 时仍允许继续配置", () => { - const llvm = toolchain("llvm", true); - const plan = buildModuleSetupPlan( - inventory({ - installed: [llvm], - effective: llvm, - projectOverridesGlobal: true, - }), - "full", - true, - false, - ); - - assert.deepEqual(plan, { - kind: "ready", - installLlvm: false, - switchDefault: false, - }); -}); - -test("阻止原因按未信任、未知清单、忙碌、项目覆盖的顺序判定", () => { - const gcc = toolchain("gcc", true); - const conflicting = inventory({ - installed: [gcc], - effective: gcc, - projectOverridesGlobal: true, - recognized: false, - }); - - assert.deepEqual( - buildModuleSetupPlan(conflicting, "unavailable", false, true), - { kind: "blocked", reason: "untrusted" }, - ); - assert.deepEqual( - buildModuleSetupPlan(conflicting, "unavailable", true, true), - { kind: "blocked", reason: "unrecognized-inventory" }, - ); - assert.deepEqual( - buildModuleSetupPlan({ ...conflicting, recognized: true }, "unavailable", true, true), - { kind: "blocked", reason: "busy" }, - ); -}); - -test("模块设置命令仅在工具链变化时清缓存", () => { - assert.deepEqual( - mcppModuleSetupCommands({ kind: "ready", installLlvm: true, switchDefault: true }), - [ - { stage: "install", mode: "task", args: ["toolchain", "install", "llvm"] }, - { stage: "default", mode: "process", args: ["toolchain", "default", "llvm"] }, - { stage: "build", mode: "task", args: ["build", "--no-cache"] }, - ], - ); - assert.deepEqual( - mcppModuleSetupCommands({ kind: "ready", installLlvm: false, switchDefault: false }), - [{ stage: "build", mode: "task", args: ["build"] }], - ); -}); - -test("确认文案按能力解释现状并完整说明副作用边界", () => { - const plan = { kind: "ready", installLlvm: true, switchDefault: true } as const; - const confirmations = [ - moduleSetupConfirmation("full", plan), - moduleSetupConfirmation("syntax-only", plan), - moduleSetupConfirmation("unavailable", plan), - ]; - - assert.match(confirmations[0].message, /模块代码提示/); - assert.match(confirmations[1].message, /语法高亮/); - assert.match(confirmations[2].message, /编译数据库/); - assert.equal(new Set(confirmations.map((item) => item.message)).size, 3); - for (const confirmation of confirmations) { - assert.match(confirmation.detail, /LLVM\/llvm-tools/); - assert.match(confirmation.detail, /全局默认/); - assert.match(confirmation.detail, /不会编辑项目.*mcpp\.toml/); - } -}); - -test("确认文案说明将复用已安装的 LLVM", () => { - const confirmation = moduleSetupConfirmation( - "syntax-only", - { kind: "ready", installLlvm: false, switchDefault: true }, - ); - - assert.match(confirmation.detail, /将复用已安装的 LLVM/); - assert.match(confirmation.detail, /补齐配套组件时仍可能下载 LLVM\/llvm-tools/); -}); - -test("确认文案在无需切换时明确不修改全局默认值", () => { - const llvm = toolchain("llvm", true); - const plan = ready(buildModuleSetupPlan( - inventory({ installed: [llvm], effective: llvm }), - "full", - true, - false, - )); - const confirmation = moduleSetupConfirmation( - "unavailable", - plan, - ); - - assert.match(confirmation.detail, /不会修改全局默认工具链/); - assert.doesNotMatch(confirmation.detail, /设为全局默认工具链/); - assert.doesNotMatch(confirmation.detail, /当前 LLVM 已生效/); -}); - -interface OperationFixture { - calls: ModuleSetupStage[]; - operations: ModuleSetupOperations; -} - -function result(stage: ModuleSetupStage, state: ModuleSetupStepState): ModuleSetupStepResult { - return { stage, state }; -} - -function operationsWith( - states: Partial> = {}, -): OperationFixture { - const calls: ModuleSetupStage[] = []; - const run = async (stage: ModuleSetupStage): Promise => { - calls.push(stage); - return result(stage, states[stage] ?? "succeeded"); - }; - - return { - calls, - operations: { - prepare: async (command: { stage: ModuleSetupStage }) => run(command.stage), - reload: async () => run("reload"), - ensureClangd: async () => run("clangd"), - checkModules: async () => run("check"), +test("成功路径严格执行 build 后刷新语言服务", async () => { + const calls: string[] = []; + const operations: ModuleSetupOperations = { + build: async () => { + calls.push("build"); + return { stage: "build", state: "succeeded" }; + }, + refreshLanguageServer: async () => { + calls.push("language-server"); + return { stage: "language-server", state: "succeeded" }; }, }; -} -test("成功流程严格按安装、默认、构建、重载、clangd、检查执行", async () => { - const fixture = operationsWith(); - const outcome = await executeModuleSetup( - { kind: "ready", installLlvm: true, switchDefault: true }, - fixture.operations, - ); - - assert.deepEqual(fixture.calls, ["install", "default", "build", "reload", "clangd", "check"]); - assert.equal(outcome.state, "succeeded"); - assert.equal(outcome.degraded, false); - assert.deepEqual( - outcome.steps.map((step: ModuleSetupStepResult) => step.stage), - fixture.calls, - ); -}); - -test("批量准备操作只执行一次并继续恢复链", async () => { - const fixture = operationsWith(); - let preparePlanCalls = 0; - const outcome = await executeModuleSetup( - { kind: "ready", installLlvm: true, switchDefault: true }, - { - ...fixture.operations, - preparePlan: async () => { - preparePlanCalls += 1; - return result("build", "succeeded"); - }, + const decision = buildModuleSetupPlan(true, false); + assert.equal(decision.kind, "ready"); + if (decision.kind !== "ready") return; + const outcome = await executeModuleSetup(decision, operations); + assert.deepEqual(outcome, { state: "succeeded", degraded: false, steps: [ + { stage: "build", state: "succeeded" }, + { stage: "language-server", state: "succeeded" }, + ] }); + assert.deepEqual(calls, ["build", "language-server"]); +}); + +test("build 失败但未取消时仍刷新已有语言服务描述", async () => { + const calls: string[] = []; + const decision = buildModuleSetupPlan(true, false); + assert.equal(decision.kind, "ready"); + if (decision.kind !== "ready") return; + const outcome = await executeModuleSetup(decision, { + build: async () => { + calls.push("build"); + return { stage: "build", state: "failed", exitCode: 2 }; + }, + refreshLanguageServer: async () => { + calls.push("language-server"); + return { stage: "language-server", state: "succeeded" }; }, - ); + }); - assert.equal(preparePlanCalls, 1); - assert.deepEqual(fixture.calls, ["reload", "clangd", "check"]); assert.equal(outcome.state, "succeeded"); -}); - -test("安装或默认步骤取消、失败后立即停止", async () => { - const cases: Array<{ - stage: "install" | "default"; - state: "cancelled" | "failed"; - plan: Extract; - expectedCalls: ModuleSetupStage[]; - }> = [ - { - stage: "install", - state: "cancelled", - plan: { kind: "ready", installLlvm: true, switchDefault: true }, - expectedCalls: ["install"], - }, - { - stage: "install", - state: "failed", - plan: { kind: "ready", installLlvm: true, switchDefault: true }, - expectedCalls: ["install"], - }, - { - stage: "default", - state: "cancelled", - plan: { kind: "ready", installLlvm: false, switchDefault: true }, - expectedCalls: ["default"], + if (outcome.state === "succeeded") assert.equal(outcome.degraded, true); + assert.deepEqual(calls, ["build", "language-server"]); +}); + +test("build 取消后不刷新语言服务", async () => { + const calls: string[] = []; + const decision = buildModuleSetupPlan(true, false); + assert.equal(decision.kind, "ready"); + if (decision.kind !== "ready") return; + const outcome = await executeModuleSetup(decision, { + build: async () => { + calls.push("build"); + return { stage: "build", state: "cancelled" }; }, - { - stage: "default", - state: "failed", - plan: { kind: "ready", installLlvm: false, switchDefault: true }, - expectedCalls: ["default"], + refreshLanguageServer: async () => { + calls.push("language-server"); + return { stage: "build", state: "succeeded" }; }, - ]; - - for (const item of cases) { - const fixture = operationsWith({ [item.stage]: item.state }); - const outcome = await executeModuleSetup(item.plan, fixture.operations); - assert.deepEqual(fixture.calls, item.expectedCalls); - assert.equal(outcome.state, item.state); - assert.equal(outcome.degraded, false); - assert.equal(outcome.stage, item.stage); - } -}); - -test("构建取消后停止,构建失败则尝试后续恢复", async () => { - const fixture = operationsWith({ build: "cancelled" }); - const outcome = await executeModuleSetup( - { kind: "ready", installLlvm: false, switchDefault: false }, - fixture.operations, - ); + }); - assert.deepEqual(fixture.calls, ["build"]); assert.equal(outcome.state, "cancelled"); - assert.equal(outcome.degraded, false); -}); - -test("构建失败后的重载失败会终止恢复流程", async () => { - const fixture = operationsWith({ build: "failed", reload: "failed" }); - const outcome = await executeModuleSetup( - { kind: "ready", installLlvm: false, switchDefault: false }, - fixture.operations, - ); - - assert.deepEqual(fixture.calls, ["build", "reload"]); - assert.equal(outcome.state, "failed"); - assert.equal(outcome.degraded, false); - assert.equal(outcome.stage, "reload"); -}); - -test("构建失败但 IDE 恢复链成功时返回降级成功", async () => { - const fixture = operationsWith({ build: "failed" }); - const outcome = await executeModuleSetup( - { kind: "ready", installLlvm: false, switchDefault: false }, - fixture.operations, - ); - - assert.deepEqual(fixture.calls, ["build", "reload", "clangd", "check"]); - assert.equal(outcome.state, "succeeded"); - assert.equal(outcome.degraded, true); -}); - -test("clangd 失败后不检查模块", async () => { - const fixture = operationsWith({ clangd: "failed" }); - const outcome = await executeModuleSetup( - { kind: "ready", installLlvm: false, switchDefault: false }, - fixture.operations, - ); - - assert.deepEqual(fixture.calls, ["build", "reload", "clangd"]); + assert.equal(outcome.stage, "build"); + assert.deepEqual(calls, ["build"]); +}); + +test("build 未启动时不刷新语言服务", async () => { + const calls: string[] = []; + const decision = buildModuleSetupPlan(true, false); + assert.equal(decision.kind, "ready"); + if (decision.kind !== "ready") return; + const outcome = await executeModuleSetup(decision, { + build: async () => { + calls.push("build"); + return { stage: "build", state: "not-started", detail: "未启动 mcpp 构建。" }; + }, + refreshLanguageServer: async () => { + calls.push("language-server"); + return { stage: "language-server", state: "succeeded" }; + }, + }); assert.equal(outcome.state, "failed"); - assert.equal(outcome.stage, "clangd"); + if (outcome.state === "failed") assert.equal(outcome.stage, "build"); + assert.deepEqual(calls, ["build"]); }); -test("模块检查失败时返回检查阶段", async () => { - const fixture = operationsWith({ check: "failed" }); - const outcome = await executeModuleSetup( - { kind: "ready", installLlvm: false, switchDefault: false }, - fixture.operations, - ); +test("语言服务依赖缺失时不伪装成成功", async () => { + const result: ModuleSetupStepResult = { + stage: "language-server", + state: "failed", + detail: "C++ 模块语言服务依赖未安装或已禁用。", + }; + const decision = buildModuleSetupPlan(true, false); + assert.equal(decision.kind, "ready"); + if (decision.kind !== "ready") return; + const outcome = await executeModuleSetup(decision, { + build: async () => ({ stage: "build", state: "succeeded" }), + refreshLanguageServer: async () => result, + }); - assert.deepEqual(fixture.calls, ["build", "reload", "clangd", "check"]); assert.equal(outcome.state, "failed"); - assert.equal(outcome.stage, "check"); + if (outcome.state === "failed") assert.equal(outcome.stage, "language-server"); + assert.deepEqual(outcome.steps, [ + { stage: "build", state: "succeeded" }, + result, + ]); }); diff --git a/test/process.test.ts b/test/process.test.ts index dc2d61d..363d0d0 100644 --- a/test/process.test.ts +++ b/test/process.test.ts @@ -2,12 +2,7 @@ import assert from "node:assert/strict"; import process from "node:process"; import test from "node:test"; -import { - runClangdCheck, - runMcppBuild, - runProcess, - runToolVersion, -} from "../src/process"; +import { runProcess } from "../src/process"; test("captures output and exit status from a real child process", async () => { const result = await runProcess(process.execPath, ["-e", "process.stdout.write('ok')"]); @@ -16,66 +11,3 @@ test("captures output and exit status from a real child process", async () => { assert.equal(result.stdout, "ok"); assert.equal(result.stderr, ""); }); - -test("parses a tool identity from the version command", async () => { - const result = await runToolVersion(process.execPath, [ - "-e", - "process.stdout.write('clangd version 22.1.8 (git ca7933e47d3a3451d81e72ac174dcb5aa28b59d1)')", - ]); - - assert.deepEqual(result.identity, { - major: 22, - minor: 1, - patch: 8, - revision: "ca7933e47d3a3451d81e72ac174dcb5aa28b59d1", - }); -}); - -test("classifies clangd check output at the process boundary", async () => { - const calls: Array<{ executable: string; arguments: string[]; cwd?: string }> = []; - const timeouts: Array = []; - const result = await runClangdCheck( - "/tools/clangd", - "/work/app/src/main.cpp", - "/work/app", - ["--background-index"], - async ( - executable: string, - args: string[], - cwd: string | undefined, - options?: { timeoutMs?: number }, - ) => { - calls.push({ executable, arguments: args, cwd }); - timeouts.push(options?.timeoutMs); - return { - exitCode: 1, - stdout: "", - stderr: "error: ast_file_different_branch", - }; - }, - ); - - assert.deepEqual(calls, [{ - executable: "/tools/clangd", - arguments: ["--check=/work/app/src/main.cpp", "--background-index"], - cwd: "/work/app", - }]); - assert.deepEqual(timeouts, [60_000]); - assert.equal(result.classification, "pcm-mismatch"); -}); - -test("uses the configured mcpp executable for builds", async () => { - const calls: Array<{ executable: string; arguments: string[]; cwd?: string }> = []; - const runner = async (executable: string, args: string[], cwd?: string) => { - calls.push({ executable, arguments: args, cwd }); - return { exitCode: 0, stdout: "", stderr: "" }; - }; - - await Reflect.apply(runMcppBuild, undefined, ["/work/app", "/tools/mcpp", runner]); - - assert.deepEqual(calls, [{ - executable: "/tools/mcpp", - arguments: ["build"], - cwd: "/work/app", - }]); -}); diff --git a/test/tasks.test.ts b/test/tasks.test.ts index 9b9f8b2..86ca012 100644 --- a/test/tasks.test.ts +++ b/test/tasks.test.ts @@ -5,7 +5,7 @@ import { McppOperationRegistry, classifyTaskExit, projectTaskPlan, - shouldReconcileAfterTask, + shouldRefreshLanguageServerAfterTask, } from "../src/tasks"; test("基础项目任务使用固定的 mcpp 参数数组", () => { @@ -31,18 +31,16 @@ test("基础项目任务使用固定的 mcpp 参数数组", () => { }); }); -test("构建类项目任务结束后都需要重新协调 IDE", () => { - for (const kind of ["build", "run", "test", "clean"] as const) { - assert.equal(shouldReconcileAfterTask(kind), true); +test("只有非取消的 build 任务刷新 C++ 模块语言服务", () => { + assert.equal(shouldRefreshLanguageServerAfterTask("build", { state: "succeeded", exitCode: 0 }), true); + assert.equal(shouldRefreshLanguageServerAfterTask("build", { state: "failed", exitCode: 2 }), true); + assert.equal(shouldRefreshLanguageServerAfterTask("build", { state: "cancelled" }), false); + assert.equal(shouldRefreshLanguageServerAfterTask("build"), false); + for (const kind of ["run", "test", "clean"] as const) { + assert.equal(shouldRefreshLanguageServerAfterTask(kind, { state: "succeeded", exitCode: 0 }), false); } }); -test("取消的项目任务不进入成功后的 IDE 重协调", () => { - assert.equal(shouldReconcileAfterTask("build", { state: "succeeded", exitCode: 0 }), true); - assert.equal(shouldReconcileAfterTask("build", { state: "failed", exitCode: 2 }), true); - assert.equal(shouldReconcileAfterTask("build", { state: "cancelled" }), false); -}); - test("退出码区分成功、失败和取消", () => { assert.deepEqual(classifyTaskExit(0), { state: "succeeded", exitCode: 0 }); assert.deepEqual(classifyTaskExit(2), { state: "failed", exitCode: 2 }); diff --git a/test/workflow.test.ts b/test/workflow.test.ts deleted file mode 100644 index 7c3b1c3..0000000 --- a/test/workflow.test.ts +++ /dev/null @@ -1,499 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import * as workflow from "../src/workflow"; -import { - createSerialExecutor, - createSingleFlightReconciler, - configurationAffectsMcppExecution, - configurationAffectsModuleSupport, - describeConfigureOnlyOutcome, - describeRefreshOutcome, - statusCommandForCapability, -} from "../src/workflow"; - -test("selects refresh when the compilation database is unavailable", () => { - assert.equal(statusCommandForCapability("unavailable"), "mcpp.refreshCompilationDatabase"); - assert.equal(statusCommandForCapability("full"), "mcpp.checkModuleSupport"); - assert.equal(statusCommandForCapability("syntax-only"), "mcpp.checkModuleSupport"); -}); - -test("keeps IDE configuration when a source build fails after producing a database", () => { - assert.deepEqual(describeRefreshOutcome(1, true, true), { - level: "warning", - message: "mcpp 构建失败,但检测到可用的编译数据库,clangd 配置已刷新。请继续修复源码构建错误。", - }); - assert.deepEqual(describeRefreshOutcome(0, true, true), { - level: "information", - message: "mcpp 构建完成,clangd 配置已刷新。", - }); - assert.equal(describeRefreshOutcome(0, true, false).level, "error"); - assert.equal(describeRefreshOutcome(1, false, false).level, "error"); -}); - -test("describes configure-only results without treating an old CDB as newly generated", () => { - assert.deepEqual(describeConfigureOnlyOutcome(0, true, false, true), { - level: "information", - message: "编译数据库已刷新,clangd 配置已重新加载。", - }); - assert.deepEqual(describeConfigureOnlyOutcome(0, true, false, false), { - level: "warning", - message: "编译数据库已刷新,但 clangd 配置未完成。请查看 mcpp 输出频道。", - }); - assert.deepEqual(describeConfigureOnlyOutcome(0, false, false, false), { - level: "error", - message: "mcpp configure-only 已完成,但没有生成可用的 compile_commands.json。", - }); - assert.deepEqual(describeConfigureOnlyOutcome(2, true, true, false), { - level: "warning", - message: "mcpp configure-only 失败;继续使用原有编译数据库。请查看 mcpp 输出频道。", - }); - assert.deepEqual(describeConfigureOnlyOutcome(2, true, false, false), { - level: "warning", - message: "mcpp configure-only 失败,但检测到可用的编译数据库。请查看 mcpp 输出频道。", - }); - assert.deepEqual(describeConfigureOnlyOutcome(2, false, false, false), { - level: "error", - message: "mcpp configure-only 失败,且没有可用的编译数据库。请确认 mcpp 版本支持 build --configure-only。", - }); -}); - -test("releases an IDE operation queue when an external operation exceeds its deadline", async () => { - const stalled = new Promise(() => { - // 模拟 clangd.restart 或 mcpp configure-only 永久等待。 - }); - - const result = await workflow.withTimeout(stalled, 10, "timed-out"); - - assert.equal(result, "timed-out"); -}); - -test("serializes reconciliation and merges pending restart requests", async () => { - let releaseFirst: (() => void) | undefined; - const firstGate = new Promise((resolve) => { - releaseFirst = resolve; - }); - let active = 0; - let maxConcurrent = 0; - const restartRequests: boolean[] = []; - const reconcile = createSingleFlightReconciler(async (forceRestart) => { - restartRequests.push(forceRestart); - active += 1; - maxConcurrent = Math.max(maxConcurrent, active); - try { - if (restartRequests.length === 1) { - await firstGate; - } - return restartRequests.length; - } finally { - active -= 1; - } - }); - - const first = reconcile(false); - const second = reconcile(true); - const third = reconcile(true); - assert.deepEqual(restartRequests, [false]); - assert.ok(releaseFirst); - releaseFirst(); - - assert.deepEqual(await Promise.all([first, second, third]), [2, 2, 2]); - assert.deepEqual(restartRequests, [false, true]); - assert.equal(maxConcurrent, 1); -}); - -test("coalesces reconciliation requests independently for each project", async () => { - const createKeyedReconciler = ( - workflow as typeof workflow & { - createKeyedSingleFlightReconciler?: ( - operation: (key: K, forceRestart: boolean) => Promise, - ) => (key: K, forceRestart?: boolean) => Promise; - } - ).createKeyedSingleFlightReconciler; - assert.equal(typeof createKeyedReconciler, "function"); - assert.ok(createKeyedReconciler); - - let releaseFirstProject: (() => void) | undefined; - const firstProjectGate = new Promise((resolve) => { - releaseFirstProject = resolve; - }); - const calls: string[] = []; - const reconcile = createKeyedReconciler(async (project: string, forceRestart: boolean) => { - calls.push(`${project}:${forceRestart}`); - if (project === "/work/a" && calls.filter((call) => call.startsWith("/work/a")).length === 1) { - await firstProjectGate; - } - return `${project}:${forceRestart}`; - }); - - const firstA = reconcile("/work/a", false); - const secondA = reconcile("/work/a", true); - const firstB = reconcile("/work/b", true); - - assert.equal(await firstB, "/work/b:true"); - assert.deepEqual(calls, ["/work/a:false", "/work/b:true"]); - assert.ok(releaseFirstProject); - releaseFirstProject(); - assert.deepEqual(await Promise.all([firstA, secondA]), ["/work/a:true", "/work/a:true"]); - assert.deepEqual(calls, ["/work/a:false", "/work/b:true", "/work/a:true"]); -}); - -test("serializes operations that share the workspace clangd configuration", async () => { - const createSerialExecutor = ( - workflow as typeof workflow & { - createSerialExecutor?: () => (operation: () => Promise) => Promise; - } - ).createSerialExecutor; - assert.equal(typeof createSerialExecutor, "function"); - assert.ok(createSerialExecutor); - - let releaseFirst: (() => void) | undefined; - const firstGate = new Promise((resolve) => { - releaseFirst = resolve; - }); - const calls: string[] = []; - const execute = createSerialExecutor(); - const first = execute(async () => { - calls.push("a:start"); - await firstGate; - calls.push("a:end"); - return "a"; - }); - const second = execute(async () => { - calls.push("b:start"); - calls.push("b:end"); - return "b"; - }); - - await Promise.resolve(); - assert.deepEqual(calls, ["a:start"]); - assert.ok(releaseFirst); - releaseFirst(); - assert.deepEqual(await Promise.all([first, second]), ["a", "b"]); - assert.deepEqual(calls, ["a:start", "a:end", "b:start", "b:end"]); -}); - -test("queues an unrelated concurrent caller behind an in-flight operation", async () => { - let releaseFirst: (() => void) | undefined; - const firstGate = new Promise((resolve) => { - releaseFirst = resolve; - }); - const calls: string[] = []; - let maxConcurrent = 0; - let active = 0; - const execute = createSerialExecutor(); - - const first = execute(async () => { - active += 1; - maxConcurrent = Math.max(maxConcurrent, active); - calls.push("a:start"); - await firstGate; - calls.push("a:end"); - active -= 1; - return "a"; - }); - - // A second, unrelated invocation arrives while the first is still in flight, - // from a separate async context (a macrotask, like a user command event). - let second: Promise | undefined; - const secondArrived = new Promise((resolve) => { - setTimeout(() => { - second = execute(async () => { - active += 1; - maxConcurrent = Math.max(maxConcurrent, active); - calls.push("b:start"); - calls.push("b:end"); - active -= 1; - return "b"; - }); - resolve(); - }, 10); - }); - await secondArrived; - - assert.deepEqual(calls, ["a:start"]); - assert.ok(releaseFirst); - releaseFirst(); - assert.ok(second); - assert.deepEqual(await Promise.all([first, second]), ["a", "b"]); - assert.deepEqual(calls, ["a:start", "a:end", "b:start", "b:end"]); - assert.equal(maxConcurrent, 1); -}); - -test("runs a reentrant call from inside an operation without queuing", async () => { - const calls: string[] = []; - let releaseInner: (() => void) | undefined; - const innerGate = new Promise((resolve) => { - releaseInner = resolve; - }); - const execute = createSerialExecutor(); - - const outer = execute(async () => { - calls.push("outer:start"); - await Promise.resolve(); - const inner = execute(async () => { - calls.push("inner:start"); - await innerGate; - calls.push("inner:end"); - return "inner"; - }); - calls.push("outer:mid"); - const value = await inner; - calls.push("outer:end"); - return value; - }); - - // The outer operation's continuation (which makes the reentrant call) runs in - // the microtask queue, before the timer below fires. - await new Promise((resolve) => setTimeout(resolve, 0)); - assert.ok(releaseInner); - releaseInner(); - assert.equal(await outer, "inner"); - assert.deepEqual(calls, ["outer:start", "inner:start", "outer:mid", "inner:end", "outer:end"]); -}); - -test("retries a queued reconciliation after a transient failure", async () => { - let releaseFirst: (() => void) | undefined; - const firstGate = new Promise((resolve) => { - releaseFirst = resolve; - }); - const restartRequests: boolean[] = []; - const reconcile = createSingleFlightReconciler(async (forceRestart) => { - restartRequests.push(forceRestart); - if (restartRequests.length === 1) { - await firstGate; - throw new Error("temporary CDB read failure"); - } - return 2; - }); - - const first = reconcile(false); - const second = reconcile(true); - assert.ok(releaseFirst); - releaseFirst(); - - assert.deepEqual(await Promise.allSettled([first, second]), [ - { status: "fulfilled", value: 2 }, - { status: "fulfilled", value: 2 }, - ]); - assert.deepEqual(restartRequests, [false, true]); -}); - -test("forces a clangd restart when the compilation database changes", () => { - const shouldRestart = ( - workflow as typeof workflow & { - shouldRestartClangd?: ( - configurationChanged: boolean, - interactive: boolean, - forceRestart: boolean, - ) => boolean; - } - ).shouldRestartClangd; - assert.equal(typeof shouldRestart, "function"); - assert.ok(shouldRestart); - - assert.equal(shouldRestart(false, false, true), true); - assert.equal(shouldRestart(false, false, false), false); - assert.equal(shouldRestart(true, false, false), true); - assert.equal(shouldRestart(false, true, false), true); -}); - -test("does not call clangd configuration ready when a required restart fails", () => { - const configurationReady = ( - workflow as typeof workflow & { - configurationReadyAfterRestart?: ( - restartRequired: boolean, - restartSucceeded: boolean, - ) => boolean; - } - ).configurationReadyAfterRestart; - assert.equal(typeof configurationReady, "function"); - assert.ok(configurationReady); - - assert.equal(configurationReady(false, false), true); - assert.equal(configurationReady(true, true), true); - assert.equal(configurationReady(true, false), false); -}); - -test("maps compilation database file events to reconciliation requests", () => { - type EventName = "create" | "change" | "delete"; - const registerEvents = ( - workflow as typeof workflow & { - registerCompilationDatabaseReconciliation?: ( - watcher: { - onDidCreate(listener: (event: E) => void): T; - onDidChange(listener: (event: E) => void): T; - onDidDelete(listener: (event: E) => void): T; - }, - requestReconciliation: (event: E, forceRestart: boolean) => void, - ) => T[]; - } - ).registerCompilationDatabaseReconciliation; - assert.equal(typeof registerEvents, "function"); - assert.ok(registerEvents); - - const listeners: Partial void>> = {}; - const watcher = { - onDidCreate(listener: (event: string) => void): EventName { - listeners.create = listener; - return "create"; - }, - onDidChange(listener: (event: string) => void): EventName { - listeners.change = listener; - return "change"; - }, - onDidDelete(listener: (event: string) => void): EventName { - listeners.delete = listener; - return "delete"; - }, - }; - const requests: Array<{ event: string; forceRestart: boolean }> = []; - - assert.deepEqual(registerEvents(watcher, (event: string, forceRestart: boolean) => { - requests.push({ event, forceRestart }); - }), [ - "create", - "change", - "delete", - ]); - listeners.create?.("/work/a/compile_commands.json"); - listeners.change?.("/work/b/compile_commands.json"); - listeners.delete?.("/work/c/compile_commands.json"); - - assert.deepEqual(requests, [ - { event: "/work/a/compile_commands.json", forceRestart: true }, - { event: "/work/b/compile_commands.json", forceRestart: true }, - { event: "/work/c/compile_commands.json", forceRestart: false }, - ]); -}); - -test("accepts module status only from the latest check for the current project", () => { - const createTracker = ( - workflow as typeof workflow & { - createLatestOperationTracker?: () => { - begin(key: K): number; - invalidate(key: K): void; - isCurrent(key: K, token: number): boolean; - clear(): void; - }; - } - ).createLatestOperationTracker; - const shouldRender = ( - workflow as typeof workflow & { - shouldRenderProjectStatus?: ( - currentProjectRoot: string | undefined, - statusProjectRoot: string, - ) => boolean; - } - ).shouldRenderProjectStatus; - assert.equal(typeof createTracker, "function"); - assert.equal(typeof shouldRender, "function"); - assert.ok(createTracker); - assert.ok(shouldRender); - - const tracker = createTracker(); - const first = tracker.begin("/work/a"); - const second = tracker.begin("/work/a"); - const other = tracker.begin("/work/b"); - assert.equal(tracker.isCurrent("/work/a", first), false); - assert.equal(tracker.isCurrent("/work/a", second), true); - assert.equal(tracker.isCurrent("/work/b", other), true); - - tracker.invalidate("/work/a"); - assert.equal(tracker.isCurrent("/work/a", second), false); - tracker.clear(); - assert.equal(tracker.isCurrent("/work/b", other), false); - - assert.equal(shouldRender("/work/a", "/work/a"), true); - assert.equal(shouldRender("/work/b", "/work/a"), false); - assert.equal(shouldRender(undefined, "/work/a"), false); -}); - -test("separates mcpp executable changes from clangd-only settings", () => { - assert.equal( - configurationAffectsModuleSupport((section) => section === "mcpp.clangd.path"), - true, - ); - assert.equal( - configurationAffectsModuleSupport((section) => section === "mcpp.modulesSupport"), - true, - ); - assert.equal( - configurationAffectsModuleSupport((section) => section === "mcpp.path"), - false, - ); - assert.equal( - configurationAffectsMcppExecution((section) => section === "mcpp.path"), - true, - ); - assert.equal( - configurationAffectsMcppExecution((section) => section === "mcpp.clangd.path"), - false, - ); - assert.equal(configurationAffectsModuleSupport(() => false), false); - assert.equal(configurationAffectsMcppExecution(() => false), false); -}); - -test("only executes workspace-selected tools in trusted workspaces", () => { - const allowsToolExecution = ( - workflow as typeof workflow & { - workspaceAllowsToolExecution?: (trusted: boolean) => boolean; - } - ).workspaceAllowsToolExecution; - assert.equal(typeof allowsToolExecution, "function"); - assert.ok(allowsToolExecution); - - assert.equal(allowsToolExecution(true), true); - assert.equal(allowsToolExecution(false), false); -}); - -test("only lets the current project own the shared workspace clangd", () => { - const shouldUseWorkspaceClangd = ( - workflow as typeof workflow & { - shouldUseWorkspaceClangd?: ( - currentProjectRoot: string | undefined, - projectRoot: string, - ) => boolean; - } - ).shouldUseWorkspaceClangd; - assert.equal(typeof shouldUseWorkspaceClangd, "function"); - assert.ok(shouldUseWorkspaceClangd); - - assert.equal(shouldUseWorkspaceClangd("/work/a", "/work/a"), true); - assert.equal(shouldUseWorkspaceClangd("/work/b", "/work/a"), false); - assert.equal(shouldUseWorkspaceClangd(undefined, "/work/a"), false); -}); - -test("only runs automatic module checks for configured LLVM source commands", () => { - const shouldCheck = ( - workflow as typeof workflow & { - shouldCheckModuleSupport?: ( - capability: "full" | "syntax-only" | "unavailable", - configured: boolean, - sourceFile?: string, - ) => boolean; - } - ).shouldCheckModuleSupport; - assert.equal(typeof shouldCheck, "function"); - assert.ok(shouldCheck); - - assert.equal(shouldCheck("full", true, "/work/src/main.cpp"), true); - assert.equal(shouldCheck("full", false, "/work/src/main.cpp"), false); - assert.equal(shouldCheck("full", true), false); - assert.equal(shouldCheck("syntax-only", true, "/work/src/main.cpp"), false); - assert.equal(shouldCheck("unavailable", true, "/work/src/main.cpp"), false); -}); - -test("maps clangd check classifications to module availability", () => { - const moduleSupportState = ( - workflow as typeof workflow & { - moduleSupportState?: (classification: string) => "available" | "unavailable"; - } - ).moduleSupportState; - assert.equal(typeof moduleSupportState, "function"); - assert.ok(moduleSupportState); - - assert.equal(moduleSupportState("ready"), "available"); - assert.equal(moduleSupportState("module-unavailable"), "unavailable"); - assert.equal(moduleSupportState("wrong-language-mode"), "unavailable"); -}); From 4940647510a4cb92d01de6877235318039010422 Mon Sep 17 00:00:00 2001 From: wellwei Date: Fri, 25 Sep 2026 21:59:31 +0800 Subject: [PATCH 059/119] =?UTF-8?q?test:=20E2E=20=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E4=BE=9D=E8=B5=96=20stub=20=E7=94=B1=20clang?= =?UTF-8?q?d=20=E6=8D=A2=E6=88=90=20mcppls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extension Host 冒烟测试原本安装 clangd stub,与新依赖不符。改为安装 sunrisepeak.mcpp-language-server stub,并断言真实桥接行为: - stub 注册 mcppls.restartServer / selectContext / showModuleGraph / showLogs, 只记录调用、不启动 LSP、不改配置 - E2E 验证扩展激活、依赖扩展同时安装、build 后触发 restart, 以及三个公开转发命令各调用一次 - 运行时 VS Code 版本对齐到 1.91.0 npm run test:e2e 通过。 --- test/e2e/fixtures/clangd-stub/extension.js | 9 -- test/e2e/fixtures/clangd-stub/package.json | 9 -- test/e2e/fixtures/mcppls-stub/extension.js | 27 ++++++ test/e2e/fixtures/mcppls-stub/package.json | 14 +++ test/e2e/runTest.ts | 16 ++-- test/e2e/suite/extension.test.ts | 101 ++++++++++----------- 6 files changed, 96 insertions(+), 80 deletions(-) delete mode 100644 test/e2e/fixtures/clangd-stub/extension.js delete mode 100644 test/e2e/fixtures/clangd-stub/package.json create mode 100644 test/e2e/fixtures/mcppls-stub/extension.js create mode 100644 test/e2e/fixtures/mcppls-stub/package.json diff --git a/test/e2e/fixtures/clangd-stub/extension.js b/test/e2e/fixtures/clangd-stub/extension.js deleted file mode 100644 index 97cc7ef..0000000 --- a/test/e2e/fixtures/clangd-stub/extension.js +++ /dev/null @@ -1,9 +0,0 @@ -const vscode = require("vscode"); - -function activate(context) { - context.subscriptions.push( - vscode.commands.registerCommand("clangd.restart", () => undefined), - ); -} - -module.exports = { activate }; diff --git a/test/e2e/fixtures/clangd-stub/package.json b/test/e2e/fixtures/clangd-stub/package.json deleted file mode 100644 index 19a5588..0000000 --- a/test/e2e/fixtures/clangd-stub/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "vscode-clangd", - "displayName": "clangd stub", - "version": "0.0.0", - "publisher": "llvm-vs-code-extensions", - "engines": { "vscode": "^1.90.0" }, - "main": "./extension.js", - "activationEvents": ["onCommand:clangd.restart"] -} diff --git a/test/e2e/fixtures/mcppls-stub/extension.js b/test/e2e/fixtures/mcppls-stub/extension.js new file mode 100644 index 0000000..3f7f3c1 --- /dev/null +++ b/test/e2e/fixtures/mcppls-stub/extension.js @@ -0,0 +1,27 @@ +const fs = require("node:fs"); +const vscode = require("vscode"); + +function record(command) { + const logPath = process.env.MCPP_E2E_MCPPLS_LOG; + if (typeof logPath === "string") { + fs.appendFileSync(logPath, `${command}\n`); + } +} + +function activate(context) { + for (const command of [ + "mcppls.restartServer", + "mcppls.selectContext", + "mcppls.showModuleGraph", + "mcppls.showLogs", + ]) { + context.subscriptions.push( + vscode.commands.registerCommand(command, () => { + record(command); + return undefined; + }), + ); + } +} + +module.exports = { activate }; diff --git a/test/e2e/fixtures/mcppls-stub/package.json b/test/e2e/fixtures/mcppls-stub/package.json new file mode 100644 index 0000000..177b905 --- /dev/null +++ b/test/e2e/fixtures/mcppls-stub/package.json @@ -0,0 +1,14 @@ +{ + "name": "mcpp-language-server", + "displayName": "C++ Modules Language Server stub", + "version": "0.0.0", + "publisher": "sunrisepeak", + "engines": { "vscode": "^1.91.0" }, + "main": "./extension.js", + "activationEvents": [ + "onCommand:mcppls.restartServer", + "onCommand:mcppls.selectContext", + "onCommand:mcppls.showModuleGraph", + "onCommand:mcppls.showLogs" + ] +} diff --git a/test/e2e/runTest.ts b/test/e2e/runTest.ts index e7d5699..1ec677a 100644 --- a/test/e2e/runTest.ts +++ b/test/e2e/runTest.ts @@ -12,23 +12,24 @@ async function main(): Promise { const extensionsDir = join(tempRoot, "extensions"); const workspaceDir = join(tempRoot, "project"); const fakeMcpp = join(tempRoot, "mcpp"); - const logPath = join(tempRoot, "mcpp.log"); - const clangdStub = join(extensionsDir, "llvm-vs-code-extensions.vscode-clangd-0.0.0"); + const mcppLogPath = join(tempRoot, "mcpp.log"); + const mcpplsLogPath = join(tempRoot, "mcppls.log"); + const mcpplsStub = join(extensionsDir, "sunrisepeak.mcpp-language-server-0.0.0"); mkdirSync(userDataDir, { recursive: true }); mkdirSync(extensionsDir, { recursive: true }); mkdirSync(workspaceDir, { recursive: true }); - mkdirSync(clangdStub, { recursive: true }); + mkdirSync(mcpplsStub, { recursive: true }); copyFileSync(join(fixtureRoot, "fake-mcpp.js"), fakeMcpp); chmodSync(fakeMcpp, 0o755); - copyFileSync(join(fixtureRoot, "clangd-stub/package.json"), join(clangdStub, "package.json")); - copyFileSync(join(fixtureRoot, "clangd-stub/extension.js"), join(clangdStub, "extension.js")); + copyFileSync(join(fixtureRoot, "mcppls-stub/package.json"), join(mcpplsStub, "package.json")); + copyFileSync(join(fixtureRoot, "mcppls-stub/extension.js"), join(mcpplsStub, "extension.js")); copyFileSync(join(fixtureRoot, "project/mcpp.toml"), join(workspaceDir, "mcpp.toml")); copyFileSync(join(fixtureRoot, "project/main.cpp"), join(workspaceDir, "main.cpp")); try { await runTests({ - version: "1.90.2", + version: "1.91.0", extensionDevelopmentPath: repositoryRoot, extensionTestsPath: join(repositoryRoot, "dist/test/e2e/suite/index.js"), launchArgs: [ @@ -43,7 +44,8 @@ async function main(): Promise { ], extensionTestsEnv: { MCPP_E2E_FAKE_MCPP: fakeMcpp, - MCPP_E2E_LOG: logPath, + MCPP_E2E_LOG: mcppLogPath, + MCPP_E2E_MCPPLS_LOG: mcpplsLogPath, }, }); } finally { diff --git a/test/e2e/suite/extension.test.ts b/test/e2e/suite/extension.test.ts index 3f211cb..3dcbe28 100644 --- a/test/e2e/suite/extension.test.ts +++ b/test/e2e/suite/extension.test.ts @@ -1,77 +1,68 @@ import assert from "node:assert/strict"; -import { existsSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import * as path from "node:path"; import * as vscode from "vscode"; +async function waitForFile(file: string, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (existsSync(file)) { + const content = readFileSync(file, "utf8"); + if (content.trim().length > 0) { + return content; + } + } + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); + } + return existsSync(file) ? readFileSync(file, "utf8") : ""; +} + suite("mcpp extension smoke", () => { - test("configures a missing CDB once before executing a full build", async () => { + test("activates, registers commands, builds and refreshes C++ Modules", async () => { const extension = vscode.extensions.getExtension("mcpp-community.mcpp-vscode"); assert.ok(extension, "mcpp extension should be installed in development host"); await extension.activate(); + const languageServer = vscode.extensions.getExtension("sunrisepeak.mcpp-language-server"); + assert.ok(languageServer, "mcppls dependency should be installed next to the extension"); + await languageServer.activate(); + const commands = await vscode.commands.getCommands(true); - assert.ok(commands.includes("mcpp.build")); - assert.ok(commands.includes("mcpp.autoConfigureModules")); + for (const command of [ + "mcpp.build", + "mcpp.autoConfigureModules", + "mcpp.configureLanguageServer", + "mcpp.configureClangd", + "mcpp.refreshCompilationDatabase", + "mcpp.checkModuleSupport", + "mcpp.showModuleGraph", + "mcpp.showLanguageServerLogs", + ]) { + assert.ok(commands.includes(command), `missing command: ${command}`); + } const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, "fixture workspace should be open"); const fakeMcpp = process.env.MCPP_E2E_FAKE_MCPP; - const logPath = process.env.MCPP_E2E_LOG; + const mcppLog = process.env.MCPP_E2E_LOG; + const mcpplsLog = process.env.MCPP_E2E_MCPPLS_LOG; assert.ok(fakeMcpp); - assert.ok(logPath); + assert.ok(mcppLog); + assert.ok(mcpplsLog); await vscode.workspace.getConfiguration("mcpp", workspaceFolder.uri) .update("path", fakeMcpp, vscode.ConfigurationTarget.Workspace); - const configureDeadline = Date.now() + 15_000; - let invocations: string[] = []; - while (Date.now() < configureDeadline) { - if (existsSync(logPath)) { - invocations = readFileSync(logPath, "utf8").trim().split("\n"); - if (invocations.includes("build --configure-only")) { - break; - } - } - await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); - } - assert.ok(existsSync(logPath), "fake mcpp should have been invoked"); + void vscode.commands.executeCommand("mcpp.build"); + assert.equal((await waitForFile(mcppLog, 15_000)).trim(), "build"); + assert.equal((await waitForFile(mcpplsLog, 15_000)).trim(), "mcppls.restartServer"); - await vscode.commands.executeCommand("mcpp.build"); - const buildDeadline = Date.now() + 15_000; - while (Date.now() < buildDeadline) { - invocations = readFileSync(logPath, "utf8").trim().split("\n"); - if (invocations.includes("build")) { - break; - } - await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); - } - assert.deepEqual(invocations, ["build --configure-only", "build"]); - assert.ok(existsSync(path.join(workspaceFolder.uri.fsPath, "compile_commands.json"))); + await vscode.commands.executeCommand("mcpp.configureClangd"); + await vscode.commands.executeCommand("mcpp.showModuleGraph"); + await vscode.commands.executeCommand("mcpp.showLanguageServerLogs"); + assert.deepEqual( + (await waitForFile(mcpplsLog, 15_000)).trim().split("\n"), + ["mcppls.restartServer", "mcppls.selectContext", "mcppls.showModuleGraph", "mcppls.showLogs"], + ); assert.equal(path.basename(workspaceFolder.uri.fsPath), "project"); - - const compilationDatabase = path.join(workspaceFolder.uri.fsPath, "compile_commands.json"); - const executeWithinDeadline = async (command: string): Promise => { - let timer: NodeJS.Timeout | undefined; - try { - await Promise.race([ - vscode.commands.executeCommand(command), - new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error(`${command} did not finish`)), 5_000); - }), - ]); - } finally { - if (timer !== undefined) { - clearTimeout(timer); - } - } - }; - - rmSync(compilationDatabase); - await executeWithinDeadline("mcpp.refreshCompilationDatabase"); - assert.ok(existsSync(compilationDatabase), "refresh should recreate a missing CDB"); - - rmSync(compilationDatabase); - await executeWithinDeadline("mcpp.configureClangd"); - await executeWithinDeadline("mcpp.checkModuleSupport"); - await executeWithinDeadline("mcpp.autoConfigureModules"); }); }); From 3403deb3b7373d234f6e2d7d73fb2f0e7ffed7d1 Mon Sep 17 00:00:00 2001 From: wellwei Date: Fri, 25 Sep 2026 22:00:11 +0800 Subject: [PATCH 060/119] =?UTF-8?q?docs:=20=E9=87=8D=E5=86=99=20README=20?= =?UTF-8?q?=E4=B8=8E=E6=9B=B4=E6=96=B0=E6=97=A5=E5=BF=97=EF=BC=8C=E8=A1=A5?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E8=AE=A1=E5=88=92=E4=B8=8E=20CI=20=E9=97=A8?= =?UTF-8?q?=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README 改为按职责边界组织:mcpp-vscode 负责工程发现、构建/运行/测试、 工具链与 mcpp.toml;C++ 模块语义交给 mcppls;不再描述 clangd 配置流程 - 记录升级要点:新依赖、VS Code 1.91 下限、mcpp.path 作用域限制、 旧 clangd 设置不再生效、darwin-x64 暂无上游平台包 - CHANGELOG 增加 0.4.0 未发布段落,说明依赖切换、旧层删除与一键流程变更 - 新增迁移实现计划文档,含冻结决策、门禁、9 个任务与执行状态 - CI 增加扩展依赖声明检查,以及“不得引入第二个语言客户端”的打包检查 npm run package 通过,VSIX 归档校验通过。 --- .github/workflows/ci.yml | 6 + CHANGELOG.md | 18 + README.md | 501 ++++--------- ...26-09-25-mcpp-language-server-migration.md | 669 ++++++++++++++++++ 4 files changed, 840 insertions(+), 354 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-25-mcpp-language-server-migration.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 330891c..dae166a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,8 @@ jobs: - run: npm run package - name: Verify VSIX archive run: unzip -t mcpp-vscode-*.vsix + - name: Verify extension dependency is declared + run: node -e 'const p=require("./package.json"); if (p.extensionDependencies?.[0] !== "sunrisepeak.mcpp-language-server") process.exit(1)' extension-host-e2e: runs-on: ubuntu-latest @@ -39,3 +41,7 @@ jobs: - run: npm ci - name: Run Extension Host smoke tests run: xvfb-run -a npm run test:e2e + - name: Check packaged extension is not a second language client + run: | + test ! -e dist/src/languageClient.js + ! rg -n 'vscode-languageclient|mcppls\.serve' dist/src package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 831d24c..c439cbda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,24 @@ 显式刷新会分别报告 CDB 生成与 clangd 协调结果。 - 对齐 mcpp #387 的最终 workspace 契约:virtual workspace 根不作为单一 clangd 工程, 扩展消费当前活动 member 根的 CDB;rooted workspace 仍按根 package 处理。 +## 0.4.0(未发布) + +### Changed + +- 将 C++ 模块语言服务依赖从官方 `llvm-vs-code-extensions.vscode-clangd` 切换为 + `sunrisepeak.mcpp-language-server`(C++ Modules Language Server / mcppls);VS Code 最低版本 + 提升到 1.91。 +- 删除本扩展中的 clangd 解析、匹配、配置、PCM 检查、CDB 监听和自动重启逻辑。mcpp-vscode + 不再启动第二个 LSP 客户端,也不再打包语言服务 payload。 +- mcpp build 完成后只通过 mcppls 公开命令刷新语言服务;新增模块图、语言服务日志和分析 + 上下文转发命令。 +- 一键模块配置改为一次确认后执行 `mcpp build` 并刷新 mcppls,不再安装 llvm-tools、切换 + LLVM 全局默认或修改 clangd 配置。 + +### Fixed + +- 移除 mcpp 已删除的 `[xlings.envs]` manifest 补全,并将 `[xlings.workspace]` 示例版本更新为 + 当前索引可用的 Node 条目。 ## 0.2.7 diff --git a/README.md b/README.md index b96ae2a..31cd324 100644 --- a/README.md +++ b/README.md @@ -4,427 +4,220 @@ # mcpp VS Code 扩展 -把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 +把 mcpp 工程的构建、运行、测试、工具链管理和 `mcpp.toml` 编辑支持接入 VS Code。 -当前版本为 `0.3.1`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 -mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 +当前版本为 `0.4.0`。C++20/23 模块的诊断、补全、跳转、引用、状态和模块图由 +[`sunrisepeak.mcpp-language-server`](https://github.com/Sunrisepeak/mcpp-language-server) +(显示名为 **C++ Modules Language Server**,下文简称 **mcppls**)提供。本扩展不启动第二个 +LSP 客户端,也不配置用户安装的 clangd。 -> 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 -> 模块语法高亮、mcpp 命令和工具链管理,但插件不为其提供模块诊断、悬停或跳转。 +> mcppls 会在自己的 payload 中携带固定版本的语义引擎;这不表示 mcpp-vscode 仍依赖官方 +> `llvm-vs-code-extensions.vscode-clangd` 扩展,也不表示本扩展读取用户的 `clangd.*` 设置。 -## 能力矩阵 +## 职责边界 -| 能力 | LLVM/Clang | GCC | MSVC | -| --- | --- | --- | --- | -| 识别 mcpp 工程 | 支持 | 支持 | 支持 | -| `mcpp.toml` TOML 语法高亮 | 支持 | 支持 | 支持 | -| `mcpp.toml` 结构补全(段头 + 写法模板) | 支持 | 支持 | 支持 | -| `build.mcpp`、`.cppm`、`.ixx`、`.mpp`、`.ccm` 文件关联 | 支持 | 支持 | 支持 | -| `module`、`export module`、`import` 语法高亮 | 支持 | 支持 | 支持 | -| 构建、运行、测试、清理命令 | 支持 | 支持 | 支持 | -| 查看、安装或选择 mcpp 工具链 | 支持 | 支持 | 系统检测与选择 | -| 自动配置语言服务 | clangd | 不支持 | 不支持 | -| 消费模块产物 | `.pcm` | 不读取 `.gcm` | 不读取 `.ifc` | -| 模块诊断、悬停、补全和跳转 | 实验性支持 | 不支持 | 不支持 | - -这里的“GCC/MSVC 不支持”只表示插件当前没有对应的模块语义后端,不表示 mcpp -不能使用这些工具链构建项目。mcpp 可以生成 GCC `.gcm` 和 MSVC `.ifc`,但 clangd -不能直接消费这些编译器专用格式。 +| 能力 | 负责方 | +| --- | --- | +| mcpp 工程发现、`mcpp.toml` 语法与结构补全 | mcpp-vscode | +| build / run / test / clean 任务 | mcpp-vscode | +| mcpp 工具链查看、安装、选择和项目操作 | mcpp-vscode | +| C++ 模块诊断、补全、悬停、定义、引用 | mcppls | +| C++ Modules 状态、模块图、分析上下文和语言服务日志 | mcppls | +| 语言服务冲突与其他 C++ 扩展的管理 | mcppls | ## 安装 -从 [GitHub Releases](https://github.com/mcpp-community/mcpp-vscode/releases) 下载对应版本的 +从 [GitHub Releases](https://github.com/mcpp-community/mcpp-vscode/releases) 下载本扩展的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.3.1.vsix +code --install-extension /path/to/mcpp-vscode-0.4.0.vsix +``` + +本扩展在 `package.json` 中声明了 `sunrisepeak.mcpp-language-server` 依赖。安装后确认当前 +VS Code profile 中同时存在: + +```text +mcpp-community.mcpp-vscode +sunrisepeak.mcpp-language-server ``` -安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 -`llvm-vs-code-extensions.vscode-clangd`。当前发布流程不会自动发布到 VS Marketplace。 +当前 mcppls 发布包支持 `linux-x64`、`linux-arm64`、`darwin-arm64` 和 `win32-x64`。 +在这些平台之外(例如 `darwin-x64`)先确认上游已提供对应 VSIX;本扩展不会把不匹配的 +payload 伪装成可用。 ## 使用条件 -- VS Code 1.90 或更高版本。 -- 包含 `mcpp.toml` 的 mcpp 工程。 -- 包含 mcpp #387(合并提交 `3f237ed`)的可执行文件;在该能力进入正式版本前, - 可使用对应主线构建。旧版 mcpp 不支持 configure-only 时,扩展会保留原有 CDB 并提示升级。 - 它可以由 xlings、官方独立安装脚本或其他受支持方式安装。 -- 官方 `llvm-vs-code-extensions.vscode-clangd` 扩展。它是本扩展的功能依赖。 -- 要启用模块语义,需要 LLVM mcpp 工具链、工程编译数据库,以及与该工具链来自 - 同一 LLVM revision 的 clangd。 +- VS Code `1.91` 或更高版本。 +- 包含 `mcpp.toml` 的 mcpp 工程,或从 mcpp-vscode 的新建工程命令创建工程。 +- 可执行的 `mcpp`。它可以由 xlings、官方独立安装脚本或其他受支持方式安装。 +- mcppls 扩展及其对应平台包。mcppls 自己负责语言服务进程和语义 payload。 -安装来源彼此独立:mcpp、构建编译器和 clangd 不必位于同一个目录,也不要求都由 -xlings 安装。 +`mcpp.path` 只控制 **mcpp-vscode 执行的 mcpp CLI 命令**。它不会跨扩展写入 mcppls 的 +启动参数;如果 mcppls 从 PATH 找到的 mcpp 与该设置不同,请分别配置并查看两个输出频道。 ## 快速开始 -1. 使用 VS Code 打开包含 `mcpp.toml` 的目录,并按需信任工作区。 -2. 工程没有有效 `compile_commands.json` 时,扩展会运行 - `mcpp build --configure-only` 生成配置阶段数据,不编译普通对象或链接产物。 -3. 扩展会分析编译数据库、选择匹配的 clangd、写入工作区配置并检查模块支持;缺少 - 项目 PCM 或需要验证正式产物时,仍应执行完整构建或使用一键配置向导。 -4. 查看状态栏中的 `mcpp: 模块可用`、`mcpp: 模块不可用` 或 - `mcpp: 缺少模块 CDB` 状态。 - -工程还没有编译数据库时,可以点击“缺少模块 CDB”,或执行 -**mcpp: 刷新编译数据库**。这个命令执行 `mcpp build --configure-only`,只在退出码为 -0 且新 CDB 可解析时将其视为成功;失败时继续保留并使用原有可用 CDB。 +1. 使用 VS Code 打开包含 `mcpp.toml` 的目录。 +2. 打开 C++ 源文件,观察 C++ Modules 语言状态项;它由 mcppls 显示当前工程、工具链和降级原因。 +3. 执行 **mcpp: 构建**,或在终端运行 `mcpp build`。 +4. 构建任务结束后,mcpp-vscode 请求 mcppls 重启并重新读取构建描述。 -virtual workspace 根不对应单一 CDB:mcpp 会把数据库分别发布到各 member 根。扩展 -因此只接管当前活动 member;请先打开该 member 的源码或 `mcpp.toml`,不会把多个 member -的 CDB 合并到虚拟根。 +打开工程本身不会静默执行 `mcpp build`,也不会下载或切换工具链。需要刷新模块语义时, +使用 **mcpp: 刷新模块构建描述** 或 **mcpp: 一键构建并刷新模块语言服务**。 ## 已实现功能 ### 工程发现与自动激活 -- 工作区包含 `mcpp.toml`,或者打开 C++ 文件时激活扩展。 -- 从活动文件或工作区目录查找最近的 mcpp 工程。 -- 监听 `mcpp.toml`、`compile_commands.json`、活动编辑器、工作区和相关设置变化。 -- 打开已有有效 CDB 的 LLVM 工程后自动配置并检查,不需要先点击状态栏或 Reload - Window。 - -扩展激活不会静默运行完整 `mcpp build`,也不会自动下载、安装或切换工具链;受信任 -工程缺少有效 CDB 时会自动运行配置阶段的 `mcpp build --configure-only`。 - -### 语法高亮 - -- 将精确文件名 `mcpp.toml` 识别为独立的 mcpp TOML 语言,内置表、键、字符串、数字、 - 布尔、日期时间和注释高亮。 -- 将精确文件名 `build.mcpp` 识别为独立的 mcpp build language,并复用 C++ TextMate - grammar;`.cppm`、`.ixx`、`.mpp` 和 `.ccm` 继续关联到 VS Code 内置 `cpp` 语言。 -- 向 C++ 源文件注入模块语法规则。 -- 覆盖 `module`、`export module`、`import`、`export import`、模块名和模块分区。 -- 支持尚未输入分号的编辑中间态。 - -语法高亮只负责词法着色。错误模块名、不可见声明等红色诊断来自语言服务器,不能由 -TextMate 语法规则提供。 - -`build.mcpp` 是由 mcpp 宿主执行的构建脚本,不是普通 C++ 翻译单元。当前扩展只为它 -提供 C++ 风格的语法高亮和编辑器括号/注释规则,不启动 clangd 语义诊断;因此 -`import mcpp;` 不会再被误报为缺少 C++ 模块。真正的 mcpp API 补全需要 mcpp 核心 -未来生成宿主 helper 的 CDB 和 PCM 映射。 - -### mcpp.toml 结构补全 - -- 段头补全:26 个段(`[package]`、`[targets.]`、`[dependencies]`、`[build-dependencies]`、 - `[features]`、`[indices]`、`[pack]` 等),参数化段带可跳转占位符。 -- 写法模板:依赖段的依赖写法(版本 / 路径 / git / features / tools)、`[features]` 表形式、 - capabilities / xlings / tools.overrides / generated_files 等开放段的条目形态。 -- 每条建议携带显式替换范围,部分输入(`[dep`、`na`)不会残留无效文本;所有语义规则有真实 - mcpp 契约测试(44 例,无 mcpp 环境自动跳过)。 -- 范围边界:不提供静态字段键/枚举(等上游版本化 manifest schema),不提供依赖包名/版本候选 - (等上游批量 catalog 接口);未知自定义段与 `[[...]]` 数组表不提供建议。 -- 由 `mcpp.tomlCompletion` 设置控制(默认开启);未受信任工作区仅做纯文本分析,不执行任何 - 外部程序。 - -### LLVM 与 clangd 集成 - -扩展读取 mcpp 生成的 `compile_commands.json`,然后: - -1. 识别其中的 LLVM 编译器及待检查源文件。 -2. 从显式设置、编译器目录、匹配的 xlings `llvm-tools` 目录和 `PATH` 中查找 clangd。 -3. 执行编译器和 clangd 的版本命令,比较 LLVM 版本与 revision。 -4. 保留用户已有的 `clangd.arguments`,展开 `${workspaceFolder}` 和 - `${workspaceRoot}`;只有在 CDB 未提供 `--no-default-config`、`-nostdinc++` 和显式 - libc++ 路径时,才添加精确的 `--query-driver`。 -5. 根据 CDB 中是否已有显式 PCM 映射以及 `mcpp.modulesSupport` 设置,决定是否增加 - clangd 实验模块参数。 -6. 必要时重启官方 clangd 扩展,并执行一次最长 60 秒的 `clangd --check`。 - -检查可以区分模块可用、PCM 不匹配、模块产物不可用、语言模式错误和一般检查失败。 -完整命令及输出会写入 `mcpp` 输出频道。 - -**mcpp: 一键配置模块代码提示** 会先读取工具链状态,再显示一次模态确认。确认后, -它可以自动安装 host LLVM、设置 mcpp 全局默认、执行 `mcpp build`,并通过已安装的 -xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB、配置 clangd 和检查 -模块状态,无需 Reload Window。项目在 `mcpp.toml` 或 target 中显式固定 GCC/MSVC 时, -一键流程会停止并提示手动处理,绝不修改项目配置或伪装成全局切换成功。 - -### 自动协调与多根工作区 - -- CDB 创建或变化后自动重新分析、配置、重启和检查;删除后更新为缺少 CDB 状态。 -- 同一工程的重复事件通过 singleflight 合并,共享的 clangd 操作全局串行。 -- 只接受当前工程最新一次模块检查的结果,迟到结果不会覆盖新状态。 -- 多根工作区中只有活动 mcpp 工程接管窗口级 clangd 配置;后台工程只失效缓存,切换 - 过去时再协调。 -- 修改资源域 `mcpp.path` 时按工程标记重新生成 CDB;修改 `mcpp.clangd.path` 或 - `mcpp.modulesSupport` 时只重新配置和检查,不重复运行 mcpp。 -- 完整构建即使因普通源码错误退出,只要留下可用 CDB,扩展仍会尝试恢复 IDE 状态,并把 - “构建失败”和“IDE 数据可用”分别报告。 - -### mcpp CLI 与工具链管理 - -状态栏中的 `$(tools) mcpp: 快捷菜单` 提供: - -- 在当前工程根目录执行 `mcpp build`、`run`、`test` 和 `clean`。 -- 在 VS Code 任务终端中实时显示完整输出。 -- 同一工程同时只运行一个由扩展启动的项目任务。 -- 项目任务结束后先释放任务锁,再重新协调 CDB 和 clangd。 -- 清理当前工程的 `target/`;不会删除 mcpp 全局 BMI 缓存。 -- 读取并展示 `mcpp toolchain list` 的当前有效工具链、全局默认、系统工具链、target - 状态和可安装版本。 -- 安装 mcpp 支持的工具链 spec,或从已安装/系统项中选择全局默认工具链。 - -安装和修改默认值都需要确认。取消操作不会被当成成功,工具链安装、默认值修改和构建 -共享一个全局互斥锁;一键配置只在开始处确认一次,之后不会弹出工具链选择或“是否刷新 -编译数据库”的二次询问。 +- 工作区包含 `mcpp.toml`,或打开 C/C++ 文件时激活。 +- 从活动文件或工作区目录查找最近的 mcpp 工程,并支持工作区成员边界。 +- 监听 `mcpp.toml` 创建、修改和删除,更新当前工程的快捷菜单与运行/测试按钮。 +- 不再监听或解析 `compile_commands.json`,也不再替 mcppls 重启或配置 clangd。 +- 只有用户主动执行 mcpp 任务时才启动 mcpp;任务结束后仅对 build 请求 mcppls 刷新。 -## 命令 +### 语法高亮与 `mcpp.toml` -| 命令 | 行为 | -| --- | --- | -| **mcpp: 打开快捷菜单** | 打开项目、工具链和 IDE 操作入口 | -| **mcpp: 构建** | 在当前工程执行 `mcpp build` | -| **mcpp: 运行** | 在当前工程执行 `mcpp run` | -| **mcpp: 测试** | 在当前工程执行 `mcpp test` | -| **mcpp: 清理 target** | 确认后执行 `mcpp clean` | -| **mcpp: 查看工具链** | 只读展示 `mcpp toolchain list` 的解析结果和原始输出 | -| **mcpp: 安装工具链** | 确认后执行 `mcpp toolchain install ` | -| **mcpp: 选择全局默认工具链** | 确认后执行 `mcpp toolchain default ` | -| **mcpp: 配置 clangd** | 手动重新应用当前 LLVM 工程的 clangd 配置 | -| **mcpp: 刷新编译数据库** | 执行 `mcpp build --configure-only`,验证 CDB 后协调 clangd;失败时保留原有可用 CDB | -| **mcpp: 检查模块支持** | 立即执行 clangd 直接检查并刷新模块状态 | -| **mcpp: 一键配置模块代码提示** | 一次确认后自动安装/切换 host LLVM、执行 `mcpp build`,通过 xlings 补齐匹配 llvm-tools,重载 CDB 并配置 clangd;项目显式固定非 LLVM 工具链时停止 | +- 将精确文件名 `mcpp.toml` 识别为 mcpp TOML 语言。 +- 将精确文件名 `build.mcpp` 识别为 mcpp build language,并复用 C++ TextMate grammar。 +- `.cppm`、`.ixx`、`.mpp`、`.ccm` 文件继续关联为 C++。 +- 注入 `module`、`export module`、`import`、模块名和模块分区语法。 +- 为 `mcpp.toml` 提供段头和写法模板补全;建议只覆盖结构,不猜测动态依赖版本。 +- 补全清单与当前 mcpp 契约测试同步;已被新版 mcpp 删除的 `[xlings.envs]` 段不再建议。 -## 设置 +语法高亮只负责词法着色。错误模块名、不可见声明和跨模块引用诊断由 mcppls 提供。 -```json -{ - "mcpp.path": "/path/to/mcpp", - "mcpp.clangd.path": "/path/to/matching/clangd", - "mcpp.modulesSupport": "auto", - "mcpp.configureCppTools": true, - "mcpp.tomlCompletion": true -} -``` +### 构建、运行与工具链 -| 设置 | 默认值 | 说明 | -| --- | --- | --- | -| `mcpp.path` | 空 | mcpp 可执行文件;空值表示从 VS Code 进程的 `PATH` 查找 | -| `mcpp.clangd.path` | 空 | 与 CDB 中 LLVM 编译器匹配的 clangd;空值表示自动发现 | -| `mcpp.modulesSupport` | `auto` | `auto`、`on` 或 `off`,控制 clangd 实验模块参数 | -| `mcpp.configureCppTools` | `true` | 手动配置 clangd 时,是否询问关闭当前工作区的 cpptools IntelliSense | -| `mcpp.tomlCompletion` | `true` | 为 `mcpp.toml` 提供结构补全:段头与写法模板(snippet);所有建议带显式替换范围,并经真实 mcpp 契约测试验证 | - -`mcpp.path` 只影响插件执行 mcpp CLI 命令。工程实际编译器来自 -`compile_commands.json`,`mcpp.clangd.path` 只指定语言服务器,三者相互独立。 - -`mcpp.modulesSupport=auto` 检测到 CDB 已含 `-fmodule-file` 或 -`-fprebuilt-module-path` 时,直接使用 mcpp 生成的 PCM;否则只为 clangd 21 及以上 -请求实验模块构建。`on` 强制请求该参数,`off` 始终不请求。这个设置不能生成缺失的 -PCM。 - -## 工具链管理边界 - -- mcpp 的规范工具链 family 是 `gcc`、`llvm` 和 `msvc`;target 是独立的 - `arch-os[-env]` 维度。 -- 安装入口接受 family、`family@version`、namespace、部分版本及 mcpp 兼容旧拼写, - 例如 `gcc`、`llvm@20.1.7`、`xim:gcc@16`、`msvc` 和 `mingw`。插件只做单参数 - 规范化,最终合法性和具体版本由 mcpp 判断。 -- 当前 UI 只读展示 target,不提供结构化 `--target` 选择器。target 专用默认值继续 - 使用 mcpp CLI 设置。 -- 选择全局默认值会修改 mcpp 的全局配置;项目或工作区的 `[toolchain]`、 - `[build].target` 和命令行参数仍可按 mcpp 规则覆盖它。 -- MSVC 是 Windows 系统工具链。mcpp 负责检测和构建,不负责下载、升级或删除 Visual - Studio;缺失时只显示官方安装指引。 -- 0.0.90 起 native `cl.exe` 后端支持 `import std`、命名模块、`.ifc`、增量构建和 - 运行。这是 mcpp 的构建能力,不代表当前插件或 clangd 能读取 `.ifc`。 -- 当 mcpp 工程位于 VS Code 工作区目录的子目录时,插件不会仅凭路径关系判断它一定是 - mcpp 工作区成员;实际工具链以 mcpp 在该目录中的解析结果为准。 - -## 当前限制与责任边界 - -### 模块语义仅支持 LLVM - -GCC `.gcm` 和 MSVC `.ifc` 是编译器专用产物。mcpp 可以正常构建它们,但 clangd -不能直接消费;当前版本没有 GCC 原生模块语言服务器、cpptools 模块后端或 LLVM 影子 -分析副本。因此 GCC/MSVC 工程只保证模块语法高亮和 mcpp 操作。 - -### 首次语义配置仍需要 mcpp 生成的数据 - -当前插件只读取 `compile_commands.json`,不解析 `mcpp.toml`,也不自行推导依赖和 -模块图。受信任工程没有有效 CDB 时,插件会调用 `mcpp build --configure-only`,但不会 -自行生成或改写 mcpp 没有输出的 CDB。完整模块语义仍取决于 CDB 引用的兼容 PCM;正式 -构建、链接和测试仍由普通 `mcpp build`、`run` 或 `test` 负责。 - -### clangd 模块支持仍是实验能力 - -- CDB 必须包含正确语言标准、编译器路径和有效 PCM 映射。 -- clangd、mcpp LLVM 编译器、C++ 标准库和 PCM 应来自同一个 LLVM 构建;只匹配主版本 - 或版本号并不足够。 -- 语法颜色正常不代表语义诊断可用,`clangd --check` 成功也不替代完整构建和测试。 -- clangd 负责编辑期诊断、悬停、补全和跳转;链接、代码生成、运行期和打包错误仍由 - `mcpp build`、`run` 或 `test` 报告。 -- 当前扩展不调用 `clang-tidy` 或 `clang-format`,缺少它们不会影响本扩展的模块检查。 - -### 工作区信任与外部操作 - -未受信任工作区只启用文件关联和语法高亮,不执行 CDB 中的编译器、mcpp、clangd 或 -工程指定程序,也不接管 clangd 配置。授予信任后,扩展会自动重新协调当前工程;仍不 -会在没有用户操作时下载或切换工具链,也不会发起完整构建;缺少有效 CDB 时可以执行 -配置阶段的 configure-only。该命令不是只读操作:mcpp 仍可能执行 `build.mcpp`、解析或 -安装缺失依赖和工具链,并写入 lock、构建目录与相关元数据。 - -## 当前版本能达到的效果 - -在 LLVM 工程已经具有有效 CDB、完整 PCM 映射和匹配 clangd 的前提下,当前版本可以: - -- 打开工程后自动恢复 clangd 配置和模块状态; -- 由 clangd 对错误导入、不可用模块及跨模块声明提供诊断; -- 由 clangd 提供悬停、补全、定义跳转、引用查找和后台索引; -- 在重新构建或 CDB 变化后自动刷新,不要求 Reload Window; -- 通过 VS Code 任务完成常用 mcpp 项目和工具链操作。 - -这些能力受 clangd 实验模块实现和 PCM 兼容性约束。工程没有 CDB 时,扩展可以请求 -mcpp 生成配置阶段 CDB;若 mcpp 版本不支持该命令、配置失败或仍缺少兼容 PCM,则只能 -提供语法高亮、mcpp 命令和引导操作。 - -## 理想最终效果 - -理想目标仍聚焦 LLVM 模块语义。用户打开一个受信任的 mcpp 工程后,不需要先执行 -完整的 `mcpp build`,也不需要 Reload Window: - -1. mcpp 解析工作区、目标、依赖、工具链、生成源和完整模块图。 -2. 按用户授权准备缺失依赖和工具链,并生成专供 IDE 使用的 CDB 和 PCM。 -3. 插件显示可取消进度,成功后原子切换到新 IDE 数据并启动匹配 clangd。 -4. 即使普通实现源码暂时无法编译或链接,编辑器仍能对错误模块名、不可见模块和导入 - 关系给出精确诊断。 -5. 模块导出的声明可以稳定悬停、补全、跳转定义、查找引用和重命名。 -6. 修改源码或 `mcpp.toml` 后增量刷新;离线时复用本机缓存和最后一次可用数据。 -7. 多根工作区中的工程拥有独立状态、进度、取消操作和 last-known-good 数据。 - -这里的“不需要完整构建”是指不编译普通对象文件、不链接最终程序或库。clangd 要进行 -跨模块语义分析,仍然必须由兼容 LLVM 工具链生成 IDE 专用 PCM,因此不等于完全不运行 -编译器。 - -## 达到理想效果所需支持 - -下面按完成程度区分必要工作。`mcpp build --configure-only` 是当前扩展采用的已实现接口; -其余机器可读工程模型和结构化诊断仍是后续协议工作。 - -| 支持项 | 当前程度 | 理想完成标准 | 优先级 | -| --- | --- | --- | --- | -| 机器可读工程模型 | 插件只能读取 CDB,mcpp 主要输出面向人的文本 | mcpp 输出带版本 schema 的成员、目标、源码、模块、依赖、工具链、缓存与诊断 | 必需 | -| IDE 配置阶段 | 已使用 `mcpp build --configure-only` 生成并验证 CDB | 增加结构化进度、取消和能力协商,不把人类 stdout 当协议解析 | 部分完成 | -| IDE 专用模块产物 | PCM 是正式构建的副产物 | 只构建 `std`、依赖和项目模块 PCM,不编译普通对象或链接,并按工具链身份隔离缓存 | 必需 | -| 完整 CDB 与模块图 | LLVM CDB 已有部分显式 PCM 参数 | producer 和 consumer 都使用确定的绝对 PCM 路径,覆盖分区、依赖包、成员和测试目标 | 必需 | -| 结构化诊断 | 未知导入等信息主要来自构建或 clangd 文本 | mcpp 输出文件、行、列、严重级别和稳定错误码,插件可精确定位配置期错误 | 必需 | -| 原子快照与回退 | CDB 变化后直接重新读取 | 新数据完整后原子切换,失败时保留 last-known-good CDB、PCM 和状态 | 必需 | -| 插件生命周期 | 已有监听、singleflight、串行化和工作区信任 | 增加进度、取消、在线/离线/仅缓存模式、过期状态和按工程隔离的配置任务 | 必需 | -| 增量更新 | CDB 变化后整体重新协调 | 只重扫受影响模块并复用兼容 PCM,`mcpp.toml` 变化时增量解析依赖 | 增强 | -| 端到端验证 | 主要是单元测试和直接 `clangd --check` | 在真实 VS Code、LLVM 和示例工程中验证诊断、悬停、补全、跳转、离线及多根行为 | 发布门槛 | -| GCC/MSVC 模块语义 | 仅语法高亮 | 等待成熟原生语言服务,或另行设计 LLVM 影子分析;不属于当前路线 | 暂缓 | - -推荐的职责边界接近 Cargo 与 rust-analyzer:mcpp 提供稳定、可查询的工程模型和 IDE -产物,clangd 消费 CDB 与 PCM,VS Code 扩展只负责授权、生命周期、状态和错误展示。 -插件不应重新实现一套 `mcpp.toml` 解析器、依赖解析器或包管理器。 - -## 路线图 +- 构建、运行、测试、清理 target 使用 VS Code 原生 `ProcessExecution` 和任务终端。 +- 同一工程任务互斥;取消不会被误报为成功。 +- 工具链列表、安装和全局默认选择继续由 mcpp CLI 决定,本扩展不复制版本解析策略。 +- 工具链安装或默认工具链变化不会静默切换语言服务;mcppls 独立读取工程描述。 -### 阶段 1:当前版本 +### C++ Modules 语言服务桥接 -- 缺少有效 CDB 时,在受信任工作区自动执行 `mcpp build --configure-only`;失败时保留 - last-known-good CDB。 -- 使用 configure-only 或用户完整构建产生的 LLVM CDB 和已有 PCM。 -- 自动配置 clangd、检查模块、监听变化并提供 CLI/工具链菜单。 -- 一键自动安装匹配的 llvm-tools 并配置模块代码提示。 -- GCC/MSVC 保持语法高亮和 mcpp 构建操作,不增加语义后端。 +本扩展只调用 mcppls 公开的 VS Code 命令: -### 阶段 2:机器可读工程协议增强 +- 选择 C++ 模块分析上下文:`mcppls.selectContext` +- 重启语言服务:`mcppls.restartServer` +- 查看模块图:`mcppls.showModuleGraph` +- 打开 C++ Modules 日志:`mcppls.showLogs` -- 在 mcpp 核心增加带版本的机器可读工程模型,不改变 configure-only 的退出码 + CDB - 客户端契约。 -- 补充完整模块图、结构化诊断、IDE 专用 PCM 和能力协商。 -- 明确定义工作区成员、配置身份、缓存路径、进度和取消协议。 +build 任务成功后,桥接调用 restart,让 mcppls 重新读取描述。桥接不解析 mcppls 内部 LSP +消息,不读取 `mcppls.*` 设置,也不创建 `vscode-languageclient`。如果依赖未安装或被禁用, +命令会显示明确提示并提供打开扩展搜索的操作,不会静默失败。 -### 阶段 3:完善打开即用的 LLVM 体验 +## 命令 + +| 命令 | 作用 | +| --- | --- | +| **mcpp: 打开快捷菜单** | 汇总工程、工具链和 C++ Modules 操作 | +| **mcpp: 新建工程** | 创建并打开新的 mcpp 工程,不自动构建 | +| **mcpp: 构建** | 执行 `mcpp build`;结束后刷新 mcppls | +| **mcpp: 运行 / 测试 / 清理 target** | 执行对应 mcpp 任务 | +| **mcpp: 查看工具链** | 查看 mcpp 报告的工具链状态 | +| **mcpp: 安装工具链** | 通过 mcpp 安装工具链 | +| **mcpp: 选择全局默认工具链** | 修改 mcpp 的全局默认工具链 | +| **mcpp: 选择 C++ 模块分析上下文** | 转发到 mcppls 的上下文选择 | +| **mcpp: 刷新模块构建描述** | 执行 `mcpp build` 并请求 mcppls 重新加载 | +| **mcpp: 重启 C++ Modules 语言服务** | 转发到 `mcppls.restartServer` | +| **mcpp: 查看模块图** | 转发到 `mcppls.showModuleGraph` | +| **mcpp: 打开 C++ Modules 日志** | 转发到 `mcppls.showLogs` | +| **mcpp: 一键构建并刷新模块语言服务** | 一次确认后 build,再刷新 mcppls | + +旧的 `mcpp.configureClangd` 命令 ID 暂时保留为弃用别名,行为与“选择 C++ 模块分析上下文”相同; +请把自定义 keybinding 迁移到 `mcpp.configureLanguageServer`。 -- 在已有自动 configure-only 基础上增加可取消进度、离线缓存、增量刷新和过期状态。 -- 完善按工程隔离的 last-known-good 数据与多根集成验证。 -- 以真实 VS Code 端到端测试作为模块诊断和跳转能力的发布门槛。 +## 设置 + +### mcpp-vscode 设置 -### 未来重新评估 GCC/MSVC +| 设置 | 默认值 | 作用 | +| --- | --- | --- | +| `mcpp.path` | 空 | mcpp-vscode 执行 mcpp CLI 时使用的可执行文件;空值表示从 VS Code 的 PATH 查找 | +| `mcpp.tomlCompletion` | `true` | 是否为 `mcpp.toml` 提供结构补全 | -当前不适配 GCC/MSVC 模块语义。只有满足以下条件之一时再单独设计: +### 迁移后弃用设置 -- 出现能够稳定消费 `.gcm` 或 `.ifc`,并提供诊断、索引、悬停和跳转的语言服务; -- mcpp 能生成与正式 GCC/MSVC 构建隔离的 LLVM IDE 分析副本,并明确接受两套编译器 - 在宏、扩展和诊断上的差异。 +以下旧设置仍保留在 manifest 中,便于旧工作区平滑升级,但 mcpp-vscode 不再读取或写入它们: -重新评估不会改变当前原则:正式构建结果以项目选择的 mcpp 工具链为准,IDE 分析不能 -代替 `mcpp build` 和 `mcpp test`。 +- `mcpp.clangd.path` +- `mcpp.modulesSupport` +- `mcpp.configureCppTools` + +mcppls 的语言服务、冲突处理和语义引擎设置由 mcppls 自己的 `mcppls.*` 设置管理。不要把 +旧 `clangd.*` 工作区设置当作 mcppls 的配置。 + +## 未受信任工作区 + +Restricted Mode 下,mcpp-vscode 只提供纯文本的模块语法高亮和 `mcpp.toml` 结构补全,不执行 +mcpp CLI,也不接管语言服务配置。mcppls 自身也有受限模式:它不运行工作区指定的构建工具 +或编译器,并使用受限的模块索引/语义工具包。 ## 故障排查 -### 扩展没有激活 +### C++ Modules 没有出现 -- 确认工作区包含 `mcpp.toml`,或已经打开一个识别为 `cpp` 的文件。 -- 执行 `code --list-extensions`,确认当前 VS Code profile 中存在 - `mcpp-community.mcpp-vscode`。 -- 只有扩展目录但没有登记到当前 profile 时,语法注入和自动配置都不会发生。 +1. 确认 mcppls 扩展已安装并启用: -### 找不到 mcpp + ```sh + code --list-extensions --show-versions + ``` -macOS 从图形界面启动 VS Code 时可能没有继承终端 `PATH`。将 `mcpp.path` 设置为实际 -可执行文件,例如 `/opt/homebrew/bin/mcpp`。这不会改变工程编译器或 clangd 路径。 +2. 确认当前 VS Code 平台有 mcppls 对应 VSIX。 +3. 打开 **C++ Modules** 日志,查看 payload、编译器或工作区信任错误。 +4. 确认工作区受信任;未受信任时不会执行 mcpp。 -### 缺少模块 CDB +### mcpp 找不到 -受信任工程缺少有效 CDB 时会自动尝试 `mcpp build --configure-only`,也可以手动执行 -**mcpp: 刷新编译数据库**。若 mcpp 版本不支持该参数、命令失败或生成的 CDB 无法解析, -插件会保持“缺少模块 CDB”状态,并保留原有可用 CDB;请查看 `mcpp` 输出频道。需要 -编译普通对象、生成完整项目产物或验证链接时,再在终端运行 `mcpp build`。 +macOS 从 Dock 或桌面入口启动 VS Code 时,PATH 可能与终端不同。设置 `mcpp.path` 为实际 +可执行文件,例如 `/opt/homebrew/bin/mcpp`。该设置只作用于 mcpp-vscode 自己的 CLI 调用。 -### clangd 未安装或无法匹配 +### mcpp 构建完成但模块描述没有更新 -- 执行 **mcpp: 一键配置模块代码提示**,扩展会自动通过 xlings 安装与当前编译器 - 版本匹配的 llvm-tools(含 clangd)。 -- xlings 可执行文件的定位以 `mcpp self env` 报告为准(覆盖 install.sh、AUR 等 - mcpp 内置安装布局),找不到时再回退到独立 `~/.xlings` 与 `PATH`。 -- `mcpp.clangd.path` 可以指向任意安装来源的真实 clangd,不要求与 mcpp 同目录。 -- 自动发现会尝试编译器同目录、匹配的 xlings `xim-x-llvm-tools//bin` 和 - `PATH`。 -- 独立 mcpp 的 LLVM 编译器可以配合单独安装的 xlings llvm-tools 或其他匹配 clangd。 -- Homebrew clangd 即使版本号相同,也可能与 mcpp 工具链来自不同 revision。 +手动执行 **mcpp: 刷新模块构建描述**,或查看 mcppls 日志。build 失败时,mcppls 可能继续使用 +最后一次可用描述并显示 degraded;这不等于模块语义一定可用。 -### 模块不可用或跳转失败 +### 出现两套 C++ 诊断 -- 查看 `mcpp` 输出频道中的 `clangd --check` 完整输出。 -- 确认 CDB 使用 C++20 或更高标准,并包含当前文件的编译命令。 -- 确认 PCM 路径存在,且 clangd、编译器、标准库和 PCM 来自同一 LLVM 构建。 -- 修改工具链或 CDB 后等待自动协调,必要时执行 **mcpp: 检查模块支持**。 +mcppls 会提示关闭其他 C++ 扩展的语言功能。可使用 mcppls 提供的 +**C++ Modules: Turn Off Other C++ Language Features** 和 +**C++ Modules: Restore Other C++ Language Features**。mcpp-vscode 不再自动修改 +`C_Cpp.intelliSenseEngine` 或官方 clangd 设置。 -### 出现两套诊断 +### 出现旧 clangd 配置提示 -clangd 和 Microsoft C/C++ IntelliSense 可能同时报告诊断。执行 -**mcpp: 配置 clangd** 时,扩展可以询问是否仅在当前工作区关闭 cpptools -IntelliSense;也可以将 `mcpp.configureCppTools` 设为 `false` 保留它。 +旧 `clangd.*` 和 `mcpp.clangd.*` 设置已不再由 mcpp-vscode 使用。请删除不再需要的设置, +并以 mcppls 的状态、日志和设置为准。 -## 开发 +## 开发与验证 ```sh -npm install -npm test +npm ci npm run compile +npm test +npm run package +npm run test:e2e ``` -生成的 `dist/` 是扩展运行时目录。请在 Extension Development Host 中验证真实 VS Code -API、状态栏、任务和 clangd 集成。 +`npm test` 包含纯 Node 单元测试和真实 mcpp 契约测试;没有 mcpp 时契约测试会跳过。 +`npm run test:e2e` 使用隔离的 mcppls stub 验证扩展激活、任务回调和公开命令桥接,不启动真实 +LSP。真实 mcppls 语义验证应在安装对应平台 VSIX 的 Extension Development Host 或发布前 +验收中执行。 ## 发布 -先更新 `package.json`、`package-lock.json` 和更新日志中的版本并提交,再推送与扩展 -版本完全一致的 tag: +先更新 `package.json`、`package-lock.json` 和更新日志中的版本并提交,再推送与扩展版本完全 +一致的 tag: ```sh -git tag -a v0.3.1 -m "mcpp-vscode 0.3.1" -git push origin v0.3.1 +git tag -a v0.4.0 -m "mcpp-vscode 0.4.0" +git push origin v0.4.0 ``` -`.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, -并创建或更新对应的 GitHub Release。工作流当前不会发布到 VS Marketplace。 +`.github/workflows/release.yml` 会校验 tag、运行测试和打包,生成 VSIX 与 SHA-256 文件, +并创建或更新 GitHub Release。发布前必须在干净 extensions 目录安装 VSIX,确认 mcppls +依赖被自动解析,并记录未覆盖的平台。 项目地址:[mcpp-community/mcpp-vscode](https://github.com/mcpp-community/mcpp-vscode) diff --git a/docs/superpowers/plans/2026-09-25-mcpp-language-server-migration.md b/docs/superpowers/plans/2026-09-25-mcpp-language-server-migration.md new file mode 100644 index 0000000..1617eec --- /dev/null +++ b/docs/superpowers/plans/2026-09-25-mcpp-language-server-migration.md @@ -0,0 +1,669 @@ +# mcpp-language-server 迁移实现计划 + +> **执行约定:**按任务顺序实施。每个任务先写失败测试,再写最小实现,最后运行该任务列出的验证命令。不要跨任务顺手改无关功能。 + +**目标:**让 `mcpp-community.mcpp-vscode` 不再安装、配置、探测或启动官方 clangd 扩展;把 C++ 模块语言服务职责交给 `sunrisepeak.mcpp-language-server`,同时保留 mcpp 的构建、工具链、任务和项目 UI。 + +**基线:**本计划编写于 `main@736ecbb`、扩展 `0.2.7`;本次迁移发布版本为 `0.4.0`(远端已占用 v0.3.0/v0.3.1);上游 `sunrisepeak.mcpp-language-server@v0.0.4`(commit `04b186a`)。实现前重新核对上游 tag、VS Code 引擎要求、平台矩阵与 `extensionDependencies` 行为。 + +**架构:**本插件不引入 `vscode-languageclient`,不创建第二个 LanguageClient,不打包 mcppls payload。它只通过 VS Code 命令边界与语言服务依赖协作。语言服务依赖负责 LSP、内置语义引擎、项目描述、诊断、状态和模块请求;本插件负责 mcpp CLI。 + +**技术栈:**TypeScript、VS Code Extension API、Node `node:test`、Mocha + `@vscode/test-electron`、GitHub Actions。 + +## 执行状态(2026-09-25) + +- [x] Task 1:manifest、依赖声明、隔离 VSIX 安装验证 +- [x] Task 2:mcppls 公开命令桥接与命令契约 +- [x] Task 3:build 任务后的语言服务刷新 +- [x] Task 4:一键流程改为 build + mcppls restart +- [x] Task 5:删除 clangd/CDB/PCM 专属实现与测试 +- [x] Task 6:E2E clangd stub 替换为 mcppls stub +- [x] Task 8:弃用设置、README、CHANGELOG 与兼容 alias +- [x] Task 9:版本、CI、残留审计、打包与隔离安装门禁 +- [ ] Task 7:真实 mcppls 跨工具链语义验收(LLVM 22.1.8 单次通过,需 CI 复验;GCC 16 需 Linux runner) + +实现结果:本地 `npm test`、`npm run test:e2e`、`npm run package`、VSIX 隔离安装均通过; +安装的 VSIX 自动解析到 `sunrisepeak.mcpp-language-server@0.0.4`。真实 mcppls 在 LLVM 22.1.8 +mcpp 工程中已完成一次空诊断、定义、悬停、引用和补全验证;重复运行时 references 查询曾超时, +因此 Task 7 仍不是发布门禁。GCC 16 fixture 因当前 macOS 无对应工具链包未完成。当前仍按计划明确 +记录 `darwin-x64` 尚无上游平台包。 + +--- + +## 先冻结决策 + +以下决策在实现期间不再重新讨论,除非上游发生破坏性变化: + +1. **使用独立扩展依赖,不内置 payload。** `extensionDependencies` 只写 `sunrisepeak.mcpp-language-server`。 +2. **不保留官方 clangd 扩展作为 fallback。** 两条语言服务不能并行,否则诊断、跳转与补全重复。 +3. **不 fork mcppls,不在本插件内启动 `mcppls serve`。** 那会复制上游的客户端、状态 UI 和冲突处理。 +4. **不向 `mcppls.*` 写入本插件的 `mcpp.path`。** 配置属于语言服务扩展;本插件只让 `mcpp.path` 作用于自己的 CLI 调用。 +5. **保留稳定 ID 兼容映射。** 新的 mcpp 命令使用 mcppls 语义;旧 `mcpp.configureClangd` 作为弃用 alias 转发到新实现。 +6. **本插件不再拥有 C++ 模块语义状态栏。** mcppls 已提供 C++ Language Status Item;本插件只保留 mcpp 快捷菜单状态栏。 +7. **旧 mcpp 不阻断安装。** mcppls 会从 `compile_commands.json` 或源码扫描降级;本插件只在用户主动构建时执行 mcpp。 +8. **先迁移命令、再删旧设置。** 旧 `mcpp.clangd.*` 与 `clangd.*` 工作区设置在兼容窗口内保留为无效配置,不再被本插件读取或写入。 + +## 不可越过的门禁 + +- 不得在 `package.json` 中保留 `llvm-vs-code-extensions.vscode-clangd`。 +- 不得在 `src/`、`test/`、`README.md` 中保留“本插件启动/匹配/配置 clangd”的行为描述。 +- 不得启动第二个 LSP 客户端,也不得把 mcppls payload 复制进本 VSIX。 +- 不得静默修改用户已有的 `clangd.path`、`clangd.arguments`、`clangd.enable`。 +- 不得删除 `mcpp.path`,直到 Task 8 明确 mcppls 能获得同一个 mcpp 可执行文件,或用户接受它只对 mcpp-vscode 生效。 +- 不得在普通 `npm test` 中下载 Marketplace 扩展。 + +## 暂不处理 + +- 不合并两个 Marketplace 扩展。 +- 不把 mcppls 的 `cxxModules/*` 协议复制成第二套客户端协议。 +- 不处理上游未发布的平台;`darwin-x64` 缺失是明确的支持边界,在上游发布前不宣称支持。 +- 不重写 mcpp.toml 补全、工具链管理、任务互斥、多根工程发现与新工程流程。 +- 不处理与迁移无关的既有失败:`test/mcppTomlContract.test.ts` 的 3 个 `[xlings.envs]` 失败来自本机 mcpp 2026.9.25.1 移除了该段,应独立修复或固定测试用 mcpp 版本。 + +--- + +## File Map + +| 文件 | 计划中的责任 | +| --- | --- | +| `package.json` | 用 mcppls 替换 clangd 依赖;VS Code 下限升到 1.91;声明新命令。旧设置先保留、最终在 Task 8 删除。 | +| `package-lock.json` | 只随 manifest 的必要变化更新;不新增语言客户端依赖。 | +| `src/languageServer.ts` | 纯命令桥接:探测 mcppls 是否存在、调用其公开命令、把失败映射为稳定结果。 | +| `src/extension.ts` | 删除 clangd 生命周期;保留 mcpp.toml provider、CLI controller 与命令注册。 | +| `src/commands.ts` | 统一公开命令 ID;快捷菜单改为 mcppls 语义。 | +| `src/moduleSetup.ts` | 一键流程改为“构建 + 刷新语言服务”,不再修改工具链。 | +| `src/cliController.ts` | 任务完成回调交给语言服务桥接;一键向导直接复用 `runProjectTask("build")`,删除专用 setup 锁与 command 序列。 | +| `src/workflow.ts` | 删除 CDB/clangd 单飞与状态渲染,只保留桥接需要的纯函数。 | +| `src/discovery.ts` | 保留 mcpp 工程发现并删除 `compilationDatabasePath`;删除 clangd 候选推导。 | +| `src/process.ts` | 保留 `runProcess`;删除 `runClangdCheck`、`runToolVersion`。 | +| `test/languageServer.test.ts` | 桥接契约:依赖 ID、命令 ID、参数、失败与去重。 | +| `test/artifacts.test.ts` | manifest、VSIX 依赖、禁止 clangd 行为残留。 | +| `test/e2e/fixtures/mcppls-stub/` | 隔离 Extension Host 测试中的 mcppls 依赖,不启动真实 LSP。 | +| `README.md`、`CHANGELOG.md` | 能力矩阵、依赖说明、升级迁移说明。 | +| `.github/workflows/ci.yml` | 增加 VSIX 依赖安装与桥接 E2E 检查。 | + +--- + +# Task 1:锁定依赖、平台与发布门禁 + +**Files:** +- Modify: `test/artifacts.test.ts` +- Modify: `package.json` +- Modify: `package-lock.json` +- Modify: `.github/workflows/ci.yml` + +- [ ] **Step 1: 先改 manifest 测试为新契约** + +将“声明官方 clangd 依赖”改成: + +```ts +assert.equal(manifest.engines?.vscode, "^1.91.0"); +assert.deepEqual(manifest.extensionDependencies, ["sunrisepeak.mcpp-language-server"]); +assert.ok(!manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); +assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.configureLanguageServer")); +assert.ok(!manifest.activationEvents?.includes("onCommand:mcpp.configureClangd")); +``` + +保留 `workspaceContains:mcpp.toml`、`onCommand:mcpp.run`、editor title 按钮和 TOML 补全断言。 + +- [ ] **Step 2: 写禁止行为测试** + +断言以下内容不存在: + +- manifest 中的官方 clangd 扩展 ID; +- `mcpp.clangd.path`、`mcpp.modulesSupport`、`mcpp.configureCppTools` 的新功能描述; +- `vscode-languageclient` 依赖; +- `clangd.restart` 调用; +- `runClangdCheck` / `resolveClangd` 等源码符号。 + +- [ ] **Step 3: 改 manifest** + +- `engines.vscode` 改成 `^1.91.0`; +- `extensionDependencies` 改成 `["sunrisepeak.mcpp-language-server"]`; +- 新增 `mcpp.configureLanguageServer`、`mcpp.showModuleGraph`、`mcpp.showLanguageServerLogs`; +- 新 ID 与旧 ID 的 alias 在 Task 8 完成; +- 暂时保留旧设置,但本插件不再读取。 + +- [ ] **Step 4: 验证本地 VSIX 会拉取依赖** + +```sh +code --extensions-dir "$TMP/extensions" --user-data-dir "$TMP/user-data" \ + --install-extension "$VSIX" +code --extensions-dir "$TMP/extensions" --user-data-dir "$TMP/user-data" \ + --list-extensions --show-versions +``` + +必须同时看到 `sunrisepeak.mcpp-language-server` 与本插件。若自动拉取失败,保留依赖声明,但 README 与发布脚本必须明确先装 mcppls,并将该行为升级为发布阻断。 + +- [ ] **Step 5: 确认平台矩阵** + +Task 1 结束前记录 CI runner 覆盖的 `darwin-arm64`、`linux-x64`、`win32-x64`;`darwin-x64` 明确标为不支持,不能宣称全平台支持。 + +**Gate:** manifest 测试、VSIX 隔离安装、平台结论三项通过。 + +--- + +# Task 2:建立 mcppls 命令桥接 + +**Files:** +- Create: `src/languageServer.ts` +- Create: `test/languageServer.test.ts` +- Modify: `src/commands.ts` +- Modify: `test/commands.test.ts` + +- [ ] **Step 1: 定义与 VS Code 分离的内部接口** + +```ts +export const MCPPLS_EXTENSION_ID = "sunrisepeak.mcpp-language-server"; + +export const MCPPLS_COMMANDS = { + restart: "mcppls.restartServer", + selectContext: "mcppls.selectContext", + graph: "mcppls.showModuleGraph", + logs: "mcppls.showLogs", +} as const; + +export interface LanguageServerCommandResult { + state: "completed" | "unavailable" | "failed"; + message: string; +} + +export interface LanguageServerCommandExecutor { + extensionInstalled(id: string): boolean; + executeCommand(command: string, ...args: unknown[]): Thenable; +} +``` + +刷新构建描述不是上游贡献的 VS Code 命令,而是一次 LSP execute-command 请求;本插件不能直接发起该请求。因此桥接采用下面的有限能力: + +- 可直接调用 `mcppls.restartServer`、`mcppls.selectContext`、`mcppls.showModuleGraph`、`mcppls.showLogs`; +- 构建后调用 `mcppls.restartServer`,不在本插件伪造 `mcppls.reloadBuildDescription`; +- 未来若上游贡献一个公开的刷新命令,再替换 Task 3 的 build 后动作。 + +- [ ] **Step 2: 写失败测试** + +覆盖:依赖存在时调用正确命令;依赖缺失返回 `unavailable`;命令抛错返回 `failed`;同一任务只刷新一次;错误文字包含 mcppls 扩展 ID,方便用户定位。 + +- [ ] **Step 3: 实现最小桥接** + +提供: + +- `restartLanguageServer()`; +- `selectContext()`; +- `showModuleGraph()`; +- `showLanguageServerLogs()`; +- `refreshLanguageServerAfterBuild()`(内部调用 restart)。 + +桥接不解析 mcppls 状态、不读取 `mcppls.*` 设置、不 spawn 子进程。 + +- [ ] **Step 4: 更新命令与 Quick Pick** + +命令 ID: + +```ts +export const CLI_COMMANDS = { + showMenu: "mcpp.showMenu", + newProject: "mcpp.newProject", + build: "mcpp.build", + run: "mcpp.run", + test: "mcpp.test", + clean: "mcpp.clean", + showToolchains: "mcpp.showToolchains", + installToolchain: "mcpp.installToolchain", + selectDefaultToolchain: "mcpp.selectDefaultToolchain", + configureLanguageServer: "mcpp.configureLanguageServer", + refreshCompilationDatabase: "mcpp.refreshCompilationDatabase", + checkModuleSupport: "mcpp.checkModuleSupport", + autoConfigureModules: "mcpp.autoConfigureModules", + showModuleGraph: "mcpp.showModuleGraph", + showLanguageServerLogs: "mcpp.showLanguageServerLogs", +} as const; +``` + +旧 `mcpp.configureClangd` 保留为弃用 alias,迁移说明从本版本开始保留。快捷菜单标题改为“配置 C++ 模块语言服务”“刷新模块构建描述”“查看模块图”“打开 C++ Modules 日志”。 + +- [ ] **Step 5: 验证** + +```sh +npm run compile +node --test dist/test/languageServer.test.js dist/test/commands.test.js +``` + +**Gate:** 桥接测试不依赖真实 VS Code 进程;命令 ID 全部在 `package.json` 与 `src/commands.ts` 一致。 + +--- + +# Task 3:迁移构建任务后的协调 + +**Files:** +- Modify: `src/tasks.ts` +- Modify: `src/cliController.ts` +- Modify: `src/extension.ts` +- Modify: `test/tasks.test.ts` +- Modify: `test/artifacts.test.ts` +- Modify: `test/e2e/suite/extension.test.ts` + +- [ ] **Step 1: 先写任务回调契约** + +- build 成功:调用一次 `refreshLanguageServerAfterBuild`; +- build 失败:也调用一次,使 mcppls 使用最后一次可用描述或显示降级; +- build 取消:不调用; +- run/test/clean:不调用; +- 桥接失败:不把成功的 mcpp build 改成失败,只追加 warning。 + +- [ ] **Step 2: 修改任务层** + +把 `shouldReconcileAfterTask` 改为 `shouldRefreshLanguageServerAfterTask`,并只允许 `kind === "build" && completion.state !== "cancelled"`。 + +- [ ] **Step 3: 修改控制器与扩展** + +`cliController.afterProjectTask` 调用桥接;删除: + +- `executeWithWorkspaceClangd`; +- `reconcileProjectContext` / `reconcileProjectByRoot`; +- `compilationDatabaseWatcher`; +- `configurationWatcher` / `trustWatcher` 中的 clangd 配置重启; +- `lastReconciledProjectRoot`。 + +manifest watcher 仍负责 mcpp 项目状态,不负责语言服务。 + +- [ ] **Step 4: 改用户文案** + +- “clangd/CDB 状态已重新检查” → “C++ 模块语言服务已刷新”; +- “mcpp 构建完成,clangd 配置已刷新” → “mcpp 构建完成,模块语言服务已刷新”; +- “刷新编译数据库”保留 ID,标题改成“刷新模块构建描述”; +- “检查模块支持”改成“重启 C++ Modules 语言服务”,直接调用 mcppls restart。 + +- [ ] **Step 5: 验证** + +```sh +node --test dist/test/tasks.test.js dist/test/artifacts.test.js +``` + +**Gate:** 本插件不再因 build 事件主动读取 `compile_commands.json` 或启动任何语言服务器。 + +--- + +# Task 4:重写一键模块配置 + +**Files:** +- Modify: `src/moduleSetup.ts` +- Modify: `test/moduleSetup.test.ts` +- Modify: `src/extension.ts` +- Modify: `src/cliController.ts` +- Modify: `test/cli.test.ts` +- Modify: `test/artifacts.test.ts` + +- [ ] **Step 1: 先定义新状态机测试** + +```ts +export type ModuleSetupStage = + | "prepare" + | "build" + | "language-server"; +``` + +不再有 `default`、`clangd`、`check` 阶段。测试覆盖: + +- 未受信任 / 忙碌 → blocked; +- 工具链清单无法识别 → blocked; +- GCC/MSVC/LLVM 项目均允许继续; +- `build` 固定执行普通 `mcpp build`; +- build 成功 → 只刷新语言服务一次; +- build 失败且 command 未被取消 → 仍刷新,让 mcppls 显示降级; +- build 失败但已有可用描述 → 结果为 degraded; +- 语言服务依赖缺失 → failed,不自动安装; +- 取消不产生后续调用。 + +- [ ] **Step 2: 简化 planner** + +`buildModuleSetupPlan` 不再分析 CDB 工具链家族,也不再阻塞非 LLVM 项目。计划只决定是否已忙碌;默认无需修改工具链。 + +删除 `installLlvm`、`switchDefault` 与 `project-toolchain-override` 阻塞;一键命令固定执行普通 `mcpp build`,因为 mcppls 能从任意 mcpp 工具链读取构建描述。不得继续切换 LLVM。 + +- [ ] **Step 3: 改执行接口** + +```ts +export interface ModuleSetupOperations { + build(): Promise; + refreshLanguageServer(): Promise; +} +``` + +删除 `preparePlan()`、`runAutomaticModuleSetup()`、专用 `executeAutomaticModuleSetupCommand()` 与独立 clangd check。`build()` 直接调用普通 `runProjectTask("build")` 的任务执行器,避免嵌套项目锁。 + +- [ ] **Step 4: 改向导 UI** + +一次 modal 确认后只执行普通 `mcpp build` → mcppls restart。确认文案明确:不会安装官方 clangd 扩展,不会修改 `clangd.*`。 + +- [ ] **Step 5: 验证** + +```sh +node --test dist/test/moduleSetup.test.js dist/test/artifacts.test.js +``` + +**Gate:** 测试与源码中不再出现 `llvmToolsVersionSpec`、`xlingsInstallArgs`、`ensureClangd`、`runClangdCheck`。 + +--- + +# Task 5:拆除 clangd 专属实现 + +**Files:** +- Delete: `src/analysis.ts` +- Delete: `src/llvmTools.ts` +- Delete: `test/analysis.test.ts` +- Delete: `test/llvmTools.test.ts` +- Modify: `src/process.ts` +- Modify: `test/process.test.ts` +- Modify: `src/discovery.ts` +- Modify: `test/discovery.test.ts` +- Modify: `src/extension.ts` +- Modify: `src/workflow.ts` +- Modify: `test/workflow.test.ts` + +- [ ] **Step 1: 用引用图确认删除范围** + +```sh +rg -n -i 'clangd|compile_commands|PCMs?|ToolIdentity|ModuleCapability|CheckResult' src test package.json README.md +``` + +逐项分类:历史 CHANGELOG、迁移说明可保留;活动代码、旧测试和当前能力描述必须清除。 + +- [ ] **Step 2: 精简 process** + +保留 `runProcess`(`cliController` 与契约测试仍需要);删除 `runToolVersion`、`runClangdCheck`、`runMcppBuild` 及 ToolIdentity 类型。 + +- [ ] **Step 3: 精简 discovery** + +保留 `McppProjectDiscovery`、manifest glob 与 nearest member 逻辑;删除 `compilationDatabasePath` 和 `deriveClangdCandidates` 及对应测试。任务/工具链逻辑只需要 `root`。 + +- [ ] **Step 4: 精简 workflow** + +保留与 CLI 任务/操作注册相关且有非 clangd 用户的函数;删除: + +- `shouldRestartClangd`; +- `configurationReadyAfterRestart`; +- `registerCompilationDatabaseReconciliation`; +- `configurationAffectsModuleSupport`; +- `shouldUseWorkspaceClangd`; +- `describeRefreshOutcome`; +- `statusCommandForCapability`; +- `shouldCheckModuleSupport`; +- `moduleSupportState`。 + +纯 single-flight/serial executor 若没有生产调用者,一并删除;不要为了测试保活死代码。 + +- [ ] **Step 5: 精简 extension** + +删除 ProjectContext、CDB 读取、状态栏、clangd resolver/config、模块检查、CDB watcher、自动重协调与所有相关命令分支。`activate()` 最终只保留: + +- mcpp.toml completion provider; +- mcpp project context; +- `McppCliController`; +- 语言服务桥接命令; +- 新的一键向导; +- 输出频道与资源释放。 + +- [ ] **Step 6: 清理测试** + +删除纯 clangd 测试;重写 `artifacts.test.ts` 和 `workflow.test.ts`,只钉住迁移后仍有生产价值的行为。不得把“源码没有 clangd 字符串”作为唯一断言,行为测试优先。 + +- [ ] **Step 7: 验证** + +```sh +npm run compile +node --test dist/test/process.test.js dist/test/discovery.test.js dist/test/workflow.test.js dist/test/artifacts.test.js +``` + +**Gate:** `rg -n 'llvm-vs-code-extensions.vscode-clangd|clangd.restart|runClangdCheck|resolveClangd|configureClangd' src test package.json` 无结果(历史文档除外)。 + +--- + +# Task 6:替换 E2E clangd stub 为 mcppls stub + +**Files:** +- Delete: `test/e2e/fixtures/clangd-stub/` +- Create: `test/e2e/fixtures/mcppls-stub/package.json` +- Create: `test/e2e/fixtures/mcppls-stub/extension.js` +- Modify: `test/e2e/runTest.ts` +- Modify: `test/e2e/suite/extension.test.ts` + +- [ ] **Step 1: 写 stub 契约** + +stub 扩展: + +- ID 为 `sunrisepeak.mcpp-language-server`; +- 激活时注册 `mcppls.restartServer`、`mcppls.showModuleGraph`、`mcppls.showLogs`; +- 每次调用向环境变量指定的日志文件追加 JSON; +- 不启动 LSP,不读取 workspace,不改配置。 + +- [ ] **Step 2: 改 Extension Host runner** + +隔离目录中复制 mcppls stub,而不是 clangd stub。E2E 断言: + +1. mcpp-vscode 激活; +2. `mcpp.refreshCompilationDatabase` 执行 fake mcpp build; +3. build 结束后调用 mcppls restart; +4. `mcpp.showModuleGraph` 与 `mcpp.showLanguageServerLogs` 只转发一次; +5. build 取消时不调用 restart。 + +- [ ] **Step 3: 加依赖缺失测试** + +用一个不安装 mcppls stub 的显式环境测试桥接返回 `unavailable`,不抛出未处理异常。该测试不得依赖 Marketplace。 + +- [ ] **Step 4: 验证** + +```sh +npm run test:e2e +``` + +在 Linux CI 继续用: + +```sh +xvfb-run -a npm run test:e2e +``` + +**Gate:** E2E 证明任务与 mcppls 命令桥接正确,但不冒充真实 LSP 语义测试。 + +--- + +# Task 7:补真实 mcppls 契约与跨工具链验收 + +**Files:** +- Create: `test/e2e/suite/mcppls-contract.test.ts`(opt-in) +- Modify: `test/e2e/runTest.ts` +- Modify: `package.json` +- Modify: `.github/workflows/ci.yml` +- Create: `test/e2e/fixtures/mcpppls-project/`(如需要) + +- [ ] **Step 1: 建立 opt-in 环境** + +新增环境变量 `MCPP_E2E_REAL_MCPPLS=1`。默认 E2E 不下载、不运行真实 payload;专用 CI job 或人工验收设置该变量。 + +- [ ] **Step 2: 验证 mcpp 工程** + +在 mcpp 工程中验证: + +- Language Status Item 出现并最终 ready/degraded,而非 error; +- `import` 补全、模块定义跳转、hover、references; +- GCC 与 LLVM 至少各一个 fixture; +- build 前后 module graph/status 变化; +- mcpp 任务完成后 mcppls 重启且重新加载。 + +- [ ] **Step 3: 验证降级** + +- 无 `compile_commands.json`; +- mcpp 版本低于 `2026.9.15.1`; +- `mcpp.path` 自定义且 mcppls 仍从 PATH 找 mcpp; +- 非受信任 workspace; +- mcppls 依赖被禁用。 + +每种情况都只验证明确状态/提示,不把环境差异误报成 mcpp-vscode 构建失败。 + +- [ ] **Step 4: 记录平台结论** + +CI 至少覆盖 `ubuntu-24.04 x64` 与 `macos-14 arm64`;Windows 可在 release 前人工/自托管 runner 验证。`darwin-x64` 在上游发布前写“不支持”。 + +**Gate:** 真实 mcppls 测试通过,四个公开转发命令和 build 后重启均可用。 + +--- + +# Task 8:设置、旧命令与兼容迁移 + +**Files:** +- Modify: `package.json` +- Modify: `src/extension.ts` +- Modify: `test/artifacts.test.ts` +- Modify: `README.md` +- Modify: `CHANGELOG.md` + +- [ ] **Step 1: 明确 `mcpp.path` 的作用域** + +保留 `mcpp.path`,但文档写清:它只控制 mcpp-vscode 自己的 CLI;mcppls 的 `--mcpp` 当前只能从其自己的启动参数获得,本插件不能跨扩展写 `mcppls.*` 配置。 + +向上游提一个独立 issue/change request:增加 `mcppls.mcpp` 或由 extension 启动时把当前 mcpp-vscode 设置传给 server。上游支持前,不做同步写配置的隐式耦合。 + +- [ ] **Step 2: 移除旧 mcpp 设置的功能描述** + +- `mcpp.clangd.path` → deprecationMessage; +- `mcpp.modulesSupport` → deprecationMessage; +- `mcpp.configureCppTools` → deprecationMessage; +- 不自动删除用户值; +- mcppls 自己管理官方 clangd/cpptools 冲突。 + +- [ ] **Step 3: 决定旧命令 ID 的最终形态** + +本版本注册旧 `mcpp.configureClangd` 为 `mcpp.configureLanguageServer` 的弃用 alias;alias 进入 artifact test,且不得再读取 clangd 配置。 + +- [ ] **Step 4: 写升级说明** + +README 说明: + +- 安装本插件会自动安装 `sunrisepeak.mcpp-language-server`; +- 不再需要官方 `llvm-vs-code-extensions.vscode-clangd`; +- `clangd.*`、`mcpp.clangd.*` 不再生效; +- mcppls 自带服务端与语义 payload; +- mcpp 仍负责 build/run/test/toolchain; +- 旧设置可保留但应手动删除; +- `mcpp.path` 的作用域限制。 + +**Gate:** 新安装、已有 clangd 用户、GCC/MSVC 用户都能从 README 找到下一步。 + +--- + +# Task 9:文档、发布与清理门禁 + +**Files:** +- Modify: `README.md` +- Modify: `CHANGELOG.md` +- Modify: `package.json` +- Modify: `package-lock.json` +- Modify: `.github/workflows/ci.yml` +- Modify: `.github/workflows/release.yml` +- Modify: `test/artifacts.test.ts` + +- [ ] **Step 1: 重写 README 能力矩阵** + +删除“仅 LLVM 支持模块语义”的旧结论。区分两层: + +| 能力 | 负责方 | +| --- | --- | +| mcpp 工程发现、build/run/test、工具链 | mcpp-vscode | +| C++ 模块诊断、补全、跳转、引用、状态、模块图 | mcppls | +| mcpp.toml 补全 | mcpp-vscode | +| 官方 clangd 扩展配置 | 不再由 mcpp-vscode 管理 | + +GCC/MSVC 的支持程度引用 mcppls 自己的当前文档,不在 mcpp-vscode 复制一套可能过时的能力矩阵。 + +- [ ] **Step 2: 更新故障排查** + +删除 clangd revision、PCM mismatch、llvm-tools 安装、`mcpp.clangd.path` 的主流程。新增: + +- mcppls 没安装/被禁用; +- mcppls 平台不受支持; +- mcpp 版本太旧; +- build 后语言服务没有刷新; +- `mcpp.path` 与 mcppls 所见 PATH 不同。 + +- [ ] **Step 3: 发布前完整验证** + +```sh +npm ci +npm run compile +npm test +npm run package +unzip -t "$VSIX" +npm run test:e2e +``` + +真实语义 gate: + +```sh +MCPP_E2E_REAL_MCPPLS=1 npm run test:e2e +``` + +- [ ] **Step 4: 发布依赖完整性** + +在干净 extensions 目录安装打包 VSIX,列出扩展,确认 mcppls 被拉取。记录 mcppls 最低/推荐版本;暂不写死具体 patch 版本,发布时锁定已在 CI 验证的版本。 + +- [ ] **Step 5: 最终残留审计** + +```sh +rg -n -i 'clangd|compile_commands|PCM|llvm-tools' \ + src test package.json README.md CHANGELOG.md +``` + +允许命中:迁移说明、CHANGELOG 历史、mcppls 自带语义引擎的事实说明。禁止命中:活动依赖、命令执行、设置写入、状态判断。 + +- [ ] **Step 6: 发布检查** + +- package version / lock / CHANGELOG / tag 一致; +- VSIX 中无 payload、无第二个 LSP client; +- 官方 clangd 扩展不再是依赖; +- Marketplace/Open VSX 安装路径已验证; +- 四个上游发布平台与本项目 CI 覆盖差异在 release note 中明示; +- `darwin-x64` 有明确支持决定。 + +--- + +## 推荐提交顺序 + +1. `manifest + tests`:只切依赖与引擎版本; +2. `language-server bridge + unit tests`; +3. `task coordination + E2E stub`; +4. `module setup rewrite`; +5. `delete clangd layer`; +6. `real mcppls contract tests`; +7. `docs/settings/release gates`。 + +每个提交保持可编译;不把“删 1000 行 clangd 代码”和“改一键向导”混在同一个提交。 + +## 完成定义 + +只有同时满足以下条件,迁移才算完成: + +- 本插件只依赖 mcppls,不依赖官方 clangd 扩展; +- 本插件不启动第二个 LSP、不打包 payload; +- build/run/test/toolchain/TOML 补全原有行为保持; +- build 后能可靠刷新 mcppls; +- mcppls 缺失时提示明确且不崩溃; +- 旧 clangd 设置不再被读取或写入; +- Node、package、Extension Host、真实 mcppls 跨工具链测试通过; +- 干净 VSIX 安装会自动获得 mcppls; +- README、CHANGELOG、版本与发布资产一致。 + +## 已知阻塞与决策点 + +1. **`darwin-x64` 无上游包。** 在上游发布前,mcpp-vscode 不能宣称支持 Intel macOS。 +2. **上游成熟度低。** `0.0.4` 且协议 Draft;发布门槛必须是真实 mcppls conformance/E2E,而不是只跑 manifest 测试。 +3. **`mcpp.path` 作用域。** 本插件不能可靠传给 mcppls;先公开限制,再推动上游正式设置。 +4. **mcppls 仍内部使用 clangd。** “放弃 clangd 依赖”指放弃官方 clangd 扩展和用户 clangd 配置,不指移除 mcppls payload 内固定语义引擎。 +5. **构建后刷新只能先 restart。** 公开 `mcppls.reloadBuildDescription` VS Code 命令尚未贡献;不要从本插件伪造 LSP 请求。 + +## 后续:Task 7 真实语义验收 + +Task 7 不是代码阻塞项。LLVM 22.1.8 已在当前 darwin-arm64 环境使用真实 +`sunrisepeak.mcpp-language-server@0.0.4` VSIX 和真实 mcpp 工程完成过一次空诊断、定义、悬停、 +引用和补全验证;重复运行时 references 查询曾超时,因此需在 CI 中重复验证。GCC 16 fixture +需要 Linux runner:当前 `xim:gcc@16.1.0` 没有 macOS build,无法在本机诚实宣称跨工具链完成。 +在 GCC/Linux 验收前,不把“LLVM 与桥接通过”等同于“C++ 模块语义已在所有工具链和平台验证通过”。 From 20f1076e2c485f0bbd807773d80107caa1df4b1d Mon Sep 17 00:00:00 2001 From: wellwei Date: Sat, 26 Sep 2026 00:51:12 +0800 Subject: [PATCH 061/119] =?UTF-8?q?release:=20mcpp-vscode=200.4.0=EF=BC=88?= =?UTF-8?q?mcppls=20=E8=AF=AD=E8=A8=80=E6=9C=8D=E5=8A=A1=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c439cbda..797efc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ 显式刷新会分别报告 CDB 生成与 clangd 协调结果。 - 对齐 mcpp #387 的最终 workspace 契约:virtual workspace 根不作为单一 clangd 工程, 扩展消费当前活动 member 根的 CDB;rooted workspace 仍按根 package 处理。 -## 0.4.0(未发布) +## 0.4.0 - 2026-09-26 ### Changed From 26283eac9444fb69f4524514fd43464f208942b4 Mon Sep 17 00:00:00 2001 From: SPeak Date: Sun, 4 Oct 2026 02:15:53 +0800 Subject: [PATCH 062/119] =?UTF-8?q?mcpp-vscode=200.6.0:=20=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E9=87=8D=E7=BB=84=E3=80=81=E5=BA=93=E7=94=9F?= =?UTF-8?q?=E6=80=81=E3=80=81=E7=BC=93=E5=AD=98=E8=A7=86=E5=9B=BE=E4=B8=8E?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=B7=A5=E7=A8=8B=E5=8C=96=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.5.0 基座(注册表单一事实源、纯模块门禁、设置面板、build.mcpp/mcpp.toml 智能、双语)与 0.6.0 侧边栏重组(工程/库生态/缓存三视图、C++ Modules 折叠入工程视图、快捷菜单彩色图标)+ 23 轮作者实测反馈修复 + 外部深度评审修复(工作区信任收口 runMcpp、Windows 参数引号、WebviewDocument 渲染骨架)+ 双市场发布流水线(Open VSX 必发、Marketplace 机会发布)+ README/目录工程化与死代码清理。669 单测,CI 三平台 11/11 绿。 --- .agents/docs/README.md | 32 + .agents/docs/architecture.md | 77 + .../archive/2026-10-02-implementation-plan.md | 342 ++++ .../2026-10-02-plugin-optimisation-plan.md | 1115 ++++++++++++ .../2026-10-02-ui-ux-optimisation-plan.md | 1466 ++++++++++++++++ .../2026-10-03-repo-engineering-plan.md | 279 +++ .agents/docs/mcpp-integration.md | 80 + .agents/docs/mcppls-integration.md | 179 ++ .../plans/2026-07-31-mcpp-cli-integration.md | 0 .../2026-08-01-tag-release-implementation.md | 0 ...-08-05-clangd-hermetic-cdb-build-script.md | 0 .../2026-08-08-issues-6-7-implementation.md | 0 ...26-09-25-mcpp-language-server-migration.md | 0 .../2026-07-31-mcpp-cli-integration-design.md | 0 .../specs/2026-08-01-tag-release-design.md | 0 ...026-08-08-one-click-module-setup-design.md | 0 .../specs/2026-08-08-pr-ci-design.md | 0 .github/workflows/ci.yml | 207 ++- .github/workflows/release.yml | 51 + .gitignore | 9 + .vscodeignore | 6 + CHANGELOG.md | 207 ++- README.md | 255 +-- README.zh-CN.md | 79 + data/buildscript-api.json | 432 +++++ data/config-registry.json | 1072 ++++++++++++ data/i18n/zh-cn.json | 471 +++++ data/toml-schema.json | 822 +++++++++ docs/architecture.md | 130 ++ docs/build-script.md | 137 ++ docs/cache.md | 138 ++ docs/commands.md | 121 ++ docs/compatibility.md | 136 ++ docs/mcpp-toml.md | 134 ++ docs/settings.md | 1023 +++++++++++ docs/troubleshooting.md | 134 ++ images/logo.png | Bin 6719 -> 0 bytes l10n/bundle.l10n.json | 1 + l10n/bundle.l10n.zh-cn.json | 464 +++++ media/activity-bar.png | Bin 0 -> 2426 bytes media/cache.css | 570 +++++++ media/library.css | 632 +++++++ media/logo.png | Bin 0 -> 31749 bytes media/quick-menu/archive--purple--dark.svg | 1 + media/quick-menu/archive--purple--light.svg | 1 + media/quick-menu/beaker--purple--dark.svg | 1 + media/quick-menu/beaker--purple--light.svg | 1 + media/quick-menu/chip--yellow--dark.svg | 1 + media/quick-menu/chip--yellow--light.svg | 1 + .../quick-menu/cloud-download--blue--dark.svg | 1 + .../cloud-download--blue--light.svg | 1 + .../cloud-download--yellow--dark.svg | 1 + .../cloud-download--yellow--light.svg | 1 + .../debug-restart--neutral--dark.svg | 1 + .../debug-restart--neutral--light.svg | 1 + media/quick-menu/file-zip--neutral--dark.svg | 1 + media/quick-menu/file-zip--neutral--light.svg | 1 + media/quick-menu/folder--neutral--dark.svg | 1 + media/quick-menu/folder--neutral--light.svg | 1 + .../folder-opened--neutral--dark.svg | 1 + .../folder-opened--neutral--light.svg | 1 + media/quick-menu/heart--green--dark.svg | 1 + media/quick-menu/heart--green--light.svg | 1 + media/quick-menu/library--blue--dark.svg | 1 + media/quick-menu/library--blue--light.svg | 1 + media/quick-menu/output--neutral--dark.svg | 1 + media/quick-menu/output--neutral--light.svg | 1 + media/quick-menu/play--green--dark.svg | 1 + media/quick-menu/play--green--light.svg | 1 + media/quick-menu/refresh--blue--dark.svg | 1 + media/quick-menu/refresh--blue--light.svg | 1 + media/quick-menu/refresh--green--dark.svg | 1 + media/quick-menu/refresh--green--light.svg | 1 + media/quick-menu/report--neutral--dark.svg | 1 + media/quick-menu/report--neutral--light.svg | 1 + .../settings-gear--neutral--dark.svg | 1 + .../settings-gear--neutral--light.svg | 1 + media/quick-menu/star-full--yellow--dark.svg | 1 + media/quick-menu/star-full--yellow--light.svg | 1 + .../symbol-interface--neutral--dark.svg | 1 + .../symbol-interface--neutral--light.svg | 1 + media/quick-menu/tools--blue--dark.svg | 1 + media/quick-menu/tools--blue--light.svg | 1 + media/quick-menu/trash--neutral--dark.svg | 1 + media/quick-menu/trash--neutral--light.svg | 1 + media/quick-menu/trash--red--dark.svg | 1 + media/quick-menu/trash--red--light.svg | 1 + .../type-hierarchy--neutral--dark.svg | 1 + .../type-hierarchy--neutral--light.svg | 1 + media/quick-menu/verified--green--dark.svg | 1 + media/quick-menu/verified--green--light.svg | 1 + media/settings.css | 318 ++++ package-lock.json | 1303 +++++++++++++- package.json | 1046 +++++++++++- package.nls.json | 207 +++ package.nls.zh-cn.json | 207 +++ src/buildscript/analysis.ts | 674 ++++++++ src/buildscript/api.ts | 111 ++ src/buildscript/modules.ts | 294 ++++ src/buildscript/providers.ts | 221 +++ src/buildscript/settings.ts | 70 + src/buildscript/snippets.ts | 116 ++ src/cache/cachePanel.ts | 393 +++++ src/cache/cachePanelHtml.ts | 949 ++++++++++ src/cache/cacheState.ts | 188 ++ src/cache/cacheView.ts | 762 +++++++++ src/cli/artifacts.ts | 197 +++ src/cli/cache.ts | 293 ++++ src/cli/clean.ts | 242 +++ src/cli/controller.ts | 1160 +++++++++++++ src/cli/errors.ts | 156 ++ src/cli/labels.ts | 43 + src/{ => cli}/newProject.ts | 26 +- src/cli/process.ts | 120 ++ src/cli/protocol.ts | 259 +++ src/cli/search.ts | 175 ++ src/cli/selfCheck.ts | 98 ++ src/cli/statusBar.ts | 45 + src/{ => cli}/tasks.ts | 36 +- src/{cli.ts => cli/toolchain.ts} | 0 src/cliController.ts | 759 -------- src/commands.ts | 45 - src/commands/ids.ts | 114 ++ src/commands/menu.ts | 103 ++ src/config/access.ts | 133 ++ src/config/migrate.ts | 66 + src/config/panel.ts | 403 +++++ src/config/panelHtml.ts | 612 +++++++ src/config/presets.ts | 110 ++ src/config/registry.ts | 184 ++ src/config/validate.ts | 93 + src/configureOnly.ts | 18 - src/extension.ts | 463 ++++- src/i18n/t.ts | 81 + src/i18n/translate.ts | 55 + src/ideWorkflow.ts | 45 - src/languageServer.ts | 80 - src/library/addDependency.ts | 142 ++ src/library/detailHtml.ts | 713 ++++++++ src/library/detailPanel.ts | 531 ++++++ src/library/indexLocator.ts | 559 ++++++ src/library/indexModel.ts | 1520 +++++++++++++++++ src/library/libraryHtml.ts | 445 +++++ src/library/libraryView.ts | 462 +++++ src/library/xpkg.ts | 434 +++++ src/mcppTomlCompletion.ts | 223 --- src/mcppls/bridge.ts | 106 ++ src/mcppls/capabilities.ts | 212 +++ src/mcppls/contract.ts | 260 +++ src/mcppls/messages.ts | 61 + src/mcppls/state.ts | 229 +++ src/mcppls/stateSource.ts | 71 + src/mcppls/timers.ts | 80 + src/process.ts | 53 - src/{inProject.ts => projects/context.ts} | 23 + src/{ => projects}/discovery.ts | 24 +- src/projects/summary.ts | 111 ++ src/toml/completion.ts | 523 ++++++ src/toml/diagnostics.ts | 629 +++++++ src/toml/hover.ts | 206 +++ src/toml/navigation.ts | 306 ++++ src/{mcppTomlParser.ts => toml/parser.ts} | 12 +- src/toml/providers.ts | 284 +++ src/toml/schema.ts | 125 ++ src/util/format.ts | 153 ++ src/util/log.ts | 82 + src/util/text.ts | 79 + src/views/languageServerView.ts | 345 ++++ src/views/models.ts | 878 ++++++++++ src/views/projectView.ts | 148 ++ src/views/treeProvider.ts | 111 ++ src/views/viewPolicy.ts | 46 + src/webview/document.ts | 64 + src/webview/render.ts | 18 + src/{ => workflows}/moduleSetup.ts | 6 +- test/architecture.test.ts | 141 ++ test/artifacts.test.ts | 439 ++++- test/buildscript/analysis.test.ts | 199 +++ test/buildscript/api.test.ts | 113 ++ test/buildscript/modules.test.ts | 118 ++ test/buildscript/settings.test.ts | 136 ++ test/buildscript/snippets.test.ts | 74 + test/cache/cacheCss.test.ts | 115 ++ test/cache/cacheLegacy.test.ts | 73 + test/cache/cachePanelHtml.test.ts | 456 +++++ test/cache/cacheState.test.ts | 110 ++ test/cli/artifacts.test.ts | 99 ++ test/cli/cache.test.ts | 264 +++ test/cli/clean.test.ts | 94 + test/cli/controller.settings.test.ts | 193 +++ test/cli/errors.test.ts | 105 ++ test/cli/labels.test.ts | 57 + test/{ => cli}/newProject.test.ts | 13 +- test/cli/process.test.ts | 49 + test/cli/protocol.test.ts | 192 +++ test/cli/search.test.ts | 141 ++ test/cli/selfCheck.test.ts | 89 + test/cli/statusBar.test.ts | 37 + test/{ => cli}/tasks.test.ts | 13 +- test/{cli.test.ts => cli/toolchain.test.ts} | 2 +- test/commands.test.ts | 57 - test/commands/ids.test.ts | 118 ++ test/commands/menu.test.ts | 157 ++ test/config/panelHtml.test.ts | 243 +++ test/config/presets.test.ts | 51 + test/config/registry.test.ts | 86 + test/config/validate.test.ts | 82 + test/config/wiring.test.ts | 188 ++ test/configureOnly.test.ts | 58 - test/e2e/fixtures/mcppls-stub/extension.js | 27 - test/e2e/fixtures/mcppls-stub/package.json | 14 - test/e2e/mcpplsStub.ts | 110 ++ test/e2e/runTest.ts | 65 +- test/e2e/suite/extension.e2e.ts | 127 ++ test/e2e/suite/extension.test.ts | 68 - test/e2e/suite/index.ts | 2 +- test/i18n/hardcoded.test.ts | 98 ++ test/i18n/translate.test.ts | 40 + test/ideWorkflow.test.ts | 118 -- test/languageServer.test.ts | 107 -- test/library/detailHtml.test.ts | 407 +++++ test/library/indexModel.test.ts | 666 ++++++++ test/library/libraryHtml.test.ts | 263 +++ test/library/xpkg.test.ts | 169 ++ test/mcppTomlCompletion.test.ts | 113 -- test/mcppls/bridge.test.ts | 154 ++ test/mcppls/capabilities.test.ts | 180 ++ test/mcppls/contract.test.ts | 57 + test/mcppls/state.test.ts | 140 ++ test/mcppls/stateSource.test.ts | 54 + test/mcppls/timers.test.ts | 163 ++ test/process.test.ts | 13 - .../context.test.ts} | 2 +- test/{ => projects}/discovery.test.ts | 32 +- test/projects/summary.test.ts | 78 + test/toml/completion.test.ts | 300 ++++ .../contract.test.ts} | 4 +- test/toml/diagnostics.test.ts | 235 +++ test/toml/hover.test.ts | 83 + test/toml/navigation.test.ts | 103 ++ .../parser.test.ts} | 2 +- test/toml/schema.test.ts | 98 ++ test/util/format.test.ts | 109 ++ test/util/log.test.ts | 63 + test/util/text.test.ts | 56 + test/views/models.test.ts | 99 ++ test/views/projectLayout.test.ts | 416 +++++ test/webview/render.test.ts | 14 + test/{ => workflows}/moduleSetup.test.ts | 5 +- tools/check-config.mjs | 162 ++ tools/check-generators.mjs | 94 + tools/ci-vscode-cli.mjs | 51 + tools/dev-profile.mjs | 161 ++ tools/generate-activitybar-icon.mjs | 284 +++ tools/generate-buildscript-api.mjs | 374 ++++ tools/generate-l10n.mjs | 59 + tools/generate-quick-menu-icons.mjs | 171 ++ tools/generate-settings-docs.mjs | 100 ++ tools/generate-toml-schema.mjs | 553 ++++++ tools/l10n-check.mjs | 116 ++ tsconfig.json | 1 + 261 files changed, 43202 insertions(+), 2336 deletions(-) create mode 100644 .agents/docs/README.md create mode 100644 .agents/docs/architecture.md create mode 100644 .agents/docs/archive/2026-10-02-implementation-plan.md create mode 100644 .agents/docs/archive/2026-10-02-plugin-optimisation-plan.md create mode 100644 .agents/docs/archive/2026-10-02-ui-ux-optimisation-plan.md create mode 100644 .agents/docs/archive/2026-10-03-repo-engineering-plan.md create mode 100644 .agents/docs/mcpp-integration.md create mode 100644 .agents/docs/mcppls-integration.md rename {docs => .agents}/superpowers/plans/2026-07-31-mcpp-cli-integration.md (100%) rename {docs => .agents}/superpowers/plans/2026-08-01-tag-release-implementation.md (100%) rename {docs => .agents}/superpowers/plans/2026-08-05-clangd-hermetic-cdb-build-script.md (100%) rename {docs => .agents}/superpowers/plans/2026-08-08-issues-6-7-implementation.md (100%) rename {docs => .agents}/superpowers/plans/2026-09-25-mcpp-language-server-migration.md (100%) rename {docs => .agents}/superpowers/specs/2026-07-31-mcpp-cli-integration-design.md (100%) rename {docs => .agents}/superpowers/specs/2026-08-01-tag-release-design.md (100%) rename {docs => .agents}/superpowers/specs/2026-08-08-one-click-module-setup-design.md (100%) rename {docs => .agents}/superpowers/specs/2026-08-08-pr-ci-design.md (100%) create mode 100644 README.zh-CN.md create mode 100644 data/buildscript-api.json create mode 100644 data/config-registry.json create mode 100644 data/i18n/zh-cn.json create mode 100644 data/toml-schema.json create mode 100644 docs/architecture.md create mode 100644 docs/build-script.md create mode 100644 docs/cache.md create mode 100644 docs/commands.md create mode 100644 docs/compatibility.md create mode 100644 docs/mcpp-toml.md create mode 100644 docs/settings.md create mode 100644 docs/troubleshooting.md delete mode 100644 images/logo.png create mode 100644 l10n/bundle.l10n.json create mode 100644 l10n/bundle.l10n.zh-cn.json create mode 100644 media/activity-bar.png create mode 100644 media/cache.css create mode 100644 media/library.css create mode 100644 media/logo.png create mode 100644 media/quick-menu/archive--purple--dark.svg create mode 100644 media/quick-menu/archive--purple--light.svg create mode 100644 media/quick-menu/beaker--purple--dark.svg create mode 100644 media/quick-menu/beaker--purple--light.svg create mode 100644 media/quick-menu/chip--yellow--dark.svg create mode 100644 media/quick-menu/chip--yellow--light.svg create mode 100644 media/quick-menu/cloud-download--blue--dark.svg create mode 100644 media/quick-menu/cloud-download--blue--light.svg create mode 100644 media/quick-menu/cloud-download--yellow--dark.svg create mode 100644 media/quick-menu/cloud-download--yellow--light.svg create mode 100644 media/quick-menu/debug-restart--neutral--dark.svg create mode 100644 media/quick-menu/debug-restart--neutral--light.svg create mode 100644 media/quick-menu/file-zip--neutral--dark.svg create mode 100644 media/quick-menu/file-zip--neutral--light.svg create mode 100644 media/quick-menu/folder--neutral--dark.svg create mode 100644 media/quick-menu/folder--neutral--light.svg create mode 100644 media/quick-menu/folder-opened--neutral--dark.svg create mode 100644 media/quick-menu/folder-opened--neutral--light.svg create mode 100644 media/quick-menu/heart--green--dark.svg create mode 100644 media/quick-menu/heart--green--light.svg create mode 100644 media/quick-menu/library--blue--dark.svg create mode 100644 media/quick-menu/library--blue--light.svg create mode 100644 media/quick-menu/output--neutral--dark.svg create mode 100644 media/quick-menu/output--neutral--light.svg create mode 100644 media/quick-menu/play--green--dark.svg create mode 100644 media/quick-menu/play--green--light.svg create mode 100644 media/quick-menu/refresh--blue--dark.svg create mode 100644 media/quick-menu/refresh--blue--light.svg create mode 100644 media/quick-menu/refresh--green--dark.svg create mode 100644 media/quick-menu/refresh--green--light.svg create mode 100644 media/quick-menu/report--neutral--dark.svg create mode 100644 media/quick-menu/report--neutral--light.svg create mode 100644 media/quick-menu/settings-gear--neutral--dark.svg create mode 100644 media/quick-menu/settings-gear--neutral--light.svg create mode 100644 media/quick-menu/star-full--yellow--dark.svg create mode 100644 media/quick-menu/star-full--yellow--light.svg create mode 100644 media/quick-menu/symbol-interface--neutral--dark.svg create mode 100644 media/quick-menu/symbol-interface--neutral--light.svg create mode 100644 media/quick-menu/tools--blue--dark.svg create mode 100644 media/quick-menu/tools--blue--light.svg create mode 100644 media/quick-menu/trash--neutral--dark.svg create mode 100644 media/quick-menu/trash--neutral--light.svg create mode 100644 media/quick-menu/trash--red--dark.svg create mode 100644 media/quick-menu/trash--red--light.svg create mode 100644 media/quick-menu/type-hierarchy--neutral--dark.svg create mode 100644 media/quick-menu/type-hierarchy--neutral--light.svg create mode 100644 media/quick-menu/verified--green--dark.svg create mode 100644 media/quick-menu/verified--green--light.svg create mode 100644 media/settings.css create mode 100644 package.nls.json create mode 100644 package.nls.zh-cn.json create mode 100644 src/buildscript/analysis.ts create mode 100644 src/buildscript/api.ts create mode 100644 src/buildscript/modules.ts create mode 100644 src/buildscript/providers.ts create mode 100644 src/buildscript/settings.ts create mode 100644 src/buildscript/snippets.ts create mode 100644 src/cache/cachePanel.ts create mode 100644 src/cache/cachePanelHtml.ts create mode 100644 src/cache/cacheState.ts create mode 100644 src/cache/cacheView.ts create mode 100644 src/cli/artifacts.ts create mode 100644 src/cli/cache.ts create mode 100644 src/cli/clean.ts create mode 100644 src/cli/controller.ts create mode 100644 src/cli/errors.ts create mode 100644 src/cli/labels.ts rename src/{ => cli}/newProject.ts (70%) create mode 100644 src/cli/process.ts create mode 100644 src/cli/protocol.ts create mode 100644 src/cli/search.ts create mode 100644 src/cli/selfCheck.ts create mode 100644 src/cli/statusBar.ts rename src/{ => cli}/tasks.ts (68%) rename src/{cli.ts => cli/toolchain.ts} (100%) delete mode 100644 src/cliController.ts delete mode 100644 src/commands.ts create mode 100644 src/commands/ids.ts create mode 100644 src/commands/menu.ts create mode 100644 src/config/access.ts create mode 100644 src/config/migrate.ts create mode 100644 src/config/panel.ts create mode 100644 src/config/panelHtml.ts create mode 100644 src/config/presets.ts create mode 100644 src/config/registry.ts create mode 100644 src/config/validate.ts delete mode 100644 src/configureOnly.ts create mode 100644 src/i18n/t.ts create mode 100644 src/i18n/translate.ts delete mode 100644 src/ideWorkflow.ts delete mode 100644 src/languageServer.ts create mode 100644 src/library/addDependency.ts create mode 100644 src/library/detailHtml.ts create mode 100644 src/library/detailPanel.ts create mode 100644 src/library/indexLocator.ts create mode 100644 src/library/indexModel.ts create mode 100644 src/library/libraryHtml.ts create mode 100644 src/library/libraryView.ts create mode 100644 src/library/xpkg.ts delete mode 100644 src/mcppTomlCompletion.ts create mode 100644 src/mcppls/bridge.ts create mode 100644 src/mcppls/capabilities.ts create mode 100644 src/mcppls/contract.ts create mode 100644 src/mcppls/messages.ts create mode 100644 src/mcppls/state.ts create mode 100644 src/mcppls/stateSource.ts create mode 100644 src/mcppls/timers.ts delete mode 100644 src/process.ts rename src/{inProject.ts => projects/context.ts} (50%) rename src/{ => projects}/discovery.ts (53%) create mode 100644 src/projects/summary.ts create mode 100644 src/toml/completion.ts create mode 100644 src/toml/diagnostics.ts create mode 100644 src/toml/hover.ts create mode 100644 src/toml/navigation.ts rename src/{mcppTomlParser.ts => toml/parser.ts} (99%) create mode 100644 src/toml/providers.ts create mode 100644 src/toml/schema.ts create mode 100644 src/util/format.ts create mode 100644 src/util/log.ts create mode 100644 src/util/text.ts create mode 100644 src/views/languageServerView.ts create mode 100644 src/views/models.ts create mode 100644 src/views/projectView.ts create mode 100644 src/views/treeProvider.ts create mode 100644 src/views/viewPolicy.ts create mode 100644 src/webview/document.ts create mode 100644 src/webview/render.ts rename src/{ => workflows}/moduleSetup.ts (88%) create mode 100644 test/architecture.test.ts create mode 100644 test/buildscript/analysis.test.ts create mode 100644 test/buildscript/api.test.ts create mode 100644 test/buildscript/modules.test.ts create mode 100644 test/buildscript/settings.test.ts create mode 100644 test/buildscript/snippets.test.ts create mode 100644 test/cache/cacheCss.test.ts create mode 100644 test/cache/cacheLegacy.test.ts create mode 100644 test/cache/cachePanelHtml.test.ts create mode 100644 test/cache/cacheState.test.ts create mode 100644 test/cli/artifacts.test.ts create mode 100644 test/cli/cache.test.ts create mode 100644 test/cli/clean.test.ts create mode 100644 test/cli/controller.settings.test.ts create mode 100644 test/cli/errors.test.ts create mode 100644 test/cli/labels.test.ts rename test/{ => cli}/newProject.test.ts (88%) create mode 100644 test/cli/process.test.ts create mode 100644 test/cli/protocol.test.ts create mode 100644 test/cli/search.test.ts create mode 100644 test/cli/selfCheck.test.ts create mode 100644 test/cli/statusBar.test.ts rename test/{ => cli}/tasks.test.ts (90%) rename test/{cli.test.ts => cli/toolchain.test.ts} (99%) delete mode 100644 test/commands.test.ts create mode 100644 test/commands/ids.test.ts create mode 100644 test/commands/menu.test.ts create mode 100644 test/config/panelHtml.test.ts create mode 100644 test/config/presets.test.ts create mode 100644 test/config/registry.test.ts create mode 100644 test/config/validate.test.ts create mode 100644 test/config/wiring.test.ts delete mode 100644 test/configureOnly.test.ts delete mode 100644 test/e2e/fixtures/mcppls-stub/extension.js delete mode 100644 test/e2e/fixtures/mcppls-stub/package.json create mode 100644 test/e2e/mcpplsStub.ts create mode 100644 test/e2e/suite/extension.e2e.ts delete mode 100644 test/e2e/suite/extension.test.ts create mode 100644 test/i18n/hardcoded.test.ts create mode 100644 test/i18n/translate.test.ts delete mode 100644 test/ideWorkflow.test.ts delete mode 100644 test/languageServer.test.ts create mode 100644 test/library/detailHtml.test.ts create mode 100644 test/library/indexModel.test.ts create mode 100644 test/library/libraryHtml.test.ts create mode 100644 test/library/xpkg.test.ts delete mode 100644 test/mcppTomlCompletion.test.ts create mode 100644 test/mcppls/bridge.test.ts create mode 100644 test/mcppls/capabilities.test.ts create mode 100644 test/mcppls/contract.test.ts create mode 100644 test/mcppls/state.test.ts create mode 100644 test/mcppls/stateSource.test.ts create mode 100644 test/mcppls/timers.test.ts delete mode 100644 test/process.test.ts rename test/{inProject.test.ts => projects/context.test.ts} (98%) rename test/{ => projects}/discovery.test.ts (59%) create mode 100644 test/projects/summary.test.ts create mode 100644 test/toml/completion.test.ts rename test/{mcppTomlContract.test.ts => toml/contract.test.ts} (98%) create mode 100644 test/toml/diagnostics.test.ts create mode 100644 test/toml/hover.test.ts create mode 100644 test/toml/navigation.test.ts rename test/{mcppTomlParser.test.ts => toml/parser.test.ts} (99%) create mode 100644 test/toml/schema.test.ts create mode 100644 test/util/format.test.ts create mode 100644 test/util/log.test.ts create mode 100644 test/util/text.test.ts create mode 100644 test/views/models.test.ts create mode 100644 test/views/projectLayout.test.ts create mode 100644 test/webview/render.test.ts rename test/{ => workflows}/moduleSetup.test.ts (95%) create mode 100644 tools/check-config.mjs create mode 100644 tools/check-generators.mjs create mode 100644 tools/ci-vscode-cli.mjs create mode 100644 tools/dev-profile.mjs create mode 100644 tools/generate-activitybar-icon.mjs create mode 100644 tools/generate-buildscript-api.mjs create mode 100644 tools/generate-l10n.mjs create mode 100644 tools/generate-quick-menu-icons.mjs create mode 100644 tools/generate-settings-docs.mjs create mode 100644 tools/generate-toml-schema.mjs create mode 100644 tools/l10n-check.mjs diff --git a/.agents/docs/README.md b/.agents/docs/README.md new file mode 100644 index 0000000..c2d029b --- /dev/null +++ b/.agents/docs/README.md @@ -0,0 +1,32 @@ +# .agents/docs + +本目录存放 **面向贡献者与 agent 的设计/背景文档**:对 mcpp-vscode 当前架构、跨仓库契约 +与决策的记录。使用说明请看仓库根目录的 `README.md`。 + +| 文件 | 内容 | 状态 | +| --- | --- | --- | +| [`architecture.md`](architecture.md) | mcpp-vscode 的分层结构、模块职责与扩展点 | 现状 | +| [`mcpp-integration.md`](mcpp-integration.md) | 与 `mcpp-community/mcpp`(mcpp CLI)的接口契约 | 现状 | +| [`mcppls-integration.md`](mcppls-integration.md) | 与 `sunrisepeak.mcpp-language-server`(mcppls)的依赖与命令桥接契约 | 现状 | + + +边界: + +- `docs/`(仓库根)= **用户文档**(安装、命令、设置、排错); +- `.agents/docs/` = **贡献者/agent 文档**(设计、契约、方案、决策); +- `.agents/superpowers/` = **过程文档**(原 `docs/superpowers/`,plans/specs); +- `.agents/reviews/` = **分析与评审记录**,已加入 `.gitignore`,不进入版本库。 + +背景分析见 `.agents/reviews/2026-10-02-mcpp-vscode-architecture-and-mcppls-dependency-review.md`。 + +## archive/ + +`archive/` 存放**已按其执行完毕**的方案文档(round 制的评审与决定记录),按日期命名。 +当前实现与其偏差以代码与 CHANGELOG 为准: + +| 文档 | 内容 | +| --- | --- | +| [`archive/2026-10-02-implementation-plan.md`](archive/2026-10-02-implementation-plan.md) | 0.5.0 基座实施(纯模块、注册表、门禁) | +| [`archive/2026-10-02-plugin-optimisation-plan.md`](archive/2026-10-02-plugin-optimisation-plan.md) | 插件优化方案 v4(侧边栏、编辑体验、缓存、i18n、配置面板) | +| [`archive/2026-10-02-ui-ux-optimisation-plan.md`](archive/2026-10-02-ui-ux-optimisation-plan.md) | 0.6.0 UI/UX 方案与 23 轮作者反馈记录 | +| [`archive/2026-10-03-repo-engineering-plan.md`](archive/2026-10-03-repo-engineering-plan.md) | 仓库工程化(双市场发布、WebviewDocument、目录收敛) | diff --git a/.agents/docs/architecture.md b/.agents/docs/architecture.md new file mode 100644 index 0000000..c213cf5 --- /dev/null +++ b/.agents/docs/architecture.md @@ -0,0 +1,77 @@ +# mcpp-vscode 架构 + +> 状态:0.4.0(mcppls 语言服务迁移之后)。本文记录**当前**结构;变动请同步更新。 + +## 1. 一句话定位 + +mcpp-vscode 是 **mcpp CLI 的 IDE 前端**:工程发现、任务(build/run/test/clean)、工具链管理、 +`mcpp.toml` 的语法与结构补全。C++ 模块语义(诊断/补全/跳转/引用/模块图/状态)由扩展依赖 +`sunrisepeak.mcpp-language-server`(mcppls)提供,本扩展**不创建 LSP 客户端**。 + +## 2. 分层 + +``` +extension.ts VS Code 装配层:激活、命令注册、provider 注册、资源释放 +├── cliController.ts CLI 编排层:快捷菜单、任务、工具链、新工程、操作互斥 +│ ├── newProject.ts 新建工程流程(纯函数 + 注入的动作) +│ └── commands.ts 命令 ID 与快捷菜单清单(纯数据) +├── languageServer.ts mcppls 桥接层:只调用 4 个公开 VS Code 命令 +├── moduleSetup.ts 一键流程状态机(build → 刷新语言服务) +├── inProject.ts `mcpp.inProject` 上下文键 +├── mcppTomlCompletion.ts mcpp.toml 补全查询层(段头 + 写法模板) +├── mcppTomlParser.ts 容错 TOML 解析器(纯函数) +├── discovery.ts 最近的 mcpp.toml 发现(纯函数) +├── cli.ts mcpp CLI 输出解析(工具链清单) +├── process.ts runProcess 封装 +└── tasks.ts 任务计划、退出码分类、操作注册表(纯函数) +``` + +**约束**:`tasks.ts` / `moduleSetup.ts` / `discovery.ts` / `cli.ts` / `mcppTomlParser.ts` / +`newProject.ts` 不 import `vscode`,因此可用 `node --test` 直接测。`extension.ts` 与 +`cliController.ts` 是唯一接触 VS Code API 的地方。 + +## 3. 运行时边界 + +| 触发 | 行为 | +| --- | --- | +| 打开含 `mcpp.toml` 的工作区 | 激活;不执行 `mcpp`,不下载工具链 | +| 执行 mcpp 命令 | 通过 `vscode.Task` + `ProcessExecution` 在专用终端运行 | +| build 结束(含失败) | 调用一次 `mcppls.restartServer`(单飞,取消除外) | +| run/test/clean 结束 | 不触碰语言服务 | +| 未受信任工作区 | 只保留语法高亮与 TOML 结构补全 | + +## 4. 并发模型 + +`McppOperationRegistry`(`tasks.ts`)做两级互斥: + +- **项目级**(键 = 工程根):build/run/test/clean 互斥; +- **全局**:工具链安装 / 设置全局默认,与所有项目级操作互斥。 + +任务结束**先释放锁、再刷新语言服务**,避免刷新期间用户操作被拒。该顺序由 +`test/artifacts.test.ts` 钉住。 + +## 5. 与 mcppls 的唯一接口 + +`languageServer.ts` 是全部耦合面: + +| 本扩展命令 | 转发到 | +| --- | --- | +| `mcpp.configureLanguageServer`(+ 弃用别名 `mcpp.configureClangd`) | `mcppls.selectContext` | +| `mcpp.checkModuleSupport` | `mcppls.restartServer` | +| `mcpp.showModuleGraph` | `mcppls.showModuleGraph` | +| `mcpp.showLanguageServerLogs` | `mcppls.showLogs` | +| build 之后 | `mcppls.restartServer` | + +不读 `mcppls.*` 设置、不解析 LSP、不写跨扩展配置。详见 +[`mcppls-integration.md`](mcppls-integration.md)。 + +## 6. 已知的结构性债务 + +- `src/configureOnly.ts`、`src/ideWorkflow.ts` 在 0.4.0 迁移后已无生产调用者(仅被自身测试引用), + 它们描述的是 `compile_commands.json` 时代的行为,与当前职责边界冲突。 +- `cli.ts` 用正则解析 `mcpp toolchain list` 的**人类输出**,而 mcpp 已提供 + `--format json` 的稳定契约。 +- 命令 ID `mcpp.refreshCompilationDatabase` / `mcpp.checkModuleSupport` 是历史拼写,与当前语义 + (build / restart)不符。 + +具体建议见 `.agents/reviews/` 下的评审报告。 diff --git a/.agents/docs/archive/2026-10-02-implementation-plan.md b/.agents/docs/archive/2026-10-02-implementation-plan.md new file mode 100644 index 0000000..229e9b2 --- /dev/null +++ b/.agents/docs/archive/2026-10-02-implementation-plan.md @@ -0,0 +1,342 @@ +# mcpp-vscode 0.5.0 实施计划:任务拆分、依赖关系与验收 + +- 日期:2026-10-02 +- 上游设计:[`2026-10-02-plugin-optimisation-plan.md`](2026-10-02-plugin-optimisation-plan.md)(方案 v4) +- 分支:`feat/plugin-optimisation-v0.5.0`,**单 PR**,目标版本 `0.5.0` +- 本文只讲"怎么落地":任务、依赖、并行分组、每个任务自己的验收方式。 + +--- + +## 1. 六个评审角度 → 落到哪条硬约束 + +| 角度 | 硬约束(可机械检查) | 落在 | +|---|---|---| +| **架构** | `src/` 按用途分组;纯逻辑模块不 import `vscode`;每个新模块有对应测试 | T01、T03、T18、T24、T30、T33 | +| **稳定性** | 所有 `runProcess` 有超时;解析优先 JSON;任何上游失败都不能把成功的 `mcpp build` 变成失败 | T05–T09、T14 | +| **优雅简洁** | 单一事实源(配置 registry、i18n bundle、schema 快照);无重复的命令 ID 字面量;无死代码 | T02、T03、T24、T30 | +| **用户体验** | modal 只用于不可逆操作;每个视图有三态;每个清理有两步以上确认;文案跟随语言 | T15、T20–T23、T34、T37、T02 | +| **兼容性** | 不注册 `mcppls.*` 命令;`extensionDependencies` 语义不变;旧命令/旧设置保留为别名 | T11–T14、T04、T38 | +| **跨平台** | 无平台假设的路径/分隔符;CI 覆盖 linux-x64 + darwin-arm64;平台矩阵只在文档与 release note 声明 | T39、T40、T42 | +| **一致性** | 命令 ID / 设置键 / 文案 key 三者在 registry 与 package.json 之间由 CI 校验 | T03(check-config)、T02(l10n-check) | +| **无感升级** | 0.4.x 用户的设置、键位、命令 ID 全部继续工作;新增行为默认"不打扰";CHANGELOG 写迁移说明 | T04、T10、T38、T41 | + +--- + +## 2. 任务表 + +> **并行组**:同一组内的任务互不触碰同一文件,可以同时做;跨组有依赖。 +> **状态**:✅ 已完成 / 🔄 进行中 / ⬜ 未开始 + +### P0 地基(必须先做:后面所有任务都往这三处加东西) + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T01 | 目录按用途分组 | `src/{cli,projects,toml,mcppls,commands,workflows}/` | — | P0-a | `npm test` 179 通过;`git log --follow` 可用 | ✅ | +| T02 | i18n 机制 | `data/i18n/{en,zh-cn}.json`、`l10n/bundle.l10n*.json`(生成)、`package.nls*.json`、`src/i18n/t.ts`、`tools/generate-l10n.mjs`、`tools/l10n-check.mjs` | T01 | P0-b | 缺 key 时 `l10n-check` 退出 1;`mcpp.ui.language` 三态有单测 | ⬜ | +| T03 | 配置模块 | `data/config-registry.json`、`src/config/{registry,access,validate,migrate}.ts`、`tools/check-config.mjs` | T01 | P0-c | registry ↔ package.json 语义一致的 CI 门禁;60 项全部可读且默认值正确 | ⬜ | +| T04 | 版本与迁移说明 | `package.json` 0.5.0、`CHANGELOG.md`(英) | — | P0-d | tag 与版本一致(release workflow 已有校验) | ⬜ | + +### P1 稳定性基座 + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T05 | mcpp 协议探测 | `src/cli/protocol.ts` | T03 | P1-a | 对真实 mcpp 解析出 `mcpp.protocol`;对旧输出返回"不支持"且不抛 | ⬜ | +| T06 | 工具链清单 JSON 优先 | `src/cli/toolchain.ts` | T05 | P1-a | JSON 路径与文本路径对同一份样本给出一致结果 | ⬜ | +| T07 | 超时与输出上限 | `src/cli/process.ts` | T03 | P1-b | 超时返回可识别错误;截断保留尾部并置标志 | ⬜ | +| T08 | 错误分层(SPEC-003) | `src/cli/errors.ts` | T07 | P1-b | 2/4/1/70/127 各有断言 | ⬜ | +| T09 | `mcpp.path` 校验 | `src/cli/controller.ts` | T05 | P1-c | 配置变更时校验一次并在频道输出 | ⬜ | +| T10 | 激活面收敛 | `package.json` activationEvents | T03 | P1-c | 只留 workspaceContains + onLanguage:mcpp-* | ⬜ | + +### P2 mcppls 韧性与状态(方案 §2、§3.9) + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T11 | 能力表 | `src/mcppls/contract.ts` | T02 | P2-a | 表驱动单测:ID 唯一、danger 合法、无 `mcppls.` 前缀的本扩展命令 | ⬜ | +| T12 | 能力探测 | `src/mcppls/capabilities.ts` | T11 | P2-a | 静态声明 + 运行期分类;`onDidChange` 失效有单测 | ⬜ | +| T13 | 状态读取适配层 | `src/mcppls/state.ts` | T11 | P2-a | 5 种输入(正常/无 API/抛异常/未知 state/字段缺失)各有断言 | ⬜ | +| T14 | 桥接改造 | `src/mcppls/bridge.ts` | T12、T13 | P2-b | 候选链回退;危险级确认;`build` 永不因 mcppls 失败 | ⬜ | +| T15 | 「C++ Modules」视图 | `src/views/languageServerView.ts` | T14、T33 | P2-c | 三态(未安装/未启用/未暴露状态)可测 | ⬜ | +| T16 | e2e 夹具 5 个 + 套件 | `test/e2e/fixtures/mcppls-stub*` | T12 | P2-d | 5 个变体各自断言降级 | ⬜ | +| T17 | 环境自检命令 | `src/cli/selfCheck.ts` | T05、T13 | P2-e | 快照含版本/协议/能力/状态/已改设置 | ⬜ | + +### P3 缓存统计与两级清理(方案 §3.4) + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T18 | 缓存聚合(纯函数) | `src/cli/cache.ts` | T07 | P3-a | 空/单条/incomplete/大数/缺 accessed 各有用例 | ⬜ | +| T19 | 项目产物体积 | `src/cli/artifacts.ts` | T07 | P3-a | 临时目录树用例;越界与权限失败降级 | ⬜ | +| T20 | 缓存 TreeView | `src/views/cacheView.ts` | T18、T19、T33 | P3-b | 节点由纯函数产出,可单测 | ⬜ | +| T21 | 缓存面板 webview | `src/views/cachePanel.ts`、`media/*` | T18、T33 | P3-c | HTML 由纯函数生成;CSP 无外链;预算模拟器有单测 | ⬜ | +| T22 | 清理命令族 | `src/cli/clean.ts` | T18、T19 | P3-a | 五级危险分级各有参数断言 | ⬜ | +| T23 | 清理确认流 | `src/views/*` + `src/cli/clean.ts` | T22 | P3-d | L1 与 `--all` 走两步确认;`--dry-run` 预演先行 | ⬜ | + +### P4 `mcpp.toml` 编辑体验(方案 §3.3) + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T24 | schema 快照 + 生成 | `data/toml-schema.json`、`tools/generate-toml-schema.mjs`、`src/toml/schema.ts` | T03 | P4-a | 生成物稳定;读不到源时**失败而非产出空表** | ⬜ | +| T25 | 键与枚举值补全 | `src/toml/completion.ts` | T24 | P4-b | 段/键/值三类上下文各有断言 | ⬜ | +| T26 | 悬停 | `src/toml/hover.ts` | T24 | P4-b | 段头、键、legacy 键各有断言 | ⬜ | +| T27 | 诊断 | `src/toml/diagnostics.ts` | T24 | P4-c | 7 条规则正反例;严重度可配 | ⬜ | +| T28 | 跳转 | `src/toml/navigation.ts` | T24 | P4-c | `workspace`/`path`/`features` 三类 | ⬜ | +| T29 | 依赖版本补全(默认关) | `src/cli/search.ts` | T25 | P4-d | 解析样本;超时/离线降级为空 | ⬜ | + +### P5 `build.mcpp` 智能(方案 §3.2) + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T30 | API 快照 + 生成 | `data/buildscript-api.json`、`tools/generate-buildscript-api.mjs` | T03 | P5-a | 31 条指令 / 5 role / 协议 15;源缺失时失败 | ⬜ | +| T31 | 符号表与模块清单 | `src/buildscript/{api,modules}.ts` | T30 | P5-b | `std`/`mcpp.*` 被判为已知模块 | ⬜ | +| T32 | provider 集 | `src/buildscript/providers.ts` | T31、T24 | P5-c | 7 条静态诊断正反例;补全/hover/片段 | ⬜ | + +### P6 视图、视觉与格式化 + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T33 | 主题与格式化 | `src/views/theme.ts`、`src/util/format.ts`、`src/util/text.ts` | T02 | P6-a | 字节/时长/相对时间/截断各有断言 | ⬜ | +| T34 | 工程视图与状态栏 | `src/views/{projectView,status}.ts` | T33、T06 | P6-b | 状态栏三态由纯函数产出 | ⬜ | +| T35 | contributions | `package.json` viewsContainers/views/colors/menus | T20、T15、T34 | P6-c | `artifacts` 测试断言结构 | ⬜ | +| T36 | 命令清单落地 | `src/commands/ids.ts` + 注册 | T20–T23、T15、T37 | P6-d | 每个 ID 在 package.json 与会话中一致 | ⬜ | + +### P7 配置面板(方案 §4.3) + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T37 | 配置面板 webview | `src/config/panel.ts`、`media/config.css/js` | T03、T33 | P7-a | HTML 由纯函数生成;scope/来源/生效时机都渲染 | ⬜ | +| T38 | 面板设置写入与边界提示 | `src/config/access.ts` | T37 | P7-b | 不写 `mcppls.*`;资源型设置带 URI | ⬜ | + +### P8 CI、文档与交付 + +| ID | 任务 | 产物 | 依赖 | 并行组 | 验收 | 状态 | +|---|---|---|---|---|---|---| +| T39 | CI 体系 | `.github/workflows/ci.yml`(漂移 job、l10n/check-config 门禁、linux+macos matrix、VSIX 依赖版本断言) | T02、T03、T16 | P8-a | 本地可复跑的等价命令写进 docs | ⬜ | +| T40 | 用户文档 | `README.md`(en)、`README.zh-CN.md`、`docs/*.md`(8 篇) | T02–T38 | P8-b | README 的每个链接都存在(测试断言) | ⬜ | +| T41 | CHANGELOG(英) | `CHANGELOG.md` | T04 | P8-c | 0.5.0 段含迁移与新增 | ⬜ | +| T42 | 本地验收 profile | `tools/dev-profile.mjs`(隔离 extensions/user-data + 安装 VSIX + 打印启动命令) | T39 | P8-d | 脚本自身可跑通并打印可复制的命令 | ⬜ | +| T43 | 自我 review | `.agents/reviews/` 下的 v0.5.0 review | 全部 | P8-e | 每条风险有对应的测试或文档 | ⬜ | + +--- + +## 3. 依赖图(关键路径) + +``` +T01 ─┬─ T02 ─┬─ T11 ─┬─ T12 ─┐ + │ │ └─ T13 ─┼─ T14 ─┬─ T15 ─┐ + │ │ │ └─ T16 │ + │ └─ T33 ─────────┼────────────────┼─ T35 ─ T36 + ├─ T03 ─┬─ T05 ─ T06 ───┘ │ + │ ├─ T07 ─┬─ T08 │ + │ │ └─ T18 ─┬─ T20 ──────────┤ + │ │ ├─ T21 ──────────┤ + │ │ └─ T22 ─ T23 ────┤ + │ ├─ T09 │ + │ ├─ T10 │ + │ ├─ T24 ─┬─ T25 ─ T29 ───────────┤ + │ │ ├─ T26 │ + │ │ ├─ T27 │ + │ │ └─ T28 │ + │ ├─ T30 ─ T31 ─ T32 ─────────────┤ + │ ├─ T37 ─ T38 ───────────────────┤ + │ └─ T17 ──────────────────────────┘ + └─ T04 ─ T41 + T34 ─┘ + T39 ─ T42 + T40, T43 在最后 +``` + +**关键路径**:`T01 → T03 → T24/T30 → T32 → T35 → T36 → T39 → T40 → T43`。 +`T02`(i18n)必须在任何"写文案"的任务之前完成,否则要写两遍。 + +--- + +## 4. 提交策略(单 PR、多 commit) + +| # | commit | 内容 | +|---|---|---| +| 1 | `refactor: 目录按用途分组…` | ✅ T01 | +| 2 | `feat(i18n): 中英文案跟随 VS Code` | T02 | +| 3 | `feat(config): 统一配置注册表与一致性门禁` | T03 | +| 4 | `feat(cli): mcpp 协议探测、超时与错误分层` | T05–T10 | +| 5 | `feat(mcppls): 能力探测与 C++ Modules 状态视图` | T11–T17 | +| 6 | `feat(cache): 缓存统计与两级清理` | T18–T23 | +| 7 | `feat(toml): mcpp.toml 键/值补全、悬停、诊断与跳转` | T24–T29 | +| 8 | `feat(buildscript): build.mcpp 智能` | T30–T32 | +| 9 | `feat(views): 视图容器、状态栏、配色与配置面板` | T33–T38 | +| 10 | `ci+docs: CI 门禁、双语文档、0.5.0 迁移说明` | T39–T42 | +| 11 | `docs(review): 0.5.0 自我 review` | T43 | + +每个 commit 必须保持 `npm test` 绿;`npm run test:e2e` 在 CI 与本地(有显示时)绿。 + +--- + +## 5. 验证矩阵 + +| 层 | 命令 | 覆盖 | +|---|---|---| +| 类型 | `npm run compile` | 全部 | +| 单测 | `npm test` | 纯逻辑:协议、能力、状态、缓存聚合、schema、诊断、格式化、生成物一致性 | +| 契约 | `npm test`(含 `test/toml/contract.test.ts`) | 真实 mcpp 的 `mcpp.toml` 段清单(无 mcpp 时跳过) | +| 生成物 | `tools/check-config.mjs`、`tools/l10n-check.mjs`、漂移 job | registry ↔ package.json ↔ nls ↔ docs | +| E2E | `npm run test:e2e` | 激活、命令注册、5 个 mcppls stub 变体、缓存视图节点、清理走 dry-run | +| 打包 | `npm run package` + `unzip -t` | VSIX 结构与依赖声明 | +| 人工 | `tools/dev-profile.mjs` 产出的隔离 profile | 用户在真实 VS Code 里验收 | + +--- + +## 6. 无感升级检查表(发布前逐条确认) + +- [ ] 0.4.x 的全部命令 ID 仍然存在(旧的转发命令与 `mcpp.clean` 保留为弃用别名) +- [ ] 0.4.x 的设置键仍然存在(`mcpp.path`、`mcpp.tomlCompletion`、3 个弃用设置) +- [ ] 新增设置默认值全部"不打扰"(新视图不影响既有布局的最小化:面板与视图都可关) +- [ ] `build` 之后的行为与 0.4.x 一致(刷新语言服务),只是优先用轻量重载 +- [ ] 未受信任工作区的行为不放松 +- [ ] CHANGELOG 写清"什么变了 / 什么没变 / 需要手动做什么" + +--- + +## 7. 进展(round 1,2026-10-02) + +分支 `feat/plugin-optimisation-v0.5.0`,`npm test` 390 通过,三个生成物/一致性门禁全绿。 + +| 任务 | 状态 | 产物 | +|---|---|---| +| T01 目录重构 | ✅ | `9 个提交之一`;179→390 测试 | +| T02 i18n 机制 | ✅ | `data/i18n/zh-cn.json`、`l10n/`(生成)、`package.nls*.json`、`src/i18n/{t,translate}.ts`、`tools/{generate-l10n,l10n-check}.mjs`;`package.json` 用户可见字符串全部 `%key%` | +| T03 配置模块 | ✅ | `data/config-registry.json`(64 项/10 组/29 公开)、`src/config/{registry,validate,access,migrate,presets}.ts`、`tools/{check-config,generate-settings-docs}.mjs`、`docs/settings.md`(生成) | +| T05 协议探测 | ✅ | `src/cli/protocol.ts`(已对真实 mcpp 2026.9.30.2 交叉验证) | +| T08 错误分层 | ✅ | `src/cli/errors.ts`(SPEC-003,含 101 仅 mcpp run / 4 / 127) | +| T11–T14 mcppls | ✅ | `src/mcppls/{contract,capabilities,state,bridge,messages}.ts`;15 项能力、候选链、惰性分类、状态白名单、结构化结果 | +| T18/T19 缓存聚合 | ✅ | `src/cli/{cache,artifacts}.ts` | +| T22 清理计划 | ✅(表) | `src/cli/clean.ts`(五级危险、预演、二次确认);命令接线待做 | +| T24/T27 TOML | ✅ | `data/toml-schema.json`(30 段/97 键)、`src/toml/{schema,diagnostics}.ts`、`tools/generate-toml-schema.mjs` | +| T30–T32 buildscript | ✅(分析层) | `data/buildscript-api.json`(31 指令/5 role/协议 15)、`src/buildscript/{api,modules,analysis,providers}.ts` | +| T33 格式化 | ✅ | `src/util/{format,text}.ts` | +| 视图模型 | ✅(模型层) | `src/views/models.ts`(三棵树,标签即 key)、`src/projects/summary.ts` | + +**下一轮的起点**:`package.json` 的 contributions(commands / viewsContainers / views / colors / +menus / activationEvents)、`src/views/` 的 provider 与命令实现、`src/extension.ts` 接线、 +`tools/dev-profile.mjs`、CI 工作流、README 双语与 `docs/*`、0.5.0 版本与 CHANGELOG。 + +**上游反馈已记录**:生成脚本对照 mcpp 源码时发现方案文档里两个不存在的键名 +(`[profile.].opt_level` 实为 `opt`;`bidi_schedule` 实为 `bmi_schedule`),已修正。 + +--- + +## 8. Round 2 审计出的缺口(下一轮的输入) + +文档子代理在写 `docs/` 时逐项核对了代码,发现"设置已声明但无人读取"的地方。这不是文档 +缺陷,而是**功能缺陷**:声明了却不生效,比没有这个设置更糟。按严重度排列: + +| # | 缺口 | 证据 | 处理 | +|---|---|---|---| +| G1 | `mcpp.toml` 的 hover 与跳转不存在 | `src/toml/providers.ts` 只注册补全与诊断;`mcpp.toml.hover`/`navigation` 无人读 | 实现 `src/toml/hover.ts` + `src/toml/navigation.ts`(schema 已有键与文档锚点) | +| G2 | 键/枚举值补全不存在 | `src/toml/completion.ts` 用的是手写 25 条段头表,**没有读** `data/toml-schema.json` | 让补全读 schema(键 + 枚举 + legacy 提示) | +| G3 | 依赖版本补全不存在 | `mcpp.toml.indexCompletion*` 无人读,`mcpp search` 无人调用 | 实现 `src/cli/search.ts` + 接入补全 | +| G4 | 缓存面板不是 webview | `mcpp.showCachePanel` 打开只读 Markdown 预览 | 实现 `src/views/cachePanelHtml.ts` + `cachePanel.ts`(叠条/年龄分布/TopN/预算模拟器) | +| G5 | 未贡献任何快捷键 | `contributes.keybindings` 缺失,方案 §3.5 承诺了 ctrl+alt+b/r/t/m/, | 加 keybindings | +| G6 | `mcpp.cache.gc.confirmAboveGiB` 无人读 | gc 超过阈值不会追加确认 | 接入 | +| G7 | pre-v1 遗留缓存节点永不显示 | `state.legacyBytes` 从未赋值 | 从 `cache dir` 的 legacy 路径估算体积并赋值 | +| G8 | 大量设置无人读 | 64 项中仅 16 项被生产代码读取 | 逐项接线:`mcpp.task.*`、`mcpp.languageService.*`、`mcpp.views.*.show`、`mcpp.ui.*`、`mcpp.runtime.*`、`mcpp.log.level`、`mcpp.buildScript.intelligence/snippets/imports.knownModules`、`mcpp.cache.statusBar/warnAboveGiB/autoRefreshSeconds/showLegacy` | +| G9 | 环境自检的缓存段永远是"not read" | `showSelfCheck` 没有传 `cache` | 采集后传入 | +| G10 | `src/cli/errors.ts` 与 `src/config/migrate.ts` 没有生产调用者 | 退出码分层与旧键迁移提示都没接 | 接入错误提示与首次激活的迁移提示 | +| G11 | i18n 仍有硬编码中文 | `src/cli/controller.ts`、`src/extension.ts` 多处直接写中文 | 全部改走 `t()`,并补 zh 条目 | +| G12 | `VERIFIED_MCPPLS_RANGE` 无人读 | 没有版本提示 | 在会话头/自检里提示(不作为门禁) | +| G13 | `mcpp.ui.numberFormat` 无人读 | `formatBytes` 恒为二进制单位 | 接入 | + +**验收方式**:新增一条单测,遍历 `data/config-registry.json`,断言每个设置键在 `src/` +中至少被"读取一次"(用一张显式的"由谁读取"映射表,而不是正则扫描),缺一项即失败。 +这条测试是这一轮的产出物之一——它把"声明了就要生效"变成可执行的约束。 + +--- + +## 9. 进展(round 2,2026-10-02) + +`npm test` 420 通过;`check-config` / `l10n-check` / `check-generators` 全绿; +VSIX 打包 69 文件 180 KB;本地隔离 profile 已就绪(含 mcppls 0.0.9 与一个可用的 mcpp 工程)。 + +| 任务 | 状态 | 说明 | +|---|---|---| +| T15/T20/T21/T34–T38 视图与命令 | ✅ | 三棵视图、42 个命令、视图容器、两个颜色 ID、配置面板 | +| T16 e2e 五变体 | ✅(代码)/ ⚠️(本地未跑通) | 变体生成 + 每变体一次隔离 Extension Host;本地因无法下载固定版 VS Code 未完成 | +| T39 CI | ⬜ | 工作流尚未更新 | +| T40 文档 | ✅ | README 英/中 + 7 篇 `docs/`,52 条链接全部解析 | +| T41 CHANGELOG | ✅ | 0.5.0 英文段 | +| T42 本地 profile | ✅ | `tools/dev-profile.mjs` | +| G1–G13(§8 审计缺口) | ⬜ | 下一轮的输入 | + +**本地 e2e 的限制**:`@vscode/test-electron` 需要下载 135 MB 的 1.91 稳定版,本机网络在 +下载中途反复中断;改用已安装的 VS Code(`MCPP_E2E_CODE`,本轮新增)时 Extension Host +以 0 退出但没有执行套件。因此 **e2e 的结论仍以 CI 为准**(CI 里有 xvfb 与固定版本), +本地只验证到"编译通过、变体生成正确、命令面完整"。这一条必须在本轮报告里如实说明。 + +--- + +## 10. 进展(round 3,2026-10-02) + +`npm test` **471 通过**(round 2 为 420);三个门禁全绿;CI 重写。 + +| 项 | 状态 | 说明 | +|---|---|---| +| G1 `mcpp.toml` hover/跳转 | ✅ | `src/toml/{hover,navigation}.ts` + `providers.ts` 注册,两个开关生效 | +| G2 键/枚举补全读 schema | ✅ | `completion.ts` 改为快照驱动;快照没有但 mcpp 接受的段(如 `[workspace.dependencies]`)由手写清单补齐 | +| G3 依赖版本补全 | 🟡 | `src/cli/search.ts` 解析器已写并测试;尚未接进补全(唯一一处解析人类输出,注释已说明) | +| G4 缓存 webview 面板 | ✅ | `src/views/cachePanel{,Html}.ts` + `media/cache.css`,叠条/年龄分布/TopN/预算模拟器,已替换原来的 Markdown 预览 | +| G5 快捷键 | ⬜ | 仍未贡献 keybindings | +| G6 gc 阈值与遗留缓存节点 | 🟡 | `warnAboveGiB` 已在面板生效;`confirmAboveGiB` 与 `legacyBytes` 未接 | +| G8 设置接线 | 🟡 | 已接:task 参数四键、状态栏开关、菜单开关、并发作用域、超时、输出缓冲、语言服务刷新模式、视图可见性、启动自检、buildscript 总开关、数字格式。其余见下面的例外表 | +| G11 i18n | 🟡 | 新增一条"硬编码中文不得增长"的门禁(当前 162 行封顶) | +| G13 数字格式 | ✅ | `mcpp.ui.numberFormat` 已作用于缓存状态栏与面板 | +| T39 CI | ✅ | 见下 | + +### 新增的两条"不得倒退"门禁 + +- `test/config/wiring.test.ts`:注册表里每个设置必须被代码读取,或在 `EXCEPTIONS` 里 + 写明理由并指向 §8 的缺口编号。例外表当前 **40** 条,只允许变小(有一条测试专门断言这点)。 + 已弃用的 4 个设置反过来断言"绝不被读取"。 +- `test/i18n/hardcoded.test.ts`:`src/` 里未走 `t()` 的中文行数封顶 162,只允许下降。 + +### CI(`.github/workflows/ci.yml`) + +| job | 内容 | +|---|---| +| `gates` | ubuntu + macos-14 矩阵跑 `npm test`(注册表/i18n/生成物漂移/单测) | +| `generated-drift` | 检出 mcpp 仓库并设置 `MCPP_REPO`,让快照漂移门禁真正执行;另断言 `docs/settings.md` 无 diff | +| `package` | 打包、`unzip -t`、清单承诺校验(依赖未变、无第二个语言客户端、无 `onCommand:*`)、VSIX 必须包含 l10n/nls/registry/css、上传产物 | +| `extension-host-e2e` | `xvfb-run` 下跑全部五种 mcppls 变体 | +| `isolated-install` | 把 VSIX 装进私有 profile,让 VS Code 解析依赖并断言 `sunrisepeak.mcpp-language-server@<版本>` 真实存在 | + +--- + +## 11. 进展(round 4,2026-10-02) + +| 项 | 状态 | 说明 | +|---|---|---| +| G5 快捷键 | ✅ | ctrl/cmd+alt + b/r/t/l/m,作用域 `mcpp.inProject`,并有断言 | +| G8 设置接线 | 🟡 | 三个并行工作流(controller 组 / views+mcppls 组 / buildscript+toml 组)在跑 | +| G10 `errors.ts`、`migrate.ts` | ✅ | 重命名提示接入激活路径(每工作区一次,旧键保留);退出码指引由 controller 组接入 | +| G12 版本提示 | ✅ | 低于 `VERIFIED_MCPPLS_RANGE` 时在输出频道写一句,仅提示不做门禁 | +| 视图可见性 | ✅ | `setContext` + `contributes.views.*.when` | +| 例外表 | 🟡 | 31 条,新增一条'已接线的设置必须从例外表删除'的断言,防止例外表腐烂 | + +--- + +## 12. 进展(round 5,2026-10-02) + +| 项 | 状态 | 说明 | +|---|---|---| +| 设置文档中英对照 | ✅ | `gen:docs` 直接复用 `package.nls.zh-cn.json` 的描述,64 个设置中英并列,不产生第三份译文 | +| 发布流程守卫 | ✅ | `release.yml` 与 CI 同样校验 VSIX 体积与目录(那个 64 MB 的事故若发生在发布流程里会直接进 Release 附件) | +| 死代码 | ✅ | 除入口 `src/extension.ts` 外,`src/` 已无未被引用的模块 | +| G11 i18n(162 行硬编码中文) | 🟡 | 后台工作流正在清零,完成后需合并译文并把门禁上限降到实际值 | +| Windows CI | ⬜ | 仍未覆盖;macOS 也在矩阵里,但同样未经本机验证(本机只有 Linux) | + +### round 5 收尾 + +| 项 | 状态 | +|---|---| +| G11 i18n | ✅ **162 → 0**,门禁改为断言 0;`t.ts` 真正变成 vscode-free(import type + 惰性 require) | +| 架构门禁 | ✅ 新增 `test/architecture.test.ts`:41 个纯模块既不得 `import vscode`,也必须能在无编辑器进程中加载 | +| 设置文档 | ✅ 64 个设置中英对照 | +| 发布守卫 | ✅ `release.yml` 与 CI 同样校验体积与目录 | +| Windows CI | ⬜ 已知缺口(本机只有 Linux,macOS 同样未经本机验证) | +| e2e / CI 本机运行 | ⬜ 环境限制,结论以 CI 为准 | diff --git a/.agents/docs/archive/2026-10-02-plugin-optimisation-plan.md b/.agents/docs/archive/2026-10-02-plugin-optimisation-plan.md new file mode 100644 index 0000000..df0e0bf --- /dev/null +++ b/.agents/docs/archive/2026-10-02-plugin-optimisation-plan.md @@ -0,0 +1,1115 @@ +# mcpp-vscode 插件优化方案(v4,待评审) + +- 日期:2026-10-02 +- 基线:`mcpp-vscode` `main@20f1076`(0.4.0) +- **硬约束:不改动 `mcpp` 与 `mcppls` 的任何代码。** 全部改动落在 mcpp-vscode 仓库内; + 对上游只读(贡献者机器上读源码生成快照,运行期不新增依赖)。 +- 前置分析:`.agents/reviews/2026-10-02-mcpp-vscode-architecture-and-mcppls-dependency-review.md` +- 版本历史:v1 首版 → v2 用真实 mcppls payload 实测后重写 `build.mcpp` 一节、缓存收敛为两级、 + 补 i18n / TOML 编辑 / 配色 / UI 对比 → v3 定稿评审意见、新增主线 D「统一配置模块与配置面板」、 + 补完整设置清单、去掉模式 B 的实现(仅文档备注)、加自我 review → + **v4 新增 §3.9「把 mcppls 的状态与管理并入 mcpp」、`cache clean --all` 的确认分级、 + 第一版公开设置清单(§4.6)、mcppls 状态读取的防御式设计与两个新 e2e 夹具。** + +--- + +## 0. 已确认的决策 + +| # | 决策 | 落点 | +|---|---|---| +| 1 | 目录树重构 + 两提交执行;UI 采用 **U2**(视图容器 + 2 TreeView + 1 Webview 面板) | §1.2、§3.6 | +| 2 | README 英文主 + `README.zh-CN.md`;`package.json.description` 与 `CHANGELOG.md` 改英文 | §1.3 | +| 3 | `docs/superpowers/` → `.agents/superpowers/` | §1.4 | +| 4/5 | `build.mcpp` **只做模式 A(隔离 + 自研 mcpp:: 智能)**;模式 B 不实现,仅在文档备注 | §3.2、附录 D | +| 6 | 清理收敛为**两级**:`mcpp clean` / `mcpp clean --stale` | §3.4 | +| 7 | i18n 第一版:`package.nls.*` 全量 + 关键流程文案;运行时文案渐进迁移 | §3.7 | +| 8 | 激活面收敛(去掉 `onLanguage:cpp`) | §3.1 | +| 9 | 查询类命令默认超时 30 s(`0` = 不限) | §3.1 | +| 10 | 配色 / 可视化 / `build.mcpp` / `mcpp.toml` 编辑 / mcpp 功能都要完整设计 | §3.3–§3.9 | +| 11 | `mcpp.cache.staleDays` 默认 **3** 天 | §3.4 | +| 12 | 全局缓存**暴露更多功能**,并配更好的可视化 | §3.4.3 | +| 13 | `mcpp.toml` 未知段/未知键的默认严重度 = **warning** | §3.3 | +| 14 | 依赖版本补全**进方案**(默认关,唯一的"解析人类输出"例外) | §3.3.2 | +| 15 | **凡可配置的功能都要有设置项,并由统一的配置模块管理 + 配置面板操作** | **§4(主线 D)** | +| 16 | 第一版**公开发布 29 项设置**,其余进入 `advanced` 默认隐藏 | §4.4、§4.6 | +| 17 | **保留** `mcpp.ui.language` 手动覆盖,并在面板与 docs 明确标注"部分界面不受影响" | §3.7 | +| 18 | **保留**依赖版本补全(`mcpp.toml.indexCompletion`) | §3.3.2 | +| 19 | 全局缓存**暴露 `cache clean --all`**,但点击必须走**确认提示** | §3.4.3、§3.4.4 | +| 20 | 配置面板定位:**不取代**原生设置页,只做集中 / 解释 / 预设 / 边界提示 | §4.3 | +| 21 | **把 mcppls 的核心状态与管理并入 mcpp-vscode**(LSP 状态、引擎、问题、缓存重置、诊断包…) | **§3.9** | + +--- + +## 1. 主线 A:仓库结构与文档 + +### 1.1 目标 + +1. `src/` 从"15 个文件平铺"变成"按用途分组",`test/` 镜像; +2. 新增的 UI、缓存、build 脚本、i18n、config 五块有明确归属; +3. `README.md` ≤ 120 行、英文,`README.zh-CN.md` 同结构; +4. 用户文档(`docs/`)、贡献者文档(`.agents/docs/`)、过程文档(`.agents/superpowers/`)边界明确。 + +### 1.2 目录树 + +``` +src/ + extension.ts # 唯一装配点:activate/deactivate + commands/{ids.ts,menu.ts} # 命令 ID 与快捷菜单清单(纯数据) + config/ # 【新】主线 D:统一配置模块 + registry.ts # 读 data/config-registry.json,导出类型化条目 + access.ts # 类型安全读写 + 生效值来源(default/user/workspace/folder) + validate.ts # 枚举/范围/正则校验,非法值回退默认并提示一次 + migrate.ts # 旧键 → 新键的 alias 与一次性迁移提示 + panel.ts # 配置面板 webview + cli/ # mcpp CLI 适配层 + process.ts # 原 process.ts(+ 默认超时、输出截断) + protocol.ts # 【新】--protocol-version 探测与信封解析 + toolchain.ts # 原 cli.ts(JSON 优先,文本 fallback) + controller.ts # 原 cliController.ts + tasks.ts # 原 tasks.ts + newProject.ts # 原 newProject.ts + artifacts.ts # 【新】target/ 体积估算(只读) + cache.ts # 【新】cache/clean 的调用与聚合(纯函数优先) + search.ts # 【新】`mcpp search` 解析(依赖版本补全,默认关) + projects/{discovery.ts,context.ts} + toml/{parser.ts,schema.ts,completion.ts,hover.ts,diagnostics.ts,navigation.ts} + buildscript/{api.ts,modules.ts,providers.ts} + mcppls/{contract.ts,capabilities.ts,state.ts,bridge.ts} + views/{status.ts,projectView.ts,cacheView.ts,cachePanel.ts,languageServerView.ts,theme.ts} + i18n/t.ts # 【新】文案解析(auto → vscode.l10n;否则读自带表) + util/{text.ts,format.ts} + +data/ + config-registry.json # 【新】主线 D 的单一事实源(提交) + buildscript-api.json # 【新】mcpp 构建脚本 API 快照(提交) + toml-schema.json # 【新】mcpp.toml 段/键/枚举快照(提交) + i18n/{en.json,zh-cn.json} # 【新】运行时文案的单一来源(提交) +media/ # webview 静态资源(CSS/SVG/JS),CSP 安全、无网络 +l10n/ # 由 data/i18n 生成,勿手改 + bundle.l10n.json + bundle.l10n.zh-cn.json +package.nls.json # 【新】package.json 的英文文案(默认) +package.nls.zh-cn.json # 【新】package.json 的中文文案 +tools/ + generate-buildscript-api.mjs # 从 mcpp 仓库生成 data/buildscript-api.json + generate-toml-schema.mjs # 从 mcpp docs/04 + SPEC-004 生成 data/toml-schema.json + generate-l10n.mjs # data/i18n/*.json → l10n/bundle.l10n*.json + check-config.mjs # 【新】registry ↔ package.json 语义一致性门禁 + l10n-check.mjs # 文案 key 与 bundle 覆盖门禁 +test/ # 与 src/ 镜像 + e2e/fixtures/{fake-mcpp.js, mcppls-stub/, mcppls-stub-partial/, mcppls-stub-renamed/, project/} +``` + +**必须一起改的地方**: + +| 位置 | 影响 | +|---|---| +| `tsconfig.json` | 增加 `resolveJsonModule: true`(要 `import` `data/*.json`);`rootDir`/`outDir` 不变 | +| `package.json` `main` | 不变(`./dist/src/extension.js`) | +| `package.json` `test` | `node --test dist/test/*.test.js` → `node --test "dist/test/**/*.test.js"` | +| `test/**` 的 `import "../src/xxx"` | 全部改为新路径 | +| `test/artifacts.test.ts` | 路径 + 把"源码文本断言"改成行为断言 | +| `.vscodeignore` | **排除** `tools/**`、`docs/**`、`.agents/**`、`data/**`(JSON 在编译期被 `import`,产物在 `dist/data/`);**必须保留** `media/**`、`l10n/**`、`package.nls*.json`、`dist/**` | +| `docs/superpowers/**` | `git mv` → `.agents/superpowers/{plans,specs}/` | + +> **执行建议**:提交 1 = `git mv` + import 路径替换 + 测试 glob(零行为变化,门禁 +> `npm test && npm run test:e2e`);提交 2 起才加新文件与功能。 + +### 1.3 README 双语与瘦身 + +`README.md`(英文,≤120 行)+ `README.zh-CN.md`(中文,同结构),顶部语言切换。 +章节:What this is → Why → Install(含平台矩阵)→ Quick start 60s → Features(6 条,各一行) +→ Commands(表)→ Settings(表,链接到 `docs/settings.md`)→ Editing `mcpp.toml` / `build.mcpp` +(各 3 行 + 链接)→ Troubleshooting(5 条一行 + 链接)→ Develop → License。 + +详情拆到 `docs/`:`architecture.md`、`commands.md`、`settings.md`、`mcpp-toml.md`、 +`build-script.md`、`cache.md`、`troubleshooting.md`、`compatibility.md`。 +`package.json` 的 `displayName` / `description` 改为 `%key%` 占位(见 §3.7)。 + +### 1.4 目录边界 + +| 目录 | 读者 | 进版本库 | 内容 | +|---|---|---|---| +| `docs/` | 用户 | ✅ | 用法、设置、排错、兼容性 | +| `.agents/docs/` | 贡献者/agent | ✅ | 设计、契约、决策、本方案 | +| `.agents/superpowers/` | 过程 | ✅ | 历史 plans/specs(原 `docs/superpowers/`) | +| `.agents/reviews/` | 贡献者 | ❌ 已 gitignore | 分析报告、评审记录 | + +--- + +## 2. 主线 B:mcppls 依赖的"永不折断"设计 + +### 2.1 能力模型 + +`src/mcppls/contract.ts` 只有数据。**两类能力**:`forward`(转发 mcppls 的命令)与 +`readState`(只读 mcppls 的状态,见 §3.9)。 + +```ts +export interface Capability { + key: string; // 稳定的内部名,用作日志/设置键 + kind: "forward" | "readState"; + titleKey: string; // 文案 key,走 §4 的 i18n + commands: string[]; // 候选链,按优先级(readState 为空) + required: boolean; // 全部为 false + /** 使用它需要多重的确认。 */ + danger: "none" | "confirm" | "destructive"; + degradedHintKey: string; +} + +export const MCPPLS_EXTENSION_ID = "sunrisepeak.mcpp-language-server"; +export const VERIFIED_MCPPLS_RANGE = ">=0.0.4"; // 实测 0.0.9 后收紧 + +export const CAPABILITIES: readonly Capability[] = [ + // ── 第一批:v1 已有 ── + { key: "refresh", kind: "forward", commands: ["mcppls.reloadBuildDescription", "mcppls.restartServer"], danger: "none" }, + { key: "selectContext", kind: "forward", commands: ["mcppls.selectContext"], danger: "none" }, + { key: "moduleGraph", kind: "forward", commands: ["mcppls.showModuleGraph"], danger: "none" }, + { key: "logs", kind: "forward", commands: ["mcppls.showLogs"], danger: "none" }, + // ── 第二批:§3.9 新增 ── + { key: "readState", kind: "readState", commands: [], danger: "none" }, + { key: "restartEngine", kind: "forward", commands: ["mcppls.restartClangd"], danger: "confirm" }, + { key: "resetCache", kind: "forward", commands: ["mcppls.resetWorkspaceCache"], danger: "destructive" }, + { key: "report", kind: "forward", commands: ["mcppls.collectReport"], danger: "none" }, + { key: "diagnosticBundle", kind: "forward", commands: ["mcppls.exportDiagnosticBundle"], danger: "none" }, + { key: "conflicts", kind: "forward", commands: ["mcppls.turnOffOtherCppFeatures", "mcppls.restoreOtherCppFeatures"], danger: "confirm" }, + { key: "runBuildTool", kind: "forward", commands: ["mcppls.runBuildToolInTerminal"], danger: "confirm" }, + { key: "enableInWorkspace", kind: "forward", commands: ["mcppls.turnOnInWorkspace", "mcppls.turnOffInWorkspace"], danger: "confirm" }, + { key: "installTools", kind: "forward", commands: ["mcppls.installCommandLineTools"], danger: "confirm" }, + { key: "review", kind: "forward", commands: ["mcppls.review.run", "mcppls.review.clear"], danger: "none" }, +]; +``` + +`refresh` 用候选链:上游哪天注册了 `reloadBuildDescription`,本插件自动升级,**无需改代码**。 +`mcpp.languageService.refreshAfterBuild`(§4)可把这条链强制成 `reload` / `restart` / `off`。 + +**`review.run` / `review.clear` 是服务器广告的命令**(由 `vscode-languageclient` 注册), +只在服务器运行后存在;能力探测按"调用失败即缺失"处理,无需特判。它们还受 +`mcppls.ai.enabled` 约束 —— 本插件只**转发**,不读、不写该设置。 + +### 2.2 探测:静态声明 + 惰性运行期分类 + +- **不要**在激活时逐个调用探测:多数 UI 命令都有副作用(弹 QuickPick、开面板、清缓存)。 +- **不要**把 `commands.getCommands()` 当唯一判据(是否包含未激活的贡献命令无保证)。 +- **`readState` 特殊**:它不靠命令,靠 `extension.exports`(见 §3.9),因此有自己的探测方式 + (形状探测 + try/catch),与命令能力分开。 +- **静态**:`getExtension(id)?.packageJSON.contributes.commands` —— 零副作用,激活时 + + `extensions.onDidChange` 刷新。读不到只**置灰并带说明**,不隐藏。 +- **运行期**:首次真正使用时调用一次并分类错误(`command '…' not found` → `missing`)。 + `missing` 才从菜单隐藏,并写一行日志 + 一次性提示(受 + `mcpp.languageService.notifyOnDegraded` 控制)。 + +### 2.3 降级矩阵与不变式 + +| 能力 | 缺失时 | 用户看到 | +|---|---|---| +| `refresh` | build 照常成功,不重试、不弹错 | 输出频道一行;成功提示照常 | +| `readState` | §3.9 的「C++ Modules」视图只显示版本/激活/启用 + 我们自己的刷新历史 + 转发按钮 | 状态区块显示"已安装的 mcppls 未暴露状态" | +| 其余 `forward` | 菜单/视图项隐藏(静态缺失时置灰) | 一次性 info + "查看兼容性"按钮 | + +**不变式(写成测试)**:① mcppls 的任何失败都不能把成功的 `mcpp build` 变成失败; +② 不能让其它命令不可用;③ `deactivate()` 不留悬挂 Promise; +④ `readState` 抛异常或返回未知形状时,**视图仍然可用**(只剩降级内容)。 + +### 2.4 版本只用于提示 + +`getExtension(id)?.packageJSON.version` → 会话头 + 环境自检 + 低于已验证区间的一次性提示。 +**不按版本开关功能**(版本号预测不了改名,能力探测才是判据)。 + +### 2.5 测试 + +五个 e2e 夹具:`mcppls-stub`(全部命令 + 同形状 `exports`)/ `mcppls-stub-partial` +(只有 `restartServer`)/ `mcppls-stub-renamed`(改名)/ **`mcppls-stub-noapi`**(不导出 API)/ +**`mcppls-stub-throwing`**(`lastStatus()` 抛异常、返回未知 `state`)。 +断言:降级、菜单隐藏、"build 永不因 mcppls 失败而失败"、**视图在无 API/异常时仍可用**。 +另加纯单测对 `CAPABILITIES` 与 §3.9 的状态适配层做表驱动形态断言。 + +### 2.6 新命令 `mcpp: 环境自检` + +输出可复制快照:扩展/VS Code/平台、工作区信任与根数、工程根、mcpp 路径+版本+协议+kinds、 +mcppls 版本 + 全部能力状态(`forward` 与 `readState` 分开列)、**mcppls 的状态摘要** +(`state` / `project.source` / `engine` / `issues` 的 code 列表,见 §3.9)、上次刷新时间与结果、 +缓存规模、**所有被改过的设置**(来自 §4 的 registry)、最近错误。把 90% 支持问题一次问清, +且不需要任何上游改动。 + +--- + +## 3. 主线 C:稳定性、编辑体验、缓存、交互与视觉 + +### 3.1 稳定性基座 + +| 项 | 目标 | +|---|---| +| 输出协议 | JSON 优先:`--protocol-version` 判协议 → `toolchain list --format json`;文本解析保留为 legacy 分支并单独测试 | +| 超时 | 查询类默认 30 s(`mcpp.runtime.timeoutSeconds`,`0` = 不限);`build/run/test/install` 走任务终端不设硬上限;`cache gc/prune` 300 s | +| 输出上限 | 保留 16 MiB `maxBuffer`;超限按行截断并提示"完整输出见 `mcpp` 输出频道" | +| 错误分层 | SPEC-003:`2` 用法 / `4` 环境未就绪 / `1` 运行失败 / `70` 内部 / `127` 未知命令 → 不同文案 + 下一步建议 + `mcpp self explain ` 一键跳转 | +| `mcpp.path` | 配置变更时跑一次 `mcpp --protocol-version`(无副作用),版本写进输出频道;不可执行时状态栏警告 | +| 激活面 | 收敛为 `workspaceContains:mcpp.toml` + `onLanguage:mcpp-toml` + `onLanguage:mcpp-build` | +| 未受信任工作区 | 缓存视图降级为纯文件系统估算并注明;清理一律要求信任 | + +### 3.2 `build.mcpp`:只做模式 A + +#### 3.2.1 实测结论(本机真实 mcppls 0.0.8 payload + clangd 23.1.0) + +``` +$ mcppls check build.mcpp --payload +root …/examples/11-features/greeter +database 6 entries, 2 standard library units, 0 left out +module build.mcpp:2: module 'mcpp' not found +E [module_not_found] Line 1: module 'std' not found +E Failed to build module mcpp; due to Don't get the module unit for module mcpp +clangd exit 3 + +$ mcppls check src/main.cpp --payload +database 6 entries, 2 standard library units, 0 left out +clangd exit 0 ← 零诊断 +``` + +并且 `mcpp emit build-database --spec compile-commands --format json` 的 6 条记录里 +**没有 `build.mcpp`**;它只出现在 `data.watch` 里。 + +**三条硬事实**:① mcpp 有意不把构建程序放进编译数据库;② mcppls 的模型里没有 +`build.mcpp`(它只处理 `target/.build-mcpp/deps/...`);③ 因此交给 clangd 后 +**`import std` 与 `import mcpp` 都会报 `module not found`**,不是"只有 mcpp 报错"。 + +而 **VS Code 没有公开 API 能过滤/删除另一个扩展发布的诊断**。所以"交给 C++ 语言服务、 +但抹掉 `import mcpp` 的红线"在不改上游的前提下**做不到**。 + +#### 3.2.2 本版设计:模式 A(隔离)—— 零报错 + +- `build.mcpp` 保持独立语言 id `mcpp-build`,语法即 C++(现有 `include: source.cpp` + + 模块注入语法),**mcppls 不介入 → 永不报错**; +- 在此之上补 **mcpp-vscode 自研的 `mcpp::` 智能**(数据来自 mcpp 的机器可读表,见 §3.2.3): + 补全 / 悬停 / 签名 / 静态诊断 / 片段 / 文档符号 / 折叠; +- **"认识 std 与 mcpp"**:内嵌"构建脚本可用模块名"清单 + (`std`、`std.compat`、`mcpp`、`mcpp.core`、`mcpp.plugins.*`),对其做**高亮 + 悬停说明 + + 文档链接**,并**永不对其报"找不到"**(`mcpp.buildScript.imports.knownModules`)。 + 这就是"std ok"在本层的实现:**认识它、解释它、不为难你**; +- 诚实写明代价:**没有 std 的符号级补全/跳转**。 + +#### 3.2.3 数据来源(贡献者期生成) + +`tools/generate-buildscript-api.mjs`(Node,无依赖)读 `MCPP_REPO`(默认 `../mcpp`): + +| 源 | 取什么 | +|---|---| +| `modules/buildmcpp/src/directives.cppm:297` | `kTable[31]`:wire / tag / slot / scope / transform / must / missingPrefix / missingSuffix / sinceProtocol | +| `modules/buildmcpp/src/directives.cppm:1132-1134` | 5 个 role 常量 | +| `modules/buildmcpp/src/program_protocol.cppm:121,147` | `kProtocolVersion = 15`、`kCacheEpoch = 3` | +| `modules/buildmcpp/src/provisions.cppm:80` | `tool` / `host-module` / `dep-dir` | +| `docs/specs/build-plugins.md`、`docs/30-build-mcpp.md` | 规则号与 hover 章节链接 | + +输出 `data/buildscript-api.json`(含 `sourceVersion` / `sourceCommit` / `protocolVersion`)。 +CI 漂移 job:用 `mcpp@main` 重新生成 + `git diff --exit-code`。 + +**静态诊断规则**(纯文本,不执行任何东西,未受信任工作区可用;严重度由 +`mcpp.buildScript.diagnostics.severity` 控制,默认 warning): +① 未知 `mcpp::`;② `role` 用字符串字面量(R3.6);③ `prepare` 未声明 `output_dir`(R3.3); +④ `link_flag` 含 `-Wl,-rpath`(R4.4);⑤ `cxxflag`/`link_flag` 含 `-I`/`-L`(R2.1); +⑥ action 命令含 `NAME=value cmd` 或 `cd x &&`(R3.8);⑦ `mcpp::action` 未声明 role 或输出。 + +### 3.3 `mcpp.toml` 编辑体验 + +#### 3.3.1 数据:`data/toml-schema.json` + +`tools/generate-toml-schema.mjs` 从 `docs/specs/manifest-semantics.md`(SPEC-004 §2 的平面划分) +与 `docs/04-mcpp-toml.md` 的字段小节生成,人工补**枚举值**与 **legacy 标记**: + +```jsonc +{ + "sourceVersion": "2026.10.1.3", + "sections": [ + { "header": "[package]", "plane": "identity", "doc": "docs/04-mcpp-toml.md#21-package", + "keys": [ + { "key": "name", "type": "string", "required": true }, + { "key": "standard", "type": "enum", "values": ["c++20","c++23","c++26"], "default": "c++23" }, + { "key": "mcpp", "type": "string", "pattern": "^>=.*", "since": "2026.9.28.3" } + ] }, + { "header": "[language]", "plane": "legacy", "deprecatedBy": "[package].standard" } + ], + "rules": [ { "id": "plane-separation", "messageKey": "toml.rule.planeSeparation" } ] +} +``` + +#### 3.3.2 功能矩阵 + +| 功能 | 内容 | 开关(§4) | +|---|---|---| +| 段头补全 | 已有,改为读 schema(平面分组 + legacy 标记) | `mcpp.toml.completion` | +| **键补全** | 已知段的键位置 → 该段全部键(类型、默认值、legacy),已存在的键剔除 | 同上 | +| **枚举值补全** | `standard`、`kind`、`linkage`、`opt`、`[profile.*]`、`[target.]` selector 词表 | 同上 | +| **悬停** | 段头/键 → 类型、默认值、平面、起始版本、legacy 迁移建议、`docs/04` 章节链接 | `mcpp.toml.hover` | +| **诊断** | ① TOML 语法错误;② 未知段;③ 已知段的未知键;④ `[dependencies]` 里的 `xim:` / `[xlings]` 里的 mcpp 包(SPEC-004 §2);⑤ `[package].mcpp` 非 `>=` 形式(SPEC-007 R9.8);⑥ legacy 键;⑦ `[[...]]` 数组表 | `mcpp.toml.diagnostics.*`(**未知段/未知键默认 warning**) | +| **跳转** | `workspace = true` → `[workspace.dependencies]` 同名键;`path = "../x"` → 那个 `mcpp.toml` 的 `[package]`;`features = ["a"]` → `[features.a]` | `mcpp.toml.navigation` | +| **依赖版本补全** | 在 `[dependencies]` 的值位置给候选版本 | `mcpp.toml.indexCompletion`(**默认 false**) | +| 格式化 | **不做** | — | + +**依赖版本补全的诚实边界**(本方案唯一的"解析人类输出"例外): +`mcpp search ` **没有机读格式**(实测 `mcpp search --help` 只有人类列表 + +`--all-versions`),而且它默认会刷新索引(联网)。因此: + +- 默认关闭,开启时给一次性提示"这会执行 mcpp 并可能联网"; +- 用 `mcpp search --all-versions` + `mcpp.toml.indexCompletionTimeoutSeconds`(默认 20 s); +- 解析 `名称 (版本)` 行,**按会话缓存**,失败/超时/离线即静默降级为"无候选"; +- 在 `docs/mcpp-toml.md` 写明这是临时方案,**mcpp 一旦提供机读格式就替换**。 + +### 3.4 缓存统计与清理 + +#### 3.4.1 可用数据(本机实测) + +| 命令 | 数据 | 机读 | +|---|---|---| +| `mcpp cache list --format json` | kind `mcpp.cache`:`data.root` + `data.entries[]`,每条 `{accessed(Unix 秒), bytes, complete, dir, files, key, kind, label}`。实测 **657 条 / 7.20 GiB / pkg 576 · std 81 / 2 条 incomplete / 83 个 label / 最旧 2026-09-23** | ✅ 信封 | +| `mcpp cache dir` | 缓存根 + `legacy (unused, removable with mcpp cache clean --legacy): ` | ❌ 文本 | +| `mcpp cache info ` | `dir/key/package/size/file count/last used/complete/inputs(JSON)` | ❌ 文本 | +| `mcpp cache verify` | 校验条目清单与磁盘 | ❌(看退出码与文本) | +| `mcpp cache prune --older-than ` | 按未使用时长丢弃 | — | +| `mcpp cache gc --max-size --older-than ` | LRU 收敛到预算 | — | +| `mcpp cache clean [--deps\|--std\|--all\|--legacy]` | 分类清空 | — | +| `mcpp clean [--stale] [--older-than …] [--dry-run] [--bmi-cache]` | 见 §3.4.2 | `--dry-run` 文本 | + +`target/` 体积只能自己 walk。mcpp 明确写"`target/` 下的内容不是接口",因此:体积一律标 +**"估算"**;**"过期集合"的权威来源永远是 `mcpp clean --dry-run`**;walk 失败显示"未知"。 + +#### 3.4.2 两级清理 + +| 级别 | 按钮 | 命令 | 语义 | +|---|---|---|---| +| **L1** | 清理项目产物 | `mcpp clean` | 删整个 `target/` | +| **L2** | 清理过期产物 | `mcpp clean --stale --older-than d` | 只删"不是当前构建"的 fingerprint 目录,保留最近 N 天 | + +`N` = `mcpp.cache.staleDays`,默认 **3**(下拉 1 / 3 / 7 / 30 / `0`=一个都不留)。 +不写时 mcpp 用 1 天,我们显式写出以保证可预期。 + +**全局缓存不并入这两级**(`--bmi-cache` 会清掉所有工程共享的缓存 → 全量重建)。 +「连全局缓存一起清」只作为 L1 确认框里**第三个、默认不勾选**的选项。 + +#### 3.4.3 全局缓存:暴露更多功能 + 更好的可视化 + +| 能力 | 命令 | 危险级 | 说明 | +|---|---|---|---| +| 浏览 | `cache list --format json` | 只读 | 树:按包分组 → 展开到条目(key/大小/最近使用/完整) | +| 详情 | `cache info ` | 只读 | 原文展示到只读预览文档(**不解析**) | +| 校验 | `cache verify` | 只读 | 报告损坏/缺失条目;失败项高亮 + "查看输出" | +| 按时间收敛 | `cache prune --older-than d` | 中 | 默认 `mcpp.cache.pruneAgeDays` = 30 | +| 按预算收敛 | `cache gc --max-size GiB [--older-than …]` | 中 | 默认预算 `mcpp.cache.gc.defaultBudgetGiB`(0 = 每次追问) | +| 分类清空 | `cache clean --deps` / `--std` / `--all` | 高 | 逐级更重的确认 | +| 清遗留 | `cache clean --legacy` | 低 | pre-v1 `$MCPP_HOME/bmi` | + +**可视化(webview 面板 `mcpp: 缓存统计`,纯 CSS + 内联 SVG,无图表库、无网络、严格 CSP)** + +1. **顶部两个大数字**:全局缓存 / 项目产物,各带"上次采集时间"; +2. **构成条**:按 `kind`(pkg / std)+ pre-v1 遗留 的横向堆叠条,颜色语义见 §3.5; +3. **年龄分布条**:把条目按 `accessed` 分桶(`<1d` / `1–7d` / `7–30d` / `>30d`,桶由 + `mcpp.views.cache.ageBuckets` 配置)→ 直观回答"能回收多少"; +4. **Top N 表**(`mcpp.views.cache.topN`,默认 5):label / 条目数 / 大小 / 最旧使用。 + 行内只提供「详情」(`cache info` 原文预览)与「复制命令到终端」—— + ⚠ **mcpp 没有"按包删除"的命令**(`cache clean` 只有 `--deps/--std/--all/--legacy` 四档), + 所以**不提供按包删除**,避免做出一个假的按钮; +5. **预算模拟器**:选目标 GiB → 本地按 `accessed` 升序做 LRU 模拟 → 预估"将删除 N 条 / 释放 X"。 + 标注为**预估**(mcpp 的 LRU 还受 `complete` 等影响),执行后以 mcpp 输出为准; +6. **不完整条目**区块:`complete=false` 的条目列出 + 一键 `cache verify`; +7. **pre-v1 遗留**区块:来自 `cache dir`,一键清理; +8. **项目产物**区块:按 triple 分组的条形 + 「过期产物 N 项 / X MiB」(来自 `clean --dry-run`)。 + +**危险操作分级**(`cache clean --all` **照评审意见暴露**,但确认最重): + +| 级 | 操作 | 确认方式 | +|---|---|---| +| 1 只读 | `list` / `dir` / `info` / `verify` | 无 | +| 2 中 | `prune --older-than` / `gc --max-size` | **必看预演**(列出将删除或预估释放;`clean --dry-run` 原文) | +| 3 高 | `clean --deps` / `--std` | modal,标题写明将释放的字节数 | +| 4 极重 | **`clean --all`** | **两步**:modal(写明"将删除本机所有 mcpp 工程共享的包缓存与标准库模块条目")+ 二次勾选确认 | +| 5 最重 | L1 清理 + `--bmi-cache` | modal + 二次勾选 + 文案点名"会影响本机所有 mcpp 工程" | + +所有清理都要求受信任工作区、走现有 `McppOperationRegistry`(与 build 互斥)、**绝不自动执行**。 +`mcpp.cache.gc.confirmAboveGiB`(默认 1)保证任何超过 1 GiB 的收敛动作都至少有 modal。 + +### 3.5 配色与可视化设计系统 + +单一来源 `src/views/theme.ts` + `media/*.css`。**所有颜色只用 VS Code 主题令牌**。 + +| 语义 | 令牌 | 用途 | +|---|---|---| +| 工程 / 主色 | `--vscode-charts-blue` | 工程视图、构建数值 | +| 缓存 / 正常 | `--vscode-charts-green` | 缓存总量、完整条目 | +| 标准库缓存 | `--vscode-charts-purple` | `kind: std` 分段 | +| 过期 / 可清理 | `--vscode-charts-yellow` | 过期产物、L2 按钮 | +| 警告 | `--vscode-charts-orange` + `$(warning)` | 不完整条目、超阈值 | +| 危险 | `--vscode-errorForeground` | L1 确认、`--bmi-cache` | +| C++ 语义(mcppls) | `--vscode-charts-blue` + `$(beaker)` | 「C++ Modules」视图、状态项 | +| 次要文字 | `--vscode-descriptionForeground` | 单位、时间、计数 | +| 进行中 | `--vscode-progressBar-background` + `$(sync~spin)` | 状态栏、视图 | + +- 贡献两个可覆盖颜色 ID:`mcpp.cacheOkForeground`、`mcpp.cacheStaleForeground` + (与 mcppls 的 `mcppls.statusReadyForeground` 同做法)。 +- 图标只用 codicon:`$(tools)` 构建、`$(play)` 运行、`$(beaker)` 测试、`$(trash)` 清理、 + `$(database)` 缓存、`$(graph)` 统计、`$(refresh)` 刷新、`$(history)` 按时间、`$(check)` 校验、 + `$(warning)` 警告、`$(package)` 包、`$(chip)` 工具链、`$(target)` 目标、`$(sync~spin)` 进行中、 + `$(settings-gear)` 配置面板。 +- **数字格式化统一**(`src/util/format.ts`):字节按 `mcpp.ui.numberFormat`(默认二进制 `GiB`); + 三位有效数字;时间相对("3 天前")+ 悬停绝对时间;计数千分位。 +- **三态设计**:每个视图/面板都要有「空 / 加载(`$(sync~spin)` + 上次采集时间)/ + 错误(`$(error)` + 一行原因 + 打开输出频道)」。 +- **通知策略**:modal 只用于不可逆操作(`mcpp.ui.confirmDestructiveOnly`);成功默认不弹 toast; + 失败弹 toast + "查看输出";同一原因去重 `mcpp.ui.notifications.dedupeMinutes`。 +- **状态栏**:一个 `mcpp` 项(priority 40):`$(tools) mcpp` / `$(sync~spin) mcpp: 构建中` / + `$(warning) mcpp: 缓存 8.1 GiB`。tooltip = 工程 + 工具链 + target + 缓存。 +- **快捷键**:`ctrl+alt+b` 构建、`ctrl+alt+r` 运行、`ctrl+alt+t` 测试、`ctrl+alt+m` 菜单、 + `ctrl+alt+,` 配置面板。清理类**不给快捷键**。 +- **与 mcppls 的视觉边界**(v3 修订):**不新增第二个状态栏项**(mcppls 已有 C++ Language + Status Item);输出频道保持独立(`mcpp`)与 `C++ Modules` 并列;但我们**新增第三个视图 + 「C++ Modules」**(§3.9),并在它的标题/描述里**明确标注提供方**,只做展示与转发, + 不冒充 mcppls、不复制它的状态项。 + +### 3.6 UI 形态(已定 U2) + +> v1/v2 比较过三种形态:**U1**(只加命令面板 + 状态栏,视觉侵入最低、可发现性差)、 +> **U2**(Activity Bar 容器 + 2 TreeView + 1 Webview 面板,可视化最好)、 +> **U3**(只加一个控制面板,折中)。评审选定 **U2**;**U1 保留为兜底**——状态栏项与命令面板 +> 永远可用,视图容器被拖走或视图报错时功能不减;U3 作为"若 Activity Bar 的侵入感被否决"的 +> 退路,面板内做同样的可视化,只是没有常驻条目。 + +``` +mcpp ← Activity Bar 容器 $(tools) +├── 工程 (mcpp.project) ← TreeView +│ ├── greeter 0.1.0 · c++23 +│ ├── 工具链 llvm@22.1.8 · target x86_64-unknown-linux-gnu +│ ├── 目标 greet (bin) · 测试 tests/ +│ └── [$(tools) 构建] [$(play) 运行] [$(beaker) 测试] +├── 缓存 (mcpp.cache) ← TreeView +│ ├── 项目产物 … 1.4 GiB(估算)· 3 个 fingerprint +│ │ ├── 过期产物 12 项 · 820 MiB [$(trash) 清理过期产物…] +│ │ └── [$(trash) 清理项目产物…] +│ ├── 全局构建缓存 … 7.20 GiB · 657 条目 +│ │ └── [$(refresh) 刷新] [$(graph) 统计面板] [$(history) 收敛到预算…] [$(check) 校验] +│ └── pre-v1 遗留缓存 … 167.5 MiB [$(trash) 清理遗留缓存] +└── C++ Modules (mcpp.languageServer) ← TreeView,由 mcppls 提供内容(§3.9) + ├── 状态 ready · 引擎 clangd 23.1.0 · 问题 0 + └── [$(refresh) 重启] [$(output) 日志] [$(report) 诊断报告] … +``` + +**兜底**:状态栏项与命令面板永远可用;即使视图容器被用户拖走或视图报错, +所有功能仍可从命令面板触达。 + +**新增命令清单**(全部用 `mcpp.` 前缀;**绝不使用 `mcppls.` 前缀**,见 `.agents/docs/mcppls-integration.md` §3) + +| 命令 ID | 标题 | 分类 | 视图内联 | 备注 | +|---|---|---|---|---| +| `mcpp.openSettings` | mcpp: 打开设置面板 | mcpp | — | §4.3 | +| `mcpp.showCachePanel` | mcpp: 缓存统计 | mcpp | ✅ 缓存视图 | §3.4.3 | +| `mcpp.refreshCacheStats` | mcpp: 刷新缓存统计 | mcpp | ✅ 缓存视图 | 只读 | +| `mcpp.cleanStaleArtifacts` | mcpp: 清理过期产物 | mcpp | ✅ 项目产物 | L2,必看预演 | +| `mcpp.cleanProjectArtifacts` | mcpp: 清理项目产物 | mcpp | ✅ 项目产物 | L1;替换现有 `mcpp.clean` | +| `mcpp.gcGlobalCache` | mcpp: 收敛全局缓存… | mcpp | ✅ 全局缓存 | `cache gc --max-size` | +| `mcpp.pruneGlobalCache` | mcpp: 按时间清理缓存… | mcpp | ✅ 全局缓存 | `cache prune --older-than` | +| `mcpp.verifyGlobalCache` | mcpp: 校验缓存 | mcpp | ✅ 全局缓存 | 只读 | +| `mcpp.cleanLegacyCache` | mcpp: 清理遗留缓存 | mcpp | ✅ 遗留缓存 | `cache clean --legacy` | +| `mcpp.showCacheEntry` | mcpp: 查看缓存条目详情 | mcpp | ✅ 条目 | `cache info` 原文 | +| `mcpp.updateDependencies` | mcpp: 更新依赖 | mcpp | ✅ 工程视图 | `mcpp update`,带预演 | +| `mcpp.selfDoctor` | mcpp: 环境诊断 | mcpp | — | `mcpp self doctor` | +| `mcpp.selfCheck` | mcpp: 环境自检 | mcpp | ✅ 工程视图 | §2.6 | +| `mcpp.explainCode` | mcpp: 解释错误码… | mcpp | — | `mcpp self explain` | +| `mcpp.addDependency` | mcpp: 添加依赖… | mcpp | ✅ 工程视图 | `mcpp add` 向导 | +| `mcpp.removeDependency` | mcpp: 移除依赖… | mcpp | ✅ 工程视图 | `mcpp remove` | +| `mcpp.searchPackages` | mcpp: 搜索包… | mcpp | — | `mcpp search` | +| `mcpp.openMcpplsSettings` | mcpp: 打开 C++ Modules 设置 | mcpp | — | 只打开别人的设置页,**不写** | + +**转发 mcppls 的命令**(§3.9;同样用 `mcpp.` 前缀,**不是** `mcppls.`) + +| 命令 ID | 标题 | 视图内联 | 危险 | 转发到 | +|---|---|---|---|---| +| `mcpp.languageServer.refreshState` | mcpp: 刷新 C++ Modules 状态 | ✅ C++ Modules | none | (只读 `readState`) | +| `mcpp.languageServer.restart` | mcpp: 重启 C++ Modules 语言服务 | ✅ | none | `mcppls.restartServer` | +| `mcpp.languageServer.restartEngine` | mcpp: 重启 clangd… | ✅ | confirm | `mcppls.restartClangd` | +| `mcpp.languageServer.resetWorkspaceCache` | mcpp: 重置本工作区缓存… | ✅ | **destructive** | `mcppls.resetWorkspaceCache` | +| `mcpp.languageServer.selectContext` | mcpp: 选择 C++ 模块分析上下文 | ✅ | none | `mcppls.selectContext` | +| `mcpp.languageServer.showModuleGraph` | mcpp: 查看模块图 | ✅ | none | `mcppls.showModuleGraph` | +| `mcpp.languageServer.showLogs` | mcpp: 打开 C++ Modules 日志 | ✅ | none | `mcppls.showLogs` | +| `mcpp.languageServer.collectReport` | mcpp: 收集 C++ Modules 诊断报告 | ✅ | none | `mcppls.collectReport` | +| `mcpp.languageServer.exportDiagnosticBundle` | mcpp: 导出 C++ Modules 诊断包… | ✅ | none | `mcppls.exportDiagnosticBundle` | +| `mcpp.languageServer.runBuildToolInTerminal` | mcpp: 在终端运行构建工具… | ✅ | confirm | `mcppls.runBuildToolInTerminal` | +| `mcpp.languageServer.manageConflicts` | mcpp: 处理其它 C++ 语言特性… | ✅ | confirm | `mcppls.turnOff/turnOnOtherCppFeatures` | +| `mcpp.languageServer.toggleInWorkspace` | mcpp: 在本工作区启用/停用 C++ Modules… | ✅ | confirm | `mcppls.turnOn/turnOffInWorkspace` | +| `mcpp.languageServer.installTools` | mcpp: 安装 C++ Modules 命令行工具… | ✅ | confirm | `mcppls.installCommandLineTools` | +| `mcpp.languageServer.reviewChanges` | mcpp: 审查工作区改动 | ✅ | none | `mcppls.review.run` / `review.clear` | + +> 现有 4 个转发命令(`mcpp.configureLanguageServer`、`mcpp.checkModuleSupport`、 +> `mcpp.showModuleGraph`、`mcpp.showLanguageServerLogs`)**保留为弃用别名**, +> 指向上表的新 ID —— 与 `mcpp.configureClangd → mcpp.configureLanguageServer` 同一套做法。 + +现有 `mcpp.clean` 保留为 `mcpp.cleanProjectArtifacts` 的弃用别名(与 +`mcpp.configureClangd → mcpp.configureLanguageServer` 的既有做法一致)。 + +### 3.7 国际化(i18n):中/英跟随 VS Code + +| 面 | 做法 | +|---|---| +| `package.json` 用户可见字符串 | `%key%` 占位;`package.nls.json`(英,默认)+ `package.nls.zh-cn.json`(中)。覆盖 `displayName`、`description`、命令标题、设置标题/描述/弃用信息、`untrustedWorkspaces.description` | +| 运行期字符串 | 单一来源 `data/i18n/{en,zh-cn}.json` → `tools/generate-l10n.mjs` 生成 `l10n/bundle.l10n.json` + `l10n/bundle.l10n.zh-cn.json` | +| 解析 | `src/i18n/t.ts`:`mcpp.ui.language === "auto"` → `vscode.l10n.t(key)`;否则读自带表 | +| webview | `getHtml` 时把当前语言的字符串注入,或 `vscode.l10n.uri` | +| 防漏翻译 | `tools/l10n-check.mjs`:`l10n.t(` 的字面量与 bundle 键集合比对;`package.nls.zh-cn.json` 覆盖所有 `%key%`;缺失即 CI 失败 | +| 测试影响 | `test/artifacts.test.ts` 现在断言中文字面量 → 改为断言 `%key%` 形态 + nls 覆盖完整 | + +**第一版范围(已定)**:`package.nls.*` **100% 覆盖**(命令标题、设置项、弃用信息)+ +**关键流程文案**(命令成功/失败、清理确认、降级提示、进度标题)100%; +其余运行期文案随各里程碑渐进迁移,但 `l10n-check.mjs` 从第一天起就阻止**新增硬编码**。 + +**必须写进 docs 的 API 限制**:`mcpp.ui.language` 的手动覆盖**只影响运行时提示与我们的面板**; +命令面板标题、原生设置页里的设置名称**永远跟随 VS Code 语言**(VS Code 在启动时读 +`package.nls.*`,无法运行时切换)。所以用户在 `zh-cn` 界面下把 `mcpp.ui.language` 设成 `en` +会看到"设置名是中文、提示是英文"的混搭 —— 这是有意提供的逃生门,不是缺陷。 + +### 3.8 mcpp 相关功能补齐 + +| 功能 | 说明 | 优先级 | +|---|---|---| +| 工程视图 | 包名/版本/标准/目标/工具链/target/profile | 高 | +| 缓存视图 + 面板 + 两级清理 + 全局缓存功能 | §3.4 | 高 | +| 配置面板 | §4 | 高 | +| `mcpp update` | 重解析依赖并改写 `mcpp.lock`,带"将改动 N 个包"的预演 | 高 | +| `mcpp self doctor` | 环境诊断 → 频道 + 结构化摘要 | 高 | +| `mcpp: 环境自检` | §2.6 | 高 | +| `mcpp add` / `remove` | 依赖编辑向导,与 `mcpp.toml` 键补全联动 | 中 | +| `mcpp search` | 包搜索 → 插入依赖(复用 §3.3.2 的 search 适配层) | 中 | +| `mcpp self explain ` | 失败时一键解释 | 中 | +| `mcpp pack` / `publish --dry-run` | 打包与发布前检查 | 低 | + +### 3.9 把 mcppls 的状态与管理并入 mcpp + +> 目标(评审意见):**把 mcppls 的核心状态与管理纳入 mcpp**——LSP 状态、引擎、构建描述、 +> 问题、缓存情况与清理、诊断包等,让用户在**一个地方**看清并操作。 +> 硬约束仍然是:**不改 mcppls**,所以这里的一切都建立在"**读它已有的状态**" +> 和"**转发它已有的命令**"之上,绝不重新实现、绝不写它的内部文件。 + +#### 3.9.1 能读到什么:三条通道,各有明确边界 + +| 通道 | 内容 | 契约强度 | 本方案的用法 | +|---|---|---|---| +| **A. 扩展元数据** | `packageJSON.version`、`isActive`、`contributes.commands`、平台 VSIX 是否存在、`mcppls.enable` 的当前值(只读配置) | **正式、稳定** | 基础信息与能力探测 | +| **B. `extension.exports`** | mcppls 的 `activate()` 返回一个对象(它对内是"测试 API"),其中 **`lastStatus()` 返回完整的 `CxxModulesStatus`**、`statusBarText()`、`serverRunning()`、`serverEnabled()`、`serverCommands()`、`environment()`、`waitForState()` | ⚠️ **非契约**:它是测试 API,字段可能随 mcppls 变化 | **尽力而为读取**,见 §3.9.2 | +| **C. 转发命令** | §2.1 的 `forward` 能力表(重启、重启引擎、重置缓存、报告、诊断包、冲突处理、启用/停用、在终端运行构建工具、review) | **命令 ID 是事实契约**(同现状) | 所有"管理"动作 | +| ~~D. LSP `cxxModules/status`~~ | 真正的契约来源 | 正式 | ❌ **不可用**:本扩展没有 LSP 客户端,且 CI 明确禁止再建一个 | +| ~~E. mcppls 的磁盘缓存/日志文件~~ | 缓存目录、模型、引擎数据库 | **内部布局,不是接口** | ❌ **不读、不解析、不删除** | + +`CxxModulesStatus` 的形状是 **S3 规范级契约**(`docs/specs/s3-lsp-extensions.md` §4), +所以"内容怎么解释"是稳定的;不稳定的只是"能不能拿到这个对象"。这决定了下面的防御写法。 + +#### 3.9.2 `readState`:尽力而为,坏掉就降级 + +```ts +// src/mcppls/state.ts —— 只有一个入口,返回"一定有值"的视图模型 +export interface McpplsStateView { + available: boolean; // false ⇒ 只有降级内容 + version?: string; + active: boolean; + enabled: boolean; // 只读 mcppls.enable + state?: "starting" | "loading" | "preparing" | "ready" | "degraded" | "error"; + project?: { root: string; source: string; level?: number; tier?: number }; + profile?: { kind: string; compiler?: string; stdlib: string; target: string; standard?: string }; + engine?: { name: string; version: string }; + engines?: Array<{ name: string; version: string; role: string; state: string }>; + progress?: { done: number; total: number }; + issues?: Array<{ code: string; message: string; command?: { command: string; arguments?: unknown[] } }>; + notices?: Array<{ code: string; message: string }>; + onlineRun?: { outcome: string; message: string; at: string }; +} + +export function readState(): McpplsStateView; +``` + +读取纪律(每一条都会写成测试): + +1. `getExtension(id)?.exports` 必须是对象,`typeof exports.lastStatus === "function"`, + 否则 `available: false`——**不抛错、不提示、只是少一块内容**; +2. `lastStatus()` 包在 `try/catch` 里; +3. 返回值必须是对象且 `state` 属于 S3 枚举的六个值之一,否则视为**未知形状** → `available: false`; +4. `issues[].code` 与 `engines[].state` 都用**白名单**渲染,未知值原样显示文本但**不解释**; +5. `readState` 由设置 `mcpp.languageService.readState`(默认 `true`)控制,可完全关闭; +6. **不主动轮询**:以 `vscode.extensions.onDidChange` + 我们自己的命令完成回调 + + `mcpp.languageService.stateRefreshSeconds`(默认 0 = 关)为触发点;高频轮询被明确排除; +7. 视图渲染**永远不因 `readState` 失败而报错**(不变式 ④)。 + +`issues[].command` 是 **S3 自带的"修复动作"**("an optional action that fixes the issue")。 +我们把它渲染成按钮并**直接 `executeCommand(issue.command.command, ...args)`** —— +这是契约内的字段,比我们自己猜修复方式更可靠。典型如 `producer-needs-download` +(`askOnline: true`)→ 提示用户是否允许联网重述工程。 + +#### 3.9.3 缓存与管理:能做的做,做不到的**不假装** + +| 用户想要 | mcppls 侧的可用手段 | 本方案 | +|---|---|---| +| 看 LSP 状态 | `readState()` 的 `state` / `project` / `profile` / `engine(s)` / `progress` | ✅ 视图 + 环境自检 | +| 看模块问题 | `readState()` 的 `issues` / `notices`,含修复命令 | ✅ 逐条列出 + 修复按钮 | +| 看构建描述 | `readState().project.{source, level, tier}` | ✅(**条数/单元数**只有 `mcppls check`/`model` 才有,见下) | +| 看引擎 | `readState().engine` / `engines[]` | ✅ | +| 重启语言服务 / 重启 clangd | `mcppls.restartServer` / `mcppls.restartClangd` | ✅ `confirm` | +| **清理 mcppls 的缓存** | `mcppls.resetWorkspaceCache`(它自己的"重置本工作区缓存") | ✅ **`destructive` + modal 确认**;确认框写明"会清掉本工作区的模型缓存并重新准备,可能耗时数分钟;**不影响** mcpp/全局构建缓存与项目 `target/`" | +| **mcppls 缓存的体积 / 占比 / 可视化** | ❌ 缓存目录**不在** `environment()` 里(它只给 version / vscode / appName / appHost / uiKind / platform / remote),也没有公开命令返回路径或大小;磁盘布局是内部实现 | ❌ **不做**。视图里给"**打开日志**""**收集诊断报告**""**导出诊断包**"三个入口,并在该区块写明"缓存由 C++ Modules 扩展自行管理,本扩展不读取其内部目录"。**向上游提 change request**(附录 D.5):注册一个返回缓存目录/体量的命令 | +| 看诊断 | `readState().issues` + `mcppls.showLogs` + `mcppls.collectReport` / `exportDiagnosticBundle` | ✅ | +| 关掉别的 C++ 扩展 | `mcppls.turnOffOtherCppFeatures` / `restoreOtherCppFeatures` | ✅ `confirm` | +| 在本工作区启用/停用 C++ Modules | `mcppls.turnOnInWorkspace` / `turnOffInWorkspace`(它们写 `mcppls.enable`,是 mcppls 自己的命令) | ✅ `confirm`;视图里显示当前值(只读) | +| 在终端运行构建工具 | `mcppls.runBuildToolInTerminal` | ✅ `confirm` | +| Review Changes | `mcppls.review.run` / `review.clear`(服务器命令,受 `mcppls.ai.enabled` 约束) | ✅ 仅在能力可用时显示,**只转发** | + +> **为什么"构建描述条数"要打折**:`readState()` 只给 `source/level/tier`,不给条目数。 +> 想要"6 entries / 2 std units"这种数字,只能跑 `mcppls check ` 或 `mcppls model` +> —— 那是**另起一个进程加载工程**(重则数分钟),不适合放进视图。 +> 因此本方案的视图**不显示假的条目数**;重信息只在用户**显式点击** +> "环境自检 / 收集诊断报告"时由 mcppls 自己产出。 + +#### 3.9.4 UI:第三个视图 + +`mcpp` 容器新增 **「C++ Modules」** 视图(`mcpp.languageServer`),`package.json` 的 +`views.when` = `mcpp.hasMcppls`(mcppls 存在才显示)。**必须**在视图 `name`/`description` +里写清提供方,避免冒充: + +``` +C++ Modules(由 sunrisepeak.mcpp-language-server 提供,此视图只做展示与转发) +├── 状态 ready · 项目 greeter · 级别 3 +├── 语义配置 clangd 22.1.8 · libc++ · x86_64-linux-gnu · c++26 +├── 构建描述 mcpp(来源)· S1 level 3 · tier 1 +├── 引擎 clangd 23.1.0 [core/ready] · mcppls [modules/ready] +├── 问题(2) $(warning) producer-needs-download … [$(lightbulb) 允许联网重述] +│ $(error) unresolved-module … +├── 通知(1) $(info) 上游写入工程目录 +├── 最近在线运行 fetched · 2026-10-02T12:40:11Z (仅有值时显示) +└── 操作 + [$(refresh) 重启语言服务] [$(chip) 重启 clangd…] [$(history) 重置本工作区缓存…] + [$(symbol-interface) 选择上下文] [$(type-hierarchy) 模块图] [$(output) 打开日志] + [$(report) 收集诊断报告] [$(package) 导出诊断包…] [$(debug-alt) 在终端运行构建工具…] + [$(settings-gear) 打开 C++ Modules 设置] [$(circle-slash) 在本工作区停用…] +``` + +- 问题条目:`$(warning)` / `$(error)` 图标按 `code` 白名单着色(`charts-yellow` / `errorForeground`), + 有 `command` 的显示 `$(lightbulb)` 按钮; +- 状态项用 `charts-green`(ready)/ `charts-yellow`(degraded)/ `errorForeground`(error), + `starting|loading|preparing` 用 `$(sync~spin)`; +- 多根工作区:按 workspace folder 分组(S3 明确每 root 一条状态); +- **空态**:mcppls 未安装/未启用 → 一行说明 + "安装/启用 C++ Modules"按钮; + `readState` 不可用 → "已安装的 mcppls(0.0.x)未暴露状态;下列操作仍可用"; +- **不新增第二个状态栏项**。想要的话,`mcpp.ui.statusBar.showLanguageServer`(默认 **false**) + 会在我们自己的状态栏项里追加一段 `· C++ Modules: ready`,tooltip 里说明"状态由 mcppls 提供"。 + +#### 3.9.5 与 mcppls 自己的 UI 的关系 + +mcppls 已经有一个 C++ Language Status Item、一个 "C++ Modules" 输出频道和一批调色板命令。 +本视图**不取代**它们,定位是: + +- **mcpp 工程视角的汇总**:把"这个工程的 mcpp 侧"和"这个工程的 C++ 语义侧"放在同一棵树里; +- **可发现性**:把 mcppls 散落在命令面板里的管理动作变成可见按钮; +- **诊断入口**:问题 → 修复命令 / 日志 / 诊断包,一条路径走完。 + +因此**不复制** mcppls 的状态栏项,**不合并**输出频道,**不代理**它的设置写入 +(唯一例外是转发它自己的 `turnOn/TurnOffInWorkspace` 命令,那仍然由 mcppls 执行)。 + +--- + +## 4. 主线 D:统一配置模块与配置面板 + +> 设计目标来自评审意见:「凡可配置的功能都要支持配置;配置功能作为独立模块统一管理」。 +> **上游先例**:mcppls 就是这么做的 —— `src/config/settings.cppm` 是唯一的配置注册表, +> 文档、`package.json`、命令行全部由它派生,并由 `tests/test_settings.cpp` 把三者钉在一起。 +> 本方案采用同一套思路。 + +### 4.1 单一事实源 `data/config-registry.json` + +```jsonc +{ + "version": 1, + "groups": [ + { "id": "project", "titleKey": "config.group.project", "order": 10 }, + { "id": "cache", "titleKey": "config.group.cache", "order": 60 } + ], + "settings": [ + { + "key": "mcpp.cache.staleDays", + "type": "number", + "default": 3, + "minimum": 0, + "maximum": 365, + "scope": "resource", // resource | window | machine-overridable + "group": "cache", + "order": 40, + "titleKey": "config.cache.staleDays.title", + "descriptionKey": "config.cache.staleDays.description", + "applies": "next-clean", // immediate | next-build | next-clean | view-reload + "advanced": false, + "tier": "public", // public | advanced(§4.6) + "since": "0.5.0", + "docs": "docs/cache.md#staledays", + "aliases": [] + } + ] +} +``` + +每个条目回答四件事:**是什么、默认什么、什么时候生效、在文档哪一节**。 + +### 4.2 由 registry 派生的四样东西 + +| 产物 | 由谁生成 | 门禁 | +|---|---|---| +| `package.json` 的 `contributes.configuration` | **手写,但由 `tools/check-config.mjs` 做语义一致性校验**(key 集合、type、default、enum、scope、标题 key) | CI:不一致即失败并提示"运行 `npm run gen:config` 同步" | +| `package.nls.json` / `package.nls.zh-cn.json` 的设置文案骨架 | `tools/generate-config.mjs` 生成骨架,人工填中文 | `l10n-check.mjs` 覆盖门禁 | +| `docs/settings.md` 的设置表 | `tools/generate-config.mjs` 生成表格 | `git diff --exit-code` | +| 配置面板 + `mcpp: 环境自检` 的"已改设置"清单 | 运行期读 `src/config/registry.ts` | 单测 | + +> 为什么不直接生成 `package.json`:JSON 往返会重排整个文件,产生无法 review 的巨 diff。 +> 语义校验比文本生成更稳。 + +### 4.3 配置面板(`mcpp: 打开设置面板`) + +- webview,按 registry 的 `groups` 分节渲染;纯 HTML/CSS/JS,严格 CSP,无网络,无框架; +- **每一行**:标题、说明、控件(checkbox / 下拉 / 数字 / 路径选择 / 字符串数组)、 + **当前生效值 + 来源**(默认 / 用户 / 工作区 / 工作区文件夹)、`applies` 提示 + ("下次清理生效")、「重置为默认」、「在原生设置中打开」(`workbench.action.openSettings` + + `@ext:mcpp-community.mcpp-vscode `); +- 工具栏:搜索、**仅显示已修改**、分组折叠、预设(**默认 / 极简 / 重度使用 / 只读浏览**)、 + 导出/导入设置 JSON、全部重置; +- 顶部固定一条边界提示:**这里只影响 mcpp-vscode;`mcppls.*` 由 C++ Modules 扩展自己管理, + 这个面板不写它**;并提供"打开 C++ Modules 设置"按钮 + (`workbench.action.openSettings "@ext:sunrisepeak.mcpp-language-server"`); +- `advanced: true` 的条目默认折叠; +- 多根工作区:按工作区文件夹分别显示"工作区值",并显示最终生效值; +- 写入走 `WorkspaceConfiguration.update(key, value, target)`,`resource` 型设置必须带 URI。 + +### 4.4 完整设置清单 + +> 这是评审意见「很多功能可以做成配置的都要支持配置」的落地。共 **60 项**,全部有默认值, +> 且**默认值一律是"不打扰"**。`advanced` 标记表示面板默认折叠。 + +**工程** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.path` | string | `""` | resource | immediate | +| `mcpp.project.discoveryBoundary` | `workspaceFolder` \| `filesystem` | `workspaceFolder` | resource | immediate | + +**任务** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.task.buildArgs` | string[] | `[]` | resource | next-build | +| `mcpp.task.runArgs` | string[] | `[]` | resource | next-build | +| `mcpp.task.testArgs` | string[] | `[]` | resource | next-build | +| `mcpp.task.cleanArgs` | string[] | `[]` | resource | next-clean | +| `mcpp.task.confirmClean` | boolean | `true` | resource | immediate | +| `mcpp.task.revealTerminal` | `always` \| `onFailure` \| `never` | `always` | resource | immediate | +| `mcpp.task.focusTerminal` | boolean | `false` | resource | immediate | +| `mcpp.task.clearTerminal` | boolean | `true` | resource | immediate | +| `mcpp.task.problemMatcher` | boolean | `true` | resource | immediate | +| `mcpp.task.editorTitleButtons` | boolean | `true` | resource | immediate | + +**语言服务(mcppls 桥接与状态)** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.languageService.refreshAfterBuild` | `auto` \| `reload` \| `restart` \| `off` | `auto` | resource | immediate | +| `mcpp.languageService.menuItems` | boolean | `true` | resource | immediate | +| `mcpp.languageService.notifyOnDegraded` | boolean | `true` | resource | immediate | +| `mcpp.languageService.readState` | boolean | `true` | resource | immediate | +| `mcpp.languageService.stateRefreshSeconds` | number | `0`(关) | resource | immediate | +| `mcpp.languageService.confirmResetCache` | boolean | `true` | resource | immediate | + +**`mcpp.toml` 编辑** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.toml.completion` | boolean | `true` | resource | immediate | +| `mcpp.toml.hover` | boolean | `true` | resource | immediate | +| `mcpp.toml.navigation` | boolean | `true` | resource | immediate | +| `mcpp.toml.diagnostics.enabled` | boolean | `true` | resource | immediate | +| `mcpp.toml.diagnostics.syntax` | severity | `error` | resource | immediate | +| `mcpp.toml.diagnostics.unknownSection` | severity | **`warning`** | resource | immediate | +| `mcpp.toml.diagnostics.unknownKey` | severity | **`warning`** | resource | immediate | +| `mcpp.toml.diagnostics.planeSeparation` | severity | `warning` | resource | immediate | +| `mcpp.toml.diagnostics.legacyKeys` | severity | `info` | resource | immediate | +| `mcpp.toml.indexCompletion` | boolean | `false` | resource | immediate | +| `mcpp.toml.indexCompletionTimeoutSeconds` | number | `20` | resource | immediate | + +(`severity` = `error` \| `warning` \| `info` \| `off`) + +**`build.mcpp` 编辑** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.buildScript.intelligence` | boolean | `true` | resource | immediate | +| `mcpp.buildScript.diagnostics` | boolean | `true` | resource | immediate | +| `mcpp.buildScript.diagnostics.severity` | `warning` \| `info` \| `off` | `warning` | resource | immediate | +| `mcpp.buildScript.imports.knownModules` | boolean | `true` | resource | immediate | +| `mcpp.buildScript.snippets` | boolean | `true` | resource | immediate | + +**缓存与清理** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.cache.statusBar` | boolean | `false` | resource | immediate | +| `mcpp.cache.warnAboveGiB` | number | `0`(关) | resource | immediate | +| `mcpp.cache.staleDays` | number | **`3`** | resource | next-clean | +| `mcpp.cache.autoRefreshSeconds` | number | `0`(关) | resource | immediate | +| `mcpp.cache.estimateProjectBytes` | boolean | `true` | resource | immediate | +| `mcpp.cache.showLegacy` | boolean | `true` | resource | immediate | +| `mcpp.cache.gc.defaultBudgetGiB` | number | `0`(每次追问) | resource | immediate | +| `mcpp.cache.gc.confirmAboveGiB` | number | `1` | resource | immediate | +| `mcpp.cache.pruneAgeDays` | number | `30` | resource | immediate | + +**视图** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.views.project.show` | boolean | `true` | window | view-reload | +| `mcpp.views.cache.show` | boolean | `true` | window | view-reload | +| `mcpp.views.languageServer.show` | boolean | `true` | window | view-reload | +| `mcpp.views.cache.topN` | number | `5` | window | immediate | +| `mcpp.views.cache.ageBuckets` | string[] | `["1d","7d","30d"]` | window | immediate | + +**界面与通知** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.ui.language` | `auto` \| `en` \| `zh-cn` | `auto` | window | immediate | +| `mcpp.ui.statusBar.show` | boolean | `true` | window | immediate | +| `mcpp.ui.statusBar.showLanguageServer` | boolean | **`false`** | window | immediate | +| `mcpp.ui.notifications.success` | `silent` \| `statusBar` \| `toast` | `statusBar` | window | immediate | +| `mcpp.ui.notifications.dedupeMinutes` | number | `5` | window | immediate | +| `mcpp.ui.confirmDestructiveOnly` | boolean | `true` | window | immediate | +| `mcpp.ui.numberFormat` | `binary` \| `decimal` | `binary` | window | immediate | + +**诊断与日志** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.log.level` | `error` \| `warn` \| `info` \| `debug` | `info` | window | immediate | +| `mcpp.diagnostics.selfCheckOnStartup` | boolean | `false` | window | immediate | + +**高级(面板默认折叠)** + +| key | 类型 | 默认 | 作用域 | 生效 | +|---|---|---|---|---| +| `mcpp.runtime.timeoutSeconds` | number | `30`(0 = 不限) | resource | immediate | +| `mcpp.runtime.maxOutputMiB` | number | `16` | resource | immediate | +| `mcpp.runtime.concurrency` | `perProject` \| `global` | `perProject` | resource | immediate | + +**弃用(保留为别名,面板不显示)**:`mcpp.clangd.path`、`mcpp.modulesSupport`、 +`mcpp.configureCppTools`、`mcpp.tomlCompletion`(→ `mcpp.toml.completion`)。 + +### 4.5 配置模块的职责边界 + +| 归 mcpp-vscode 配置 | 不归 | +|---|---| +| 上表全部 `mcpp.*` | `mcppls.*`(C++ Modules 扩展自己管理,本插件**只读不写**) | +| 面板里明确提示这条边界,并给"打开 C++ Modules 设置"按钮 | 用户的 `clangd.*`(已不在依赖面内) | + +### 4.6 第一版公开发布的设置(29 项) + +按评审意见,第一版**只在原生设置页与面板的常用区公开这些**;其余进入 `advanced` / +`tier: "advanced"`,默认折叠、文档里可查、面板里可搜到。registry 的每个条目因此多一个 +`tier: "public" | "advanced"` 字段。 + +**public(29 项)** + +| 分组 | 键 | +|---|---| +| 工程(1) | `mcpp.path` | +| 任务(8) | `buildArgs`、`runArgs`、`testArgs`、`cleanArgs`、`confirmClean`、`problemMatcher`、`focusTerminal`、`editorTitleButtons` | +| 语言服务(1) | `refreshAfterBuild` | +| `mcpp.toml`(6) | `completion`、`hover`、`navigation`、`diagnostics.enabled`、`diagnostics.unknownSection`、`diagnostics.unknownKey` | +| `build.mcpp`(2) | `intelligence`、`diagnostics` | +| 缓存(4) | `staleDays`、`statusBar`、`warnAboveGiB`、`gc.defaultBudgetGiB` | +| 视图(3) | `views.project.show`、`views.cache.show`、`views.languageServer.show` | +| 界面(3) | `ui.language`、`ui.notifications.success`、`ui.numberFormat` | +| 日志(1) | `log.level` | +| **合计** | **29** | + +**advanced(其余,默认折叠)**:`project.discoveryBoundary`、`revealTerminal`、`clearTerminal`、 +`languageService.{menuItems,notifyOnDegraded,readState,stateRefreshSeconds,confirmResetCache}`、 +`toml.{diagnostics.syntax,diagnostics.planeSeparation,diagnostics.legacyKeys,indexCompletion,indexCompletionTimeoutSeconds}`、 +`buildScript.{diagnostics.severity,imports.knownModules,snippets}`、 +`cache.{autoRefreshSeconds,estimateProjectBytes,showLegacy,gc.confirmAboveGiB,pruneAgeDays}`、 +`views.cache.{topN,ageBuckets}`、`ui.{statusBar.show,statusBar.showLanguageServer,notifications.dedupeMinutes,confirmDestructiveOnly}`、 +`diagnostics.selfCheckOnStartup`、`runtime.*`。 + +> 面板里有一个"显示高级设置"开关;公开与高级**只影响默认展开与设置页排序**, +> 不影响功能可用性。下一版根据使用反馈决定是否把某些项提升为 public。 + +--- + +## 5. 实施顺序与里程碑 + +| 里程碑 | 内容 | 预估 | +|---|---|---| +| **M0** | 目录纯移动 + `docs/superpowers/` → `.agents/superpowers/` + README 中英 + `docs/*` + `tools/` 与 CI 漂移 job 骨架 | 3 天 | +| **M1** | i18n:`data/i18n/*` + `generate-l10n` + `l10n-check` + `package.nls.*` 全量 + 关键流程文案 + 测试改写 | 2–3 天 | +| **M2** | 配置模块:`config-registry.json` + `check-config` + `access/validate/migrate` + `docs/settings.md` 生成 | 2–3 天 | +| **M3** | 配置面板(`mcpp: 打开设置面板`):分组渲染、来源显示、预设、搜索、仅显示已修改 | 2–3 天 | +| **M4** | 稳定性基座(协议优先、超时分级、错误分层、`mcpp.path` 校验、激活面收敛) | 2–3 天 | +| **M5** | 依赖韧性(能力模型、降级矩阵、五个 stub、`mcpp: 环境自检`) | 2–3 天 | +| **M5.5** | §3.9 mcppls 状态与管理:「C++ Modules」视图、`readState` 适配层、转发命令(13 个)、问题与修复按钮 | 3 天 | +| **M6** | `mcpp.toml` 编辑体验(schema 快照 + 键/值补全 + 悬停 + 诊断 + 跳转 + 依赖版本补全) | 4 天 | +| **M7** | `build.mcpp` 智能(快照 + provider + 静态诊断 + 模块清单 + 片段) | 3–4 天 | +| **M8** | 缓存统计与清理(聚合 + TreeView + 面板可视化 + 预算模拟器 + 命令) | 5 天 | +| **M9** | 视觉与交互统一(主题令牌、三态、状态栏、快捷键、进度) | 2 天 | + +M0–M2 先做:M0 定文件位置,M1 定文案写法,M2 定设置写法 —— 后面所有功能都会往这三处加东西, +越晚做返工越大。 + +--- + +## 6. 风险与取舍 + +| 风险 | 说明 | 缓解 | +|---|---|---| +| **设置项数量(60 项)本身就是风险** | 认识负担与"简单易用"相矛盾;测试面变大 | 面板默认只展开常用分组,`advanced` 折叠;全部默认"不打扰";`docs/settings.md` 给"推荐配置";预设一键切换 | +| 目录大移动污染 review | 触碰所有 import | M0 拆两提交,提交 1 零行为变化 | +| 配置面板与原生设置页重复 | 用户困惑"改了不生效" | 每项显示 `applies` 与**生效值来源**;给"在原生设置中打开";面板顶部写明与 `mcppls.*` 的边界 | +| 生成的 nls/docs 与 registry 漂移 | 三份文件要同步 | `check-config.mjs` + `l10n-check.mjs` + `git diff --exit-code` 三重门禁 | +| `mcpp.ui.language` 的能力边界 | manifest 文案无法运行时切换 | 写进 `docs/settings.md`;默认 `auto`;定位为"逃生门" | +| 模式 A 下 std 没有符号级补全 | 真实体验缺口 | 文档写清;上游诉求(把构建程序写进 build database);模式 B 的设计保留在附录 D 备查 | +| 依赖版本补全靠解析人类输出 | 与本方案"JSON 优先"原则冲突 | 明确列为唯一例外、默认关、带超时/离线降级/一次性提示;docs 写明"上游给机读格式就替换" | +| 预算模拟器是本地 LRU 近似 | 与 mcpp 实际策略可能有差异 | 标注"预估",只用于"选预算",不承诺删多少;执行后以 mcpp 输出为准 | +| `cache info` 无机器格式 | 详情视图只能展示原文 | 只读预览文档,不解析、不据此决策 | +| U2 的实现成本最高 | TreeView + 面板 + CSP 资源维护面 | 面板可延后到 M8 后半段;先出 TreeView 与命令;逻辑抽成纯函数,UI 只渲染 | +| `onLanguage:cpp` 收敛有副作用 | 无 `mcpp.toml` 的目录不再出现 mcpp 状态栏 | 有意的;写进 CHANGELOG 与 `docs/architecture.md` | +| TreeView/webview 的 e2e 覆盖有限 | Extension Host 里不能点 UI | 逻辑抽纯函数(聚合、格式化、模拟器)单测覆盖;e2e 只断言命令与视图注册 | +| 里程碑估算偏乐观 | M6–M8 都依赖"生成脚本 + 快照 + CI 门禁"三件套 | M0 就把 `tools/` 与漂移 job 骨架搭好 | +| **`extension.exports` 是 mcppls 的测试 API** | 不是契约;上游重构可能让 §3.9 的状态区整块消失 | 形状探测 + try/catch + 枚举白名单;坏掉只少一块内容(不变式 ④);两个 e2e 夹具(`noapi` / `throwing`)钉住降级行为;由 `mcpp.languageService.readState` 可关 | +| **mcppls 的缓存体量无法可视化** | 缓存目录不在任何公开面里(`environment()` 只给版本/平台/宿主),磁盘布局是内部实现 | **不做假数字**;该区块只给"重置本工作区缓存(它自己的命令)+ 日志 + 诊断包",并向上游提 change request(附录 D.5)。若评审坚持要有数字,需要先拿到上游接口 | +| **`readState` 的轮询会放大风险** | 高频调用可能影响 mcppls 主线程 | 默认 `stateRefreshSeconds = 0`(不轮询),只在 `onDidChange`、命令完成、用户手动刷新时读 | + +--- + +## 7. 自我 review(v4) + +### 7.1 这一版改对了什么 + +1. **v2 的 `build.mcpp` 结论基于实测**,v3 把它固化成"只做模式 A",v4 把模式 B 留在附录 D 备查。 +2. **把配置提升为主线**:设置从"功能的一部分"变成"功能的入口"(registry + 面板 + 四重门禁)。 +3. **两级清理与全局缓存分离**:语义上正确(项目级 vs 机器级),避免 `--bmi-cache` 误点导致全机重建。 +4. **i18n 从"改字符串"变成"改机制"**:单一来源 + 生成 + 门禁。 +5. **v4 新增 §3.9**:把 mcppls 的状态与管理并入 mcpp,且**先说清"拿不到什么"** + —— 缓存体量拿不到就不做数字,构建描述条数拿不到就不显示假条数。 + 这一节的价值不在于多做了功能,而在于**把"能做的"和"做不到的"划在同一条界线上**, + 避免以后有人补一个看起来很美的假面板。 + +### 7.2 我仍然不放心的地方(按严重度) + +| # | 问题 | 我的判断 | +|---|---|---| +| 1 | **设置总数 60 项依然偏多**,即便第一版只公开 29 项 | 已按评审意见分 `public`/`advanced`(§4.6);建议下一版按使用反馈只增不减地调整,不新增"品味型"设置 | +| 2 | **§3.9 建在 `extension.exports` 上**,它明确是 mcppls 的**测试 API** | 这是"在不改上游的前提下能拿到 LSP 状态"的唯一通道。已做形状探测 + 白名单 + 两个 e2e 夹具 + 可关设置。**风险与收益都在这里**:如果评审认为不能用,§3.9 就只剩"转发命令"和版本信息 | +| 3 | **mcppls 缓存体量确实看不到** | 不假装。给它的三个替代入口(重置/日志/诊断包)+ 上游 change request。若你希望一定要有数字,需要先推动上游 | +| 4 | **`mcpp.ui.language` 的手动覆盖只能覆盖一半界面** | 保留但标注(已定) | +| 5 | **配置面板与原生设置页的长期关系** | 定位为"集中 + 解释 + 预设 + 边界提示",不取代(已定) | +| 6 | **`mcpp.toml` schema 的枚举需人工补** | 唯一无法全自动处;写进 `tools/` 注释说明来源 | +| 7 | **缓存面板的预算模拟器可能过度设计** | 若 M8 时间紧,**第一个砍它** | +| 8 | **`mcpp clean --dry-run` 只有文本** | 可接受;将来 mcpp 给机读就换 | +| 9 | **§3.9 会让视图容器从 2 个视图变成 3 个** | Activity Bar 的侵入感增加;但 `mcpp.views.languageServer.show` 可关,且视图在 mcppls 不存在时不显示 | +| 10 | **`mcpp search` 解析人类输出仍是唯一的口子** | 已列为待替换项 | +| 11 | **模式 A 的 `mcpp::` 智能依赖 mcpp 源码结构**(`directives.cppm` 的表位置) | 生成脚本读不到就**失败而不是产出空表**;快照带 `sourceVersion`;漂移 job 在 CI 里跑 | + +### 7.3 如果时间不够,砍的顺序 + +1. `mcpp.toml.indexCompletion`(依赖版本补全)—— 唯一需要联网、唯一解析人类输出; +2. 缓存面板的预算模拟器 —— 保留"选预算 + 执行 + 看结果"; +3. `mcpp.ui.language` 的手动覆盖 —— 只保留跟随 VS Code; +4. `mcpp.pack` / `publish --dry-run` 入口 —— 与 IDE 体验关系最弱; +5. `mcpp.ui.notifications.success` / `mcpp.ui.numberFormat` 这类"品味型"设置 —— 固定为默认值即可。 + +**不砍的**:两级清理、缓存统计与可视化、`build.mcpp` 智能、`mcpp.toml` 诊断、配置模块与面板、 +mcppls 降级能力与状态视图、i18n 机制本身。 + +**v4 新增第 0 顺位(最先砍)**:`mcpp.languageService.readState` 走 `exports` 的那条路 +—— 如果评审认为"依赖测试 API"不可接受,§3.9 退化成"只有转发命令 + 版本信息", +其余设计不受影响。 + +--- + +## 8. 评审已决的问题与遗留 + +**v3 的 5 个问题已全部拍板**(见 §0 第 16–20 条):① 第一版公开 31 项、其余 advanced; +② 保留 `mcpp.ui.language` 并标注;③ 保留依赖版本补全;④ 暴露 `cache clean --all` 但走两步确认; +⑤ 配置面板定位为"集中/解释/预设/边界提示",不取代原生设置页。 + +**v4 遗留(只有一条需要你决定)**: + +1. **§3.9 的状态读取是否接受 `extension.exports`**(mcppls 的测试 API)? + - **接受**(推荐):能显示 `state`/`project`/`profile`/`engine(s)`/`progress`/`issues` + (含 S3 自带的修复命令),代价是上游重构后这一块可能消失(已用形状探测 + 白名单 + + 两个 e2e 夹具 + 可关设置兜住)。 + - **不接受**:§3.9 只保留"转发命令 + 版本/激活/启用信息",视图仍然有用但内容少很多; + 我会同时把"上游提供一个正式的状态/缓存查询命令"提为**更高优先级的 change request**。 + +**v4 记录在案的上游诉求**(本方案不改上游,只记录): + +| # | 对象 | 诉求 | 本插件因此获得 | +|---|---|---|---| +| U.1 | mcpp | 在 `mcpp emit build-database` 里描述构建程序自身(含 `-fmodule-file=mcpp=…` 与 std 单元),该命令不写工程目录 | `build.mcpp` 的 std + mcpp 完整语义(附录 D.1 的模式 B 变成默认,无需改本插件) | +| U.2 | mcppls | 把 `mcppls.reloadBuildDescription` 注册为 VS Code 命令 | build 后从"整机重启"降级为"轻量重载" | +| U.3 | mcppls | 把 `mcppls.mcpp` 暴露为可配置设置 | `mcpp.path` 与语言服务看到同一个 mcpp | +| U.4 | mcppls | 书面承诺 4 个命令 ID 的稳定性 | 把下游测试护栏升级为上游承诺 | +| U.5 | mcppls | 注册一个返回**缓存目录与体量**(或直接清理)的公开命令 | §3.9.3 里目前做不到的"缓存占比可视化" | +| U.6 | mcpp | `mcpp search` 提供机读格式 | 去掉方案里唯一的"解析人类输出" | + +--- + +## 附录 A:本方案用到的 mcpp 命令 + +| 命令 | 用途 | 写盘 | 机读 | +|---|---|---|---| +| `mcpp --protocol-version` | 协议/能力探测 | 否 | ✅ | +| `mcpp self env --format json` | 版本、MCPP_HOME、默认工具链 | 否 | ✅ | +| `mcpp toolchain list --format json` | 工具链/target 矩阵 | 否 | ✅ | +| `mcpp cache list --format json` | 缓存条目与体积 | 否 | ✅ | +| `mcpp cache dir` / `mcpp cache info ` | 缓存根 / 条目详情 | 否 | ❌ | +| `mcpp cache verify` | 缓存校验 | 否 | ❌ | +| `mcpp cache prune --older-than d` | 按时间收敛 | 是 | — | +| `mcpp cache gc --max-size GiB` | 按预算收敛(LRU) | 是 | — | +| `mcpp cache clean --deps\|--std\|--all\|--legacy` | 分类清空 | 是 | — | +| `mcpp clean --stale --dry-run` | 过期产物**预演** | 否 | ❌ | +| `mcpp clean` | L1 清理 | 是 | — | +| `mcpp clean --stale --older-than d` | L2 清理 | 是 | — | +| `mcpp clean --bmi-cache` | L1 + 全局缓存(单独、更重确认) | 是 | — | +| `mcpp search [--all-versions]` | 依赖版本补全(默认关) | 否 | ❌ | +| `mcpp update` / `mcpp self doctor` | 依赖更新 / 环境诊断 | 是 / 否 | — | +| `mcpp build/run/test`、`mcpp toolchain install/default` | 现有任务 | 是 | — | + +## 附录 B:实测证据 + +| 结论 | 证据 | +|---|---| +| mcpp 不把 `build.mcpp` 放进编译数据库 | `examples/11-features/greeter` 上 `mcpp emit build-database --spec compile-commands --format json` → 6 条(counters.cppm / greeter.cppm / main.cpp / test_greet.cpp / std.cppm / std.compat.cppm);`build.mcpp` 只在 `data.watch` | +| mcppls 不认识 `build.mcpp` | 全仓 grep 只命中 `target/.build-mcpp/deps/...`(依赖的构建程序产物) | +| 交给 clangd 后 std 与 mcpp 都报错 | `mcppls check build.mcpp` → `module 'std' not found` + `module 'mcpp' not found` + `Failed to build module mcpp`,clangd exit 3 | +| 数据库内的文件零诊断 | `mcppls check src/main.cpp` → clangd exit 0 | +| 构建脚本 API 可机读 | `modules/buildmcpp/src/directives.cppm:297`(31 行表)、`:1132-1134`(5 role)、`program_protocol.cppm:121,147`、`provisions.cppm:80` | +| 缓存数据可机读 | `mcpp cache list --format json` → kind `mcpp.cache`,657 条 / 7.20 GiB / pkg 576 · std 81 / 2 incomplete | +| `--stale` 语义与默认 | `mcpp clean --help`:`--older-than … (default 1d; 0 keeps none; implies --stale)`;`--dry-run` 隐含 `--stale` 且不删 | +| `search` / `info` / `verify` 无机器格式 | 各自 `--help` 只有人类选项;`search` 只有 `--all-versions` | + +## 附录 C:配置项 ↔ 功能 对照(抽查) + +| 功能 | 可配置项 | +|---|---| +| 构建 | `mcpp.task.buildArgs`、`revealTerminal`、`focusTerminal`、`clearTerminal`、`problemMatcher` | +| 清理 | `mcpp.task.cleanArgs`、`confirmClean`、`mcpp.cache.staleDays`、`pruneAgeDays`、`gc.defaultBudgetGiB`、`gc.confirmAboveGiB` | +| 语言服务 | `mcpp.languageService.refreshAfterBuild`、`menuItems`、`notifyOnDegraded` | +| mcppls 状态与管理(§3.9) | `mcpp.views.languageServer.show`、`mcpp.languageService.readState`、`stateRefreshSeconds`、`confirmResetCache`、`mcpp.ui.statusBar.showLanguageServer` | +| `mcpp.toml` | `mcpp.toml.*`(11 项) | +| `build.mcpp` | `mcpp.buildScript.*`(5 项) | +| 视图 | `mcpp.views.*`(5 项) | +| 通知与视觉 | `mcpp.ui.*`(7 项) | +| 排障 | `mcpp.log.level`、`mcpp.diagnostics.selfCheckOnStartup`、`mcpp.runtime.*` | + +## 附录 D:本版**不实现**、仅文档备注的项 + +1. **`build.mcpp` 模式 B(交给 C++ 语言服务)**:实测会让 `import std` 与 `import mcpp` 都报错 + (附录 B),因此不发布开关。文档备注保留这条路径,等上游把构建程序写进 + `mcpp emit build-database`(诉求 **U.1**)之后再启用 —— 那条改动只在 mcpp 一侧, + 本插件无需改动即可受益。 +2. **mcppls 缓存目录的体积与占比**:拿不到(§3.9.3),等诉求 **U.5**。 +3. **未受信任工作区下的 mcpp 只读命令白名单**:当前是一律禁用;将来若 mcpp 的 + `--protocol-version` 能证明某命令零副作用,再考虑放开。 +4. **`mcpp.toml` 格式化**:不做(会改用户文件,且不是本扩展的职责)。 +5. **遥测**:不做。本扩展不收集任何遥测数据。 diff --git a/.agents/docs/archive/2026-10-02-ui-ux-optimisation-plan.md b/.agents/docs/archive/2026-10-02-ui-ux-optimisation-plan.md new file mode 100644 index 0000000..fe4ecbb --- /dev/null +++ b/.agents/docs/archive/2026-10-02-ui-ux-optimisation-plan.md @@ -0,0 +1,1466 @@ +# UI/UX 优化方案 v2(待 review) + +v1 的四个问题已收到答复,本版按答复重做设计,并补上你新提的三件事:**图标用 mcpp 官方的**、 +**梳理侧边栏**、**侧边栏整体可开关(关掉时左边不出现 mcpp)**。 + +--- + +## 0. 你的答复与我的理解 + +| 问题 | 你的答复 | 确认后的设计 | +|---|---|---| +| Q1 缓存视图形态 | **webview view** | `mcpp.cache` 从原生树改为**侧边栏 WebviewView**,统计与图形都在侧边栏内 | +| Q2 `mcpp: Cache Statistics` | **删除** | 删掉该命令与 `createWebviewPanel` 那条路径 | +| Q3 工程视图的动作 | "什么意思 按理是要能点击有动作的" | **我原来说得不清楚,见 §2。结论:动作保留、可点,只是与数据分区** | +| Q4 图标 | "不能用 mcpp 的 icon 吗,mcppls 里也有 logo" | **可以,而且必须**——现在两个扩展的 logo **不是同一张图**,见 §4 | + +--- + +## 1. 新查明的事实(都有出处) + +| # | 事实 | 出处 | +|---|---|---| +| F1 | mcpp 官方 logo 在 mcppls 仓库里,而且**就是 mcppls 扩展的图标**(两文件字节完全相同,md5 `f9457903…`) | `mcpp-language-server/docs/imgs/mcpp-logo.png` 与 `editors/vscode/icon.png` | +| F2 | **mcpp-vscode 用的是另一张图**(200×200,md5 `2d723543…`),与 mcppls 不一致 | `mcpp-vscode/images/logo.png` | +| F3 | mcppls 的 `mcpp-logo.svg` **不是真矢量**:excalidraw 导出,内部 `href` 是 base64 PNG。**上游没有可用的单色矢量图标** | `docs/imgs/mcpp-logo.svg` 开头即 `` | +| F4 | mcppls **没有贡献 Activity Bar 容器**(`viewsContainers: null`),所以左侧那个 mcpp 图标只可能来自 mcpp-vscode,不存在冲突 | mcppls `package.json` | +| F5 | **"所有视图都隐藏 → 容器从 Activity Bar 消失"是可行的**,但 `when` 必须写成**否定形式**(`!mcpp.sidebarHidden`),否则默认就被隐藏,`activate()` 不会被调用,也就永远没机会把开关设回来 | [VS Code issue #49145](https://github.com/microsoft/vscode/issues/49145)(closed/verified)+ [#48704](https://github.com/microsoft/vscode/issues/48704)(`viewsContainers` 本身不支持 `when`) | +| F6 | 现有开关只有三个**分视图**开关,**缺总开关** | `data/config-registry.json`: `mcpp.views.{project,cache,languageServer}.show` | + +**F5 是本方案的关键**,因为它决定了"关掉侧边栏"到底能不能做到——能,但写法有讲究,不能想当然。 + +--- + +## 2. 回答 Q3:动作不会消失,只是不再和数字混在一起 + +我上一版问的是"是否完全不出现动作",措辞不好,导致看起来像要**拿掉可点击性**。不是这个意思。 + +真实问题不是"有动作",而是**动作和数据是同一层的同级行**,视觉上没有分界: + +``` +现在(工程视图) 改后 +├ 包名 greeter ├ 包名 greeter +├ 标准 c++23 ├ 标准 c++23 +├ Build ← 动作 ├ 目标 x86_64… +├ Run ← 动作 ├ 工具链 llvm@22.1.8 +├ Test ← 动作 └─ 操作 ← 可折叠分组 +├ Test ← 动作 ├ 构建 (tools) +└ Clean project artifacts ← 动作 ├ 运行 (play) + ├ 测试 (beaker) + └ 清理产物 (trash) +``` + +**修订后的总原则(比 v1 更简单)**: + +> **数据区只读;操作区可点;两区之间有明确分界,且操作区默认折叠收起。** + +- 操作**依然是树/面板里可点的行**,点一下即执行——满足你说的"要能点击有动作"。 +- 视图标题栏只放**视图级**动作(刷新、溢出菜单),不再重复放业务动作。 +- 好处:打开视图先看到"是什么状态",要看动作展开一次即可;不需要在两类语义之间反复切换注意力。 + +--- + +## 3. 侧边栏整体梳理 + +### 3.1 结构(容器 `mcpp`,三个视图,顺序固定) + +``` +mcpp +├─ 工程 mcpp.project 树 "这是什么工程" +├─ 缓存 mcpp.cache WebviewView "占了多少、谁占的" +└─ C++ Modules mcpp.languageServer 树 "语言服务什么状态、哪里不对" +``` + +**为什么还是三个而不是合并成两个**:三者的**回答对象不同**,而且 C++ Modules 的内容由另一个扩展提供(`sunrisepeak.mcpp-language-server`)。合并会让"谁负责什么"重新变模糊——这正是上一轮花力气划清的边界。三个视图的取舍权交给用户(§3.3 的开关)。 + +### 3.2 每个视图的内容分区 + +**工程(树)** +``` +身份:包名 / 标准 / 类型 +目标:triple / profile / 编译器 +工具链:当前有效 spec(只读,改它用命令) +操作(折叠):构建 / 运行 / 测试 / 清理 target +``` + +**缓存(WebviewView,三区 + 一个操作条)** +``` +┌ 缓存 ────────────────────────── [↻] [⋯] ┐ +│ 7.20 GiB · 657 项 · 最早 12 天前 │ 区1 数字摘要(只读) +│ ████████████░░░░░░░░ 项目 12% │ 区2 可视化:构成 + 时间分布 +│ ██████████░░░░░░░░░░ std 31% │ 按 200–320px 窄宽度设计 +│ ▸ 最大占用(前 5) ▸ 按类型 ▸ 按时间 │ 区3 明细:只做「查看」钻取 +│ ──────────────────────────────────────── │ 分隔(视觉 + 语义) +│ 操作 清理过期 · 清理工程 · 收敛 · 校验 │ 操作条(与数据区分离) +└──────────────────────────────────────────┘ +``` +- 现在树里的 11 个动作节点全部收敛到这一条。 +- **`mcpp.showCachePanel` 删除**(Q2),`createWebviewPanel` 路径一并删掉,`cachePanelHtml.ts` 的渲染逻辑改为喂给 WebviewView。 + +**C++ Modules(树)** +``` +状态:就绪 / 降级 / 缺失(图标 + 一句话) + C++ Modules 0.0.9(外部扩展,只读) +问题:有问题才出现(引擎未就绪 / 某能力不可用 / 依赖缺失) +操作(折叠):重启 / 重选上下文 / 模块图 / 日志 / 上报 / 诊断包 / 重置缓存 / 身份与冲突 +``` +11 个动作节点从平铺变为一个折叠分组,状态与问题在它上面。 + +### 3.3 开关:一个总闸 + 三个分闸 + +| 设置 | 默认 | 作用 | +|---|---|---| +| `mcpp.views.enabled`(**新增**) | `true` | 总闸。关掉 → **Activity Bar 上的 mcpp 图标整体消失** | +| `mcpp.views.project.show` | `true` | 单个视图开关(已有) | +| `mcpp.views.cache.show` | `true` | 同上 | +| `mcpp.views.languageServer.show` | `true` | 同上 | + +**实现(关键,照 F5 的写法)**: + +```jsonc +// package.json —— when 必须是否定形式,默认才可见 +"views": { "mcpp": [ + { "id": "mcpp.project", "when": "!mcpp.sidebarHidden && mcpp.views.project" }, + { "id": "mcpp.cache", "when": "!mcpp.sidebarHidden && mcpp.views.cache" }, + { "id": "mcpp.languageServer", "when": "!mcpp.sidebarHidden && mcpp.views.languageServer" } +]} +``` +```ts +// extension.ts:设置变化时同步 +void vscode.commands.executeCommand("setContext", "mcpp.sidebarHidden", !read("mcpp.views.enabled")); +``` + +**必须验证的一点**(写进 e2e):`activate()` 的激活事件里**不能只依赖视图**,否则一旦隐藏就再也激活不了。当前激活事件是 `workspaceContains:mcpp.toml` / `onLanguage:*` / 派生的 `onCommand:*`,不依赖视图,所以安全——但这条要作为约束记下来并测试。 + +--- + +## 4. 图标:统一到 mcpp 官方标识(回答 Q4) + +**现状问题**:F1/F2 —— mcppls 用官方的 mcpp logo,mcpp-vscode 用另一张图。同一家族两个扩展并排出现在扩展列表里,看起来像两个不相干的项目。 + +**改法分两处,注意它们的要求不同**: + +| 用途 | 现在 | 改成 | 理由 | +|---|---|---|---| +| 扩展图标(Marketplace / 扩展列表) | `images/logo.png`(200×200,与官方不同) | **官方 `mcpp-logo.png`**(396×396,与 mcppls 同一张) | 家族一致;这是你在扩展列表里看到的图 | +| Activity Bar 容器图标 | 同一张 200×200 彩色 PNG | **单色 24×24 SVG** | Activity Bar 惯例是单色、跟随主题;彩色 PNG 缩到 24px 会糊成暗块,这正是你觉得"没有图标"的原因 | + +**一个必须先说清楚的限制**:F3 —— **上游没有可用的单色矢量 logo**(那个 `.svg` 里包的是 PNG)。所以 Activity Bar 的单色图标**必须新画**,方式二选一: + +- **方案 A(推荐)**:我按官方 logo 的形态**重绘一个极简单色 SVG**(几何化的 mcpp 标记,`fill="currentColor"`),先出 2–3 个候选给你挑。 +- **方案 B**:直接用官方彩色 PNG 作为容器图标(能显示、不改主题、24px 下偏糊)。 + +> 我不建议"自动描摹"官方 PNG——那会得到一个杂乱的路径,比手绘的几何标记更糟。 + +另外,`views` 条目本身也支持 `icon`(视图标题旁的小图标),目前我们一个都没设。**建议不加**:三个视图名称已经足够清楚,再加图标只会让标题栏更挤。 + +--- + +## 5. 任务拆分与依赖 + +``` +T-UI-1 图标统一 依赖:你选 A / B + ├─ 扩展图标换成官方 mcpp-logo.png(与 mcppls 对齐) + └─ 容器图标:单色 SVG(方案 A 需你从候选中挑一个) + +T-UI-2 快捷菜单:ThemeIcon + 分组分隔 + 描述改成"动作后果" 无依赖,可立刻做,风险最低 +T-UI-3 侧边栏梳理:顺序、操作区折叠、"操作"分组 无依赖 +T-UI-4 C++ Modules 重组:状态 / 问题 / 操作(折叠) 依赖 T-UI-3 的分组约定 +T-UI-5 缓存:树 → 侧边栏 WebviewView 依赖 Q1=webview view(已确认) + └─ 删除 mcpp.showCachePanel 与 createWebviewPanel 路径 +T-UI-6 总闸 mcpp.views.enabled + setContext(!mcpp.sidebarHidden) 依赖 T-UI-3 +T-UI-7 一致性守卫(关键) 依赖 T-UI-3~6 + ├─ 断言:视图正文的数据区里不存在命令节点 + ├─ 断言:每个动作的"主入口"在登记表中唯一 + └─ e2e:三个视图全部 when=false 后容器消失;且 activate 仍能被 workspaceContains 触发 +T-UI-8 文档与截图 依赖全部 +``` + +- **T-UI-2 可以现在就做**,最直接回应你的图标反馈。 +- **T-UI-3/4 是纯重排**,不动数据层,可以并行。 +- **T-UI-5 最大**:`createWebviewPanel` → `registerWebviewViewProvider`,注意 WebviewView **不支持 `retainContextWhenHidden`**,切换视图会销毁重建,所以渲染必须是幂等的(现有 `renderPanelHtml(model)` 天然满足)。 +- **T-UI-7 是本方案能不能站住的关键**:把"数据/操作分区"和"动作主入口唯一"变成**测试断言**,否则下一轮很容易又混回去。这与上一轮"64 个设置必须被读取"用的是同一个手法。 +- **T-UI-6 有一个必须测的边界**:隐藏后 `activate()` 还能不能被触发(F5 的陷阱)。 + +--- + +## 6. 需要你定的 3 件事 + +1. **Activity Bar 图标**:走方案 A(我画 2–3 个单色候选给你挑)还是方案 B(直接用彩色 PNG)? +2. **扩展图标是否统一为官方 mcpp logo**(与 mcppls 相同)?我建议是——这正是你问的"不能用 mcpp 的 icon 吗"。 +3. **总闸设置名**:`mcpp.views.enabled` 还是 `mcpp.sidebar.enabled`?(前者与既有的 `mcpp.views.*` 命名一致,我倾向它。) + +**T-UI-2 我建议不等,直接先做**;T-UI-3/4 只要"数据区只读、操作区折叠"这条原则你认可,也可以并行开工。 + +--- + +## 7. 可交互原型(已生成,本地打开) + +**`.agents/prototypes/ui-prototype.html`** —— 单文件、自包含(内嵌 VS Code 真实的 codicon 字体与官方 logo), +双击或用浏览器打开即可。**控制台里可以切:改前 / 改后、三个视图、三种主题、三个图标候选、侧边栏开关**; +拖侧边栏右缘能改宽度,用来验证窄宽度下的表现。 + +原型里的配色不是我自己编的,取自本机 VS Code 1.125.1 的默认主题文件 +(`theme-defaults/themes/2026-dark.json`、`2026-light.json`、`hc_black.json`,含 include 链)。 + +### 图标候选 A 是"真货" + +`mcpp-mark-traced.svg` 是**从官方 `mcpp-logo.png` 自动描摹出来的单色 SVG**(44×44 网格、148 条 +水平游程、4.9 KB),不是手绘近似。做法:解码 PNG → 按亮度分离前景 → 覆盖率采样 → 输出 +`fill="currentColor"` 的路径。所以它能跟随主题变色,可以直接当 Activity Bar 图标用。 +代价是边缘是阶梯状(44 格量化),要更干净需要人工重绘成曲线。 + +另外两个候选用于对比方向:B 是手绘的"模块方块",C 就是现在那张 200×200 彩色 PNG。 + +### 想请你在原型里重点看 + +1. **缓存**:改前是树(11 个可点动作和统计混在一起)→ 改后是侧边栏内的三区布局(摘要 / 图形 / 明细 + 独立操作条)。 +2. **C++ Modules**:改前 11 个动作平铺 → 改后只剩状态与问题,动作收进折叠的「操作」。 +3. **工程**:改前 Build/Run/Test/Clean 与"标准/目标/工具链"同级 → 改后数据在上、操作在下且折叠。 +4. **快捷菜单**:改前无图标、描述是重复的分组名 → 改后有图标、有分组分隔、描述改成"动作后果"。 +5. **Activity Bar 图标**:A / B / C 三个候选直接切换对比。 +6. **侧边栏总闸**:切到"关",确认左侧 mcpp 图标消失、其余图标不动。 + +--- + +## 8. v3:按你的第二轮反馈改(原型已同步重建) + +### 8.1 缓存:分开项目 / 全局,全局默认折叠 + +- **项目缓存**常显:26px 主数字 + `MiB` 单位 + 右侧一行副信息(文件数 / 构建目录),下面一条 6px 细条, + 再一行"其中过期产物约 4.1 MiB",最后两个按钮。 +- **全局缓存**用原生 `
` **默认折叠**,摘要压在一行里(`7.20 GiB · 657 项`);展开后才是构成条、 + 时间条、"最大占用"(再嵌一层折叠)和三个次级按钮。 +- "更优雅"的具体手段(不是形容词):整块只有**一条主条**(6px,不是三条 14px);图例**压成一行**用 `·` 分隔 + 而不是每项一行;数字用 `tabular-nums` 对齐;字号分层(26 / 12 / 11);去掉多余色块边框,靠留白分节。 + +### 8.2 C++ Modules:不显示 clangd + +正文只剩 **状态 → 语言服务 → 问题 → 操作**。`clangd` / 引擎 / 语义 profile 全部移除, +只留 `语言服务 mcppls 0.0.9`。操作从 11 行压成 5 行,其余收进「其它 7 项…」。 +**已用渲染产物断言**:改后 HTML 里 `clangd` 出现 **0** 次。 + +### 8.3 图标:用 mcppls 的官方资源(实测配色,有个坑) + +实测官方 `mcpp-logo.png` 的像素构成:**纯黑 75.1%** · 橙 `#f08c00` 6.8% · 蓝 `#1971c2` 6.1% · 透明 10.4%。 + +**坑**:深色主题的 Activity Bar 背景是 `#191A1B`,**75% 的黑墨迹在它上面几乎不可见**——直接用原图, +看起来就是"没有图标",正是你最初的抱怨。浅色主题(`#F8F8F8`)则正常。 + +原型的三个候选(都不再手绘,上一版那张描摹已删): + +| 候选 | 做法 | 代价 | +|---|---|---| +| A | 官方 PNG 原样 | 深色主题下黑墨迹消失 | +| B | 官方 PNG 放在浅色圆角底上 | 保留全部色彩;Activity Bar 里多一个浅色小方块 | +| C | 官方图形里的**黑色部分机械换成 `currentColor`**(橙蓝丢弃) | 单色、随主题变色;不是手绘,但仍是一次派生 | + +### 8.4 库市场:可行性(已在 mcpp 源码里核对) + +| 能力 | 现状 | +|---|---| +| 写入 `mcpp.toml` | ✅ `mcpp add [--dev]`,官方命令就是"Add a dependency to mcpp.toml" | +| 搜索包 | ⚠️ `mcpp search [--all-versions]` — **没有 `--format json`** | +| 包详情 | ⚠️ `mcpp info ` 只对**已缓存**的包,且无 JSON | +| 包列表 | ❌ `mcpp list` 的三个都是"工具链 / 缓存条目 / 已配置 registry" | +| 索引数据 | `mcpp-index` 仓库:238 个 Lua 描述符,含 name/namespace/description/licenses/repo/deps/多平台版本 | +| 对比 | `build` `pack` `why` `build-database` `parse` `cache list` `env` 等约 10 个子命令**都有 `--format json`** | + +三条路:**(a)** 请上游加 `mcpp search --format json`(与既有 10 个命令一致,改动小)← 推荐; +**(b)** 解析人类输出(脆弱,违反本项目"只有一处解析人类输出"的原则); +**(c)** 直接读 index 仓库的 Lua(重复 mcpp 的职责,还要自己找索引路径)。 + +推荐 (a) + 优雅降级:没有机器输出时,界面显示"需要更新的 mcpp"而不是坏掉——能力探测机制已经在了。 + +形态上我倾向**轻的那条**:`mcpp: 添加依赖…` 一个 quick pick(关键词 → 列表 → `mcpp add`), +放在工程段「常用命令」里。理由是写 `mcpp.toml` 本来就是"工程"的事,也不给侧边栏再加一段噪音。 + +--- + +## 9. v4:三段结构 + 库生态(原型已同步重建) + +### 9.1 图标:采纳官方 PNG 原样 + +你确认 2026 Dark 下黑底与 Activity Bar 有可见差别。实测支持这个判断:logo 黑是 `#000000`, +深色主题 Activity Bar 是 `#191A1B`——不是纯黑对纯黑,所以能看出一块更深的底加亮色图形。 +**采纳候选 A**:扩展图标与 Activity Bar 容器图标都用官方 `mcpp-logo.png`, +两个扩展从此是同一张图(当前 mcpp-vscode 用的是另一张,md5 不同)。B/C 仅留作备查。 + +### 9.2 侧边栏改为三段:工程 / mcpp 库生态 / 缓存 + +`mcpp.languageServer` 视图取消,内容折进**工程 → 基本信息**,默认折叠。 + +**前提(否则这个折叠是有害的)**:折叠状态下必须能看出好坏。所以折叠那一行自带状态图标 +(健康绿 / 降级黄),降级时右侧显示 `mcppls 0.0.9 · 1 个问题`。**不展开也能发现异常**是 +这一条能被接受的条件;否则问题会被藏起来,比不放更糟。 + +**一个取舍**:C++ Modules 的内容由**另一个扩展**提供。折进工程会让"谁负责什么"这条边界重新 +变模糊。缓解:展开后的块里固定一行「提供方 sunrisepeak.mcpp-language-server」。 + +### 9.3 库生态:用 mcpp-index + `mcpp xpkg`,不动上游(已实测) + +| 环节 | 实测结果 | +|---|---| +| 描述符解析 | **`mcpp xpkg parse --json` 存在且好用**:`{"namespace":"compat","name":"argparse","versions":{"linux":["3.2"],"macosx":["3.2"],"windows":["3.2"]},"standard":"c++23","sources":[…],"include_dirs":[…],"targets":["argparse"],"unknown_keys":[]}` | +| 索引位置 | `mcpp index status` 给出表格,含每个 registry 的 `path`;**且 `~/.mcpp/registry/data/*/pkgs` 可直接 glob**,无需解析输出 | +| 目录规模 | `mcpplibs`(官方 C++ 库索引)**239 个包**;另有 `xim-pkgindex`、`xim-pkgindex-local` | +| 目录字段 | 每个 `.lua` 含 `namespace` / `name` / `description` / `licenses` / `repo` | +| 添加依赖 | `mcpp add @ [--dev]`,官方命令直接写 `mcpp.toml` | + +**关键限制**:`mcpp xpkg parse --json` **不含** `description` / `licenses` / `repo` +——那些属于 `package` 表的目录字段,不在 resolver 语法里。所以: + +- **列表**(239 条,必须便宜):容错读取每个 `.lua` 里的几个**单行字符串字段**。 +- **详情与版本**:按需调 `mcpp xpkg parse --json`(版本按平台分组,权威)。 +- **绝不为 239 个包各起一个进程**——那会卡死;批量阶段只读文本,进程只在点开某一行时起。 +- 读不出来的条目用文件名兜底,详情仍可用。 + +**索引路径**:默认 glob `/.mcpp/registry/data/*/pkgs`,配 `mcpp.library.indexPath` 覆盖。 +`XLINGS_HOME` 不在环境变量里,且 `mcpp index status --format json` 是 unknown option, +所以走 glob 而不是解析 `mcpp index status` 的表格——避免出现第二处"解析人类输出"。 + +### 9.4 还没定的(下一轮要你拍板) + +1. **"已添加"怎么判定**:mcpp 没有列依赖的子命令(`mcpp list` 三个分别是工具链 / 缓存条目 / + 已配置 registry)。所以只能读 `mcpp.toml` 的 `[dependencies]`——我们本来就在解析它。 +2. **要不要区分"已声明"与"已缓存"**?两者不同:声明在 `mcpp.toml`,缓存在全局缓存目录。 +3. **搜索范围**:只搜本地索引(离线、快、可预期),还是也允许 `mcpp search`(覆盖其它 registry、 + 可能联网)? +4. **版本怎么选**:默认最新 / 让用户挑 / 跟随工具链兼容性? + +--- + +## 10. v5 定稿:按你的四条决定收口 + +### 10.1 图标 +采纳官方 `mcpp-logo.png` 原样,扩展图标与容器图标同一张(B/C 撤销)。 + +### 10.2 库生态 + +**搜索分两档,开关默认关闭联网:** + +| 档 | 数据源 | 默认 | 成本 | +|---|---|---|---| +| 本地索引搜索 | `~/.mcpp/registry/data/*/pkgs` 的 239 个描述符 | **开** | 离线、一次扫完、可预期 | +| 全 registry 搜索 | `mcpp search ` | **关**(`mcpp.library.searchRegistries`) | 可能联网;且只有人类输出 | + +> 联网那一档只有人类输出,所以打开时按"尽力而为"处理:解析失败就退回本地结果并提示, +> **不抛错、不静默失败**。这条要说在设置描述里。 + +**版本默认最新——但 mcpp 要求显式版本,所以"最新"必须由我们算出来:** + +实测 `mcpp add compat.argparse`(不带版本)会被拒: +``` +error: package version required: `mcpp add compat.argparse@` (M2 supports exact-version only) +``` +所以流程是: +1. `mcpp xpkg parse --json` → `versions: {linux:[…], macosx:[…], windows:[…]}` +2. 取**当前平台**那组,按版本序取最大 → `3.2` +3. `mcpp add compat.argparse@3.2`(dev 依赖加 `--dev`) + +**语义澄清**:`mcpp add ` 与 `mcpp add @` 是两件事,前者会被拒。 +界面上显示"最新 3.2",实际执行的是带版本的命令——不要让人以为我们省略了版本。 + +### 10.3 依赖树:**只能做两级,且必须说清楚** + +我实测了三条数据源,结论如下: + +| 来源 | 能给什么 | 机器可读? | +|---|---|---| +| `mcpp.toml` | **声明的**依赖(`[dependencies]` / `[dev-dependencies]`,含 version/path/git/features) | ✅ 文本,我们已在解析 | +| `mcpp.lock` | **解析到的**包集合:`[package."ns.name"]` + `namespace`/`version`/`source`/`hash`,**扁平、无父子边** | ✅ 结构化 TOML | +| `mcpp why deps` | 传递依赖的解释 | ❌ **实测被拒**:`error: --format json is defined for 'mcpp why toolchain'; 'deps' has no machine-readable shape yet` | + +所以**今天做不出真正的传递依赖树**。方案改为诚实的**两级展示**: + +``` +依赖 (4) +├ mcpplibs.cmdline 最新 0.3.1 · 已解析 0.0.1 ← 声明 → lock 解析值 +├ compat.argparse 最新 3.2 · 已解析 — ← 声明了但还没 lock +├ compat.gtest dev 最新 1.15.2 · 已解析 — +└ counters path ../counters +``` + +- 第一级 = `mcpp.toml` 的声明(含 `dev` / `path` / `git` 标记)。 +- 第二级 = `mcpp.lock` 里按名字匹配到的解析版本;没有就是"—"(未解析或未构建过)。 +- **不画连线**,因为 lock 里没有父子关系,画了就是编的。 +- 真正的传递树挂到上游请求:**U.7 `mcpp why deps --format json`**(`why toolchain` 已经有了, + 补齐 `deps` 是同一类改动)。等它落地再把两级升级成树。 + +### 10.4 顺带修正一处此前的说法 + +v3 里我写"`mcpp why ` 有 `--format json`,可以解释谁把它拉进来的"——**这是错的**。 +实测只有 `why toolchain` 有 JSON,`why deps/sources/tool` 都没有。已在此更正。 + +--- + +## 11. 自我 review(对方案本身,不是对代码) + +### 11.1 我实测过、有证据的(可以当事实用) + +| 结论 | 证据 | +|---|---| +| `mcpp xpkg parse --json` 可用 | 实跑 `compat.argparse.lua`,返回含 `versions` 的 JSON | +| 官方 C++ 库索引 239 个包 | `find ~/.mcpp/registry/data/mcpplibs/pkgs -name '*.lua' \| wc -l` = 239 | +| 索引路径可 glob,无需解析输出 | `~/.mcpp/registry/data/*/pkgs` 三个 registry 都在 | +| `mcpp index status --format json` 不存在 | 实跑 → `error: unknown option: --format` | +| `mcpp add` 必须带精确版本 | 实跑 → `error: package version required` | +| `mcpp why --format json` 只对 toolchain | 实跑 → `'deps' has no machine-readable shape yet` | +| `mcpp.lock` 是扁平的解析集合 | 读 mcpp 仓库真实 lock:`[package."ns.name"]` + version/source/hash,无父子边 | +| 官方 logo 75.1% 纯黑 | 解码像素统计 | +| `mcpp why --format json` 走标准信封 | 顶层键 `data/diagnostics/effects/kind/kindVersion/mcpp/schemaVersion` | + +### 11.2 我没验证、属于推断的(**别当结论**) + +1. **库视图的性能**:239 个文件读一遍到底多快,我没有实测。若首次渲染 > 300ms, + 需要加缓存或懒加载——这一点等实现时先量再定。 +2. **描述符的字段容错**:我抽样读了 4 个 `.lua`,确认 `namespace/name/description` 是单行 + 字符串。**没有验证全部 239 个是否都规范**——可能有换行、引号转义、或缺失。设计里已要求 + 用文件名兜底,但没有实测兜底覆盖率。 +3. **`mcpp add` 是否联网**:加索引包大概要拉索引/校验,但我没测它的耗时与失败表现。 + 界面上"添加"必须给进度与失败路径,这一点我写进了方案但没验证。 +4. **WebviewView 在 200px 窄宽度下的真实表现**:原型是 HTML 模拟,不是真实 VS Code 窗口。 +5. **三个视图全关后容器是否真的消失**:依据是 VS Code issue #49145(closed/verified)和一篇 + 第三方文章,**我没有在真实 VS Code 里验证过**。这是整个"侧边栏可关闭"的地基,落地时 + 必须先写一个 e2e 把它钉死,再写别的。 +6. **C++ Modules 折叠的观感**:折叠后能否"不展开就看出异常",是我认为的关键,但只能靠你看原型判断。 + +### 11.3 方案自身的风险 + +| 风险 | 说明 | 对策 | +|---|---|---| +| 第二处"解析人类输出" | 联网搜索档要解析 `mcpp search` | 默认关闭 + 失败即退回本地;已登记 U.8 `mcpp search --format json` | +| 读 239 个 Lua 是否算"重造 mcpp 的职责" | 有争议 | 限度写死:只读 4 个单行目录字段;**任何权威值走 `mcpp xpkg parse`** | +| 索引路径硬编码 `~/.mcpp/registry` | 非默认安装会找不到 | `mcpp.library.indexPath` 可覆盖;找不到时给明确提示而不是空列表 | +| 三段结构让 C++ Modules 的归属变模糊 | 上一轮刚划清的边界 | 展开块固定一行「提供方」;折叠行显示 mcppls 版本 | +| 工程段因为加了「常用命令」反而变长 | 与"简洁"目标相悖 | 你已确认可接受;若要更短可把常用命令收成折叠 | + +### 11.4 我在前几版里说错、已更正的 + +1. v1 把 Q3 问成"是否完全不出现动作"——**措辞导致误解**,你回"按理是要能点击有动作的"。 + 实际问题是"动作与数据同级混排",不是"要不要动作"。 +2. v3 说 `mcpp why ` 有 JSON 可解释依赖来源——**错**,只有 `why toolchain` 有。 +3. v3 提议"我画一个单色 SVG"——你没要,实际做法是从官方图机械派生(后来连这个也撤销了, + 直接用官方原图)。 +4. v2 说容器图标"看起来像没有图标"是因为 PNG 糊——**部分对**:真实原因是当时那张与官方 + 不是同一张图,且黑墨迹在深色主题下对比度低。 + +--- + +## 12. v6 定稿:详情页 / 示例代码 / 索引站的定位 + +### 12.1 包详情放在编辑器区(已定) + +侧边栏 = 搜索 + 列表;编辑器 = 详情页。列表行必须自足(名称 / 一行描述 / 用法标签 / 版本 / 状态), +不强迫开详情页。 + +### 12.2 "要索引站干什么"——定位说清楚 + +**不是为了拿数据。** 数据全在本地(见 §13.2)。索引站只有三个作用: + +1. **深链**:实测包页 URL 是 `https://mcpplibs.github.io/mcpp-index/packages/./` + (站点根页面里就能看到 `packages/compat.argparse/`、`packages/mcpplibs.cmdline/`)。 + 详情页上放一个**低调的**「在索引站打开」链接即可。 +2. **词表来源**(真正的价值):`SURFACES` + 徽标 + facet 的权威定义就在 + `.xpkgindex/plugins/mcpp.py` 里。用它,两处说同一套词。 +3. **信息层级对齐**的参照:分组顺序、徽标位置、字段取舍。 + +结论:**索引站是一个链接和一个词表,不是数据源。** 没有它这个功能照样成立。 + +### 12.3 示例代码代替 README(已定) + +README 不做(索引里没有,要联网去上游拉)。 + +改为**官方索引的示例代码**,数据源就在索引仓库里、**完全离线**: + +| 事实 | 实测 | +|---|---| +| 位置 | `tests/examples//mcpp.toml` + `tests/examples//tests/*.cpp` | +| 规模 | **172 个示例工程** | +| 契约 | 插件 `_scan_examples` 的产物:包 → `{project, path, paths[], count}` | +| 价值 | 这些是 **CI 真实构建并运行过**的代码,不是为网站写的片段(插件注释原话) | +| 抽取 | 从 `tests/*.cpp` 里取 `import x.y;` / `#include ` 行——即 `SURFACES` 判定所依赖的同一批行 | + +### 12.4 openkal 标识保留(已定) + +`openkal-ecosystem` / `openkal-compat` 作为 facet,`posix 环境` / `使用平台接口` 作为徽标。 +注意插件里的原话:这两个是**实测结果**(`tests/openkal/compat.py` 记录在 +`.xpkgindex/openkal-compat.json`),描述符里**没有**对应字段。所以我们要读那个 json,不能推导。 + +### 12.5 标签词表最终确定 + +| 层 | 取值 | 来源 | +|---|---|---| +| **用法标签(主)** | `import` / `#include` / `tool` / 上游 mcpp.toml | `SURFACES`,由 `xpkg parse` 字段推导 | +| 徽标 | `✓ 有示例` / `国内镜像` / `openkal-ecosystem` / `openkal-compat` / `posix 环境` / `使用平台接口` | 示例扫描 + 描述符 url 里的 `CN` + `.xpkgindex/openkal-compat.json` | +| 命名空间 | `mcpplibs` / `compat` / `llvm` / `khronos` / `freedesktop` … | 描述符 `namespace` | +| 状态 | 已添加 / 可添加 / **有更新** | `mcpp.toml` + `mcpp.lock` vs 索引最新 | + +上一版的 A–I 形状**废弃**(那是"怎么构建",给索引维护者看;`SURFACES` 是"怎么使用",给读者看)。 + +--- + +## 13. 综合自我 review(v6,对整套方案) + +### 13.1 需求 → 落点对照(有没有漏的) + +| 你提的 | 落点 | 状态 | +|---|---|---| +| 缓存可视化更优雅 | §8.1 一条 6px 主条 + 单行图例 + tabular-nums + 字号分层 | ✅ | +| 项目缓存 / 全局缓存分开,全局默认折叠 | §8.1 `
` 默认折叠,摘要一行 | ✅ | +| 不显示 clangd,只显示 lsp mcppls | §8.2 正文只留 状态 → 语言服务 → 问题 → 操作 | ✅ | +| 用官方 PNG/SVG,不自己画 | §9.1 官方 `mcpp-logo.png` 同图 | ✅ | +| C++ Modules 折进工程·基本信息 | §9.2,**前提**:折叠行自带状态图标 | ✅ | +| 三段:工程 / mcpp 库生态 / 缓存 | §9.2 | ✅ | +| 工程段有基本信息 + 常用 mcpp 命令 | §9.2 两块分区 | ✅ | +| 库市场 | §10.2 + §12 | ✅ | +| 联网搜索开关,默认关 | §10.2 | ✅(v6 后只剩跨 registry 一个用途) | +| 默认最新版本 | §10.2 由 `xpkg parse` 算出再显式传入 | ✅ | +| 显示依赖树 | §10.3 **降级为两级、不画连线** | ⚠️ 受上游限制 | +| 标签参考 mcpp-index | §12.5 改用官方 `SURFACES` | ✅ | +| 包详情 | §12.1 编辑器区 | ✅ | +| README 换成官方索引的示例代码 | §12.3,172 个示例工程,离线 | ✅ | +| openkal 标识 | §12.4,读 `openkal-compat.json` | ✅ | +| 风格对齐官方网页 | 词表/层级/文案对齐;**配色只跟 VS Code 主题** | ⚠️ 有意不对齐配色 | + +### 13.2 数据源清单(每个字段从哪来、要不要联网、是不是权威) + +| 字段 | 来源 | 离线 | 权威性 | +|---|---|---|---| +| 包名 / 命名空间 | 描述符文件名 + `xpkg parse` | ✅ | 权威 | +| 描述 / 许可 / 仓库 | 描述符 `package.{description,licenses,repo}` | ✅ | 权威 | +| 版本(按平台) | `mcpp xpkg parse --json` → `versions` | ✅ | 权威 | +| 用法标签 | `xpkg parse` 字段组合 → `SURFACES` | ✅ | 推导 | +| 示例代码 | 索引仓库 `tests/examples//tests/*.cpp` | ✅ | **真实(CI 跑过)** | +| openkal facet | `.xpkgindex/openkal-compat.json` | ✅ | 实测记录 | +| 已声明依赖 | `mcpp.toml` | ✅ | 权威 | +| 已解析版本 | `mcpp.lock` | ✅ | 权威 | +| 有更新 | 索引最新 vs lock 解析 | ✅ | 推导 | +| 传递依赖树 | — | — | ❌ 上游没有机器可读形状 | +| README / star | GitHub API | ❌ | **不做** | + +**一个重要的简化**:v6 之后,**整个库视图零网络**。联网只剩"跨 registry 搜索"这一个用途, +所以那个开关的语义变清晰、风险也变小了。 + +### 13.3 我没验证的(别当结论) + +1. **"所有视图关闭 → 容器从 Activity Bar 消失"没有在真实 VS Code 里验证过**(依据是 issue #49145 + 已 closed/verified + 第三方文章)。这是"侧边栏可关闭"的地基,**落地第一步就要写 e2e 钉死**。 +2. 233 个描述符读一遍的耗时没实测;若 > 300ms 需要缓存。 +3. 全部 233 个描述符的字段规范性没验证(只抽样了 6 个)。 +4. `mcpp xpkg parse` 的输出我只看了 3 个包;`unknown_keys` 非空的包会怎样没试。 +5. WebviewView 在 200px 窄宽度的真实表现——原型是 HTML 模拟。 +6. `mcpp add` 是否联网、耗时多少、失败表现,没测。 +7. `.xpkgindex/openkal-compat.json` 的字段结构没细看(只确认它存在且是实测记录)。 + +### 13.4 风险 + +| 风险 | 对策 | +|---|---| +| 联网搜索档要解析 `mcpp search` 人类输出(项目原则是只有一处) | 默认关闭 + 失败退回本地;登记 U.8 | +| 读 Lua 描述符是否算"重造 mcpp 职责" | 限度写死:只读目录字段与示例关联;**任何权威值走 `mcpp xpkg parse`** | +| 索引路径硬编码 `~/.mcpp/registry` | `mcpp.library.indexPath` 可覆盖;找不到给明确提示而非空列表 | +| C++ Modules 归属变模糊 | 展开块固定「提供方」行 | +| 工程段因加常用命令变长 | 已确认可接受;需要更短时收成折叠 | +| 词表跟着上游变 | 词表**从插件推导而非硬编码**;上游改了词会跟着变(也可能破坏,需容错) | + +### 13.5 我在这个过程中说错、已更正的(累计 5 条) + +1. v1 把 Q3 问成"要不要动作"——措辞误导,真实问题是"动作与数据同级混排"。 +2. v2 说容器图标糊是因为 PNG——部分对;真实原因是用的是**另一张图**,加上黑墨迹在深色主题对比度低。 +3. v3 说 `mcpp why ` 有 JSON 可解释依赖来源——**错**,只有 `why toolchain` 有。 +4. v3 提议我手绘单色 SVG——你没要;后来改为从官方图机械派生,最终撤销,直接用官方原图。 +5. **v5 提的 A–I 形状标签是错的**——那是"怎么构建",官方站用的是"**怎么使用**"(`SURFACES`)。 + 你让我参考 `mcpp-index` 仓库,直接纠正了这一条。 + +--- + +## 14. round 2:一条被我自己推翻的假设(重要) + +**§13.3 第 1 条说"所有视图关闭 → 容器从 Activity Bar 消失"只是依据二手资料、没验证过。 +我在本机 VS Code 1.132 的源码里查了,结论是:不成立。** + +证据链: + +1. `paneCompositeBar.ts` 里,Activity Bar 的每一项由 + `showOrHideViewContainer(container)` 决定:`shouldBeHidden(container)` 为真才 `hideComposite`。 +2. `shouldBeHidden` 的第一段是**决定性**的: + ```ts + if (viewContainer) { + if (viewContainer.hideIfEmpty) { … } + else return false; // ← 没有这个标志就永远不隐藏 + } + ``` +3. `hideIfEmpty` 的注释是「If enabled, view container is not shown if it has no active views」, + 全仓库**只有一处**赋值:`registerGeneratedViewContainer()`(VS Code 自己给"用户自定义容器"用的), + 值为 `true`。 +4. 我在已安装的 `workbench.desktop.main.js`(1.132)里把 `hideIfEmpty` 的 **24 处**全部看了: + 每一处都是**内置容器**的注册(ports / test / voice / debug / 用户容器 …), + **没有任何一处从扩展清单读取**。 + +所以:**扩展无法把自己的容器标记为"空则隐藏"**。对第三方容器,把所有视图 `when` 设为 false 只会 +让**内容**消失,图标会留下(点开是空面板)。我原先依据的 VS Code issue #49145(closed/verified) +和那篇第三方文章,对 1.132 不再适用。 + +### 已做的修正 + +- `mcpp.views.enabled` 的标题与描述改成实话:**「隐藏 mcpp 视图内容」**,并说明图标是否移除由 + VS Code 决定、要移除可在图标上右键。中英同步,注册表与 nls 一致(`check-config` 通过)。 +- `when` 门控保留——**内容隐藏这件事是确定的**(走的是 `activeViewDescriptors.length === 0` 那条路径)。 +- 教训记在这里:**二手资料必须用一手源码或真实运行验证**。这条假设是整套里风险最高的地基, + 幸好先查了。 + +### 另外两处本轮修掉的 + +- `mcpp.internal.markIndex` 之前注册了却没人调用,`mcpp.library.updateIndex` 装好索引后 + `viewsWelcome` 不会消失。改成 `markIndexFound()`,在激活时与 `index update` 之后都重算。 + **(§15.4 更正:这条整段作废——webview 视图根本不支持 `viewsWelcome`,`markIndexFound` + 已于 round 3 连同 `mcpp.library.indexFound` 一起删除。)** +- `test/architecture.test.ts` 补上库生态的 4 个纯模块(`indexModel` / `xpkg` / `libraryHtml` / + `detailHtml`),让"纯模块不得依赖 vscode"这条覆盖到新代码。 + +## 15. round 3:你在真实实例里给的 6 条反馈 + +这一轮的每一条都是先在**一手源码**或**本机真实运行**里定位到根因,再改。下面把证据一起留下, +因为它们决定了改法——其中两条的"根因"和最初看起来的完全不是一回事。 + +### 15.1 基本信息默认折叠、常用命令默认展开(反馈 1、2 前半) + +数据层一行的事:`buildProjectTree` 里 `project.section.basic` 去掉 `expanded: true`, +`project.section.commands` 保留。测试从「两个 section 都展开」改成 +「命令展开、基本信息 `expanded === undefined`」。 +dev profile 的 `workspaceStorage` 会在重启前清掉,否则 VS Code 会用上次记住的展开状态覆盖默认。 + +### 15.2 通用命令图标带色(反馈 2 后半) + +树**支持**颜色,快捷菜单**不支持**——两者机制不同,所以这里能做的和那里能做的不是一件事。 + +树的证据(1.132 `workbench.desktop.main.js`,`CustomTreeView` 的渲染分支): + +```js +this.shouldShowThemeIcon(!!r, n.themeIcon) && (L = j.asClassName(n.themeIcon), + n.themeIcon.color ? i.icon.style.color = this.themeService.getColorTheme() + .getColor(n.themeIcon.color.id)?.toString() ?? "" : L = L + " codicon-colored") +``` + +所以 `TreeNode` 加了 `iconColor?: string`(`ThemeColor` id),只允许 `charts.*` 这类所有主题都 +会定义的 id;主题没定义时 VS Code 让颜色为空串,图标退回普通前景色,不会变成看不见。 +配色按**这一行作用于什么**分组,不按装饰:蓝=构建/添加、绿=运行/校验、紫=测试、橙=删除、 +黄=工具链、设置保持中性。 + +### 15.3 快捷菜单:图标 + 分组(反馈 3) + +`quickMenuItems` 现在每条带 `icon`,菜单按 `QUICK_MENU_GROUPS` 插 `QuickPickItemKind.Separator` +做分组;分组标题与条目共用一张表,所以不可能出现"条目的分组没有标题"。 + +**颜色做不到,这是 API 限制,不是没做。** 1.132 的 `MainThreadQuickOpen.expandIconPath`: + +```js +expandIconPath(o){ let e = o.iconPathDto; + if (e) + if (j.isThemeIcon(e)) o.iconClass = j.asClassName(e); // ← color 被丢掉 + else if (Qh(e)) { let t = P.from(e); o.iconPath = { dark: t, light: t }; } + else { … } } +``` + +`ThemeIcon` 被压成一个 codicon class,列表渲染只写 +`i.icon.className = "quick-input-list-icon " + r.iconClass`,没有任何一处读颜色。 +所以这一轮**只做了图标形状 + 分组**,没有塞一个"永远画不出来"的颜色。顺带把 5 个分组标题与 20 条 +条目全部补上中文(原来 27 个 labelKey 里 20 个没有译文,中文用户看到的是英文),并加了 +`test/commands/menu.test.ts` 把"每个 labelKey 必须有译文""分组必须连续"钉住——这两件事 +`tools/l10n-check.mjs` 看不见,因为菜单标签是 `t(item.labelKey)` 而不是字面量。 + +### 15.4 库视图与「刷新 mcpp 包索引」毫无反应(反馈 4) + +**根因不是索引定位,是 provider 从来没注册过。** 上一轮我把 `registerWebviewViewProvider` +从 `libraryView.ts` 里"交代给调用方",`extension.ts` 的注释却写着"provider 由 registerLibraryView +自己注册"——两边互相甩锅,结果**整个 `src/library/` 里一次都没有这个调用** +(`grep -rn registerWebviewViewProvider src/` 当时只有 `cachePanel.ts` 一处)。 +没有注册就没有 document,`refresh()` 永远打在 `this.view === undefined` 上: +视图是空的,刷新是无声的。修法是在 `registerLibraryView` 内部注册(与 `registerCachePanel` +同构),并加了一条**能抓住这类错误**的门禁: + +``` +test("every webview view registers the provider that fills it") + → 每个 "type": "webview" 的视图 id 必须有唯一一个 `export const *_VIEW_ID = ""` 模块, + 且该模块必须调用 registerWebviewViewProvider +``` + +索引定位按你的建议增强了,但放在**最后**,因为它不是这次的根因: + +| 顺序 | 找法 | 代价 | +| --- | --- | --- | +| 1 | `mcpp.library.indexPath`(用户显式设置) | 0 | +| 2 | `$MCPP_HOME/registry/data`、`~/.mcpp/registry/data`,**有内容的都列** | 几次 `readdir` | +| 3 | `mcpp self env --format json` 的 `mcppHome` | 每会话最多 1 个进程,且只在 2 全空时才跑 | + +第 3 步走机器协议(`schemaVersion` + `kind`,见 `src/cli/protocol.ts` 的检测规则), +在**不受信任的工作区跳过**(`mcpp.path` 是 resource 作用域,不能让工作区指定要跑的二进制)。 +本机实测(用 `Module._load` 打桩 `vscode` 后直接跑编译产物): + +| 场景 | 结果 | +| --- | --- | +| 设置指向 mcpp-index 检出 | 1 个 root / 238 包 / 146 ms,二次 0 ms(快照缓存命中) | +| 默认(不设) | 3 个 root / 543 包 / 228 ms | +| 两个 home 都空 → `mcpp self env` | 3 个 root / 543 包 / 674 ms | +| mcpp 缺失 | 空 / 5 ms,不抛 | + +(写这条时踩了自己的一个 bug:`defaultDataDirectories` 先用探针**之前**的候选列表判断 +"探针给的主目录是不是新的",而探针恰恰是把它加进列表的那一步,于是永远判定"不新"、 +永远返回旧列表。改成探针后重读候选列表。这个 bug 只有在真的构造出"两个 home 都空"的场景 +才会暴露——上面那张表就是这么写的。) + +「刷新 mcpp 包索引」另外两处修:成功后**有反馈**了(进度通知 + 「已刷新:N 个包,来自 M 个索引目录」, +之前成功时零输出,看起来就是坏的),并且补上不受信任工作区的拦截。 + +顺手删掉两处**已被证伪的死代码**:`mcpp.library.indexFound` 上下文键与 `viewsWelcome.library` +文案。1.132 里基类 `ViewPane` 的 `shouldShowWelcome(){return !1}`,只有 tree 类视图覆写它 +(`TreeViewPane` 用 `dataProvider.isTreeEmpty`),所以 `"type": "webview"` 视图**永远不会** +渲染 welcome 内容;库视图在自己的 document 里说空状态,那是唯一会被显示的地方。 + +### 15.5 全局构建缓存的分布条是黑的(反馈 5) + +根因是 CSS 选择器和渲染出来的 DOM 差了一层: + +``` +渲染:
… +样式: [data-viz="composition"] rect[data-kind="pkg"] { fill: … } ← rect 上没有 data-kind +``` + +`rect[data-kind]` 谁都不匹配 → 每个 `` 落到 SVG 的默认填充(**黑**,任何主题都是黑)。 +图例里的 `.swatch[data-kind="pkg"]` 是打在 `` 上的,所以**图例有颜色、条没有**, +正好就是你看到的样子。修法:选择器改到 ``(`fill` 是继承属性,会漏到子 ``), +并在 `test/views/cachePanelHtml.test.ts` 加了一条**文档与样式表互查**的测试: +既要求文档真的把属性打在 `` 上,也要求样式表为每个值都有 `fill` 规则,同时**禁止** +`rect[data-…]` 这种写法再出现。 + +### 15.6 活动栏的 mcpp logo 是白的 / 不显示(反馈 6) + +一手证据在 `ActivityAction.toCompositeBarActionItem`(1.132): + +```js +let c = kc(i), l = new ab; l.update(c); let u = `activity-${e.replace(/\./g,"-")}-${l.digest()}` … +Sf(p, ` + mask: ${c} no-repeat 50% 50%; + mask-size: var(--activity-bar-icon-size, ${this.options.iconSize}px); + -webkit-mask: ${c} no-repeat 50% 50%; …`) +``` + +**自定义容器图标是被当模板(mask)用的**:画出来的是图标的 **alpha 通道**,颜色来自主题 +(`.style-override` 下未选中是 `--vscode-icon-foreground`、选中是 `--vscode-foreground`, +hover/active 是 `--vscode-activityBar-foreground`)。而官方 logo 是一张 +**377×377 不透明黑色圆角方块**(实测:不透明覆盖率 90%,其中 81.5% 是纯 `#000000`、 +6.7% `#f08c00`、5.7% `#1971c2`)。拿它当模板,自然就是**一整块浅色方块**——你看到的"白色"。 + +修法:`images/activity-bar.png` 改为**派生资产**——透明底 + 只留字形的白色 alpha +(96×56,字形 88×48)。由 `tools/generate-activitybar-icon.mjs` 从 `images/logo.png` +重新生成;覆盖率用「像素 = 覆盖率 × 墨色」反解(两个平涂色的最大通道做分母), +6% 以下当作徽章边缘抗锯齿丢掉,所以不会在字外留一圈灰晕。白色 + 透明底在 alpha 模板和 +亮度模板下**都对**,不依赖 Chromium 选哪一种。`npm run check:icon` 进了 `npm run check`, +产物与来源不可能漂移。市场图标(`package.json` 的 `icon`)继续用真 logo。 + +### 15.7 这一轮的状态与仍然没做到的 + +- 646 个单元测试通过;`check:config`(68 设置 / 32 public)、`l10n-check`(382 运行串 / + 201 清单键)、`check:icon`、`check:generators` 全过;VSIX 93 文件 / 343 KiB(两道体积门禁内)。 +- dev profile 重装并重启,扩展主机日志确认激活、无错误。 +- **没做到**:快捷菜单图标没有颜色(15.3 的 API 限制);活动栏图标在真实主题下的观感只能靠眼睛, + 我没有截图能力;e2e 仍未在本地跑通(要下载固定版 VS Code,本机大文件下载会中断); + 索引定位的三条路径是靠打桩 `vscode` 后直接跑编译产物验证的,不是通过编辑器 UI 验证的。 + + +## 16. round 4:第二轮真实反馈(含一个把库视图打瘫的 bug) + +这一轮的第一条是**真 bug**:库视图一显示就抖、点不动、搜不了、CPU 飙高,折叠起来就正常。 +根因和"性能问题"完全无关。 + +### 16.1 库视图的渲染死循环(反馈 2,最高优先级) + +**根因:文档在自我介绍,宿主拿"再渲染一次"回答它。** + +`libraryHtml.ts` 的客户端脚本最后一句是 `post({ type: "ready" })`,而 `libraryView.ts` 的 +`handle()` 里恰好有: + +```ts +case "ready": + this.paint(); // ← paint() 里是 view.webview.html = … + return; +``` + +`webview.html = …` 会**重载**文档;文档重载后又发 `ready`。更致命的是每次 `paint()` 都调用 +`randomNonce()` 生成新的 CSP nonce,所以**每次渲染出来的文档都不同**——连"内容没变就别重设" +这种自然去重也永远不会命中。于是视图以毫秒级无限重载:抖动、DOM 被反复销毁(点不到、输不进)、 +CPU 打满。折叠时 webview 不渲染,所以"折叠就正常"。 + +上一轮这个 bug 之所以没暴露,恰恰因为 provider 从来没注册过——视图根本没渲染过。**修好注册, +就把它放出来了。** + +修法三条 + 一条门禁: + +1. 客户端不再发 `ready`,`decodeLibraryMessage` 不再认识它,宿主删掉 `case "ready"`: + 文档本身就是数据(每一行、每个徽章、每个计数都已经在里面),"我加载好了"这句话没有用途, + 只可能招来一次重复渲染。 +2. nonce 改成**每个视图一个**(构造时生成一次),于是"内容没变 ⇒ 文档逐字节相同"成立。 +3. `paint()` 多一道 `documentNeedsRender(this.document, document)`(纯函数,见 `libraryHtml.ts`): + 文档相同就**不重设**,滚动位置和输入到一半的搜索框都不会被丢掉。 + `resolveWebviewView` / `onDidDispose` 会把 `this.document` 清空——新解析出来的 webview 是空的, + 上一次推给旧 webview 的文档对它没有任何意义。 +4. `test/artifacts.test.ts` 新增源码级门禁:文档不得再 post `ready`、宿主不得再有 `case "ready"`、 + nonce 必须是每视图一个、比较必须存在。 + +### 16.2 快捷菜单图标配色(反馈 1) + +**能做,但只能靠图片。** 上一轮我查到 `MainThreadQuickOpen.expandIconPath` 会把 `ThemeIcon` +压成没有颜色的 codicon class;这一轮把同一函数读完了——它是**两条分支**: + +```js +expandIconPath(o){ let e = o.iconPathDto; + if (e) + if (j.isThemeIcon(e)) o.iconClass = j.asClassName(e); // 字体图标:颜色由行前景决定 + else if (Qh(e)) { let t = P.from(e); o.iconPath = { dark: t, light: t }; } // Uri:当 background-image 画 + else { … { dark, light } … } } +``` + +而列表渲染这边: + +```js +if (r.iconPath) { … i.icon.className = "quick-input-list-icon"; i.icon.style.backgroundImage = kc(f); } +``` + +`Uri` 是当**图片**画的,颜色照原样显示;`iconPath` 又恰好接受 `{ light, dark }` 对。所以路线是: +**给每一行生成一张带颜色的 SVG**。 + +- 字形来自 `@vscode/codicons`(VS Code 自己那套 codicon 字体的美术源,作为 devDependency 精确锁定 + `0.0.46-24`),颜色取对应 `charts.*` 主题令牌的**默认 light/dark 值**(从 1.132 的颜色注册表读出)。 + 于是快捷菜单和工程视图用的是同一批颜色:树把 `charts.blue` 交给 `ThemeIcon` 由主题上色, + 这里把同一个默认值烤进图里。 +- 生成器 `tools/generate-quick-menu-icons.mjs` 解析 `src/commands/menu.ts` 的表格,输出 + `media/quick-menu/----.svg`(40 个,zip 后约 34 KiB), + 支持 `--check`(多一个没人要的文件也算漂移),已进 `npm run check`。 +- 门禁在 `test/commands/menu.test.ts`:逐行走表格,两个主题的资产必须存在、非空、不再含 + `currentColor`;并且**同一个命令在菜单和工程视图里必须是同一个图标 + 同一个颜色** + (`charts.` 是两者的接缝)。 + +顺手抓出三个真问题,都是门禁逼出来的: + +| 发现 | 处理 | +| --- | --- | +| `charts.orange` 解析到 `minimap.findMatchHighlight` → `editor.findMatchHighlightBackground`,是 `#EA5C00` **33% 透明**;当字形颜色就是两头上都糊成一团 | 破坏性动作改用 `charts.red`(`editorError.foreground`,各主题都是实色)。树里的 Clean 一起改 | +| `star` 根本不是 codicon(只有 `star-full` / `star-empty` / `star-half`)——"选择全局默认工具链"那一行一直是**空图标** | 换成 `star-full` | +| "Search and add a dependency" 在树里是 `cloud`、在菜单里是 `library` | 统一成 `library`(它打开的就是 Library 视图) | + +另外菜单里那 20 条标签的中英译文上一轮已补齐,这轮没有新增文案。 + +### 16.3 cache 视图默认折叠(反馈 3) + +`contributes.views` 的条目 schema 里本来就有 `visibility: "visible" | "hidden" | "collapsed"` +(默认 `visible`),消费点是 `createView(n, { …, expanded: !collapsed })`。所以 +`mcpp.cache` 加上 `"visibility": "collapsed"` 就够了:侧边栏打开时 cache 只剩标题栏, +高度让给上面的库列表。用户手动展开/折叠之后以用户的状态为准(VS Code 把它记在 workspace state 里)。 +门禁在 `test/artifacts.test.ts`:三个视图的 `visibility` 必须恰好是 +`[undefined, undefined, "collapsed"]`。 + +### 16.4 活动栏"彩色 logo":做不到,所以不做(反馈 4) + +你想要"保留单色 / 配置里切换彩色"。**彩色这条路在活动栏是不存在的**,有两条独立的一手证据: + +1. 自定义容器图标是当**模板**画的(15.6 的 `mask: url(icon) … mask-size: 24px`), + 画出来的是图标的 **alpha 通道**,颜色来自主题变量。颜色信息在第一步就被丢掉了。 +2. 清单里 `viewsContainers.activitybar[].icon` 的 schema 是纯 `string` + (`{description:…, type:"string"}`,`required:["id","title","icon"]`), + 连 `{light, dark}` 这种写法都会被 `isValidViewsContainer` 直接判为非法; + 而且没有任何运行时 API 能改容器图标。 + +所以"配置里切换彩色/单色"会是一个**永远无效的开关**——比没有这个开关更糟。 +单色字形保留(你说它和其他图标风格匹配,这也是它本该有的样子:活动栏图标就该是主题前景色的剪影)。 + +### 16.5 状态栏:背景色可以,logo 不行(反馈 5) + +**背景色:VS Code 只允许两种,而且由扩展主机强制执行。** `StatusBarItem.backgroundColor` +的 API 文档写了"只支持 `statusBarItem.errorBackground` 与 `statusBarItem.warningBackground`", +我不满足于文档,去找了实现(`extensionHostProcess.js`): + +```js +static ALLOWED_BACKGROUND_COLORS = new Map([ + ["statusBarItem.errorBackground", new ThemeColor("statusBarItem.errorForeground")], + ["statusBarItem.warningBackground", new ThemeColor("statusBarItem.warningForeground")], +]); +set backgroundColor(t){ t && !ALLOWED_BACKGROUND_COLORS.has(t.id) && (t = void 0); this._backgroundColor = t; … } +… +this._backgroundColor && (n = ALLOWED_BACKGROUND_COLORS.get(this._backgroundColor.id)); // ← 前景色被替换 +``` + +也就是说:自建一个 `mcpp.statusBarBackground` 颜色会被**静默丢弃**;自己设 `color` 也会被覆盖。 +实现方式是新设置 `mcpp.ui.statusBar.background`(`warning` / `error` / `none`,默认 `warning`), +纯映射放在 `src/cli/statusBar.ts`(已进纯模块门禁),`applyStatusBar()` 一行接上。 +默认 `warning` = 主题的琥珀色块 + VS Code 自动配的白色前景,正好和 logo 的金橙呼应;不喜欢就设 `none`。 + +**logo 放状态栏:不行。** `StatusBarItem.text` 是字符串,里面的 `$(name)` 由 +`ThemeIcon.fromString` 解析成 **codicon 字体字形**,没有任何图片通道;`StatusBarItem` 上也没有 +`iconPath` 之类的字段。所以 `$(tools) mcpp` 保持不变——它是这套菜单(构建 / 工具链 / 缓存 / 模块) +最贴切的字形,而"换成 mcpp logo"这条路在 API 层面不存在。 + +### 16.6 这一轮的状态与仍然没做到的 + +- 653 个单元测试通过;`check:config`(69 设置 / 32 public)、`l10n-check`(382 运行串 / + 203 清单键)、`check:icon`(活动栏图 + 40 个菜单图标)、`check:generators` 全过; + VSIX 134 文件 / 377 KiB(两道体积门禁内:<200 文件、<1 MiB)。 +- dev profile 重装并重启(清掉 workspaceStorage,让 cache 的默认折叠生效),扩展主机日志确认激活、无错误。 +- **没做到 / 需要你的眼睛**:快捷菜单的彩色图标我只验证到"文件内容正确、颜色被烤进去、 + 能被栅格化成正确的颜色",**没有**在真实 quick pick 里看过——如果 `file:` URI 在快速选择里 + 加载不出来,那 16 行的槽位会是空的,那我就改走别的路子(data: URI 或退回单色)。这一条请重点看。 +- 库视图的死循环是**逻辑上**必然成立(文档 self-announce + 每次新 nonce),但没有做进程级 + 的 CPU 观测;"不抖了、能点了"需要你确认。 +- 活动栏彩色、状态栏 logo:API 层面不存在,已用一手源码说明。 +- e2e 仍未在本地跑通;索引定位的三条路径仍是打桩验证,不是 UI 验证。 + +## 17. round 5:状态栏背景回退、侧边栏配比、库标签与详情页、年龄渐变、新建工程 + +### 17.1 状态栏背景色默认关掉(反馈 1) + +`mcpp.ui.statusBar.background` 保留(它仍是唯一能做的两种背景),但默认值从 `warning` 改成 +`none`:一个常驻的琥珀色块会被读成"出问题了"。想要就设 `warning` / `error`,两个值都由 +VS Code 自己配好对比度(见 16.5 的扩展主机白名单)。 + +### 17.2 侧边栏配比:库视图默认占下面 2/3(反馈 2) + +`contributes.views[].initialSize` 不是像素,它是**视图在容器里的配比权重**(一手证据 +`computeInitialSizes()`): + +```js +let t = this.viewContainerModel.visibleViewDescriptors.reduce((i,{weight:n}) => i + (n||20), 0); +for (let i of this.viewContainerModel.visibleViewDescriptors) + e.set(i.id, this.dimension.height * (i.weight || 20) / t); // 默认每个视图 20 +``` + +所以给 `mcpp.library` 一个 `initialSize: 40`(默认 20 的两倍)就够:默认布局里项目视图拿 1/3、 +库视图拿 2/3,cache 又是折叠的(只剩标题栏,它的份额按比例回流给另外两个)。门禁在 +`test/artifacts.test.ts`:三个视图的 `initialSize` 必须恰好是 `[undefined, 40, undefined]`, +注释里带上上面那段权重公式。 + +### 17.3 库的标签去掉外框,详情页重排(反馈 3) + +**标签**(`.badge`)之前是"1px 外框 + 圆角"的小方块,一行的标签看起来像一排按钮。 +现在是不带任何框和底的**安静元数据**,相邻项之间用 `·` 分隔;只有必须被看见的两个状态 +(`Added`、`Descriptor not readable`)保留颜色并加粗——文字本身也在说同一件事,所以不靠颜色。 +**筛选 chip**(`.chip`)是交互控件,所以换成 VS Code 自己的 toggle 配色: +未选中 = 透明底、`descriptionForeground`;选中 = `inputOption.activeBackground/Foreground/Border`。 +"未选中也描一圈边"正是让它像按钮的原因。 + +**详情页**的结构问题更根本:读者来点的那颗按钮原本在**四个段落之后**(`renderActions` 在 +`
` 的最后)。现在顺序是: + +``` +标题 → 一句话描述 → 事实行(registry / surface / standard / 许可 / 徽章 / 仓库链接) +→ 主操作块([Add to mcpp.toml] [☐ dev-dependency] + 命令预览) +→ 版本矩阵 → 用法示例 → 依赖 → 其它 +``` + +同时把**版本矩阵变成选择器**:每个版本是一个 button(`data-version`),点它就把上面的命令 +指到那个版本,被选中的那枚用同一套 toggle 配色标出。原来 ``, + ` ${escapeHtml(label(CACHE_PANEL_UI.budgetUnit))}`, + ` `, + ` ${estimate}`, + ` ${button("prune", label(CACHE_PANEL_UI.prune), !sharedAvailable, sharedReason)}`, + legacyBytes > 0 + ? ` ${button("cleanLegacy", label(CACHE_PANEL_UI.cleanLegacy), false, label(CACHE_PANEL_UI.reasonLegacy))}` + : "", + ``, + ].filter((line) => line.length > 0).join("\n"); +} + +/** + * The global block. Expanded it is the composition bar, the age bar, the nested + * largest-packages list and the shared actions; collapsed it is one summary line + * (`7.20 GiB · 657 entries`). The markup carries **no `open` attribute**, which + * is what makes the first render collapsed. + */ +function renderSharedBlock(model: CachePanelModel, label: UiLabel): string { + const formatters = model.format; + const notes: string[] = []; + if (model.shared.root !== undefined) { + notes.push( + `

${escapeHtml(fill(label, CACHE_PANEL_UI.sharedRoot, [model.shared.root]))}

`, + ); + } + if (model.legacy !== undefined && model.legacy.path !== undefined) { + notes.push( + `

${escapeHtml(fill(label, CACHE_PANEL_UI.legacyPath, [model.legacy.path]))}

`, + ); + } + + const body = [ + renderComposition(model, label), + renderAge(model, label), + renderTop(model, label), + renderSharedActions(model, label), + ...notes, + ].join("\n"); + + if (!model.shared.available) { + // A failure must not be hidden behind a collapsed disclosure: render the + // block open, with the reason where the summary would have been. + return [ + `
`, + `

${escapeHtml(label(CACHE_PANEL_UI.sharedTitle))}

`, + `

${escapeHtml(model.shared.note ?? label(CACHE_PANEL_UI.sharedUnavailable))}

`, + renderSharedActions(model, label), + `
`, + ].join("\n"); + } + + const summary = `${formatters.bytes(finite(model.shared.totalBytes))} · ${fill( + label, + CACHE_PANEL_UI.sharedEntries, + [formatters.count(finite(model.shared.totalEntries))], + )}`; + return [ + `
`, + ` `, + ` ${escapeHtml(label(CACHE_PANEL_UI.sharedTitle))}`, + ` ${escapeHtml(summary)}`, + ` `, + `
`, + body, + `
`, + `
`, + ].join("\n"); +} + +// ───────────────────────────────────────────────────────────────── client + +/** + * The client. Dependency-free, no template literals of its own, and it only + * posts messages: the host re-renders the whole document after every action, so + * there is exactly one renderer instead of two. + * + * The open/closed state of the two `
` is the one thing the client + * remembers (`setState`), so re-rendering after an action does not re-collapse + * the block the user just opened. The document itself still ships collapsed. + */ +function clientScript(): string { + return `(function () { + "use strict"; + var api = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : undefined; + var saved = {}; + if (api && typeof api.getState === "function") { + try { saved = api.getState() || {}; } catch (error) { saved = {}; } + } + + function post(message) { + if (api) { api.postMessage(message); } + } + + function detailsList() { + return document.querySelectorAll("details[data-details]"); + } + + function restoreOpen() { + var open = saved && typeof saved.open === "object" && saved.open !== null ? saved.open : {}; + var all = detailsList(); + for (var index = 0; index < all.length; index += 1) { + var id = all[index].getAttribute("data-details"); + if (id && open[id] === true) { all[index].open = true; } + } + } + + function rememberOpen() { + var open = {}; + var all = detailsList(); + for (var index = 0; index < all.length; index += 1) { + var id = all[index].getAttribute("data-details"); + if (id) { open[id] = all[index].open === true; } + } + saved.open = open; + if (api && typeof api.setState === "function") { api.setState(saved); } + } + + function budgetGiB() { + var input = document.getElementById("cache-budget"); + if (!input) { return undefined; } + var text = String(input.value).trim(); + if (text.length === 0) { return undefined; } + var value = Number(text); + if (!isFinite(value) || value < 0) { return undefined; } + return value; + } + + document.addEventListener("click", function (event) { + var target = event.target; + if (!target || typeof target.closest !== "function") { return; } + var button = target.closest("button"); + if (!button || button.disabled) { return; } + var entry = button.getAttribute("data-show-entry"); + if (entry) { post({ type: "showEntry", label: entry }); return; } + var action = button.getAttribute("data-action"); + if (!action) { return; } + if (action === "collect") { + var value = budgetGiB(); + if (value === undefined) { return; } + post({ type: "collect", budgetGiB: value }); + return; + } + post({ type: action }); + }); + + // A toggle event does not bubble, so the listener captures it on the way down. + document.addEventListener("toggle", function (event) { + var target = event.target; + if (target && typeof target.getAttribute === "function" && target.hasAttribute("data-details")) { + rememberOpen(); + } + }, true); + + restoreOpen(); +})();`; +} + +// ───────────────────────────────────────────────────────────── the document + +/** The whole document. */ +export function renderCachePanelHtml(model: CachePanelModel, assets: CachePanelAssets): string { + const label: UiLabel = (key) => model.ui[key] ?? key; + const csp = `default-src 'none'; style-src ${assets.cspSource}; script-src 'nonce-${assets.nonce}'; img-src ${assets.cspSource}`; + const body = [ + renderWarnings(model, label), + renderProjectBlock(model, label), + renderSharedBlock(model, label), + `

${escapeHtml(label(CACHE_PANEL_UI.boundary))}

`, + ] + .filter((part) => part.length > 0) + .join("\n"); + return ` + + + + + + +${escapeHtml(label(CACHE_PANEL_UI.title))} + + +

${escapeHtml(label(CACHE_PANEL_UI.title))}

+
+${body} +
+ + + +`; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function nonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} + +/** + * Decode one `postMessage` payload. Webview input is untrusted: anything that is + * not exactly one of the eight shapes is dropped, and the returned object is + * rebuilt so foreign fields never travel further. + * + * `collect` needs a finite, non-negative `budgetGiB`; a payload without one is + * not a request this panel can honour. + */ +export function decodeCachePanelMessage(raw: unknown): CachePanelMessage | undefined { + if (!isRecord(raw)) { + return undefined; + } + switch (raw.type) { + case "refresh": + case "cleanStale": + case "cleanProject": + case "prune": + case "verify": + case "cleanLegacy": + return { type: raw.type }; + case "collect": { + const budget = raw.budgetGiB; + if (typeof budget !== "number" || !Number.isFinite(budget) || budget < 0) { + return undefined; + } + return { type: "collect", budgetGiB: budget }; + } + case "showEntry": + return nonEmptyString(raw.label) ? { type: "showEntry", label: raw.label } : undefined; + default: + return undefined; + } +} diff --git a/src/cache/cacheState.ts b/src/cache/cacheState.ts new file mode 100644 index 0000000..f90b9e9 --- /dev/null +++ b/src/cache/cacheState.ts @@ -0,0 +1,188 @@ +/** + * The cache view's decisions, as **data**. + * + * Everything here is deliberately free of `vscode` and of the file system, so the + * settings that shape the cache view can be tested directly: + * + * - whether the pre-v1 cache node is offered (`mcpp.cache.showLegacy`, §8 G6); + * - what the panel is allowed to say about that cache; + * - the warning node for `mcpp.cache.warnAboveGiB`; + * - the extra confirmation `mcpp.cache.gc.confirmAboveGiB` adds; + * - the snapshot the environment self-check reads (`showSelfCheck`, §8 G9). + * + * `src/cache/cacheView.ts` reads the settings and hands the values in; the + * `vscode` layer therefore stays thin enough to read in one sitting. + */ + +import { formatBytes } from "../util/format"; + +/** 1 GiB, binary — the unit both cache size settings are declared in. */ +export const BYTES_PER_GIB = 1024 ** 3; + +/** + * A node built here rather than in `./models.ts`, so the tree model needs no + * edit: the shape is structurally the tree's `TreeNode`. The cache view itself is + * a sidebar `WebviewView` since §8.1 and no longer builds a tree, so this node is + * kept as the policy (§8 G6) that the warning threshold is decided in one pure + * place — the view states the same threshold in + * `src/cache/cachePanelHtml.ts`'s warning banner. + */ +export interface CacheTreeNode { + id: string; + label: { key: string; args?: readonly (string | number)[] }; + description?: { key: string; args?: readonly (string | number)[] }; + tooltip?: { key: string; args?: readonly (string | number)[] }; + icon?: string; + contextValue?: string; + command?: { command: string; title: { key: string; args?: readonly (string | number)[] } }; + children?: readonly CacheTreeNode[]; +} + +/** `mcpp.cache.showLegacy`, as the view reads it. */ +export interface LegacyGate { + enabled: boolean; + bytes?: number; + files?: number; + truncated?: boolean; +} + +export type LegacyDisplay = "shown" | "off" | "empty" | "unknown"; + +/** Why the pre-v1 node is or is not in the tree. Pure, so the policy is testable. */ +export function legacyDisplay(gate: LegacyGate): LegacyDisplay { + if (!gate.enabled) { + return "off"; + } + if (gate.bytes === undefined) { + return "unknown"; + } + return gate.bytes > 0 ? "shown" : "empty"; +} + +/** + * The figure the tree needs, or `undefined` when no node should exist. Kept next + * to {@link legacyDisplay} so "the node renders only when the setting is on and + * the directory is non-empty" is stated once. + */ +export function legacyBytesForTree(gate: LegacyGate): number | undefined { + return legacyDisplay(gate) === "shown" ? gate.bytes : undefined; +} + +/** What the panel may show: the same rule, plus the path it can offer to clean. */ +export function legacyForPanel( + gate: LegacyGate, + path: string | undefined, +): { bytes: number; path?: string } | undefined { + if (legacyDisplay(gate) !== "shown" || path === undefined) { + return undefined; + } + return { bytes: gate.bytes ?? 0, path }; +} + +/** + * The warning the tree shows when the shared cache reached `mcpp.cache.warnAboveGiB`. + * + * `0` — and a non-positive or unreadable threshold — disables the warning + * entirely, which is what the registry's description promises. `bytes` must come + * from the same read the rest of the view uses; a missing inventory is not a + * warning (nothing was measured, so nothing is known to be large). + */ +export function buildCacheWarningNode(bytes: number, thresholdGiB: number): CacheTreeNode | undefined { + if (!Number.isFinite(thresholdGiB) || thresholdGiB <= 0) { + return undefined; + } + if (!Number.isFinite(bytes) || bytes < thresholdGiB * BYTES_PER_GIB) { + return undefined; + } + const thresholdBytes = thresholdGiB * BYTES_PER_GIB; + return { + id: "cache.warning", + label: { key: "Cache warning" }, + description: { key: "{0} · at or above {1}", args: [formatBytes(bytes), formatBytes(thresholdBytes)] }, + tooltip: { + key: "The shared build cache is {0}. Raise or clear mcpp.cache.warnAboveGiB to change when this appears.", + args: [formatBytes(bytes)], + }, + icon: "warning", + contextValue: "mcppCacheWarning", + // The cache view is a sidebar `WebviewView` now, so the node points at the + // view's own focus command (VS Code registers `.focus` for every + // contributed view) rather than at the removed `mcpp.showCachePanel`. + command: { command: "mcpp.cache.focus", title: { key: "Cache statistics" } }, + }; +} + +/** `mcpp.cache.gc.confirmAboveGiB`: does this budget add a confirmation level? */ +export function gcBudgetNeedsExtraConfirm(budgetGiB: number, confirmAboveGiB: number): boolean { + if (!Number.isFinite(budgetGiB) || budgetGiB < 0) { + return false; + } + if (!Number.isFinite(confirmAboveGiB) || confirmAboveGiB < 0) { + return false; + } + // 0 means "confirm every cleanup", including the one with no budget at all. + return confirmAboveGiB === 0 || budgetGiB >= confirmAboveGiB; +} + +export interface GcBudgetDialog { + /** Substituted into the caller's `detail` key: the budget in GiB. */ + args: readonly (string | number)[]; +} + +/** + * The data the extra level's dialogue needs. + * + * It deliberately carries no sentences: the caller writes the translation + * literals (as `src/cli/clean.ts` does with its `titleKey`/`detailKey`), which is + * what makes `tools/l10n-check.mjs` see them. This module only decides *when* the + * dialogue happens and what number it names. + */ +export function gcBudgetDialog(budgetGiB: number, confirmAboveGiB: number): GcBudgetDialog { + // A budget of 0 means "no budget given": the threshold is the figure worth + // naming in that case. + const named = budgetGiB > 0 ? budgetGiB : confirmAboveGiB; + return { args: named > 0 ? [named] : [] }; +} + +/** What the environment self-check puts under `Cache` (`cli/selfCheck.ts` §8 G9). */ +export interface CacheSnapshot { + totalBytes: number; + entries: number; + incomplete: number; +} + +/** + * The snapshot from an inventory the caller already read. No query is repeated + * here; `readCacheSnapshot()` in `src/cache/cacheView.ts` is the one that runs + * them, and it reuses the cache view's own read path. + */ +export function cacheSnapshotFrom( + inventory: { totalBytes: number; totalEntries: number; incomplete: number } | undefined, +): CacheSnapshot | undefined { + if (inventory === undefined) { + return undefined; + } + return { + totalBytes: Number.isFinite(inventory.totalBytes) ? inventory.totalBytes : 0, + entries: Number.isFinite(inventory.totalEntries) ? inventory.totalEntries : 0, + incomplete: Number.isFinite(inventory.incomplete) ? inventory.incomplete : 0, + }; +} + +export interface TimerDecision { + active: boolean; + seconds: number; +} + +/** + * Whether one of the two view timers should be running. + * + * Every condition that turns a timer off is stated once, in one place: + * `0` (or anything that is not a positive finite number) disables it, and so does + * a view that is not visible. The two settings differ only in what feeds + * `viewVisible`. + */ +export function refreshTimerDecision(seconds: number, viewVisible: boolean): TimerDecision { + const usable = Number.isFinite(seconds) && seconds > 0; + return { active: usable && viewVisible, seconds: usable ? seconds : 0 }; +} diff --git a/src/cache/cacheView.ts b/src/cache/cacheView.ts new file mode 100644 index 0000000..40ef2cc --- /dev/null +++ b/src/cache/cacheView.ts @@ -0,0 +1,762 @@ +/** + * The cache view and every cleanup command. + * + * The view is a sidebar **WebviewView** (`mcpp.cache`, plan §8.1): the numbers + * and the bars live in one document whose host is `src/cache/cachePanel.ts`, and + * whose structure is the pure `src/cache/cachePanelHtml.ts`. This file owns the + * data behind it, the status item, the auto-refresh timer and the commands. + * + * The *policy* — which argv, how much confirmation, whether a preview is shown — + * lives in `src/cli/clean.ts` and is unit-tested there. This file only asks the + * user, runs the command and reports the result, so the safety rules cannot be + * edited by accident in a UI change. + * + * Nothing here removes anything by itself: every destructive path goes through + * `planClean`, a preview when the plan asks for one, and a modal. + * + * Four settings shape this file: + * + * - `mcpp.cache.showLegacy` decides whether the pre-v1 cache is measured and + * offered at all (`src/cache/cacheState.ts`, §8 G6); + * - `mcpp.cache.warnAboveGiB` adds a warning when the shared cache is large; + * - `mcpp.cache.autoRefreshSeconds` re-reads while the view is on screen; + * - `mcpp.cache.gc.confirmAboveGiB` adds one confirmation level to a large `gc`. + */ + +import * as vscode from "vscode"; + +import { estimateArtifacts, measureDirectory, type ArtifactEstimate } from "../cli/artifacts"; +import { parseCacheDir, parseCacheList, summarizeCache, type CacheInventory, type CacheEntry } from "../cli/cache"; +import { planClean, withSharedCache, type CleanPlan } from "../cli/clean"; +import { runMcpp, type ProcessResult } from "../cli/process"; +import { CACHE_COMMANDS } from "../commands/ids"; +import { read } from "../config/access"; +import { format as formatMessage, t } from "../i18n/t"; +import { PollTimer } from "../mcppls/timers"; +import type { McppProjectDiscovery } from "../projects/discovery"; +import { formatBytes, projectGc } from "../util/format"; +import { clampOutput } from "../util/text"; +import { + cacheSnapshotFrom, + gcBudgetDialog, + gcBudgetNeedsExtraConfirm, + legacyForPanel, + refreshTimerDecision, + type CacheSnapshot, +} from "./cacheState"; +import { + CACHE_VIEW_ID, + CACHE_VIEW_FOCUS_COMMAND, + registerCachePanel, + type CachePanelData, + type CachePanelProvider, +} from "./cachePanel"; + +export { CACHE_VIEW_ID, CACHE_VIEW_FOCUS_COMMAND }; + +/** The tick granularity; `mcpp.cache.autoRefreshSeconds` is in seconds. */ +const REFRESH_TICK_MS = 100; + +/** The pre-v1 walk is a courtesy figure, so it gets a smaller budget than `target/`. */ +const LEGACY_MAX_ENTRIES = 20_000; + +/** `mcpp.runtime.timeoutSeconds`, or the built-in default when it is 0. */ +function queryTimeoutMs(): number | undefined { + const seconds = read("mcpp.runtime.timeoutSeconds"); + return seconds > 0 ? seconds * 1000 : 60_000; +} +const CLEAN_TIMEOUT_MS = 300_000; + +export interface CacheViewDeps { + output: vscode.OutputChannel; + currentProject: () => McppProjectDiscovery | undefined; + mcppExecutable: (project: McppProjectDiscovery | undefined) => string; + isTrusted: () => boolean; +} + +/** What `mcpp cache dir` reported about the pre-v1 cache, measured or not. */ +interface LegacyCache { + path?: string; + bytes?: number; + files?: number; + truncated?: boolean; +} + +interface CacheState { + entries: CacheEntry[]; + inventory?: CacheInventory; + artifacts?: ArtifactEstimate; + legacy?: LegacyCache; + error?: string; +} + +function workingDirectory(project: McppProjectDiscovery | undefined): string | undefined { + return project?.root; +} + +/** Unix seconds -> an ISO instant the panel can print; the panel does not do dates. */ +function timestamp(seconds: number | undefined): string | undefined { + return seconds === undefined || !Number.isFinite(seconds) ? undefined : new Date(seconds * 1000).toISOString(); +} + +function numberFormat(): "binary" | "decimal" { + return read("mcpp.ui.numberFormat") === "decimal" ? "decimal" : "binary"; +} + +/** + * `mcpp.cache.showLegacy`: the whole rule in one object, so the panel and the + * self-check read the same answer. + */ +function legacyGate(settings: { showLegacy: boolean }, legacy: LegacyCache | undefined): { + enabled: boolean; + bytes?: number; + files?: number; + truncated?: boolean; +} { + return { + enabled: settings.showLegacy, + bytes: legacy?.bytes, + files: legacy?.files, + truncated: legacy?.truncated, + }; +} + +function viewSettings(): { + topN: number; + ageBoundaries: string[]; + showLegacy: boolean; + warnAboveGiB: number; + autoRefreshSeconds: number; +} { + return { + topN: Math.max(1, read("mcpp.views.cache.topN")), + ageBoundaries: read("mcpp.views.cache.ageBuckets"), + showLegacy: read("mcpp.cache.showLegacy") !== false, + warnAboveGiB: read("mcpp.cache.warnAboveGiB"), + autoRefreshSeconds: read("mcpp.cache.autoRefreshSeconds"), + }; +} + +async function appendResult( + deps: CacheViewDeps, + title: string, + executable: string, + argv: readonly string[], + cwd: string | undefined, + result: ProcessResult, +): Promise { + try { + deps.output.appendLine(`\n[${new Date().toISOString()}] ${title}`); + deps.output.appendLine(`$ ${[executable, ...argv].join(" ")}`); + const stdout = clampOutput(result.stdout); + const stderr = clampOutput(result.stderr); + if (stdout.text.trim().length > 0) deps.output.appendLine(stdout.text.trimEnd()); + if (stderr.text.trim().length > 0) deps.output.appendLine(stderr.text.trimEnd()); + deps.output.appendLine(`[exit ${result.exitCode}]`); + } catch { + // The channel can already be gone during shutdown. + } +} + +async function run( + deps: CacheViewDeps, + project: McppProjectDiscovery | undefined, + argv: readonly string[], + options: { timeoutMs?: number; quiet?: boolean } = {}, +): Promise { + const executable = deps.mcppExecutable(project); + const cwd = workingDirectory(project); + const result = options.quiet === true + ? await runMcpp(deps.isTrusted(), executable, [...argv], cwd, { timeoutMs: options.timeoutMs, maxBufferMiB: read("mcpp.runtime.maxOutputMiB") }) + : await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: `mcpp ${argv[0]}` }, + () => runMcpp(deps.isTrusted(), executable, [...argv], cwd, { timeoutMs: options.timeoutMs }), + ); + if (result === undefined) { + // The seam refuses in an untrusted workspace; the callers gate with their + // own message, so this is the defensive shape of the same refusal. + return { exitCode: 1, stdout: "", stderr: t("the workspace is not trusted") }; + } + if (result.exitCode !== 0 || options.quiet !== true) { + await appendResult(deps, `mcpp ${argv.join(" ")}`, executable, argv, cwd, result); + } + return result; +} + +async function preview(title: string, content: string): Promise { + const document = await vscode.workspace.openTextDocument({ + content: `# ${title}\n\n\`\`\`\n${content.trimEnd()}\n\`\`\`\n`, + language: "markdown", + }); + await vscode.window.showTextDocument(document, { preview: true, preserveFocus: true }); +} + +/** + * Ask for confirmation, honouring the plan's level. Level 3 asks twice: once as + * a modal and once with an explicit acknowledgement, because that is the one + * that reaches every project on the machine. + */ +async function confirmPlan(plan: CleanPlan, detail: string, figures: string): Promise { + const body = `${t(plan.detailKey, figures)}\n\n${detail}`; + if (plan.level === 0) { + return true; + } + const runLabel = t("Run"); + const choice = await vscode.window.showWarningMessage( + t(plan.titleKey), + { modal: true, detail: body }, + runLabel, + ); + if (choice !== runLabel) { + return false; + } + if (!plan.acknowledge) { + return true; + } + const acknowledge = t("I understand this affects every mcpp project on this machine"); + const second = await vscode.window.showWarningMessage( + t("Confirm once more"), + { modal: true, detail: body }, + acknowledge, + ); + return second === acknowledge; +} + +/** + * The extra level `mcpp.cache.gc.confirmAboveGiB` adds. + * + * It is asked *before* the plan's own modal, so a refused acknowledgement never + * reaches the dialogue that would have run the command. It never replaces a + * level: a plan that already demands an acknowledgement is not asked a third + * time, and the plan's own modal still runs afterwards. + * + * The sentences live here rather than in `cacheState.ts` so + * `tools/l10n-check.mjs` sees them as translation call literals; the decision and + * the figure come from the pure module. + */ +export function extraGcConfirmation( + plan: CleanPlan, + budgetGiB: number, + confirmAboveGiB: number, +): { title: string; detail: string; acknowledge: string; args: readonly (string | number)[] } | undefined { + if (plan.action !== "cacheGc" || plan.acknowledge) { + return undefined; + } + if (!gcBudgetNeedsExtraConfirm(budgetGiB, confirmAboveGiB)) { + return undefined; + } + const dialog = gcBudgetDialog(budgetGiB, confirmAboveGiB); + return { + title: t("Confirm a cleanup this large"), + detail: t( + "This {0} GiB budget frees entries that every mcpp project on this machine shares. The cleanup can take a while, and any dropped entry is rebuilt on next use.", + ), + acknowledge: t("I understand this removes entries other mcpp projects may use"), + args: dialog.args, + }; +} + +/** + * The cache view and its commands. + * + * Returns the sidebar provider it registered, so a caller that would rather own + * the `registerWebviewViewProvider` call can register this value itself instead + * (see `registerCachePanel`). Ignoring the return value is the normal case: the + * view is registered here, and `extension.ts` needs no extra line. + */ +export function registerCacheView(context: vscode.ExtensionContext, deps: CacheViewDeps): CachePanelProvider { + let state: CacheState = { entries: [] }; + /** Guards the interval: one refresh at a time, and none after a failure. */ + let refreshInFlight: Promise | undefined; + /** + * The sidebar view. Assigned after the timer below is declared, because the + * timer asks the provider whether the view is on screen; everything that reads + * it does so long after `registerCacheView` has returned. + */ + let provider: CachePanelProvider | undefined; + const viewVisible = (): boolean => provider?.visible === true; + + // An optional, second status item. Off by default: the C++ Modules extension + // already owns a status item, and the mcpp quick menu owns ours. + const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 39); + // `mcpp.cache.focus` is registered by VS Code for the contributed view, so the + // status item can bring the sidebar view on screen without a command of ours. + status.command = CACHE_VIEW_FOCUS_COMMAND; + context.subscriptions.push(status); + + const updateStatus = (): void => { + if (!read("mcpp.cache.statusBar")) { + status.hide(); + return; + } + const total = state.inventory?.totalBytes; + status.text = total === undefined ? "$(database) mcpp" : `$(database) ${formatBytes(total, numberFormat())}`; + status.tooltip = t("Shared build cache"); + status.show(); + }; + + const applyState = (next: CacheState): void => { + state = next; + setLastCacheSnapshot( + next.inventory === undefined + ? undefined + : cacheSnapshotFrom({ + totalBytes: next.inventory.totalBytes, + totalEntries: next.inventory.totalEntries, + incomplete: next.inventory.incomplete.length, + }), + ); + }; + + /** + * Re-read everything the view shows. It deliberately does **not** redraw the + * webview: the draw path (`CachePanelDeps.read` is `read` + `panelData`) calls + * this first, and a redraw from inside here would ask itself for the same + * figures again. + */ + const refreshNow = async (): Promise => { + const project = deps.currentProject(); + const settings = viewSettings(); + const next: CacheState = { entries: [] }; + + if (!deps.isTrusted()) { + next.error = t("the workspace is not trusted"); + applyState(next); + return; + } + + const listed = await run(deps, project, ["cache", "list", "--format", "json"], { + timeoutMs: queryTimeoutMs(), + quiet: true, + }); + if (listed.exitCode === 0) { + const parsed = parseCacheList(listed.stdout); + if (parsed === undefined) { + next.error = t("mcpp cache list did not return the documented document"); + } else { + next.entries = parsed.entries; + next.inventory = summarizeCache(parsed.root, parsed.entries, { + topN: settings.topN, + ageBoundaries: settings.ageBoundaries, + }); + } + } else { + next.error = t("mcpp cache list failed (exit {0})", listed.exitCode); + } + + const dir = await run(deps, project, ["cache", "dir"], { timeoutMs: queryTimeoutMs(), quiet: true }); + if (dir.exitCode === 0) { + const parsed = parseCacheDir(dir.stdout); + // The pre-v1 directory is measured only when the setting offers it: a + // user who turned `mcpp.cache.showLegacy` off pays for no walk at all. + const measured = + settings.showLegacy && parsed.legacyPath !== undefined + ? measureDirectory(parsed.legacyPath, { maxEntries: LEGACY_MAX_ENTRIES }) + : undefined; + next.legacy = { + path: parsed.legacyPath, + bytes: measured?.totalBytes, + files: measured?.files, + truncated: measured?.truncated !== undefined, + }; + } + + if (project !== undefined && read("mcpp.cache.estimateProjectBytes")) { + next.artifacts = estimateArtifacts(project.root); + } + + applyState(next); + updateStatus(); + }; + + /** Single-flight: an interval tick during a read must not start a second one. */ + const refresh = (): Promise => { + if (refreshInFlight !== undefined) { + return refreshInFlight; + } + refreshInFlight = refreshNow().finally(() => { + refreshInFlight = undefined; + }); + return refreshInFlight; + }; + + /** The figures a confirmation dialogue needs, without running anything new. */ + const figures = (): string => { + const total = state.inventory?.totalBytes; + return total === undefined ? t("(size unknown)") : t("{0} in the shared cache", formatBytes(total)); + }; + + const runPlan = async (project: McppProjectDiscovery | undefined, plan: CleanPlan, extra = ""): Promise => { + const detail = `${t("Command")}: mcpp ${plan.argv.join(" ")}${extra}`; + if (!(await confirmPlan(plan, detail, figures()))) { + return; + } + const result = await run(deps, project, plan.argv, { timeoutMs: CLEAN_TIMEOUT_MS }); + if (result.exitCode === 0) { + await refresh(); + return; + } + void vscode.window.showErrorMessage(t("mcpp {0} failed with exit code {1}", plan.argv.join(" "), result.exitCode)); + }; + + /** `runPlan` plus the one extra level `mcpp.cache.gc.confirmAboveGiB` adds. */ + const runGcPlan = async ( + project: McppProjectDiscovery | undefined, + budgetGiB: number, + extra: string, + ): Promise => { + const plan = planClean("cacheGc", { budgetGiB }); + const dialog = extraGcConfirmation(plan, budgetGiB, read("mcpp.cache.gc.confirmAboveGiB")); + // The extra level is asked *before* the plan's own modal, so a refused + // acknowledgement never reaches the one that would have run the command. + // `extraGcConfirmation` already resolved the strings through `t()`. + if (dialog !== undefined) { + const choice = await vscode.window.showWarningMessage( + dialog.title, + { modal: true, detail: formatMessage(dialog.detail, dialog.args) }, + dialog.acknowledge, + ); + if (choice !== dialog.acknowledge) { + return; + } + } + await runPlan(project, plan, extra); + }; + + const requireTrusted = (): boolean => { + if (deps.isTrusted()) { + return true; + } + void vscode.window.showWarningMessage( + t("This workspace is not trusted. mcpp commands that write are disabled until you trust it."), + ); + return false; + }; + + const register = (id: string, handler: (...args: unknown[]) => Promise): void => { + context.subscriptions.push( + vscode.commands.registerCommand(id, async (...args: unknown[]) => { + try { + await handler(...args); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + deps.output.appendLine(`mcpp: ${message}`); + void vscode.window.showErrorMessage(t("mcpp: {0}", message)); + } + }), + ); + }; + + /** What the view shows, from the same state the commands use. Runs nothing. */ + const panelData = (): CachePanelData => { + const inventory = state.inventory; + const artifacts = state.artifacts; + return { + project: { + available: artifacts !== undefined && artifacts.exists, + note: + artifacts === undefined + ? t("Project artifacts could not be measured.") + : artifacts.exists + ? undefined + : t("No target/ directory"), + totalBytes: artifacts?.totalBytes ?? 0, + files: artifacts?.files ?? 0, + groups: artifacts?.byTopLevel.length ?? 0, + truncated: artifacts?.truncated, + }, + shared: { + available: inventory !== undefined, + note: inventory === undefined ? state.error ?? t("The shared build cache could not be read.") : undefined, + root: inventory?.root, + totalBytes: inventory?.totalBytes ?? 0, + totalEntries: inventory?.totalEntries ?? 0, + byKind: inventory?.byKind ?? [], + buckets: inventory?.ageBuckets ?? [], + top: (inventory?.topLabels ?? []).map((entry) => ({ + label: entry.label, + entries: entry.entries, + bytes: entry.bytes, + oldestAccessed: entry.oldestAccessed, + })), + incomplete: inventory?.incomplete.length ?? 0, + oldestAccessed: timestamp(inventory?.oldestAccessed), + newestAccessed: timestamp(inventory?.newestAccessed), + }, + legacy: legacyForPanel(legacyGate(viewSettings(), state.legacy), state.legacy?.path), + }; + }; + + // `mcpp.cache.autoRefreshSeconds`: re-read while the view is on screen, never + // in an untrusted workspace, and never on top of a read that is still running. + // The tick only redraws: the provider's own read path performs the refresh, so + // one tick costs one round of queries. + const timer = new PollTimer({ + periodMs: 0, + tick: (): void => { + if (viewVisible() && deps.isTrusted()) { + provider?.refresh(); + } + }, + }); + context.subscriptions.push(timer); + const applyTimer = (): void => { + const seconds = viewSettings().autoRefreshSeconds; + const usable = deps.isTrusted() && Number.isFinite(seconds) && seconds > 0; + const decision = refreshTimerDecision(usable ? seconds : 0, viewVisible()); + timer.start(decision.active ? decision.seconds * 1000 : 0); + }; + applyTimer(); + + // The sidebar webview view: `mcpp.cache` is a `"type": "webview"` view, so + // this provider (not a tree) is what draws it. Every destructive action still + // goes through `runPlan` above. + provider = registerCachePanel(context, { + refresh, + read: panelData, + run: async (message) => { + const project = deps.currentProject(); + switch (message.type) { + case "cleanStale": + await runPlan(project, planClean("stale", { staleDays: read("mcpp.cache.staleDays") })); + return; + case "cleanProject": + await runPlan(project, planClean("project")); + return; + case "collect": + await runGcPlan(project, message.budgetGiB, ""); + return; + case "prune": + await runPlan(project, planClean("cachePrune", { pruneAgeDays: read("mcpp.cache.pruneAgeDays") })); + return; + case "cleanLegacy": + await runPlan(project, planClean("cacheLegacy")); + return; + default: + await run(deps, project, ["cache", "verify"], { timeoutMs: CLEAN_TIMEOUT_MS }); + return; + } + }, + showEntry: async (label) => { + await vscode.commands.executeCommand(CACHE_COMMANDS.showEntry, label); + }, + onVisibilityChanged: () => applyTimer(), + }); + + register(CACHE_COMMANDS.refreshStats, async () => { + if (!requireTrusted()) return; + await refresh(); + provider?.refresh(); + }); + + register(CACHE_COMMANDS.showEntry, async (label) => { + if (!requireTrusted()) return; + if (typeof label !== "string" || label.length === 0) { + return; + } + const project = deps.currentProject(); + const result = await run(deps, project, ["cache", "info", label], { timeoutMs: queryTimeoutMs(), quiet: true }); + await preview(t("Cache entry {0}", label), result.stdout.length > 0 ? result.stdout : result.stderr); + }); + + register(CACHE_COMMANDS.cleanStale, async () => { + if (!requireTrusted()) return; + const project = deps.currentProject(); + if (project === undefined) { + void vscode.window.showWarningMessage(t("This workspace has no mcpp.toml.")); + return; + } + const plan = planClean("stale", { staleDays: read("mcpp.cache.staleDays") }); + const dry = await run(deps, project, ["clean", "--dry-run"], { timeoutMs: CLEAN_TIMEOUT_MS, quiet: true }); + await preview(t("mcpp clean --dry-run"), dry.stdout.length > 0 ? dry.stdout : dry.stderr); + await runPlan(project, plan); + }); + + register(CACHE_COMMANDS.cleanProject, async () => { + if (!requireTrusted()) return; + const project = deps.currentProject(); + if (project === undefined) { + void vscode.window.showWarningMessage(t("This workspace has no mcpp.toml.")); + return; + } + const base = planClean("project"); + const alsoCache = t("Also empty the shared build cache"); + // `mcpp.task.confirmClean` answers the same question here as it does on the + // task path. Turning it off skips only *this* modal: choosing to also empty + // the shared cache still escalates through the plan's own level 3. + const choice = read("mcpp.task.confirmClean") + ? await vscode.window.showWarningMessage( + t(base.titleKey), + { modal: true, detail: t(base.detailKey) }, + t("Run"), + alsoCache, + ) + : undefined; + if (read("mcpp.task.confirmClean") && choice === undefined) { + return; + } + const plan = choice === alsoCache ? withSharedCache(base) : base; + if (plan.acknowledge && choice === alsoCache) { + const acknowledge = t("I understand this affects every mcpp project on this machine"); + const second = await vscode.window.showWarningMessage( + t("Confirm once more"), + { modal: true, detail: t(plan.detailKey) }, + acknowledge, + ); + if (second !== acknowledge) { + return; + } + } + const result = await run(deps, project, plan.argv, { timeoutMs: CLEAN_TIMEOUT_MS }); + if (result.exitCode === 0) { + await refresh(); + } else { + void vscode.window.showErrorMessage(t("mcpp {0} failed with exit code {1}", plan.argv.join(" "), result.exitCode)); + } + }); + + register(CACHE_COMMANDS.collect, async () => { + if (!requireTrusted()) return; + const project = deps.currentProject(); + await refresh(); + const total = state.inventory?.totalBytes ?? 0; + const suggested = read("mcpp.cache.gc.defaultBudgetGiB") || Math.max(1, Math.round(total / 1024 ** 3 / 2)); + const input = await vscode.window.showInputBox({ + title: t("Keep the shared build cache under how many GiB?"), + value: String(suggested), + validateInput: (value) => (/^\d+$/.test(value.trim()) ? undefined : t("Enter a whole number of GiB")), + }); + if (input === undefined) { + return; + } + const budgetGiB = Number.parseInt(input.trim(), 10); + // Local LRU projection: an estimate, offered so the budget is chosen with + // numbers rather than guessed. mcpp's own policy decides in the end. + const projection = projectGc(state.entries, budgetGiB * 1024 ** 3); + const estimate = + projection.removed.length === 0 + ? t("Already within {0} GiB.", budgetGiB) + : t("About {0} would be freed ({1} entries).", formatBytes(projection.freedBytes), projection.removed.length); + await runGcPlan(project, budgetGiB, `\n${estimate}`); + }); + + register(CACHE_COMMANDS.prune, async () => { + if (!requireTrusted()) return; + const project = deps.currentProject(); + await runPlan(project, planClean("cachePrune", { pruneAgeDays: read("mcpp.cache.pruneAgeDays") })); + }); + + register(CACHE_COMMANDS.verify, async () => { + if (!requireTrusted()) return; + const project = deps.currentProject(); + const result = await run(deps, project, ["cache", "verify"], { timeoutMs: CLEAN_TIMEOUT_MS }); + if (result.exitCode === 0) { + const text = `${result.stdout}\n${result.stderr}`.trim(); + await preview(t("Cache verification"), text.length === 0 ? t("Every entry matches its manifest.") : text); + } else { + void vscode.window.showErrorMessage(t("mcpp cache verify failed with exit code {0}", result.exitCode)); + } + }); + + register(CACHE_COMMANDS.cleanLegacy, async () => { + if (!requireTrusted()) return; + const project = deps.currentProject(); + await runPlan(project, planClean("cacheLegacy")); + }); + + // First paint: show what the settings allow without running anything heavy. + if (deps.currentProject() !== undefined && read("mcpp.cache.estimateProjectBytes")) { + const project = deps.currentProject(); + if (project !== undefined) { + state.artifacts = estimateArtifacts(project.root); + } + } + provider.refresh(); + updateStatus(); + + // Keep the view, the timer and the status item in step with the settings. + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("mcpp.views.cache") || event.affectsConfiguration("mcpp.cache") || event.affectsConfiguration("mcpp.ui.numberFormat")) { + provider?.refresh(); + updateStatus(); + applyTimer(); + } + }), + ); + + return provider; +} + +// ── the self-check's cache snapshot (§8 G9) ────────────────────────────────── + +/** A reading older than this is re-taken rather than reported as current. */ +const SNAPSHOT_FRESH_MS = 60_000; + +let lastSnapshot: { snapshot: CacheSnapshot; at: number } | undefined; + +function setLastCacheSnapshot(snapshot: CacheSnapshot | undefined): void { + lastSnapshot = snapshot === undefined ? undefined : { snapshot, at: Date.now() }; +} + +/** The configured `mcpp.path`, or `mcpp` — the same rule the CLI controller uses. */ +function snapshotExecutable(): string { + const configured = vscode.workspace.getConfiguration("mcpp").get("path", ""); + return configured.trim().length === 0 ? "mcpp" : configured.trim(); +} + +/** + * `showSelfCheck` calls this to fill `buildSelfCheckText`'s `cache` field. + * + * It runs the same read-only `mcpp cache list --format json` the cache view runs + * and reuses that view's own aggregation, so the self-check and the view can + * never disagree. A reading the view took less than a minute ago is reused, so + * opening the self-check does not re-run a query for a number the user just saw. + * + * An untrusted workspace reuses the last reading and never runs `mcpp`; a + * missing `mcpp`, a failed query or an unreadable document all return + * `undefined`, which `buildSelfCheckText` renders as "not read". + * + * The one-line caller change is in the report: + * `cache: await readCacheSnapshot(),` inside the `buildSelfCheckText` call. + */ +export async function readCacheSnapshot(): Promise { + const cached = lastSnapshot; + if (cached !== undefined && Date.now() - cached.at < SNAPSHOT_FRESH_MS) { + return cached.snapshot; + } + if (!vscode.workspace.isTrusted) { + return cached?.snapshot; + } + return readCacheSnapshotNow(); +} + +/** + * Read regardless of what the view last showed: this is the query itself, kept + * separate so a caller with an explicit reason to re-measure can ask for one. + * Read-only, bounded, and never throws. + */ +export async function readCacheSnapshotNow(): Promise { + try { + const result = await runMcpp(vscode.workspace.isTrusted, snapshotExecutable(), ["cache", "list", "--format", "json"], undefined, { + timeoutMs: queryTimeoutMs(), + maxBufferMiB: read("mcpp.runtime.maxOutputMiB"), + }); + if (result === undefined || result.exitCode !== 0) { + return undefined; + } + const parsed = parseCacheList(result.stdout); + if (parsed === undefined) { + return undefined; + } + const inventory = summarizeCache(parsed.root, parsed.entries, { topN: 0, ageBoundaries: [] }); + return cacheSnapshotFrom({ + totalBytes: inventory.totalBytes, + totalEntries: inventory.totalEntries, + incomplete: inventory.incomplete.length, + }); + } catch { + return undefined; + } +} + diff --git a/src/cli/artifacts.ts b/src/cli/artifacts.ts new file mode 100644 index 0000000..eef7073 --- /dev/null +++ b/src/cli/artifacts.ts @@ -0,0 +1,197 @@ +/** + * Size estimate for a project's `target/` directory (plan §3.4.1). + * + * mcpp states that the contents of `target/` are **not an interface**, so this is + * an estimate for the cache view only: it never parses mcpp's fingerprints and + * never guesses which artifacts are stale. The authoritative "what would be + * removed" answer is `mcpp clean --dry-run`. + * + * Pure `node:fs`, never throws, and bounded: a hostile or huge `target/` cannot + * hang the extension host. Symlinks are counted as zero-byte entries and are + * never followed (a link back to an ancestor would otherwise loop forever). + */ + +import { lstatSync, readdirSync } from "node:fs"; +import path from "node:path"; + +import { formatBytes } from "../util/format"; + +export interface ArtifactGroup { + name: string; + bytes: number; + files: number; +} + +export interface ArtifactEstimate { + /** Absolute path of `target/`, whether or not it exists. */ + path: string; + exists: boolean; + totalBytes: number; + files: number; + /** One entry per directory directly under `/target`. */ + byTopLevel: ArtifactGroup[]; + /** Set when the walk stopped early; the numbers are then a floor. */ + truncated?: "entries" | "depth"; +} + +const DEFAULT_MAX_ENTRIES = 200_000; +const DEFAULT_MAX_DEPTH = 6; + +function budget(value: number | undefined, fallback: number): number { + if (value === undefined || !Number.isFinite(value) || value < 0) { + return fallback; + } + return Math.floor(value); +} + +/** + * Walk `/target` with a bounded budget. Never throws. + * + * Returns `exists: false` with zeros when `target/` is missing, is not a + * directory, or is itself a symlink. + */ +export function estimateArtifacts( + projectRoot: string, + options: { maxEntries?: number; maxDepth?: number } = {}, +): ArtifactEstimate { + return measureDirectory(path.join(projectRoot, "target"), options); +} + +/** + * The same bounded walk, for a directory that is already known by path. + * + * The cache view needs it for the pre-v1 cache path `mcpp cache dir` reports + * (plan §3.4 / §8 G6): that directory lives outside the workspace, so it cannot + * be reached through {@link estimateArtifacts}. The same rules apply — bounded, + * never followed through symlinks, and it reports a floor instead of throwing + * when the walk stops early. + */ +export function measureDirectory( + root: string, + options: { maxEntries?: number; maxDepth?: number } = {}, +): ArtifactEstimate { + const maxEntries = budget(options.maxEntries, DEFAULT_MAX_ENTRIES); + const maxDepth = budget(options.maxDepth, DEFAULT_MAX_DEPTH); + + const missing: ArtifactEstimate = { + path: root, + exists: false, + totalBytes: 0, + files: 0, + byTopLevel: [], + }; + let targetStat; + try { + targetStat = lstatSync(root); + } catch { + return missing; + } + // A symlinked `target/` is not followed either, so it reports as absent. + if (!targetStat.isDirectory()) { + return missing; + } + + const byTopLevel: ArtifactGroup[] = []; + let totalBytes = 0; + let files = 0; + let visited = 0; + let truncated: "entries" | "depth" | undefined; + + const add = (group: ArtifactGroup | undefined, bytes: number, count: number): void => { + totalBytes += bytes; + files += count; + if (group !== undefined) { + group.bytes += bytes; + group.files += count; + } + }; + + const hasEntries = (directory: string): boolean => { + try { + return readdirSync(directory, { withFileTypes: true }).length > 0; + } catch { + return false; + } + }; + + const walk = (directory: string, depth: number, group: ArtifactGroup | undefined): void => { + if (truncated === "entries") { + return; + } + if (depth > maxDepth) { + // Only report depth truncation when something was actually left behind. + if (hasEntries(directory)) { + truncated = truncated ?? "depth"; + } + return; + } + let dirents; + try { + dirents = readdirSync(directory, { withFileTypes: true }); + } catch { + // Unreadable directory: tolerate it, the estimate is a floor anyway. + return; + } + for (const dirent of dirents) { + if (visited >= maxEntries) { + truncated = "entries"; + return; + } + visited += 1; + + const full = path.join(directory, dirent.name); + let stat; + try { + stat = lstatSync(full); + } catch { + // Raced with a concurrent `mcpp clean`, or a permission error: skip it. + continue; + } + if (stat.isSymbolicLink()) { + // Never followed; counted as an entry of zero bytes. + add(group, 0, 1); + continue; + } + if (stat.isDirectory()) { + let child = group; + if (depth === 0) { + child = { name: dirent.name, bytes: 0, files: 0 }; + byTopLevel.push(child); + } + walk(full, depth + 1, child); + continue; + } + // Regular files carry a size; fifos/sockets/devices are counted at 0. + add(group, stat.isFile() ? stat.size : 0, 1); + } + }; + + walk(root, 0, undefined); + + byTopLevel.sort( + (left, right) => + right.bytes - left.bytes || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0), + ); + + const estimate: ArtifactEstimate = { + path: root, + exists: true, + totalBytes, + files, + byTopLevel, + }; + if (truncated !== undefined) { + estimate.truncated = truncated; + } + return estimate; +} + +/** One human line for the cache view / status bar, e.g. `"1.43 GiB in 3 groups"`. */ +export function formatArtifactEstimate(estimate: ArtifactEstimate): string { + if (!estimate.exists) { + return "no target/ directory"; + } + const groups = estimate.byTopLevel.length; + const base = `${formatBytes(estimate.totalBytes)} in ${groups} group${groups === 1 ? "" : "s"}`; + return estimate.truncated === undefined ? base : `${base} (truncated: ${estimate.truncated})`; +} diff --git a/src/cli/cache.ts b/src/cli/cache.ts new file mode 100644 index 0000000..addae80 --- /dev/null +++ b/src/cli/cache.ts @@ -0,0 +1,293 @@ +/** + * `mcpp cache …` reading and aggregation (plan §3.4). + * + * Pure functions over captured stdout plus the view model the cache TreeView and + * the statistics webview render. No `vscode` and no process spawning: the caller + * runs `mcpp` (`src/cli/process.ts`) and hands the text in. + * + * Two documents are involved: + * - `mcpp cache list --format json` — a real envelope (`kind: "mcpp.cache"`), + * parsed strictly; a foreign or unknown document is rejected outright; + * - `mcpp cache dir` — human text, so it is parsed leniently and partially. + * + * Everything is defensive: a broken entry is dropped, never thrown. A cache view + * that fails to render is worse than one that renders fewer rows. + */ + +import { ageInDays, bucketIndex, fromUnixSeconds, parseAgeBuckets } from "../util/format"; + +/** Envelope `kind` of `mcpp cache list --format json`. */ +const CACHE_LIST_KIND = "mcpp.cache"; + +/** Fallback `kind` for an entry whose own `kind` is missing or not a string. */ +const UNKNOWN_KIND = "unknown"; + +/** Default number of labels reported by {@link summarizeCache}. */ +const DEFAULT_TOP_N = 5; + +export interface CacheEntry { + accessed?: number; + bytes: number; + complete: boolean; + dir: string; + files?: number; + key: string; + kind: string; + label: string; +} + +export interface CacheInventory { + root: string; + entries: CacheEntry[]; + totalBytes: number; + totalEntries: number; + /** Per-kind totals, largest first. */ + byKind: Array<{ kind: string; entries: number; bytes: number }>; + /** Per-label totals, largest first, capped by `topN`. */ + topLabels: Array<{ label: string; entries: number; bytes: number; oldestAccessed?: number }>; + /** Entries whose `complete` is false. */ + incomplete: CacheEntry[]; + oldestAccessed?: number; + newestAccessed?: number; + /** Age histogram: index i is `[boundaries[i-1], boundaries[i])` days, last bucket is the overflow. */ + ageBuckets: Array<{ fromDays: number; toDays?: number; entries: number; bytes: number }>; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function nonEmptyString(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +function finiteNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +/** Deterministic, locale-independent string ordering. */ +function compareStrings(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} + +function parseCacheEntry(raw: unknown): CacheEntry | undefined { + if (!isRecord(raw)) { + return undefined; + } + const dir = nonEmptyString(raw.dir); + const key = nonEmptyString(raw.key); + const label = nonEmptyString(raw.label); + const bytes = finiteNumber(raw.bytes); + if (dir === undefined || key === undefined || label === undefined || bytes === undefined) { + return undefined; + } + const entry: CacheEntry = { + bytes: Math.max(0, bytes), + // A missing `complete` counts as incomplete: mcpp always emits it, so an + // absent field means a truncated record, which is exactly what + // "incomplete" should surface to the user (`mcpp cache verify`). + complete: raw.complete === true, + dir, + key, + kind: typeof raw.kind === "string" ? raw.kind : UNKNOWN_KIND, + label, + }; + const accessed = finiteNumber(raw.accessed); + if (accessed !== undefined) { + entry.accessed = accessed; + } + const files = finiteNumber(raw.files); + if (files !== undefined) { + entry.files = files; + } + return entry; +} + +/** + * Parse `mcpp cache list --format json` stdout. + * `undefined` when it is not that document (non-JSON, foreign `kind`, bad shape). + */ +export function parseCacheList(stdout: string): { root: string; entries: CacheEntry[] } | undefined { + let document: unknown; + try { + document = JSON.parse(stdout); + } catch { + return undefined; + } + if (!isRecord(document) || document.kind !== CACHE_LIST_KIND) { + return undefined; + } + const data = document.data; + if (!isRecord(data) || !Array.isArray(data.entries)) { + return undefined; + } + const entries: CacheEntry[] = []; + for (const raw of data.entries) { + const entry = parseCacheEntry(raw); + if (entry !== undefined) { + entries.push(entry); + } + } + return { root: typeof data.root === "string" ? data.root : "", entries }; +} + +/** + * Parse `mcpp cache dir` stdout: + * + * ``` + * /home/u/.mcpp/build-cache/v1 + * legacy (unused, removable with `mcpp cache clean --legacy`): /home/u/.mcpp/bmi + * ``` + * + * The legacy line is absent when there is no pre-v1 cache. The legacy path is + * whatever follows the **last** `": "` on that line — the backticked command name + * in the parenthetical must never be mistaken for a path. + */ +export function parseCacheDir(stdout: string): { root?: string; legacyPath?: string } { + let root: string | undefined; + let legacyPath: string | undefined; + for (const line of stdout.split(/\r?\n/)) { + const trimmed = line.trim(); + if (trimmed.length === 0) { + continue; + } + if (/^legacy\b/i.test(trimmed)) { + const separator = trimmed.lastIndexOf(": "); + const candidate = separator >= 0 ? trimmed.slice(separator + 2).trim() : ""; + if (candidate.length > 0) { + legacyPath = candidate; + } + continue; + } + if (root === undefined) { + root = trimmed; + } + } + const result: { root?: string; legacyPath?: string } = {}; + if (root !== undefined) { + result.root = root; + } + if (legacyPath !== undefined) { + result.legacyPath = legacyPath; + } + return result; +} + +/** + * Aggregate a cache listing into what the views need. + * + * `ageBoundaries` come from `mcpp.views.cache.ageBuckets` as strings (`"1d"`, + * `"7d"`, `"30d"`); see `parseAgeBuckets`. Entries without a usable `accessed` + * timestamp are excluded from the histogram and from oldest/newest, because + * their age is unknown — guessing would promise reclaimable space mcpp may keep. + */ +export function summarizeCache( + root: string, + entries: readonly CacheEntry[], + options: { topN?: number; ageBoundaries?: readonly string[] } = {}, +): CacheInventory { + const topN = options.topN === undefined ? DEFAULT_TOP_N : Math.max(0, Math.floor(options.topN)); + const boundaries = parseAgeBuckets(options.ageBoundaries); + const now = new Date(); + + const byKindTotals = new Map(); + const labelTotals = new Map< + string, + { entries: number; bytes: number; oldestAccessed?: number } + >(); + const incomplete: CacheEntry[] = []; + + const ageBuckets: CacheInventory["ageBuckets"] = []; + for (let index = 0; index <= boundaries.length; index += 1) { + const bucket: CacheInventory["ageBuckets"][number] = { + fromDays: index === 0 ? 0 : boundaries[index - 1], + entries: 0, + bytes: 0, + }; + if (index < boundaries.length) { + bucket.toDays = boundaries[index]; + } + ageBuckets.push(bucket); + } + + let totalBytes = 0; + let oldestAccessed: number | undefined; + let newestAccessed: number | undefined; + + for (const entry of entries) { + const bytes = Number.isFinite(entry.bytes) ? Math.max(0, entry.bytes) : 0; + totalBytes += bytes; + + const kindTotals = byKindTotals.get(entry.kind) ?? { entries: 0, bytes: 0 }; + kindTotals.entries += 1; + kindTotals.bytes += bytes; + byKindTotals.set(entry.kind, kindTotals); + + const labelEntry = labelTotals.get(entry.label) ?? { entries: 0, bytes: 0 }; + labelEntry.entries += 1; + labelEntry.bytes += bytes; + if (entry.accessed !== undefined && Number.isFinite(entry.accessed)) { + if (labelEntry.oldestAccessed === undefined || entry.accessed < labelEntry.oldestAccessed) { + labelEntry.oldestAccessed = entry.accessed; + } + if (oldestAccessed === undefined || entry.accessed < oldestAccessed) { + oldestAccessed = entry.accessed; + } + if (newestAccessed === undefined || entry.accessed > newestAccessed) { + newestAccessed = entry.accessed; + } + const at = fromUnixSeconds(entry.accessed); + if (at !== undefined) { + const bucket = ageBuckets[bucketIndex(ageInDays(at, now), boundaries)]; + bucket.entries += 1; + bucket.bytes += bytes; + } + } + labelTotals.set(entry.label, labelEntry); + + if (!entry.complete) { + incomplete.push(entry); + } + } + + const byKind = [...byKindTotals.entries()] + .map(([kind, totals]) => ({ kind, entries: totals.entries, bytes: totals.bytes })) + .sort( + (left, right) => + right.bytes - left.bytes || + right.entries - left.entries || + compareStrings(left.kind, right.kind), + ); + + const rankedLabels = [...labelTotals.entries()] + .map(([label, totals]) => ({ + label, + entries: totals.entries, + bytes: totals.bytes, + ...(totals.oldestAccessed === undefined ? {} : { oldestAccessed: totals.oldestAccessed }), + })) + .sort( + (left, right) => + right.bytes - left.bytes || + right.entries - left.entries || + compareStrings(left.label, right.label), + ); + + const inventory: CacheInventory = { + root, + entries: [...entries], + totalBytes, + totalEntries: entries.length, + byKind, + topLabels: rankedLabels.slice(0, topN), + incomplete, + ageBuckets, + }; + if (oldestAccessed !== undefined) { + inventory.oldestAccessed = oldestAccessed; + } + if (newestAccessed !== undefined) { + inventory.newestAccessed = newestAccessed; + } + return inventory; +} diff --git a/src/cli/clean.ts b/src/cli/clean.ts new file mode 100644 index 0000000..f2be9a6 --- /dev/null +++ b/src/cli/clean.ts @@ -0,0 +1,242 @@ +/** + * Cleanup, as plans rather than as calls. + * + * Every destructive action this extension can run is described here: the exact + * argv, how much confirmation it needs, and whether a preview must be shown + * first. Keeping it in one pure table means the UI cannot invent a sixth level + * of danger, and the tests can assert the policy instead of the wording. + * + * The policy (from the plan, §3.4.3): + * + * | level | what | confirmation | + * |---|---|---| + * | 0 | read-only (`cache list`/`dir`/`info`/`verify`) | none | + * | 1 | project body (`mcpp clean`) | modal, with the measured size | + * | 2 | budget/targeted (`cache gc`, `cache prune`, `cache clean --deps/--std`) | preview, then modal | + * | 3 | everything (`cache clean --all`) | preview, modal, then a second acknowledgement | + * + * The user asked for `cache clean --all` to be reachable — so it is — but it is + * the only level-3 action, and its text names every project on the machine. + */ + +export type CleanAction = + | "project" + | "stale" + | "cacheGc" + | "cachePrune" + | "cacheDeps" + | "cacheStd" + | "cacheAll" + | "cacheLegacy" + | "cacheVerify" + | "cacheList"; + +export interface CleanOptions { + /** `mcpp clean --stale --older-than d`; 0 means "keep none". */ + staleDays?: number; + /** `mcpp cache gc --max-size GiB`; 0 means "the user types a budget". */ + budgetGiB?: number; + /** `mcpp cache prune --older-than d`. */ + pruneAgeDays?: number; +} + +export interface CleanPlan { + action: CleanAction; + /** The mcpp subcommand, for logging: `clean` or `cache`. */ + group: "clean" | "cache"; + argv: readonly string[]; + /** 0 read-only, 1 modal, 2 preview+modal, 3 preview+modal+acknowledge. */ + level: 0 | 1 | 2 | 3; + /** A `--dry-run`-style pass the UI must show before asking. */ + preview: boolean; + /** A second, explicit acknowledgement (a checkbox) is required. */ + acknowledge: boolean; + /** English key for the dialogue title. */ + titleKey: string; + /** English key for the body; the caller adds the measured figures. */ + detailKey: string; + /** This action is refused when the workspace is untrusted. */ + requiresTrust: boolean; +} + +function staleDaysOf(options: CleanOptions): number { + const days = options.staleDays ?? 3; + return Number.isFinite(days) ? Math.max(0, Math.round(days)) : 3; +} + +export function planClean(action: CleanAction, options: CleanOptions = {}): CleanPlan { + switch (action) { + case "project": + return { + action, + group: "clean", + argv: ["clean"], + level: 1, + preview: false, + acknowledge: false, + titleKey: "Remove the whole target/ directory?", + detailKey: "This deletes every build directory of this project. The next build starts from scratch. The shared build cache is not touched.", + requiresTrust: true, + }; + case "stale": + return { + action, + group: "clean", + argv: ["clean", "--stale", "--older-than", `${staleDaysOf(options)}d`], + level: 2, + preview: true, + acknowledge: false, + titleKey: "Remove build directories mcpp no longer considers current?", + detailKey: "Directories written in the last {0} day(s) are kept. mcpp decides which ones are stale; the preview above is its own list.", + requiresTrust: true, + }; + case "cacheGc": { + const budget = options.budgetGiB ?? 0; + const argv = budget > 0 ? ["cache", "gc", "--max-size", `${budget}GiB`] : ["cache", "gc"]; + return { + action, + group: "cache", + argv, + level: 2, + preview: true, + acknowledge: false, + titleKey: "Collect the shared build cache to a budget?", + detailKey: "mcpp drops least-recently-used entries until the cache fits. Projects that used a dropped entry rebuild it.", + requiresTrust: true, + }; + } + case "cachePrune": { + const days = options.pruneAgeDays ?? 30; + return { + action, + group: "cache", + argv: ["cache", "prune", "--older-than", `${Number.isFinite(days) ? Math.max(1, Math.round(days)) : 30}d`], + level: 2, + preview: true, + acknowledge: false, + titleKey: "Drop cache entries that have not been used for a while?", + detailKey: "Entries unused for longer than {0} day(s) are removed. Projects that used one rebuild it.", + requiresTrust: true, + }; + } + case "cacheDeps": + return { + action, + group: "cache", + argv: ["cache", "clean", "--deps"], + level: 2, + preview: true, + acknowledge: false, + titleKey: "Drop every package entry from the shared cache?", + detailKey: "Every mcpp project on this machine rebuilds its dependencies afterwards. Standard library module entries are kept.", + requiresTrust: true, + }; + case "cacheStd": + return { + action, + group: "cache", + argv: ["cache", "clean", "--std"], + level: 2, + preview: true, + acknowledge: false, + titleKey: "Drop every standard library module entry from the shared cache?", + detailKey: "Every mcpp project on this machine re-prepares the standard library module afterwards. Package entries are kept.", + requiresTrust: true, + }; + case "cacheAll": + return { + action, + group: "cache", + argv: ["cache", "clean", "--all"], + level: 3, + preview: true, + acknowledge: true, + titleKey: "Drop the entire shared build cache?", + detailKey: "Package and standard library entries go. Every mcpp project on this machine rebuilds from scratch afterwards.", + requiresTrust: true, + }; + case "cacheLegacy": + return { + action, + group: "cache", + argv: ["cache", "clean", "--legacy"], + level: 2, + preview: false, + acknowledge: false, + titleKey: "Remove the unused pre-v1 cache?", + detailKey: "mcpp no longer reads $MCPP_HOME/bmi. Nothing is rebuilt because nothing uses it.", + requiresTrust: true, + }; + case "cacheVerify": + return { + action, + group: "cache", + argv: ["cache", "verify"], + level: 0, + preview: false, + acknowledge: false, + titleKey: "Verify the shared build cache", + detailKey: "Every entry's manifest is checked against the files on disk.", + requiresTrust: true, + }; + default: + return { + action: "cacheList", + group: "cache", + argv: ["cache", "list", "--format", "json"], + level: 0, + preview: false, + acknowledge: false, + titleKey: "Read the shared build cache", + detailKey: "Nothing is removed.", + requiresTrust: true, + }; + } +} + +/** True when `mcpp clean`'s global cache also goes — the option the UI keeps un-ticked. */ +export function withSharedCache(plan: CleanPlan): CleanPlan { + if (plan.action !== "project") { + return plan; + } + return { + ...plan, + argv: [...plan.argv, "--bmi-cache"], + level: 3, + acknowledge: true, + detailKey: "This also empties the shared build cache, so every mcpp project on this machine rebuilds from scratch.", + }; +} + +/** The single destructive action that may be shortened to one confirmation. */ +export function planProblems(): string[] { + const problems: string[] = []; + const actions: CleanAction[] = [ + "project", + "stale", + "cacheGc", + "cachePrune", + "cacheDeps", + "cacheStd", + "cacheAll", + "cacheLegacy", + "cacheVerify", + "cacheList", + ]; + for (const action of actions) { + const plan = planClean(action); + if (plan.requiresTrust === false) { + problems.push(`${action} does not require a trusted workspace`); + } + if (plan.level >= 2 && !plan.preview && plan.action !== "cacheLegacy") { + problems.push(`${action} must be previewed before it runs`); + } + if (plan.level === 3 && !plan.acknowledge) { + problems.push(`${action} must require a second acknowledgement`); + } + if (plan.argv.some((part) => part.includes(" "))) { + problems.push(`${action} passes an argument containing a space`); + } + } + return problems; +} diff --git a/src/cli/controller.ts b/src/cli/controller.ts new file mode 100644 index 0000000..087116c --- /dev/null +++ b/src/cli/controller.ts @@ -0,0 +1,1160 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import process from "node:process"; + +import * as vscode from "vscode"; + +import { + hostDefaultToolchains, + mcppCommandArguments, + normalizeToolchainSpec, + parseToolchainList, + toolchainInstallKind, + toolchainSpecTargetHint, + type ToolchainInventory, + type ToolchainItem, +} from "./toolchain"; +import { + discoveryBoundaryOf, + type DiscoveryBoundary, + type McppProjectDiscovery, +} from "../projects/discovery"; +import { updateEditorTitleButtonsContext } from "../projects/context"; +import { createLogger, type Logger } from "../util/log"; +import { runProcess } from "./process"; +import { + McppOperationRegistry, + classifyTaskExit, + projectTaskPlan, + TASK_ARGUMENT_SETTINGS, + shouldRefreshLanguageServerAfterTask, + type ProjectTaskKind, + type TaskCompletion, +} from "./tasks"; +import { classifyExit, explainHint, type McppFailureKind, type McppOutcome } from "./errors"; +import { statusBarBackgroundColour } from "./statusBar"; +import { CLI_COMMANDS } from "../commands/ids"; +import { QUICK_MENU_GROUPS, quickMenuIconAsset, quickMenuItems, quickMenuStatusText } from "../commands/menu"; +import { read } from "../config/access"; +import { t } from "../i18n/t"; +import { controllerLabels } from "./labels"; +import { runNewProjectFlow, validateNewProjectName } from "./newProject"; + +export interface McppCliControllerOptions { + output: vscode.OutputChannel; + /** + * The extension's own directory, for the assets that cannot be a `ThemeIcon` — + * today only the coloured quick menu icons, which VS Code paints as an image + * because it drops a `ThemeIcon`'s colour in a quick pick. + */ + extensionUri: vscode.Uri; + currentProject: () => McppProjectDiscovery | undefined; + afterProjectTask: ( + project: McppProjectDiscovery, + kind: ProjectTaskKind, + completion: TaskCompletion, + ) => Promise; + isTrusted: () => boolean; + /** + * `mcpp.ui.statusBar.showLanguageServer`: one short line describing the C++ + * Modules state, or `undefined` when there is nothing to show. A callback + * instead of an import so this controller never depends on `src/mcppls/**`; + * the extension layer owns the bridge and passes it in. + */ + languageServerSummary?: () => string | undefined; +} + +interface ToolchainPickItem extends vscode.QuickPickItem { + spec?: string; + toolchain?: ToolchainItem; + customInput?: boolean; +} + +type OperationToken = object; + +/** Where the generated quick menu icons live, under the extension root. */ +const QUICK_MENU_ICON_DIRECTORY = "media/quick-menu"; + +export interface ProjectTaskRunOptions { + /** Set to false when the caller performs its own post-task action. */ + notify?: boolean; +} + +function taskScope(root: string): vscode.WorkspaceFolder | vscode.TaskScope { + return vscode.workspace.getWorkspaceFolder(vscode.Uri.file(root)) + ?? vscode.TaskScope.Workspace; +} + +function workingDirectory(project: McppProjectDiscovery | undefined): string { + if (project !== undefined) { + return project.root; + } + return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd(); +} + +function commandLine(executable: string, args: string[]): string { + return [executable, ...args].join(" "); +} + +// ─── pure policy ──────────────────────────────────────────────────────────── +// +// Every decision a setting makes is computed by a pure function in this block, +// so `test/cli/controller.*.test.ts` can assert the decision without an editor. +// The class below only executes what these functions decide. + +export type TaskRevealSetting = "always" | "onFailure" | "never"; + +export interface TaskTerminalUi { + /** `mcpp.task.revealTerminal`. */ + reveal: TaskRevealSetting; + /** `mcpp.task.focusTerminal`: hand keyboard focus to the task terminal. */ + focus: boolean; + /** `mcpp.task.clearTerminal`: clear the panel before the command runs. */ + clearBeforeRun: boolean; +} + +export function taskTerminalUi( + revealTerminal: unknown, + focusTerminal: unknown, + clearTerminal: unknown, +): TaskTerminalUi { + return { + reveal: + revealTerminal === "never" || revealTerminal === "onFailure" ? revealTerminal : "always", + focus: focusTerminal === true, + clearBeforeRun: clearTerminal !== false, + }; +} + +/** `workbench.action.terminal.*` commands to run once the task has finished. */ +export function terminalCommandsAfterTask(ui: TaskTerminalUi, failed: boolean): readonly string[] { + // VS Code has no "reveal without focusing" command, so the one command below + // is both the reveal and the focus for the failure-only setting. + return failed && ui.reveal === "onFailure" ? ["workbench.action.terminal.focus"] : []; +} + +/** The problem matcher `package.json` must contribute under `contributes.problemMatchers`. */ +export const PROBLEM_MATCHER_NAME = "$mcpp"; + +export function problemMatchersFor(enabled: unknown): readonly string[] { + return enabled === false ? [] : [PROBLEM_MATCHER_NAME]; +} + +/** + * `mcpp.task.confirmClean`: this decides only the controller's own prompt for + * `mcpp.clean`. The cleanup plan (`src/cli/clean.ts`) keeps its own level-2/3 + * confirmations, which this setting can never remove. + */ +export function cleanConfirmationRequired(confirmClean: unknown): boolean { + return confirmClean !== false; +} + +export type ConfirmationStrength = "notice" | "modal"; + +export interface ConfirmationPolicy { + installToolchain: ConfirmationStrength; + globalDefault: ConfirmationStrength; +} + +/** + * `mcpp.ui.confirmDestructiveOnly` (default true) reserves a modal for actions + * that cannot be undone. Turning it off escalates these two undoable prompts + * from a dismissible notification to a modal — the prompt, its text and its + * buttons are identical in both modes, so the setting can only ask *more* + * insistently, never less often. + */ +export function confirmationPolicy(confirmDestructiveOnly: unknown): ConfirmationPolicy { + const strength: ConfirmationStrength = confirmDestructiveOnly === false ? "modal" : "notice"; + return { installToolchain: strength, globalDefault: strength }; +} + +/** `mcpp.ui.statusBar.showLanguageServer`: the suffix the status item gains. */ +export function languageServerStatusSuffix(show: unknown, summary: string | undefined): string | undefined { + if (show !== true) { + return undefined; + } + const text = summary?.trim(); + return text === undefined || text.length === 0 ? undefined : text; +} + +export type SuccessReport = "silent" | "statusBar" | "toast"; + +/** `mcpp.ui.notifications.success`; only a *successful* result consults this. */ +export function successReportOf(value: unknown): SuccessReport { + return value === "silent" || value === "toast" ? value : "statusBar"; +} + +/** + * `mcpp.ui.notifications.dedupeMinutes`: true when the same message may be + * shown again. `0` (or an unusable value) shows every notification. + */ +export function dedupeAllows(lastAt: number | undefined, now: number, dedupeMinutes: unknown): boolean { + if (typeof dedupeMinutes !== "number" || !Number.isFinite(dedupeMinutes) || dedupeMinutes <= 0) { + return true; + } + if (typeof lastAt !== "number" || !Number.isFinite(lastAt)) { + return true; + } + return now - lastAt >= dedupeMinutes * 60_000; +} + +export const STATUS_BAR_SUCCESS_MAX = 60; + +/** A status bar item is a label, not a paragraph. */ +export function statusBarSuccessText(message: string): string { + const single = message.replace(/\s+/g, " ").trim(); + return single.length <= STATUS_BAR_SUCCESS_MAX + ? single + : `${single.slice(0, STATUS_BAR_SUCCESS_MAX - 1)}…`; +} + +export interface FailureAdvice { + kind: McppFailureKind; + exitCode: number; + /** `mcpp self explain `, when the output or code names a diagnostic. */ + hint?: string; +} + +/** + * `src/cli/errors.ts` (SPEC-003) applied to a failed run: the failure kind that + * picks the guidance, plus the `mcpp self explain` follow-up when there is a + * diagnostic code to explain. + */ +export function failureAdvice(args: readonly string[], exitCode: number, output: string): FailureAdvice { + const outcome: McppOutcome = { ...classifyExit(exitCode, args), detail: output }; + return { kind: outcome.kind, exitCode: outcome.exitCode, hint: explainHint(outcome) }; +} + +/** The exit-code-specific next step, or `undefined` to keep the caller's message. */ +function failureGuidanceText(kind: McppFailureKind): string | undefined { + switch (kind) { + case "usage": + return t("mcpp rejected the command line; check the arguments and run it from the project root."); + case "environment": + return t("mcpp is installed but the environment is not ready; run `mcpp self doctor` to see what is missing."); + case "internal": + return t("mcpp reported an internal error; re-run with the mcpp output channel open and report it."); + case "unknown-command": + return t("This mcpp build does not recognise that command; update mcpp or check the spelling."); + case "build-failed": + return t("The program did not build; see the task terminal for the compiler output."); + default: + return undefined; + } +} + +/** + * The `vscode`-side read of `mcpp.project.discoveryBoundary`. `extension.ts` + * passes the result into `findNearestMcppProject`, whose walk stays pure. The + * resource is the workspace folder, because the setting is resource-scoped. + */ +export function discoveryBoundaryFromSettings(resource?: vscode.Uri): DiscoveryBoundary { + return discoveryBoundaryOf(read("mcpp.project.discoveryBoundary", resource)); +} + +const SUCCESS_STATUS_MS = 6000; + +export class McppCliController { + private readonly status: vscode.StatusBarItem; + + private readonly operations = new McppOperationRegistry(); + + /** `mcpp.log.level` decides what reaches the `mcpp` channel; see `src/util/log.ts`. */ + private readonly logger: Logger; + + /** `mcpp.ui.notifications.dedupeMinutes`: message signature -> last shown (ms). */ + private readonly lastNotified = new Map(); + + /** Reverts the transient success text in the status item. */ + private statusRevert: ReturnType | undefined; + + public constructor(private readonly options: McppCliControllerOptions) { + this.status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 40); + this.status.command = CLI_COMMANDS.showMenu; + this.logger = createLogger(this.options.output, () => read("mcpp.log.level")); + this.applyStatusBar(); + } + + public register(): vscode.Disposable[] { + this.applyStatusBar(); + const applyEditorTitleButtons = (): void => { + void updateEditorTitleButtonsContext({ + enabled: () => read("mcpp.task.editorTitleButtons"), + setContextValue: (key, value) => vscode.commands.executeCommand("setContext", key, value), + }).catch(() => undefined); + }; + applyEditorTitleButtons(); + const disposables: vscode.Disposable[] = [ + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("mcpp.ui.statusBar")) { + this.applyStatusBar(); + } + if (event.affectsConfiguration("mcpp.task.editorTitleButtons")) { + applyEditorTitleButtons(); + } + }), + this.status, + vscode.commands.registerCommand(CLI_COMMANDS.showMenu, this.guarded(() => this.showMenu())), + vscode.commands.registerCommand(CLI_COMMANDS.newProject, this.guarded(() => this.newProject())), + vscode.commands.registerCommand(CLI_COMMANDS.build, this.guarded(() => this.runProjectTask("build"))), + vscode.commands.registerCommand(CLI_COMMANDS.run, this.guarded(() => this.runProjectTask("run"))), + vscode.commands.registerCommand(CLI_COMMANDS.test, this.guarded(() => this.runProjectTask("test"))), + vscode.commands.registerCommand(CLI_COMMANDS.clean, this.guarded(() => this.runProjectTask("clean"))), + vscode.commands.registerCommand(CLI_COMMANDS.showToolchains, this.guarded(() => this.showToolchains())), + vscode.commands.registerCommand(CLI_COMMANDS.installToolchain, this.guarded(() => this.installToolchain())), + vscode.commands.registerCommand(CLI_COMMANDS.selectDefaultToolchain, this.guarded(() => this.selectDefaultToolchain())), + vscode.window.onDidChangeActiveTextEditor(() => this.refreshStatus()), + vscode.workspace.onDidChangeWorkspaceFolders(() => this.refreshStatus()), + ]; + this.refreshStatus(); + return disposables; + } + + public refreshStatus(): void { + if (this.options.currentProject() === undefined) { + this.status.hide(); + return; + } + this.applyStatusBar(); + } + + public isBusy(): boolean { + return this.operations.hasActive(); + } + + public async runProjectTask( + kind: ProjectTaskKind, + options: ProjectTaskRunOptions = {}, + ): Promise { + const project = this.requireProject(); + if (project === undefined || !this.requireTrusted()) { + return undefined; + } + + // `mcpp.task.confirmClean`: one prompt for this entry point; the cleanup + // plan's own level-2/3 confirmations are untouched by it. + if (kind === "clean" && cleanConfirmationRequired(read("mcpp.task.confirmClean", vscode.Uri.file(project.root)))) { + const labels = controllerLabels(); + const choice = await vscode.window.showWarningMessage( + t("Delete the target/ directory of this project: {0}/target", project.root), + { modal: true, detail: t("This does not clean the global BMI cache.") }, + labels.confirmClean, + ); + if (choice !== labels.confirmClean) { + return undefined; + } + } + + // `mcpp.runtime.concurrency` picks the mutual-exclusion scope: one task at a + // time per project (the default), or one task at a time for the whole window. + const globalScope = read("mcpp.runtime.concurrency") === "global"; + const token: OperationToken = {}; + const active = globalScope + ? this.operations.beginGlobal(token) + : this.operations.beginProject(project.root, token); + if (active !== undefined) { + const labels = controllerLabels(); + const choice = await vscode.window.showWarningMessage( + t("An mcpp operation is already running; not starting {0} now.", kind), + labels.showTasks, + ); + if (choice === labels.showTasks) { + await vscode.commands.executeCommand("workbench.action.tasks.showTasks"); + } + return undefined; + } + + let completion: TaskCompletion | undefined; + try { + const plan = projectTaskPlan(kind, read(TASK_ARGUMENT_SETTINGS[kind])); + completion = await this.executeTask( + project.root, + this.mcppExecutable(project), + plan.title, + plan.args, + ); + this.appendTaskCompletion(project.root, plan.title, plan.args, completion); + } finally { + if (globalScope) { + this.operations.finishGlobal(token); + } else { + this.operations.finishProject(project.root, token); + } + } + + if (options.notify !== false + && completion !== undefined + && shouldRefreshLanguageServerAfterTask(kind, completion)) { + await this.options.afterProjectTask(project, kind, completion); + } + return completion; + } + + public async showToolchains(): Promise { + if (!this.requireTrusted()) { + return; + } + const project = this.options.currentProject(); + const inventory = await this.readToolchainInventory(project); + if (inventory === undefined) { + return; + } + + const items = this.inventoryItems(inventory, false, project); + if (items.length === 0) { + await vscode.window.showInformationMessage( + t("mcpp listed no usable toolchain; see the raw result in the mcpp output channel."), + ); + return; + } + await vscode.window.showQuickPick(items, { + title: t("mcpp toolchains and targets"), + placeHolder: t("Read-only view of what mcpp currently resolves"), + matchOnDescription: true, + matchOnDetail: true, + }); + } + + public async installToolchain(): Promise { + if (!this.requireTrusted()) { + return; + } + const project = this.options.currentProject(); + const inventory = await this.readToolchainInventory(project); + const spec = await this.pickInstallSpec(inventory); + if (spec === undefined) { + return; + } + const installKind = toolchainInstallKind(spec); + const targetHint = toolchainSpecTargetHint(spec); + const labels = controllerLabels(); + const confirmLabel = installKind === "system-detect" ? labels.confirmDetect : labels.confirmInstall; + const confirmation = installKind === "system-detect" + ? t("mcpp will detect the system MSVC ({0}).", spec) + : targetHint === "target" + ? t("The compatible spec {0}, which may carry target semantics, is handed to mcpp to install, and mcpp validates it in the end.", spec) + : installKind === "managed-target" + ? t("The compatible spec {0} carrying target semantics is handed to mcpp to install.", spec) + : t("mcpp toolchain {0} is installed (no target specified, the host target is used).", spec); + const detail = installKind === "system-detect" + ? t("mcpp does not download or install MSVC; it detects Visual Studio and points to the official installation guide when it is missing.") + : targetHint === "target" + ? t("mcpp decides whether the compiler prefix is a valid triple; when it is, it may download a larger toolchain package for that target.") + : installKind === "managed-target" + ? t("mcpp normalises the compatible spelling and may download a larger toolchain package for that target.") + : t("The installation may download a large toolchain package and modifies the mcpp global cache."); + + const policy = confirmationPolicy(read("mcpp.ui.confirmDestructiveOnly")); + const choice = await vscode.window.showWarningMessage( + confirmation, + this.warningOptions(policy.installToolchain, detail), + confirmLabel, + ); + if (choice !== confirmLabel) { + return; + } + + const token: OperationToken = {}; + const active = this.operations.beginGlobal(token); + if (active !== undefined) { + const duplicateChoice = await vscode.window.showWarningMessage( + t("An mcpp operation is already running."), + labels.showTasks, + ); + if (duplicateChoice === labels.showTasks) { + await vscode.commands.executeCommand("workbench.action.tasks.showTasks"); + } + return; + } + + const taskTitle = installKind === "system-detect" + ? t("mcpp: detect system MSVC ({0})", spec) + : t("mcpp: install toolchain {0}", spec); + try { + const installArgs = mcppCommandArguments("toolchain", "install", spec); + const completion = await this.executeTask( + workingDirectory(project), + this.mcppExecutable(project), + taskTitle, + installArgs, + ); + this.appendTaskCompletion( + workingDirectory(project), + taskTitle, + installArgs, + completion, + ); + if (completion.state !== "succeeded") { + return; + } + } finally { + this.operations.finishGlobal(token); + } + + if (installKind === "managed-target") { + this.reportSuccess(t("mcpp finished {0}. This first release does not change the target default; to set one, use the mcpp CLI with --target.", spec)); + return; + } + + const refreshed = await this.readToolchainInventory(project); + const defaultChoice = await vscode.window.showInformationMessage( + installKind === "system-detect" + ? t("MSVC detection finished. Choose a global default from the latest list?") + : t("Toolchain {0} installed. Choose a global default from the latest list?", spec), + labels.chooseGlobalDefault, + ); + if (defaultChoice === labels.chooseGlobalDefault && refreshed !== undefined) { + await this.selectDefaultToolchainFromInventory(project, refreshed); + } + } + + public async selectDefaultToolchain(): Promise { + if (!this.requireTrusted()) { + return; + } + const project = this.options.currentProject(); + const inventory = await this.readToolchainInventory(project); + if (inventory !== undefined) { + await this.selectDefaultToolchainFromInventory(project, inventory); + } + } + + private async selectDefaultToolchainFromInventory( + project: McppProjectDiscovery | undefined, + inventory: ToolchainInventory, + ): Promise { + const installed = hostDefaultToolchains(inventory); + if (installed.length === 0) { + await vscode.window.showWarningMessage( + t("No installed toolchain is usable for the host target, so a global default cannot be set here. For target-specific toolchains use the mcpp CLI; on Windows install Visual Studio first."), + ); + return; + } + + const items: ToolchainPickItem[] = installed.map((toolchain) => ({ + label: `${toolchain.effective ? "$(check) " : ""}${toolchain.spec}`, + description: toolchain.source === "system" ? t("System toolchain (mcpp only detects it)") : t("Toolchain managed by mcpp"), + detail: toolchain.effective ? t("Effective toolchain of this project") : undefined, + spec: toolchain.spec, + toolchain, + })); + const picked = await vscode.window.showQuickPick(items, { + title: t("Choose the mcpp global default toolchain"), + placeHolder: this.isNestedWorkspaceProject(project) + ? t("The project root sits below the VS Code folder; the real build resolution is whatever mcpp reports") + : inventory.projectOverridesGlobal + ? t("The project currently overrides the global default; this only changes the current mcpp global configuration") + : t("Choosing here only changes the mcpp global configuration; it does not build the project"), + matchOnDescription: true, + matchOnDetail: true, + }); + if (picked?.spec === undefined) { + return; + } + + const defaultPolicy = confirmationPolicy(read("mcpp.ui.confirmDestructiveOnly")); + const warningLabels = controllerLabels(); + const choice = await vscode.window.showWarningMessage( + t("The global default pair is set to {0} + host target. The mcpp.toml/target configuration of the current project can still override it.", picked.spec), + this.warningOptions( + defaultPolicy.globalDefault, + t("mcpp also clears the global default_target; the configuration file location is decided by the current mcpp installation and MCPP_HOME."), + ), + warningLabels.confirmDefault, + ); + if (choice !== warningLabels.confirmDefault) { + return; + } + + const token: OperationToken = {}; + const active = this.operations.beginGlobal(token); + if (active !== undefined) { + await vscode.window.showWarningMessage(t("An mcpp operation is already running."), warningLabels.showTasks); + return; + } + + try { + const args = mcppCommandArguments("toolchain", "default", picked.spec); + const result = await runProcess( + this.mcppExecutable(project), + args, + workingDirectory(project), + ); + this.appendShortCommand(t("Set the global default toolchain"), this.mcppExecutable(project), args, result); + if (result.exitCode !== 0) { + this.reportCommandFailure( + args, + result.exitCode, + `${result.stdout}\n${result.stderr}`, + t("Setting the global default toolchain failed (exit code {0}). See the mcpp output channel.", result.exitCode), + ); + return; + } + + } finally { + this.operations.finishGlobal(token); + } + + const buildChoice = await vscode.window.showInformationMessage( + t("The mcpp global default is now {0} + host target. Cleaning the old toolchain artifacts and rebuilding is recommended.", picked.spec), + ...(project === undefined ? [] : [warningLabels.cleanAndBuild]), + ); + if (buildChoice === warningLabels.cleanAndBuild && project !== undefined) { + const cleanArgs = mcppCommandArguments("clean"); + const cleanResult = await runProcess(this.mcppExecutable(project), cleanArgs, project.root); + this.appendShortCommand(t("Clean old artifacts"), this.mcppExecutable(project), cleanArgs, cleanResult); + await this.runProjectTask("build"); + } + } + + private async pickInstallSpec(inventory: ToolchainInventory | undefined): Promise { + const labels = controllerLabels(); + const items: ToolchainPickItem[] = []; + for (const toolchain of inventory?.available ?? []) { + items.push({ + label: toolchain.spec, + description: t("Versions mcpp aggregates by family; this operation installs for the host target"), + spec: toolchain.spec, + }); + } + items.push({ + label: labels.installCustom, + detail: t("Accepts family, family@version, namespace, partial versions and mcpp's compatible legacy spellings"), + customInput: true, + }); + + const picked = await vscode.window.showQuickPick(items, { + title: t("Install an mcpp toolchain"), + placeHolder: t("No target is selected; for a target-specific install use the mcpp CLI"), + matchOnDescription: true, + matchOnDetail: true, + }); + if (picked === undefined) { + return undefined; + } + if (!picked.customInput) { + return picked.spec; + } + + const input = await vscode.window.showInputBox({ + title: t("Enter a toolchain spec"), + prompt: t("For example gcc, llvm@20.1.7, xim:gcc@16, msvc, mingw; mcpp normalises compatible spellings"), + placeHolder: "gcc@16", + validateInput: (value) => { + const normalized = normalizeToolchainSpec(value); + if (normalized === undefined) { + return t("Enter a family, family@version, family version, namespace or an mcpp-compatible spec"); + } + return undefined; + }, + }); + return input === undefined ? undefined : normalizeToolchainSpec(input); + } + + private inventoryItems( + inventory: ToolchainInventory, + includeActions: boolean, + project: McppProjectDiscovery | undefined, + ): ToolchainPickItem[] { + const items: ToolchainPickItem[] = []; + const nestedView = this.isNestedWorkspaceProject(project); + if (inventory.effective !== undefined) { + items.push({ + label: `$(check) ${nestedView ? t("mcpp list toolchain for this directory") : t("Current effective toolchain")}: ${inventory.effective.spec}`, + description: nestedView + ? t("This is the current-directory view; the value mcpp build resolves is authoritative") + : inventory.projectOverridesGlobal ? t("From this project's mcpp.toml, overriding the global default") : t("From the global default"), + detail: inventory.effectiveTarget === undefined + ? t("Effective target: host (mcpp shows no explicit target)") + : t("Effective target: {0}", inventory.effectiveTarget), + }); + } + if (inventory.globalDefaultSpec !== undefined) { + items.push({ + label: t("Global default: {0}", inventory.globalDefaultSpec), + description: nestedView + ? t("This is the current-directory view; overrides from the project or a parent configuration are decided by the actual build") + : inventory.projectOverridesGlobal ? t("This project may not be using this value") : t("mcpp global configuration"), + }); + } else if (inventory.recognized) { + items.push({ + label: t("Global default: "), + description: t("mcpp has no global default toolchain yet"), + }); + } + for (const toolchain of inventory.installed) { + items.push({ + label: `${toolchain.effective ? "$(check) " : ""}${toolchain.spec}`, + description: toolchain.source === "system" ? t("System: system toolchain, detection only") : t("Installed"), + detail: toolchain.effective + ? nestedView ? t("mcpp list item effective for this directory; the actual build resolution may be affected by a parent mcpp workspace") : t("Currently effective item") + : undefined, + spec: toolchain.spec, + toolchain, + }); + } + for (const target of inventory.targets) { + items.push({ + label: `${target.effective ? "$(check) " : ""}target ${target.target}`, + description: `${target.status}${target.toolchainSpec === undefined ? "" : ` · ${t("convention {0}", target.toolchainSpec)}`}`, + detail: target.note.length === 0 ? t("The target axis is shown read-only; to select a target use the mcpp CLI") : target.note, + }); + } + for (const toolchain of inventory.available) { + items.push({ + label: t("Installable: {0}", toolchain.spec), + description: t("Index versions mcpp aggregates by family; no host payload is promised"), + spec: toolchain.spec, + }); + } + if (includeActions) { + items.push({ + label: t("$(cloud-download) Install a toolchain…"), + detail: t("Back to the toolchain installation flow"), + customInput: true, + }); + } + return items; + } + + /** + * The status bar menu: one section per group, one row per command. + * + * The section titles come from `QUICK_MENU_GROUPS`, so an entry can never be + * filed under a heading that does not exist, and the headers are only emitted + * where the entries actually change group — a group whose commands are all + * filtered out (C++ Modules, when `mcpp.languageService.menuItems` is off) + * takes its heading with it. + * + * The icons are the generated ones under `media/quick-menu/`, handed over as a + * `{ light, dark }` pair so the row is legible in both themes. They cannot be + * `ThemeIcon`s: VS Code drops a `ThemeIcon`'s colour in a quick pick and paints + * a `Uri` as an image, so a coloured row needs a coloured file. See the note at + * the top of `src/commands/menu.ts`. + * + * Typing filters by command name only. The group names are separator rows, and + * VS Code hides separators as soon as a query is present, so they are not + * matchable — the headings are for reading the list, not for searching it. + */ + private async showMenu(): Promise { + const groupLabel = new Map(QUICK_MENU_GROUPS.map((group) => [group.id, t(group.labelKey)])); + const showLanguageServer = read("mcpp.languageService.menuItems"); + const items: Array = []; + let section: string | undefined; + for (const item of quickMenuItems) { + if (!showLanguageServer && item.group === "languageServer") { + continue; + } + if (item.group !== section) { + section = item.group; + items.push({ + label: groupLabel.get(item.group) ?? item.group, + kind: vscode.QuickPickItemKind.Separator, + }); + } + items.push({ + label: t(item.labelKey), + iconPath: { + light: vscode.Uri.joinPath(this.options.extensionUri, QUICK_MENU_ICON_DIRECTORY, quickMenuIconAsset(item, "light")), + dark: vscode.Uri.joinPath(this.options.extensionUri, QUICK_MENU_ICON_DIRECTORY, quickMenuIconAsset(item, "dark")), + }, + command: item.command, + }); + } + const picked = await vscode.window.showQuickPick(items, { + title: t("mcpp: quick menu"), + placeHolder: t("Choose a project, toolchain, cache or C++ Modules action"), + }); + if (picked?.command !== undefined) { + await vscode.commands.executeCommand(picked.command); + } + } + + public async newProject(): Promise { + if (!this.requireTrusted()) { + return; + } + + const input = await vscode.window.showInputBox({ + title: t("New mcpp project (1/2)"), + prompt: t("Enter a project name; a folder of that name is created at the location you pick"), + placeHolder: "hello-mcpp", + validateInput: validateNewProjectName, + }); + if (input === undefined) { + return; + } + const projectName = input.trim(); + + const picked = await vscode.window.showOpenDialog({ + title: t("Choose the project location (2/2)"), + // Start where the reader already is: `mcpp new` creates a folder *inside* + // the chosen one, so the workspace folder is the useful default — the + // project itself cannot be created in it, because `mcpp new` refuses a + // destination that already exists. + defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri, + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: t("Create the project here"), + }); + const location = picked?.[0]; + if (location === undefined) { + return; + } + + const projectRoot = join(location.fsPath, projectName); + // Shown *and* compared: one definition keeps the two in step. + const confirmCreate = t("Create and open"); + await runNewProjectFlow(projectName, location.fsPath, projectRoot, { + exists: existsSync, + confirm: async (message) => + (await vscode.window.showWarningMessage(message, { modal: true }, confirmCreate)) + === confirmCreate, + run: async (name, cwd) => { + const executable = this.mcppExecutable(undefined); + const args = mcppCommandArguments("new", name); + const result = await runProcess(executable, args, cwd); + this.appendShortCommand(t("New project"), executable, args, result); + return result.exitCode; + }, + openFolder: async (path) => { + await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(path)); + }, + showError: async (message) => { + await vscode.window.showErrorMessage(message); + }, + }); + } + + private guarded(operation: () => Promise): () => Promise { + return async () => { + try { + await operation(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error(t("mcpp CLI operation failed: {0}", message)); + await vscode.window.showErrorMessage(t("mcpp: {0}", message)); + } + }; + } + + private requireProject(): McppProjectDiscovery | undefined { + const project = this.options.currentProject(); + if (project === undefined) { + void vscode.window.showWarningMessage(t("No mcpp.toml was found in this workspace. Run this command inside an mcpp project.")); + } + return project; + } + + private requireTrusted(): boolean { + if (this.options.isTrusted()) { + return true; + } + void vscode.window.showWarningMessage( + t("This workspace is not trusted. mcpp commands may run external programs named by workspace settings; trust the workspace first."), + ); + return false; + } + + public mcppExecutable(project: McppProjectDiscovery | undefined): string { + const uri = project === undefined + ? vscode.workspace.workspaceFolders?.[0]?.uri + : vscode.Uri.file(project.root); + const configured = vscode.workspace.getConfiguration("mcpp", uri).get("path", "").trim(); + return configured.length === 0 ? "mcpp" : configured; + } + + private isNestedWorkspaceProject(project: McppProjectDiscovery | undefined): boolean { + if (project === undefined) { + return false; + } + const folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(project.root)); + return folder !== undefined && folder.uri.fsPath !== project.root; + } + + public async readToolchainInventory( + project: McppProjectDiscovery | undefined = this.options.currentProject(), + ): Promise { + const executable = this.mcppExecutable(project); + const args = mcppCommandArguments("toolchain", "list"); + const result = await runProcess(executable, args, workingDirectory(project)); + this.appendShortCommand(t("Inspect the toolchain"), executable, args, result); + if (result.exitCode !== 0) { + this.reportCommandFailure( + args, + result.exitCode, + `${result.stdout}\n${result.stderr}`, + t("mcpp toolchain list failed (exit code {0}). See the mcpp output channel.", result.exitCode), + ); + return undefined; + } + + const inventory = parseToolchainList(`${result.stdout}${result.stderr.length > 0 ? `\n${result.stderr}` : ""}`); + if (!inventory.recognized) { + await vscode.window.showErrorMessage( + t("The current mcpp toolchain list output is not recognised; the raw output is kept in the mcpp output channel, so check the mcpp version."), + ); + return undefined; + } + return inventory; + } + + private async executeTask( + root: string, + executable: string, + title: string, + args: string[], + ): Promise { + // `mcpp.task.revealTerminal` / `focusTerminal` / `clearTerminal` and + // `mcpp.task.problemMatcher` are read here, where the task object is + // assembled; the completion path re-reads them for the failure-only reveal. + const ui = this.terminalUi(root); + const task = new vscode.Task( + { type: "mcpp", command: args[0] ?? "mcpp", projectRoot: root }, + taskScope(root), + title, + "mcpp", + new vscode.ProcessExecution(executable, args, { cwd: root }), + [...problemMatchersFor(read("mcpp.task.problemMatcher", vscode.Uri.file(root)))], + ); + task.presentationOptions = { + reveal: ui.reveal === "always" + ? vscode.TaskRevealKind.Always + : ui.reveal === "never" + ? vscode.TaskRevealKind.Never + : vscode.TaskRevealKind.Silent, + panel: vscode.TaskPanelKind.Dedicated, + focus: ui.focus, + clear: ui.clearBeforeRun, + showReuseMessage: false, + }; + + let execution: vscode.TaskExecution | undefined; + let earlyCompletion: TaskCompletion | undefined; + let settled = false; + let processEndSubscription: vscode.Disposable | undefined; + let taskEndSubscription: vscode.Disposable | undefined; + const disposeListeners = (): void => { + processEndSubscription?.dispose(); + taskEndSubscription?.dispose(); + }; + const finish = (completion: TaskCompletion): void => { + if (settled) { + return; + } + settled = true; + disposeListeners(); + resolveCompletion?.(completion); + }; + let resolveCompletion: ((completion: TaskCompletion) => void) | undefined; + const completion = new Promise((resolve) => { + resolveCompletion = resolve; + processEndSubscription = vscode.tasks.onDidEndTaskProcess((event) => { + if (event.execution.task !== task) { + return; + } + const classified = classifyTaskExit(event.exitCode); + if (execution === undefined) { + earlyCompletion ??= classified; + return; + } + finish(classified); + }); + taskEndSubscription = vscode.tasks.onDidEndTask((event) => { + if (event.execution.task !== task) { + return; + } + const classified = classifyTaskExit(undefined); + if (execution === undefined) { + earlyCompletion ??= classified; + return; + } + finish(classified); + }); + }); + + try { + execution = await vscode.tasks.executeTask(task); + } catch (error) { + disposeListeners(); + throw error; + } + if (earlyCompletion !== undefined) { + finish(earlyCompletion); + } + return completion; + } + + private appendTaskCompletion( + root: string, + title: string, + args: string[], + completion: TaskCompletion, + ): void { + const suffix = completion.state === "succeeded" + ? t("Exit code {0}", completion.exitCode ?? 0) + : completion.state === "cancelled" + ? t("Cancelled") + : t("Failed with exit code {0}", completion.exitCode ?? t("unknown")); + // `mcpp.log.level` gates the verbose lines; the failed result is an error + // and is therefore never suppressed. + this.logger.info(`\n[${new Date().toISOString()}] ${title}`); + this.logger.debug(t("Working directory: {0}", root)); + this.logger.debug(t("Task arguments: {0}", args.join(" "))); + const resultLine = t("Result: {0}", suffix); + if (completion.state === "failed") { + this.logger.error(resultLine); + } else if (completion.state === "cancelled") { + this.logger.warn(resultLine); + } else { + this.logger.info(resultLine); + } + + for (const command of terminalCommandsAfterTask(this.terminalUi(root), completion.state === "failed")) { + void vscode.commands.executeCommand(command); + } + + if (completion.state === "failed") { + this.reportCommandFailure( + args, + completion.exitCode ?? 1, + "", + t("{0} failed (exit code {1}). See the task terminal.", title, completion.exitCode ?? t("unknown")), + ); + } else if (completion.state === "cancelled") { + void vscode.window.showWarningMessage(t("{0} was cancelled.", title)); + } else { + this.reportSuccess(t("{0} finished.", title)); + } + } + + /** The task-terminal settings, read together so every site agrees. */ + private terminalUi(root: string): TaskTerminalUi { + const resource = vscode.Uri.file(root); + return taskTerminalUi( + read("mcpp.task.revealTerminal", resource), + read("mcpp.task.focusTerminal", resource), + read("mcpp.task.clearTerminal", resource), + ); + } + + /** + * A *successful* result only; failures and cancellations never consult + * `mcpp.ui.notifications.success`. `mcpp.ui.notifications.dedupeMinutes` + * suppresses a repeated identical message inside its window. + */ + private reportSuccess(message: string): void { + const mode = successReportOf(read("mcpp.ui.notifications.success")); + if (mode === "silent") { + return; + } + const now = Date.now(); + if (!dedupeAllows(this.lastNotified.get(message), now, read("mcpp.ui.notifications.dedupeMinutes"))) { + return; + } + this.lastNotified.set(message, now); + if (mode === "toast") { + void vscode.window.showInformationMessage(message); + return; + } + this.showStatusSuccess(message); + } + + /** `mcpp.ui.notifications.success = statusBar`: a transient, non-blocking label. */ + private showStatusSuccess(message: string): void { + if (this.options.currentProject() === undefined || !read("mcpp.ui.statusBar.show")) { + return; + } + this.status.text = `$(check) ${statusBarSuccessText(message)}`; + this.status.show(); + if (this.statusRevert !== undefined) { + clearTimeout(this.statusRevert); + } + this.statusRevert = setTimeout(() => { + this.statusRevert = undefined; + try { + this.applyStatusBar(); + } catch { + // The status item can already be disposed during a window reload. + } + }, SUCCESS_STATUS_MS); + } + + /** + * A failed `mcpp` run: the exit-code-specific next step from + * `src/cli/errors.ts`, the caller's own message as the fallback, and the + * `mcpp self explain` hint whenever the output names a diagnostic code. + */ + private reportCommandFailure( + args: readonly string[], + exitCode: number, + output: string, + fallback: string, + ): void { + const advice = failureAdvice(args, exitCode, output); + const parts: string[] = [failureGuidanceText(advice.kind) ?? fallback]; + if (advice.hint !== undefined) { + parts.push(t("Explain this code with: {0}", advice.hint)); + } + void vscode.window.showErrorMessage(parts.join(" ")); + } + + private warningOptions(strength: ConfirmationStrength, detail: string): vscode.MessageOptions { + return strength === "modal" ? { modal: true, detail } : { detail }; + } + + /** + * `mcpp.ui.statusBar.show` plus `mcpp.ui.statusBar.showLanguageServer`; the + * C++ Modules line comes from the host callback, so this controller never + * imports the mcppls bridge. + */ + private applyStatusBar(): void { + if (this.statusRevert !== undefined) { + clearTimeout(this.statusRevert); + this.statusRevert = undefined; + } + const tooltip = t("Open the mcpp project and toolchain quick menu"); + const languageServer = languageServerStatusSuffix( + read("mcpp.ui.statusBar.showLanguageServer"), + this.options.languageServerSummary?.(), + ); + this.status.text = languageServer === undefined + ? quickMenuStatusText + : `${quickMenuStatusText} · ${t("C++ Modules: {0}", languageServer)}`; + this.status.tooltip = languageServer === undefined + ? tooltip + : `${tooltip}\n${t("C++ Modules (provided by the mcpp language server extension): {0}", languageServer)}`; + // `mcpp.ui.statusBar.background`: VS Code allows a status bar entry exactly + // two backgrounds and picks the matching foreground itself; see + // `src/cli/statusBar.ts` for the whitelist this reads. + const background = statusBarBackgroundColour(read("mcpp.ui.statusBar.background")); + this.status.backgroundColor = background === undefined ? undefined : new vscode.ThemeColor(background); + if (read("mcpp.ui.statusBar.show")) { + this.status.show(); + } else { + this.status.hide(); + } + } + + private appendShortCommand( + title: string, + executable: string, + args: string[], + result: { exitCode: number; stdout: string; stderr: string }, + ): void { + // The command echo and the output of a successful command are the verbose + // lines `mcpp.log.level` gates. A failed command's raw stdout/stderr is + // written at `error`, which no configured level may suppress. + const failed = result.exitCode !== 0; + this.logger.info(`\n[${new Date().toISOString()}] ${title}`); + this.logger.debug(`$ ${commandLine(executable, args)}`); + if (result.stdout.length > 0) { + (failed ? this.logger.error : this.logger.info)(result.stdout.trimEnd()); + } + if (result.stderr.length > 0) { + (failed ? this.logger.error : this.logger.info)(result.stderr.trimEnd()); + } + (failed ? this.logger.error : this.logger.info)(`[exit ${result.exitCode}]`); + } +} diff --git a/src/cli/errors.ts b/src/cli/errors.ts new file mode 100644 index 0000000..a24fc35 --- /dev/null +++ b/src/cli/errors.ts @@ -0,0 +1,156 @@ +/** + * mcpp's exit-code contract (SPEC-003) turned into something a UI can branch on. + * + * | code | meaning | + * |------|--------------------------------------------| + * | 0 | success | + * | 1 | ran and failed | + * | 2 | usage error | + * | 4 | environment not ready | + * | 70 | internal error | + * | 101 | build failure — **only for `mcpp run`** | + * | 127 | unknown command | + * + * Two documented quirks are respected by the callers of this module: + * `mcpp build --configure-only` returns **2** for a *planning* failure, and + * `mcpp emit build-database` returns **1** together with a usable envelope — + * see {@link hasUsableOutput}. Nothing here parses narration for meaning; the + * only text it understands is the `MCPP_*` diagnostic code that + * `mcpp self explain` accepts. + * + * Pure string/number functions: no child process, no `vscode`. + */ + +export type McppFailureKind = + | "none" + | "failed" + | "usage" + | "environment" + | "internal" + | "build-failed" + | "unknown-command" + | "cancelled"; + +export interface McppOutcome { + kind: McppFailureKind; + /** Raw exit code, or -1 when the process could not be started at all. */ + exitCode: number; + /** The single stderr line that best explains it, when there is one. */ + detail?: string; + /** A `MCPP_*` diagnostic code found in the output, when there is one. */ + diagnosticCode?: string; +} + +const USAGE = 2; +const ENVIRONMENT = 4; +const INTERNAL = 70; +const BUILD_FAILED = 101; +const UNKNOWN_COMMAND = 127; + +/** `MCPP_OFFLINE_DOWNLOAD_REQUIRED`, never a lowercase word like `mcpp_offline`. */ +const DIAGNOSTIC_CODE = /\bMCPP_[A-Z][A-Z0-9_]*/; + +/** + * Map an exit code (plus the argv that produced it, which is the only way to + * tell `mcpp run`'s build failure apart) to a failure kind. + * + * `undefined` means "the process never produced an exit code" — a spawn failure + * or a cancellation — and yields `{ kind: "cancelled", exitCode: -1 }`. Signal + * terminations reported as a negative code are treated the same way. + * + * Codes that are not in the contract (for example a program's own exit code + * relayed by `mcpp run`) fall back to `"failed"`. + */ +export function classifyExit(exitCode: number | undefined, command?: readonly string[]): McppOutcome { + if (typeof exitCode !== "number" || !Number.isFinite(exitCode) || exitCode < 0) { + return { kind: "cancelled", exitCode: -1 }; + } + if (exitCode === 0) { + return { kind: "none", exitCode }; + } + if (exitCode === UNKNOWN_COMMAND) { + return { kind: "unknown-command", exitCode }; + } + if (exitCode === BUILD_FAILED) { + // 101 is `mcpp run`'s "your program failed to build"; for anything else it + // is an ordinary runtime failure. + return { kind: command?.[0] === "run" ? "build-failed" : "failed", exitCode }; + } + if (exitCode === USAGE) { + return { kind: "usage", exitCode }; + } + if (exitCode === ENVIRONMENT) { + return { kind: "environment", exitCode }; + } + if (exitCode === INTERNAL) { + return { kind: "internal", exitCode }; + } + return { kind: "failed", exitCode }; +} + +/** The first `MCPP_*` diagnostic code in `output`, when there is one. */ +export function diagnosticCodeIn(output: string): string | undefined { + if (typeof output !== "string") { + return undefined; + } + return DIAGNOSTIC_CODE.exec(output)?.[0]; +} + +/** Minimal JSON extraction for salvage checks: the trimmed text, then its outermost `{…}` / `[…]`. */ +function jsonDocumentIn(stdout: string): unknown { + if (typeof stdout !== "string") { + return undefined; + } + const text = stdout.trim(); + if (text.length === 0) { + return undefined; + } + const attempts = [text]; + const objectStart = text.indexOf("{"); + const objectEnd = text.lastIndexOf("}"); + if (objectStart >= 0 && objectEnd > objectStart) { + attempts.push(text.slice(objectStart, objectEnd + 1)); + } + const arrayStart = text.indexOf("["); + const arrayEnd = text.lastIndexOf("]"); + if (arrayStart >= 0 && arrayEnd > arrayStart) { + attempts.push(text.slice(arrayStart, arrayEnd + 1)); + } + for (const attempt of attempts) { + try { + const parsed: unknown = JSON.parse(attempt); + if (typeof parsed === "object" && parsed !== null) { + return parsed; + } + } catch { + // Not this candidate; try the next one. + } + } + return undefined; +} + +/** + * True when stdout carried something usable **even though the exit code was + * non-zero** — the `mcpp emit build-database` partial-failure case, where the + * envelope and its `diagnostics` are still worth reading. + * + * False when the command succeeded (there is nothing to salvage) and false when + * stdout has no parseable JSON document. A bare `--json` document counts just + * like an envelope. + */ +export function hasUsableOutput(outcome: McppOutcome, stdout: string): boolean { + if (outcome.exitCode === 0) { + return false; + } + return jsonDocumentIn(stdout) !== undefined; +} + +/** + * The follow-up command that explains a failure, e.g. + * `mcpp self explain MCPP_OFFLINE_DOWNLOAD_REQUIRED`. `undefined` when there is + * no diagnostic code to explain. + */ +export function explainHint(outcome: McppOutcome): string | undefined { + const code = outcome.diagnosticCode ?? diagnosticCodeIn(outcome.detail ?? ""); + return code === undefined ? undefined : `mcpp self explain ${code}`; +} diff --git a/src/cli/labels.ts b/src/cli/labels.ts new file mode 100644 index 0000000..96a4757 --- /dev/null +++ b/src/cli/labels.ts @@ -0,0 +1,43 @@ +/** + * The controller's own buttons. + * + * `showWarningMessage` / `showInformationMessage` reply with the label the user + * pressed, so a button that is both **displayed** and **compared** must be + * defined exactly once: two literals would silently desynchronise, and the + * comparison would never match a translated label. + * + * Kept out of `controller.ts` so it stays importable without an editor (`t()` + * loads the editor API lazily) and `test/cli/labels.test.ts` can hold the + * identity rule. + */ + +import { t } from "../i18n/t"; + +export interface ControllerLabels { + installCustom: string; + confirmInstall: string; + confirmDetect: string; + confirmDefault: string; + confirmClean: string; + chooseGlobalDefault: string; + cleanAndBuild: string; + showTasks: string; +} + +/** + * Resolved on every call, so `mcpp.ui.language` applies without a reload, and + * read once per dialogue into a local `labels` value that serves both the + * prompt and the `===` comparison. + */ +export function controllerLabels(): ControllerLabels { + return { + installCustom: t("$(edit) Enter another compatible toolchain spec…"), + confirmInstall: t("Install"), + confirmDetect: t("Detect"), + confirmDefault: t("Set as global default"), + confirmClean: t("Clean target"), + chooseGlobalDefault: t("Choose a global default"), + cleanAndBuild: t("Clean and build"), + showTasks: t("Show running tasks"), + }; +} diff --git a/src/newProject.ts b/src/cli/newProject.ts similarity index 70% rename from src/newProject.ts rename to src/cli/newProject.ts index e7b6f0c..90547e6 100644 --- a/src/newProject.ts +++ b/src/cli/newProject.ts @@ -1,3 +1,5 @@ +import { t } from "../i18n/t"; + export interface NewProjectActions { exists(path: string): boolean; confirm(message: string): Promise; @@ -20,11 +22,11 @@ export async function runNewProjectFlow( actions: NewProjectActions, ): Promise { if (actions.exists(projectRoot)) { - await actions.showError(`目标路径已存在:${projectRoot}。请更换项目名或位置。`); + await actions.showError(t("The destination already exists: {0}. Choose another project name or location.", projectRoot)); return "exists"; } const confirmed = await actions.confirm( - `将在 ${location} 执行 “mcpp new ${projectName}”,创建项目文件夹 ${projectRoot} 并打开它。`, + t("Run “mcpp new {0}” in {1}, create the project folder {2} and open it.", projectName, location, projectRoot), ); if (!confirmed) { return "declined"; @@ -32,7 +34,7 @@ export async function runNewProjectFlow( const exitCode = await actions.run(projectName, location); if (exitCode !== 0) { await actions.showError( - `mcpp new ${projectName} 失败(退出码 ${exitCode})。请查看 mcpp 输出频道。`, + t("mcpp new {0} failed (exit code {1}). See the mcpp output channel.", projectName, exitCode), ); return "failed"; } @@ -59,32 +61,32 @@ const MCPP_BUILTIN_TEMPLATE_MARKER = "PROJECT"; export function validateNewProjectName(input: string): string | undefined { const name = input.trim(); if (name.length === 0) { - return "项目名不能为空"; + return t("The project name must not be empty."); } if (/[\\/]/.test(name)) { - return "项目名不能包含路径分隔符"; + return t("The project name must not contain a path separator."); } if (name.startsWith("-")) { - return "项目名不能以 - 开头,否则会被 mcpp 解析为命令行选项"; + return t("The project name must not start with -, or mcpp would read it as a command-line option."); } if (name === "." || name === "..") { - return "项目名不能是 . 或 .."; + return t("The project name must not be . or .."); } // mcpp#380:当前内置模板会重复扫描替换结果,名称包含该标记时不会终止。 if (name.includes(MCPP_BUILTIN_TEMPLATE_MARKER)) { - return "项目名不能包含 PROJECT,否则会触发当前 mcpp 模板替换缺陷"; + return t("The project name must not contain PROJECT: it triggers a defect in the current mcpp template substitution."); } if (CONTROL_CHARS.test(name)) { - return "项目名不能包含控制字符"; + return t("The project name must not contain control characters."); } if (WINDOWS_RESERVED_CHARS.test(name)) { - return '项目名不能包含 <>:"|?* 等保留字符'; + return t('The project name must not contain reserved characters such as <>:"|?*.'); } if (name.endsWith(".")) { - return "项目名不能以 . 结尾(Windows 不支持)"; + return t("The project name must not end with . (unsupported on Windows)."); } if (WINDOWS_DEVICE_NAMES.test(name)) { - return "项目名不能是 Windows 保留设备名"; + return t("The project name must not be a Windows reserved device name."); } return undefined; } diff --git a/src/cli/process.ts b/src/cli/process.ts new file mode 100644 index 0000000..a0c58a8 --- /dev/null +++ b/src/cli/process.ts @@ -0,0 +1,120 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +export interface ProcessResult { + exitCode: number; + stdout: string; + stderr: string; +} + +export interface ProcessRunOptions { + timeoutMs?: number; + /** `mcpp.runtime.maxOutputMiB`; the default matches what the extension shipped before it was configurable. */ + maxBufferMiB?: number; +} + +export type ProcessRunner = ( + executable: string, + args: string[], + cwd?: string, + options?: ProcessRunOptions, +) => Promise; + +/** + * Windows cannot `execFile` a `.cmd`/`.bat` shim directly — Node ≥ 20.12 rejects + * it with EINVAL outright, and older hosts still mis-handle shebang scripts. A + * `mcpp.path` that points at such a wrapper (npm-style shim, the e2e fake mcpp) + * therefore has to go through the shell. Everywhere else the direct spawn is + * both safer and faster, so only these two suffixes opt in. + */ +export function spawnNeedsShell(executable: string, platform: NodeJS.Platform = process.platform): boolean { + return platform === "win32" && /\.(cmd|bat)$/i.test(executable); +} + +/** + * One argument, quoted for the shell path (external review P1-2, 2026-10-03). + * + * Node joins `file` and `args` with plain spaces when `shell: true`, without + * quoting anything, so a path with a space (`C:\Users\John Doe\…`, exactly what + * `xpkg parse` receives) splits in two and a search term like `foo & calc` + * becomes two commands. The rule is the one `CommandLineToArgvW` applies when + * the target re-parses the line: double backslashes that precede a quote or end + * the argument, escape the quotes themselves, and wrap in double quotes — + * inside which cmd treats `& | < > ^` as literal. `%VAR%` expansion inside + * quotes is a cmd limitation every shell-spawner shares and stays documented + * here rather than half-fixed. + */ +export function quoteWindowsArgument(argument: string): string { + let quoted = argument.replace(/(\\*)"/g, '$1$1\\"'); + quoted = quoted.replace(/(\\*)$/, '$1$1'); + return `"${quoted}"`; +} + +export async function runProcess( + executable: string, + args: string[], + cwd?: string, + options: ProcessRunOptions = {}, +): Promise { + try { + const needsShell = spawnNeedsShell(executable); + const result = await execFileAsync( + needsShell ? quoteWindowsArgument(executable) : executable, + needsShell ? args.map(quoteWindowsArgument) : args, + { + cwd, + encoding: "utf8", + maxBuffer: Math.max(1, options.maxBufferMiB ?? 16) * 1024 * 1024, + timeout: options.timeoutMs, + ...(needsShell ? { shell: true } : {}), + }, + ); + return { + exitCode: 0, + stdout: result.stdout, + stderr: result.stderr, + }; + } catch (error) { + const processError = error as NodeJS.ErrnoException & { + stdout?: string; + stderr?: string; + code?: number | string; + }; + return { + exitCode: typeof processError.code === "number" ? processError.code : 1, + stdout: processError.stdout ?? "", + stderr: processError.stderr ?? (typeof processError.message === "string" ? processError.message : ""), + }; + } +} + +/** + * The one way the extension body runs mcpp: `runProcess` plus the + * workspace-trust gate (external review P0, 2026-10-03). + * + * `mcpp.path` is a `resource`-scoped setting, so an untrusted workspace can + * name any program there; every caller outside `src/cli/` therefore goes + * through this seam and receives `undefined` — a refusal, not an error — when + * the workspace is not trusted. The callers degrade (the detail page falls + * back to the descriptor's own text, the cross-registry search keeps its + * local results, the self-check reports the probe as unknown) instead of + * running the command. + * + * `src/cli/controller.ts` wraps its own `requireTrusted()` prompts around + * whole commands, which is why `src/cli/` may still call `runProcess` + * directly; the architecture test holds everyone else to this seam. + */ +export async function runMcpp( + trusted: boolean, + executable: string, + args: readonly string[], + cwd?: string, + options: ProcessRunOptions = {}, +): Promise { + if (!trusted) { + return undefined; + } + return runProcess(executable, [...args], cwd, options); +} diff --git a/src/cli/protocol.ts b/src/cli/protocol.ts new file mode 100644 index 0000000..a84841b --- /dev/null +++ b/src/cli/protocol.ts @@ -0,0 +1,259 @@ +/** + * mcpp's machine-output protocol (`docs/50-machine-output.md`). + * + * **Detection rule**: parse stdout and require `schemaVersion` **and** `kind`. + * The exit code is never the signal — an older mcpp answers + * `mcpp --protocol-version` with human text on stdout and exit 1, and a + * *failing* command can still emit a perfectly good envelope + * (`mcpp emit build-database` on partial failure). Everything here is a pure + * string function: no child process, no `vscode`. + * + * `mcpp --protocol-version` prints an envelope with a `mcpp.protocol` kind: + * + * ```jsonc + * { "schemaVersion": 1, "kind": "mcpp.protocol", + * "envelope": { "min": 1, "max": 1 }, + * "kinds": { "mcpp.env": 1, "mcpp.toolchain.list": 1 }, + * "commands": { "toolchain list": { "effects": ["read"] } }, + * "mcpp": { "version": "2026.9.30.2", "protocol": { "min": 1, "max": 1 } } } + * ``` + * + * `kinds` is the answer to "which `--format json` commands are safe to use" — + * see {@link supportsKind}. + */ + +export interface ProtocolEnvelope { + schemaVersion: number; + kind: string; + kindVersion?: number; + data?: T; + diagnostics?: ProtocolDiagnostic[]; + effects?: string[]; + mcpp?: { version?: string; protocol?: { min?: number; max?: number } }; +} + +export interface ProtocolDiagnostic { + code: string; + severity: "error" | "warning" | "note"; + source?: string; + message: string; + path?: string; + range?: unknown; +} + +export interface ProtocolInfo { + mcppVersion?: string; + envelopeMax?: number; + kinds: Record; + /** command name -> the effects it may have, from `mcpp --protocol-version`. */ + effects: Record; +} + +const PROTOCOL_KIND = "mcpp.protocol"; + +/** + * Upper bound on the number of substrings we are willing to try when a document + * is not pure JSON. Only reached for malformed input, and it keeps the scan + * linear-ish on a 16 MiB capture. + */ +const MAX_JSON_CANDIDATES = 64; + +function asRecord(value: unknown): Record | undefined { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return undefined; + } + return value as Record; +} + +function readNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function readString(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +/** + * Candidate object substrings for a text that is **not** pure JSON: every `{…}` + * span between the brace positions. Narration before or after the document (a + * `warning:` line, a blank line, an old-mcpp banner) therefore cannot hide it, + * and a stray `}` in trailing narration does not either. + */ +function jsonCandidates(text: string): string[] { + const candidates: string[] = []; + + const starts: number[] = []; + for (let index = text.indexOf("{"); index >= 0; index = text.indexOf("{", index + 1)) { + starts.push(index); + if (starts.length >= MAX_JSON_CANDIDATES) { + break; + } + } + const ends: number[] = []; + for (let index = text.lastIndexOf("}"); index >= 0; index = text.lastIndexOf("}", index - 1)) { + ends.push(index); + if (ends.length >= MAX_JSON_CANDIDATES) { + break; + } + } + + for (const start of starts) { + // `ends` is descending: the first entry is the outermost closing brace. + for (const end of ends) { + if (end <= start) { + break; + } + candidates.push(text.slice(start, end + 1)); + if (candidates.length >= MAX_JSON_CANDIDATES) { + return candidates; + } + } + } + return candidates; +} + +/** The first JSON object found in `stdout`; `undefined` for anything else. */ +function parseJsonObject(stdout: string): Record | undefined { + const text = stdout.trim(); + if (text.length === 0) { + return undefined; + } + try { + // The trimmed text is one complete JSON value, so it *is* the document. + // An array or a scalar is not an envelope, and we must not dig an object + // out of it. + return asRecord(JSON.parse(text) as unknown); + } catch { + // Narration around the document: fall through to the outermost object. + } + for (const candidate of jsonCandidates(text)) { + let parsed: unknown; + try { + parsed = JSON.parse(candidate); + } catch { + continue; + } + const record = asRecord(parsed); + if (record !== undefined) { + return record; + } + } + return undefined; +} + +/** Parse stdout as one enveloped document; `undefined` when it is not one. */ +export function parseEnvelope(stdout: string): ProtocolEnvelope | undefined { + if (typeof stdout !== "string") { + return undefined; + } + const record = parseJsonObject(stdout); + if (record === undefined) { + return undefined; + } + // Presence *and* type: a string `schemaVersion` is not a document we can trust. + if (typeof record.schemaVersion !== "number") { + return undefined; + } + if (typeof record.kind !== "string" || record.kind.length === 0) { + return undefined; + } + return record as unknown as ProtocolEnvelope; +} + +function kindVersionOf(value: unknown): number | undefined { + const direct = readNumber(value); + if (direct !== undefined) { + return direct; + } + const record = asRecord(value); + if (record === undefined) { + return undefined; + } + return readNumber(record.version) ?? readNumber(record.max); +} + +function readKindVersions(source: Record | undefined): Record { + const versions: Record = {}; + if (source === undefined) { + return versions; + } + for (const [kind, value] of Object.entries(source)) { + const version = kindVersionOf(value); + if (version !== undefined) { + versions[kind] = version; + } + } + return versions; +} + +function readCommandEffects(source: Record | undefined): Record { + const effects: Record = {}; + if (source === undefined) { + return effects; + } + for (const [command, value] of Object.entries(source)) { + const entry = asRecord(value); + if (entry === undefined || !Array.isArray(entry.effects)) { + continue; + } + effects[command] = entry.effects.filter((item): item is string => typeof item === "string"); + } + return effects; +} + +/** + * Parse `mcpp --protocol-version`. Returns `undefined` for older mcpp (human + * text on stdout, exit 1) and for any document that is not the protocol + * envelope, so the caller can fall back to the legacy text path. + * + * The fields sit at the envelope's top level in the real output; `data` is + * accepted as well so a future nesting cannot silently disable the probe. + */ +export function parseProtocolInfo(stdout: string): ProtocolInfo | undefined { + const envelope = parseEnvelope(stdout) as + | (ProtocolEnvelope & Record) + | undefined; + if (envelope === undefined || envelope.kind !== PROTOCOL_KIND) { + return undefined; + } + + const data = asRecord(envelope.data); + const mcpp = asRecord(envelope.mcpp) ?? asRecord(data?.mcpp); + const envelopeRange = asRecord(envelope.envelope) ?? asRecord(data?.envelope); + const protocolRange = asRecord(mcpp?.protocol); + + const info: ProtocolInfo = { + kinds: readKindVersions(asRecord(envelope.kinds) ?? asRecord(data?.kinds)), + effects: readCommandEffects(asRecord(envelope.commands) ?? asRecord(data?.commands)), + }; + const mcppVersion = readString(mcpp?.version); + if (mcppVersion !== undefined) { + info.mcppVersion = mcppVersion; + } + const envelopeMax = readNumber(envelopeRange?.max) ?? readNumber(protocolRange?.max); + if (envelopeMax !== undefined) { + info.envelopeMax = envelopeMax; + } + return info; +} + +/** The kinds mcpp advertised, i.e. which `--format json` commands are safe to use. */ +export function supportsKind(info: ProtocolInfo | undefined, kind: string): boolean { + if (info === undefined || typeof kind !== "string") { + return false; + } + const kinds = info.kinds; + if (typeof kinds !== "object" || kinds === null) { + return false; + } + return Object.prototype.hasOwnProperty.call(kinds, kind); +} + +/** `parseEnvelope` and `kind` check in one step, returning `data` or `undefined`. */ +export function readData(stdout: string, kind: string): T | undefined { + const envelope = parseEnvelope(stdout); + if (envelope === undefined || envelope.kind !== kind) { + return undefined; + } + return envelope.data; +} diff --git a/src/cli/search.ts b/src/cli/search.ts new file mode 100644 index 0000000..8cf21ef --- /dev/null +++ b/src/cli/search.ts @@ -0,0 +1,175 @@ +// `mcpp search` 的适配层(依赖版本补全,方案默认关)。 +// +// **这是全项目唯一解析「人类输出」的地方。** `mcpp search` 没有机读格式 +// (`--all-versions` 也只改人类列表的详细程度,不是 JSON),所以这里只能按 +// 实测行形状解析,并且必须防御性地失败:看不懂的行一律跳过,绝不抛异常、 +// 绝不半猜。mcpp 一旦提供机读格式就替换本模块(方案 §3.3.2 已登记)。 +// +// 纯字符串函数:不启动进程、不读盘、不依赖 vscode。执行 `mcpp`、超时、联网 +// 判定与会话缓存都由调用方负责(`mcpp.toml.indexCompletion*`)。 +// +// 实测形状(mcpp 2026.9.30.2): +// 兼容索引行 ` compat:zlib A compression library (1.3.2)` +// --all-versions 多版本 ` mcpplibs:aarch64-virt-rt … (0.2.1, 0.2.0, 0.1.1, ...)` +// 没有版本的行(如纯 xim 工具)没有尾括号,直接跳过。 + +export interface PackageVersion { + version: string; + summary?: string; +} + +/** semver 近似:数字点分,可带 `-`/`+` 后缀(`2026.07.09` 这类日期版本也算)。 */ +const VERSION = /^\d+(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/; +/** 行尾的括号组:实测版本都写在这里。 */ +const TRAILING_PARENS = /\(([^()]*)\)\s*$/; +/** 有些终端会给输出上色;颜色码不属于内容。 */ +const ANSI = /\u001b\[[0-9;]*m/g; + +function isVersionToken(text: string): boolean { + return VERSION.test(text); +} + +/** 去掉包标识后的描述文本;没有描述时返回 undefined。 */ +function summaryOf(head: string): string | undefined { + const match = /^\S+\s+(.*)$/.exec(head.trim()); + const summary = match?.[1]?.trim() ?? ""; + return summary === "" ? undefined : summary; +} + +/** 简单 semver 比较:数字段按数值比,缺段视为更小,后缀按字典序。 */ +function compareVersions(a: string, b: string): number { + const left = a.split(/[.+-]/); + const right = b.split(/[.+-]/); + const length = Math.max(left.length, right.length); + for (let index = 0; index < length; index += 1) { + const l = left[index]; + const r = right[index]; + if (l === undefined) { + return -1; + } + if (r === undefined) { + return 1; + } + const ln = /^\d+$/.test(l) ? Number(l) : undefined; + const rn = /^\d+$/.test(r) ? Number(r) : undefined; + if (ln !== undefined && rn !== undefined) { + if (ln !== rn) { + return ln < rn ? -1 : 1; + } + continue; + } + const order = l.localeCompare(r); + if (order !== 0) { + return order < 0 ? -1 : 1; + } + } + return 0; +} + +/** + * 从 `mcpp search --all-versions` 的 stdout 提取候选版本, + * 去重后按新→旧排序。空输入、只有旁白、或形状不认识时返回空数组。 + */ +export function parseSearchOutput(stdout: string): PackageVersion[] { + if (typeof stdout !== "string" || stdout === "") { + return []; + } + const byVersion = new Map(); + for (const rawLine of stdout.split(/\r?\n/)) { + const line = rawLine.replace(ANSI, "").trim(); + if (line === "") { + continue; + } + const match = TRAILING_PARENS.exec(line); + if (match === null) { + continue; + } + const versions = match[1] + .split(",") + .map((part) => part.trim()) + .filter(isVersionToken); + if (versions.length === 0) { + continue; + } + const summary = summaryOf(line.slice(0, match.index)); + for (const version of versions) { + if (byVersion.has(version)) { + continue; + } + const entry: PackageVersion = { version }; + if (summary !== undefined) { + entry.summary = summary; + } + byVersion.set(version, entry); + } + } + return [...byVersion.values()].sort((a, b) => compareVersions(b.version, a.version)); +} + +/** `mcpp` 的 argv(不含可执行文件名)。 */ +export function searchArguments(name: string): string[] { + return ["search", name, "--all-versions"]; +} + +/** + * 是否执行索引查询:显式开启、工作区受信任、且未离线。 + * `name` 留给将来的 deny-list;当前判定只是三个开关的合取。 + */ +export function shouldSearch( + name: string, + options: { enabled: boolean; offline: boolean; trusted: boolean }, +): boolean { + return options.enabled && options.trusted && !options.offline; +} + +/** `mcpp.toml.indexCompletionTimeoutSeconds` 的注册表默认值。 */ +const DEFAULT_INDEX_TIMEOUT_SECONDS = 20; + +/** 一次索引查询的完整描述:argv 与硬超时。 */ +export interface IndexSearchRequest { + name: string; + args: string[]; + timeoutMs: number; +} + +/** + * 把设置解析成"要跑什么",或者 `undefined` 表示不跑。 + * + * 纯函数:进程、会话缓存、vscode 判定都在调用方(`src/toml/providers.ts`)。 + * 超时从 `mcpp.toml.indexCompletionTimeoutSeconds` 来,至少 1 秒;非有限值 + * 回落到注册表默认的 20 秒。这样"绝不超过设置的超时"有一个可单测的落点。 + */ +export function indexCompletionRequest( + name: string, + options: { enabled: boolean; trusted: boolean; offline: boolean; timeoutSeconds: number }, +): IndexSearchRequest | undefined { + if (!shouldSearch(name, options)) { + return undefined; + } + const seconds = Number.isFinite(options.timeoutSeconds) + ? Math.max(1, Math.floor(options.timeoutSeconds)) + : DEFAULT_INDEX_TIMEOUT_SECONDS; + return { name, args: searchArguments(name), timeoutMs: seconds * 1000 }; +} + +/** + * 等 `work`,但最多等 `timeoutMs`;超时得到 `undefined`。 + * + * 调用方自己的进程超时(`execFile` 的 `timeout`)才是第一道闸;这是第二道: + * 即便进程卡住不退出,补全也必须在超时后返回,绝不把编辑器挂住。 + */ +export async function withDeadline(work: Promise, timeoutMs: number): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + work, + new Promise((resolve) => { + timer = setTimeout(() => resolve(undefined), timeoutMs); + }), + ]); + } finally { + if (timer !== undefined) { + clearTimeout(timer); + } + } +} diff --git a/src/cli/selfCheck.ts b/src/cli/selfCheck.ts new file mode 100644 index 0000000..4f5f517 --- /dev/null +++ b/src/cli/selfCheck.ts @@ -0,0 +1,98 @@ +/** + * `mcpp: Environment Self-check` — one copyable snapshot. + * + * The point is to answer the questions a support thread always asks, in one + * place: which versions, which protocol, what mcppls can do, what the state is, + * which settings were changed. Pure, so the layout is testable and the extension + * host only has to gather the input. + */ + +import { formatBytes, formatCount } from "../util/format"; + +export interface SelfCheckInput { + extensionVersion: string; + vscodeVersion: string; + platform: string; + languagePreference: string; + trusted: boolean; + workspaceRoots: readonly string[]; + projectRoot?: string; + mcppPath: string; + mcppProbe?: { + version?: string; + envelopeMax?: number; + kinds: readonly string[]; + effects?: Record; + }; + mcppls: { + installed: boolean; + version?: string; + enabled: boolean; + state: string; + capabilities: ReadonlyArray<{ key: string; state: string; command?: string }>; + }; + cache?: { totalBytes: number; entries: number; incomplete?: number }; + changedSettings: ReadonlyArray<{ key: string; value: unknown; source: string }>; + lastRefresh?: { at: string; state: string; command?: string }; +} + +function line(label: string, value: string): string { + return ` ${label.padEnd(14)}${value}`; +} + +/** + * The snapshot. It says "unknown" where something could not be read — an empty + * field reads as "nothing is wrong", which is the opposite of the truth. + */ +export function buildSelfCheckText(input: SelfCheckInput): string { + const lines: string[] = []; + lines.push(`mcpp-vscode ${input.extensionVersion} · VS Code ${input.vscodeVersion} · ${input.platform}`); + lines.push(line("Language", input.languagePreference)); + lines.push(line("Workspace", `${input.trusted ? "trusted" : "NOT trusted"} · ${input.workspaceRoots.length} root(s)`)); + for (const root of input.workspaceRoots) { + lines.push(line(" folder", root)); + } + lines.push(line("Project", input.projectRoot ?? "no mcpp.toml found")); + + lines.push(""); + lines.push("mcpp"); + lines.push(line("path", input.mcppPath)); + const probe = input.mcppProbe; + if (probe === undefined) { + lines.push(line("version", "unknown (mcpp --protocol-version did not answer)")); + } else { + lines.push(line("version", probe.version ?? "unknown")); + lines.push(line("envelope", probe.envelopeMax === undefined ? "unknown" : String(probe.envelopeMax))); + lines.push(line("kinds", probe.kinds.length === 0 ? "none advertised" : probe.kinds.join(", "))); + } + + lines.push(""); + lines.push("C++ Modules (mcppls)"); + lines.push(line("installed", input.mcppls.installed ? "yes" : "no")); + lines.push(line("version", input.mcppls.version ?? "unknown")); + lines.push(line("enabled", input.mcppls.enabled ? "yes" : "disabled in this workspace")); + lines.push(line("state", input.mcppls.state)); + for (const capability of input.mcppls.capabilities) { + lines.push(line(` ${capability.key}`, `${capability.state}${capability.command === undefined ? "" : ` → ${capability.command}`}`)); + } + lines.push(line("last refresh", input.lastRefresh === undefined ? "never" : `${input.lastRefresh.state} at ${input.lastRefresh.at}${input.lastRefresh.command === undefined ? "" : ` (${input.lastRefresh.command})`}`)); + + lines.push(""); + lines.push("Cache"); + if (input.cache === undefined) { + lines.push(line("shared", "not read")); + } else { + lines.push(line("shared", `${formatBytes(input.cache.totalBytes)} · ${formatCount(input.cache.entries)} entries${input.cache.incomplete === undefined ? "" : ` · ${input.cache.incomplete} incomplete`}`)); + } + + lines.push(""); + lines.push(`Settings changed from their default (${input.changedSettings.length})`); + if (input.changedSettings.length === 0) { + lines.push(line("", "none")); + } else { + for (const entry of input.changedSettings) { + lines.push(line(` ${entry.source}`, `${entry.key} = ${JSON.stringify(entry.value)}`)); + } + } + return lines.join("\n"); +} diff --git a/src/cli/statusBar.ts b/src/cli/statusBar.ts new file mode 100644 index 0000000..e7fc67c --- /dev/null +++ b/src/cli/statusBar.ts @@ -0,0 +1,45 @@ +/** + * The one thing the status bar item can be painted with. + * + * `StatusBarItem.backgroundColor` looks like it takes any `ThemeColor`, and the + * API reference says so — but the extension host enforces a whitelist, and it is + * the *host* that also swaps the foreground so the text stays readable: + * + * ```js + * // extensionHostProcess.js, ExtHostStatusBarEntry + * static ALLOWED_BACKGROUND_COLORS = new Map([ + * ["statusBarItem.errorBackground", new ThemeColor("statusBarItem.errorForeground")], + * ["statusBarItem.warningBackground", new ThemeColor("statusBarItem.warningForeground")], + * ]); + * set backgroundColor(t) { t && !ALLOWED_BACKGROUND_COLORS.has(t.id) && (t = void 0); … } + * ``` + * + * So a contributed `mcpp.statusBarBackground` would be silently dropped, and a + * `color` set alongside one of the two allowed backgrounds would be overridden. + * This module is the whole answer to "can the mcpp item have a background?" and + * it is pure, so the answer is testable without an editor host. + */ + +/** The value of `mcpp.ui.statusBar.background`. */ +export type StatusBarBackground = "none" | "warning" | "error"; + +/** Every value `mcpp.ui.statusBar.background` accepts, in menu order. */ +export const STATUS_BAR_BACKGROUNDS: readonly StatusBarBackground[] = ["warning", "error", "none"]; + +/** + * The `ThemeColor` id for a setting value, or `undefined` for "no background". + * + * An unknown value is treated as `none` rather than as an error: a settings file + * with a stale value should leave the status bar plain, not throw during + * activation. + */ +export function statusBarBackgroundColour(value: unknown): string | undefined { + switch (value) { + case "warning": + return "statusBarItem.warningBackground"; + case "error": + return "statusBarItem.errorBackground"; + default: + return undefined; + } +} diff --git a/src/tasks.ts b/src/cli/tasks.ts similarity index 68% rename from src/tasks.ts rename to src/cli/tasks.ts index 912e692..a480b31 100644 --- a/src/tasks.ts +++ b/src/cli/tasks.ts @@ -1,3 +1,5 @@ +import { t } from "../i18n/t"; + export type ProjectTaskKind = "build" | "run" | "test" | "clean"; export type TaskState = "succeeded" | "failed" | "cancelled"; @@ -12,18 +14,36 @@ export interface TaskCompletion { exitCode?: number; } -const TASK_TITLES: Record = { - build: "mcpp: 构建", - run: "mcpp: 运行", - test: "mcpp: 测试", - clean: "mcpp: 清理", +/** + * The task's display name. Resolved per call, not at import time, so a change to + * `mcpp.ui.language` applies without reloading the window. + */ +function taskTitle(kind: ProjectTaskKind): string { + switch (kind) { + case "build": + return t("mcpp: Build"); + case "run": + return t("mcpp: Run"); + case "test": + return t("mcpp: Test"); + case "clean": + return t("mcpp: Clean"); + } +} + +/** `mcpp.task.Args`, so a user can pass `-j 4` or `--quiet` without a wrapper. */ +export const TASK_ARGUMENT_SETTINGS: Readonly> = { + build: "mcpp.task.buildArgs", + run: "mcpp.task.runArgs", + test: "mcpp.task.testArgs", + clean: "mcpp.task.cleanArgs", }; -export function projectTaskPlan(kind: ProjectTaskKind): ProjectTaskPlan { +export function projectTaskPlan(kind: ProjectTaskKind, extraArgs: readonly string[] = []): ProjectTaskPlan { return { kind, - title: TASK_TITLES[kind], - args: [kind], + title: taskTitle(kind), + args: [kind, ...extraArgs], }; } diff --git a/src/cli.ts b/src/cli/toolchain.ts similarity index 100% rename from src/cli.ts rename to src/cli/toolchain.ts diff --git a/src/cliController.ts b/src/cliController.ts deleted file mode 100644 index 79e9fc9..0000000 --- a/src/cliController.ts +++ /dev/null @@ -1,759 +0,0 @@ -import { existsSync } from "node:fs"; -import { join } from "node:path"; -import process from "node:process"; - -import * as vscode from "vscode"; - -import { - hostDefaultToolchains, - mcppCommandArguments, - normalizeToolchainSpec, - parseToolchainList, - toolchainInstallKind, - toolchainSpecTargetHint, - type ToolchainInventory, - type ToolchainItem, -} from "./cli"; -import type { McppProjectDiscovery } from "./discovery"; -import { runProcess } from "./process"; -import { - McppOperationRegistry, - classifyTaskExit, - projectTaskPlan, - shouldRefreshLanguageServerAfterTask, - type ProjectTaskKind, - type TaskCompletion, -} from "./tasks"; -import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; -import { runNewProjectFlow, validateNewProjectName } from "./newProject"; - -export interface McppCliControllerOptions { - output: vscode.OutputChannel; - currentProject: () => McppProjectDiscovery | undefined; - afterProjectTask: ( - project: McppProjectDiscovery, - kind: ProjectTaskKind, - completion: TaskCompletion, - ) => Promise; - isTrusted: () => boolean; -} - -interface ToolchainPickItem extends vscode.QuickPickItem { - spec?: string; - toolchain?: ToolchainItem; - customInput?: boolean; -} - -type OperationToken = object; - -export interface ProjectTaskRunOptions { - /** Set to false when the caller performs its own post-task action. */ - notify?: boolean; -} - -const INSTALL_CUSTOM_LABEL = "$(edit) 输入其他兼容工具链 spec…"; -const CONFIRM_INSTALL = "安装"; -const CONFIRM_DETECT = "检测"; -const CONFIRM_DEFAULT = "设为全局默认"; -const CONFIRM_CLEAN = "清理 target"; -const SHOW_TASKS = "显示正在运行的任务"; - -function taskScope(root: string): vscode.WorkspaceFolder | vscode.TaskScope { - return vscode.workspace.getWorkspaceFolder(vscode.Uri.file(root)) - ?? vscode.TaskScope.Workspace; -} - -function workingDirectory(project: McppProjectDiscovery | undefined): string { - if (project !== undefined) { - return project.root; - } - return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd(); -} - -function commandLine(executable: string, args: string[]): string { - return [executable, ...args].join(" "); -} - -export class McppCliController { - private readonly status: vscode.StatusBarItem; - - private readonly operations = new McppOperationRegistry(); - - public constructor(private readonly options: McppCliControllerOptions) { - this.status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 40); - this.status.command = CLI_COMMANDS.showMenu; - this.status.text = quickMenuStatusText; - this.status.tooltip = "打开 mcpp 项目和工具链快捷菜单"; - } - - public register(): vscode.Disposable[] { - const disposables: vscode.Disposable[] = [ - this.status, - vscode.commands.registerCommand(CLI_COMMANDS.showMenu, this.guarded(() => this.showMenu())), - vscode.commands.registerCommand(CLI_COMMANDS.newProject, this.guarded(() => this.newProject())), - vscode.commands.registerCommand(CLI_COMMANDS.build, this.guarded(() => this.runProjectTask("build"))), - vscode.commands.registerCommand(CLI_COMMANDS.run, this.guarded(() => this.runProjectTask("run"))), - vscode.commands.registerCommand(CLI_COMMANDS.test, this.guarded(() => this.runProjectTask("test"))), - vscode.commands.registerCommand(CLI_COMMANDS.clean, this.guarded(() => this.runProjectTask("clean"))), - vscode.commands.registerCommand(CLI_COMMANDS.showToolchains, this.guarded(() => this.showToolchains())), - vscode.commands.registerCommand(CLI_COMMANDS.installToolchain, this.guarded(() => this.installToolchain())), - vscode.commands.registerCommand(CLI_COMMANDS.selectDefaultToolchain, this.guarded(() => this.selectDefaultToolchain())), - vscode.window.onDidChangeActiveTextEditor(() => this.refreshStatus()), - vscode.workspace.onDidChangeWorkspaceFolders(() => this.refreshStatus()), - ]; - this.refreshStatus(); - return disposables; - } - - public refreshStatus(): void { - if (this.options.currentProject() === undefined) { - this.status.hide(); - return; - } - this.status.show(); - } - - public isBusy(): boolean { - return this.operations.hasActive(); - } - - public async runProjectTask( - kind: ProjectTaskKind, - options: ProjectTaskRunOptions = {}, - ): Promise { - const project = this.requireProject(); - if (project === undefined || !this.requireTrusted()) { - return undefined; - } - - if (kind === "clean") { - const choice = await vscode.window.showWarningMessage( - `将删除当前工程的 target 目录:${project.root}/target`, - { modal: true, detail: "此操作不会清理全局 BMI 缓存。" }, - CONFIRM_CLEAN, - ); - if (choice !== CONFIRM_CLEAN) { - return undefined; - } - } - - const token: OperationToken = {}; - const active = this.operations.beginProject(project.root, token); - if (active !== undefined) { - const choice = await vscode.window.showWarningMessage( - `已有 mcpp 操作正在运行,暂不启动 ${kind}。`, - SHOW_TASKS, - ); - if (choice === SHOW_TASKS) { - await vscode.commands.executeCommand("workbench.action.tasks.showTasks"); - } - return undefined; - } - - let completion: TaskCompletion | undefined; - try { - const plan = projectTaskPlan(kind); - completion = await this.executeTask( - project.root, - this.mcppExecutable(project), - plan.title, - plan.args, - ); - this.appendTaskCompletion(project.root, plan.title, plan.args, completion); - } finally { - this.operations.finishProject(project.root, token); - } - - if (options.notify !== false - && completion !== undefined - && shouldRefreshLanguageServerAfterTask(kind, completion)) { - await this.options.afterProjectTask(project, kind, completion); - } - return completion; - } - - public async showToolchains(): Promise { - if (!this.requireTrusted()) { - return; - } - const project = this.options.currentProject(); - const inventory = await this.readToolchainInventory(project); - if (inventory === undefined) { - return; - } - - const items = this.inventoryItems(inventory, false, project); - if (items.length === 0) { - await vscode.window.showInformationMessage( - "mcpp 没有列出可用工具链;请查看 mcpp 输出频道中的原始结果。", - ); - return; - } - await vscode.window.showQuickPick(items, { - title: "mcpp 工具链与 target", - placeHolder: "只读查看 mcpp 当前解析结果", - matchOnDescription: true, - matchOnDetail: true, - }); - } - - public async installToolchain(): Promise { - if (!this.requireTrusted()) { - return; - } - const project = this.options.currentProject(); - const inventory = await this.readToolchainInventory(project); - const spec = await this.pickInstallSpec(inventory); - if (spec === undefined) { - return; - } - const installKind = toolchainInstallKind(spec); - const targetHint = toolchainSpecTargetHint(spec); - const confirmLabel = installKind === "system-detect" ? CONFIRM_DETECT : CONFIRM_INSTALL; - const confirmation = installKind === "system-detect" - ? `将调用 mcpp 检测系统 MSVC(${spec})。` - : targetHint === "target" - ? `将把可能携带 target 语义的兼容 spec ${spec} 交给 mcpp 安装,最终由 mcpp 校验。` - : installKind === "managed-target" - ? `将把携带 target 语义的兼容 spec ${spec} 交给 mcpp 安装。` - : `将安装 mcpp 工具链 ${spec}(不指定 target,使用 host target)。`; - const detail = installKind === "system-detect" - ? "mcpp 不会下载或安装 MSVC;它会检测 Visual Studio,并在缺失时给出官方安装指引。" - : targetHint === "target" - ? "mcpp 会判断编译器前缀是否为有效 triple;有效时可能下载对应 target 的较大工具链包。" - : installKind === "managed-target" - ? "mcpp 会规范化兼容写法,并可能下载对应 target 的较大工具链包。" - : "安装可能下载较大的工具链包,并修改 mcpp 全局缓存。"; - - const choice = await vscode.window.showWarningMessage( - confirmation, - { modal: true, detail }, - confirmLabel, - ); - if (choice !== confirmLabel) { - return; - } - - const token: OperationToken = {}; - const active = this.operations.beginGlobal(token); - if (active !== undefined) { - const duplicateChoice = await vscode.window.showWarningMessage( - "已有 mcpp 操作正在运行。", - SHOW_TASKS, - ); - if (duplicateChoice === SHOW_TASKS) { - await vscode.commands.executeCommand("workbench.action.tasks.showTasks"); - } - return; - } - - const taskTitle = installKind === "system-detect" - ? `mcpp: 检测系统 MSVC(${spec})` - : `mcpp: 安装工具链 ${spec}`; - try { - const installArgs = mcppCommandArguments("toolchain", "install", spec); - const completion = await this.executeTask( - workingDirectory(project), - this.mcppExecutable(project), - taskTitle, - installArgs, - ); - this.appendTaskCompletion( - workingDirectory(project), - taskTitle, - installArgs, - completion, - ); - if (completion.state !== "succeeded") { - return; - } - } finally { - this.operations.finishGlobal(token); - } - - if (installKind === "managed-target") { - await vscode.window.showInformationMessage( - `mcpp 已完成 ${spec}。首版插件不修改 target 默认;如需设为默认,请使用带 --target 的 mcpp CLI。`, - ); - return; - } - - const refreshed = await this.readToolchainInventory(project); - const defaultChoice = await vscode.window.showInformationMessage( - installKind === "system-detect" - ? "MSVC 检测完成。是否从最新列表中选择全局默认?" - : `工具链 ${spec} 安装完成。是否从最新列表中选择全局默认?`, - "选择全局默认", - ); - if (defaultChoice === "选择全局默认" && refreshed !== undefined) { - await this.selectDefaultToolchainFromInventory(project, refreshed); - } - } - - public async selectDefaultToolchain(): Promise { - if (!this.requireTrusted()) { - return; - } - const project = this.options.currentProject(); - const inventory = await this.readToolchainInventory(project); - if (inventory !== undefined) { - await this.selectDefaultToolchainFromInventory(project, inventory); - } - } - - private async selectDefaultToolchainFromInventory( - project: McppProjectDiscovery | undefined, - inventory: ToolchainInventory, - ): Promise { - const installed = hostDefaultToolchains(inventory); - if (installed.length === 0) { - await vscode.window.showWarningMessage( - "没有可用于 host target 的已安装工具链,不能在此处设置全局默认。target 专用工具链请使用 mcpp CLI;Windows MSVC 请先安装 Visual Studio。", - ); - return; - } - - const items: ToolchainPickItem[] = installed.map((toolchain) => ({ - label: `${toolchain.effective ? "$(check) " : ""}${toolchain.spec}`, - description: toolchain.source === "system" ? "系统工具链(mcpp 只检测)" : "mcpp 管理的工具链", - detail: toolchain.effective ? "当前工程有效工具链" : undefined, - spec: toolchain.spec, - toolchain, - })); - const picked = await vscode.window.showQuickPick(items, { - title: "选择 mcpp 全局默认工具链", - placeHolder: this.isNestedWorkspaceProject(project) - ? "当前工程根位于 VS Code 文件夹子目录;实际构建解析以 mcpp 为准" - : inventory.projectOverridesGlobal - ? "项目当前覆盖全局默认;这里只修改 mcpp 当前全局配置" - : "选择后只修改 mcpp 全局配置,不会自动构建工程", - matchOnDescription: true, - matchOnDetail: true, - }); - if (picked?.spec === undefined) { - return; - } - - const choice = await vscode.window.showWarningMessage( - `将把全局默认对设为 ${picked.spec} + host target。当前项目的 mcpp.toml/target 配置仍可能覆盖它。`, - { modal: true, detail: "mcpp 会同时清空全局 default_target;配置文件位置由当前 mcpp 安装及 MCPP_HOME 决定。" }, - CONFIRM_DEFAULT, - ); - if (choice !== CONFIRM_DEFAULT) { - return; - } - - const token: OperationToken = {}; - const active = this.operations.beginGlobal(token); - if (active !== undefined) { - await vscode.window.showWarningMessage("已有 mcpp 操作正在运行。", SHOW_TASKS); - return; - } - - try { - const args = mcppCommandArguments("toolchain", "default", picked.spec); - const result = await runProcess( - this.mcppExecutable(project), - args, - workingDirectory(project), - ); - this.appendShortCommand("设置全局默认工具链", this.mcppExecutable(project), args, result); - if (result.exitCode !== 0) { - await vscode.window.showErrorMessage( - `设置全局默认工具链失败(退出码 ${result.exitCode})。请查看 mcpp 输出频道。`, - ); - return; - } - - } finally { - this.operations.finishGlobal(token); - } - - const buildChoice = await vscode.window.showInformationMessage( - `mcpp 全局默认已更新为 ${picked.spec} + host target。建议清理旧工具链产物后重新构建。`, - ...(project === undefined ? [] : ["清理并构建"]), - ); - if (buildChoice === "清理并构建" && project !== undefined) { - const cleanArgs = mcppCommandArguments("clean"); - const cleanResult = await runProcess(this.mcppExecutable(project), cleanArgs, project.root); - this.appendShortCommand("清理旧产物", this.mcppExecutable(project), cleanArgs, cleanResult); - await this.runProjectTask("build"); - } - } - - private async pickInstallSpec(inventory: ToolchainInventory | undefined): Promise { - const items: ToolchainPickItem[] = []; - for (const toolchain of inventory?.available ?? []) { - items.push({ - label: toolchain.spec, - description: "mcpp 按 family 聚合的可用版本;本操作按 host target 安装", - spec: toolchain.spec, - }); - } - items.push({ - label: INSTALL_CUSTOM_LABEL, - detail: "支持 family、family@version、namespace、部分版本和 mcpp 兼容旧拼写", - customInput: true, - }); - - const picked = await vscode.window.showQuickPick(items, { - title: "安装 mcpp 工具链", - placeHolder: "不选择 target;target 专用安装请使用 mcpp CLI", - matchOnDescription: true, - matchOnDetail: true, - }); - if (picked === undefined) { - return undefined; - } - if (!picked.customInput) { - return picked.spec; - } - - const input = await vscode.window.showInputBox({ - title: "输入工具链 spec", - prompt: "例如 gcc、llvm@20.1.7、xim:gcc@16、msvc、mingw;兼容写法由 mcpp 规范化", - placeHolder: "gcc@16", - validateInput: (value) => { - const normalized = normalizeToolchainSpec(value); - if (normalized === undefined) { - return "请输入 family、family@version、family version、namespace 或 mcpp 兼容 spec"; - } - return undefined; - }, - }); - return input === undefined ? undefined : normalizeToolchainSpec(input); - } - - private inventoryItems( - inventory: ToolchainInventory, - includeActions: boolean, - project: McppProjectDiscovery | undefined, - ): ToolchainPickItem[] { - const items: ToolchainPickItem[] = []; - const nestedView = this.isNestedWorkspaceProject(project); - if (inventory.effective !== undefined) { - items.push({ - label: `$(check) ${nestedView ? "mcpp list 当前目录工具链" : "当前有效工具链"}:${inventory.effective.spec}`, - description: nestedView - ? "当前目录视图;实际生效值以 mcpp build 解析为准" - : inventory.projectOverridesGlobal ? "来自当前项目 mcpp.toml,覆盖全局默认" : "来自全局默认", - detail: inventory.effectiveTarget === undefined - ? "有效 target:host(mcpp 未显示显式 target)" - : `有效 target:${inventory.effectiveTarget}`, - }); - } - if (inventory.globalDefaultSpec !== undefined) { - items.push({ - label: `全局默认:${inventory.globalDefaultSpec}`, - description: nestedView - ? "当前目录视图;项目或父级配置的覆盖以实际构建为准" - : inventory.projectOverridesGlobal ? "当前项目可能没有使用此值" : "mcpp 全局配置", - }); - } else if (inventory.recognized) { - items.push({ - label: "全局默认:", - description: "mcpp 尚未设置全局默认工具链", - }); - } - for (const toolchain of inventory.installed) { - items.push({ - label: `${toolchain.effective ? "$(check) " : ""}${toolchain.spec}`, - description: toolchain.source === "system" ? "System:系统工具链,仅检测" : "已安装", - detail: toolchain.effective - ? nestedView ? "mcpp list 当前目录有效项;实际构建解析可能受父级 mcpp 工作区影响" : "当前有效项" - : undefined, - spec: toolchain.spec, - toolchain, - }); - } - for (const target of inventory.targets) { - items.push({ - label: `${target.effective ? "$(check) " : ""}target ${target.target}`, - description: `${target.status}${target.toolchainSpec === undefined ? "" : ` · 约定 ${target.toolchainSpec}`}`, - detail: target.note.length === 0 ? "target 轴只读展示;选择 target 请使用 mcpp CLI" : target.note, - }); - } - for (const toolchain of inventory.available) { - items.push({ - label: `可安装:${toolchain.spec}`, - description: "mcpp 按 family 聚合的索引版本;未承诺 host payload", - spec: toolchain.spec, - }); - } - if (includeActions) { - items.push({ - label: "$(cloud-download) 安装工具链…", - detail: "回到工具链安装流程", - customInput: true, - }); - } - return items; - } - - private async showMenu(): Promise { - const items = quickMenuItems.map((item) => ({ - label: item.label, - description: item.group === "project" - ? "当前 mcpp 工程" - : item.group === "toolchain" ? "mcpp 工具链管理" : "C++ Modules 语言服务", - command: item.command, - })); - const picked = await vscode.window.showQuickPick(items, { - title: "mcpp 快捷菜单", - placeHolder: "选择项目、工具链或 IDE 操作", - matchOnDescription: true, - }); - if (picked !== undefined) { - await vscode.commands.executeCommand(picked.command); - } - } - - public async newProject(): Promise { - if (!this.requireTrusted()) { - return; - } - - const input = await vscode.window.showInputBox({ - title: "新建 mcpp 工程(1/2)", - prompt: "输入项目名,将在所选位置创建同名项目文件夹", - placeHolder: "hello-mcpp", - validateInput: validateNewProjectName, - }); - if (input === undefined) { - return; - } - const projectName = input.trim(); - - const picked = await vscode.window.showOpenDialog({ - title: "选择项目位置(2/2)", - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: "在此创建项目", - }); - const location = picked?.[0]; - if (location === undefined) { - return; - } - - const projectRoot = join(location.fsPath, projectName); - const confirmCreate = "创建并打开"; - await runNewProjectFlow(projectName, location.fsPath, projectRoot, { - exists: existsSync, - confirm: async (message) => - (await vscode.window.showWarningMessage(message, { modal: true }, confirmCreate)) - === confirmCreate, - run: async (name, cwd) => { - const executable = this.mcppExecutable(undefined); - const args = mcppCommandArguments("new", name); - const result = await runProcess(executable, args, cwd); - this.appendShortCommand("新建工程", executable, args, result); - return result.exitCode; - }, - openFolder: async (path) => { - await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(path)); - }, - showError: async (message) => { - await vscode.window.showErrorMessage(message); - }, - }); - } - - private guarded(operation: () => Promise): () => Promise { - return async () => { - try { - await operation(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - try { - this.options.output.appendLine(`mcpp CLI 操作失败:${message}`); - } catch { - // 输出频道可能已经在窗口重载时释放。 - } - await vscode.window.showErrorMessage(`mcpp:${message}`); - } - }; - } - - private requireProject(): McppProjectDiscovery | undefined { - const project = this.options.currentProject(); - if (project === undefined) { - void vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。请在 mcpp 工程中执行此命令。"); - } - return project; - } - - private requireTrusted(): boolean { - if (this.options.isTrusted()) { - return true; - } - void vscode.window.showWarningMessage( - "当前工作区未受信任。mcpp 命令可能执行工作区设置指定的外部程序,请先信任工作区。", - ); - return false; - } - - public mcppExecutable(project: McppProjectDiscovery | undefined): string { - const uri = project === undefined - ? vscode.workspace.workspaceFolders?.[0]?.uri - : vscode.Uri.file(project.root); - const configured = vscode.workspace.getConfiguration("mcpp", uri).get("path", "").trim(); - return configured.length === 0 ? "mcpp" : configured; - } - - private isNestedWorkspaceProject(project: McppProjectDiscovery | undefined): boolean { - if (project === undefined) { - return false; - } - const folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(project.root)); - return folder !== undefined && folder.uri.fsPath !== project.root; - } - - public async readToolchainInventory( - project: McppProjectDiscovery | undefined = this.options.currentProject(), - ): Promise { - const executable = this.mcppExecutable(project); - const args = mcppCommandArguments("toolchain", "list"); - const result = await runProcess(executable, args, workingDirectory(project)); - this.appendShortCommand("查看工具链", executable, args, result); - if (result.exitCode !== 0) { - await vscode.window.showErrorMessage( - `mcpp toolchain list 失败(退出码 ${result.exitCode})。请查看 mcpp 输出频道。`, - ); - return undefined; - } - - const inventory = parseToolchainList(`${result.stdout}${result.stderr.length > 0 ? `\n${result.stderr}` : ""}`); - if (!inventory.recognized) { - await vscode.window.showErrorMessage( - "无法识别当前 mcpp toolchain list 输出;原始输出已保留在 mcpp 输出频道,请检查 mcpp 版本。", - ); - return undefined; - } - return inventory; - } - - private async executeTask( - root: string, - executable: string, - title: string, - args: string[], - ): Promise { - const task = new vscode.Task( - { type: "mcpp", command: args[0] ?? "mcpp", projectRoot: root }, - taskScope(root), - title, - "mcpp", - new vscode.ProcessExecution(executable, args, { cwd: root }), - ); - task.presentationOptions = { - reveal: vscode.TaskRevealKind.Always, - panel: vscode.TaskPanelKind.Dedicated, - focus: true, - clear: true, - showReuseMessage: false, - }; - - let execution: vscode.TaskExecution | undefined; - let earlyCompletion: TaskCompletion | undefined; - let settled = false; - let processEndSubscription: vscode.Disposable | undefined; - let taskEndSubscription: vscode.Disposable | undefined; - const disposeListeners = (): void => { - processEndSubscription?.dispose(); - taskEndSubscription?.dispose(); - }; - const finish = (completion: TaskCompletion): void => { - if (settled) { - return; - } - settled = true; - disposeListeners(); - resolveCompletion?.(completion); - }; - let resolveCompletion: ((completion: TaskCompletion) => void) | undefined; - const completion = new Promise((resolve) => { - resolveCompletion = resolve; - processEndSubscription = vscode.tasks.onDidEndTaskProcess((event) => { - if (event.execution.task !== task) { - return; - } - const classified = classifyTaskExit(event.exitCode); - if (execution === undefined) { - earlyCompletion ??= classified; - return; - } - finish(classified); - }); - taskEndSubscription = vscode.tasks.onDidEndTask((event) => { - if (event.execution.task !== task) { - return; - } - const classified = classifyTaskExit(undefined); - if (execution === undefined) { - earlyCompletion ??= classified; - return; - } - finish(classified); - }); - }); - - try { - execution = await vscode.tasks.executeTask(task); - } catch (error) { - disposeListeners(); - throw error; - } - if (earlyCompletion !== undefined) { - finish(earlyCompletion); - } - return completion; - } - - private appendTaskCompletion( - root: string, - title: string, - args: string[], - completion: TaskCompletion, - ): void { - const suffix = completion.state === "succeeded" - ? `退出码 ${completion.exitCode ?? 0}` - : completion.state === "cancelled" - ? "已取消" - : `失败,退出码 ${completion.exitCode ?? "未知"}`; - try { - this.options.output.appendLine(`\n[${new Date().toISOString()}] ${title}`); - this.options.output.appendLine(`工作目录:${root}`); - this.options.output.appendLine(`任务参数:${args.join(" ")}`); - this.options.output.appendLine(`结果:${suffix}`); - } catch { - // 窗口重载时输出频道可能早于任务事件被释放。 - } - if (completion.state === "failed") { - void vscode.window.showErrorMessage(`${title}失败(退出码 ${completion.exitCode ?? "未知"})。请查看任务终端。`); - } else if (completion.state === "cancelled") { - void vscode.window.showWarningMessage(`${title}已取消。`); - } - } - - private appendShortCommand( - title: string, - executable: string, - args: string[], - result: { exitCode: number; stdout: string; stderr: string }, - ): void { - try { - this.options.output.appendLine(`\n[${new Date().toISOString()}] ${title}`); - this.options.output.appendLine(`$ ${commandLine(executable, args)}`); - if (result.stdout.length > 0) { - this.options.output.appendLine(result.stdout.trimEnd()); - } - if (result.stderr.length > 0) { - this.options.output.appendLine(result.stderr.trimEnd()); - } - this.options.output.appendLine(`[exit ${result.exitCode}]`); - } catch { - // 窗口重载时输出频道可能早于短命令结束被释放。 - } - } -} diff --git a/src/commands.ts b/src/commands.ts deleted file mode 100644 index eb60afe..0000000 --- a/src/commands.ts +++ /dev/null @@ -1,45 +0,0 @@ -export const CLI_COMMANDS = { - showMenu: "mcpp.showMenu", - newProject: "mcpp.newProject", - build: "mcpp.build", - run: "mcpp.run", - test: "mcpp.test", - clean: "mcpp.clean", - showToolchains: "mcpp.showToolchains", - installToolchain: "mcpp.installToolchain", - selectDefaultToolchain: "mcpp.selectDefaultToolchain", - configureLanguageServer: "mcpp.configureLanguageServer", - refreshCompilationDatabase: "mcpp.refreshCompilationDatabase", - checkModuleSupport: "mcpp.checkModuleSupport", - autoConfigureModules: "mcpp.autoConfigureModules", - showModuleGraph: "mcpp.showModuleGraph", - showLanguageServerLogs: "mcpp.showLanguageServerLogs", -} as const; - -export const DEPRECATED_COMMANDS = { - configureClangd: "mcpp.configureClangd", -} as const; - -export const quickMenuStatusText = "$(tools) mcpp: 快捷菜单"; - -export interface QuickMenuItem { - label: string; - command: string; - group: "project" | "toolchain" | "ide"; -} - -export const quickMenuItems: readonly QuickMenuItem[] = [ - { label: "$(gear) 构建", command: CLI_COMMANDS.build, group: "project" }, - { label: "$(play) 运行", command: CLI_COMMANDS.run, group: "project" }, - { label: "$(beaker) 测试", command: CLI_COMMANDS.test, group: "project" }, - { label: "$(trash) 清理 target", command: CLI_COMMANDS.clean, group: "project" }, - { label: "$(list-unordered) 查看工具链", command: CLI_COMMANDS.showToolchains, group: "toolchain" }, - { label: "$(cloud-download) 安装工具链", command: CLI_COMMANDS.installToolchain, group: "toolchain" }, - { label: "$(settings-gear) 选择全局默认工具链", command: CLI_COMMANDS.selectDefaultToolchain, group: "toolchain" }, - { label: "$(symbol-interface) 选择 C++ 模块分析上下文", command: CLI_COMMANDS.configureLanguageServer, group: "ide" }, - { label: "$(database) 刷新模块构建描述", command: CLI_COMMANDS.refreshCompilationDatabase, group: "ide" }, - { label: "$(check) 重启 C++ Modules 语言服务", command: CLI_COMMANDS.checkModuleSupport, group: "ide" }, - { label: "$(type-hierarchy) 查看模块图", command: CLI_COMMANDS.showModuleGraph, group: "ide" }, - { label: "$(output) 打开 C++ Modules 日志", command: CLI_COMMANDS.showLanguageServerLogs, group: "ide" }, - { label: "$(rocket) 一键构建并刷新模块语言服务", command: CLI_COMMANDS.autoConfigureModules, group: "ide" }, -]; diff --git a/src/commands/ids.ts b/src/commands/ids.ts new file mode 100644 index 0000000..fc08ce9 --- /dev/null +++ b/src/commands/ids.ts @@ -0,0 +1,114 @@ +/** + * Every command id this extension contributes, in one place. + * + * Two rules live here: + * + * 1. **`mcpp.` only.** Our ids must never collide with a command the C++ Modules + * server advertises (`mcppls.*`): `vscode-languageclient` registers those + * itself, and a duplicate id stops the language client from starting + * (mcppls's S3-5.6-3). `test/mcppls/contract.test.ts` asserts this. + * 2. **Ids do not change once shipped.** A rename keeps the old id and points it + * at the new implementation, so a user's keybinding keeps working — see + * `DEPRECATED_COMMANDS` below. + */ + +/** mcpp CLI commands that existed in 0.4.x. */ +export const CLI_COMMANDS = { + showMenu: "mcpp.showMenu", + newProject: "mcpp.newProject", + build: "mcpp.build", + run: "mcpp.run", + test: "mcpp.test", + clean: "mcpp.clean", + showToolchains: "mcpp.showToolchains", + installToolchain: "mcpp.installToolchain", + selectDefaultToolchain: "mcpp.selectDefaultToolchain", + autoConfigureModules: "mcpp.autoConfigureModules", +} as const; + +/** The language-service entries that existed in 0.4.x (kept as aliases, see below). */ +export const LEGACY_LANGUAGE_SERVER_COMMANDS = { + configureLanguageServer: "mcpp.configureLanguageServer", + refreshCompilationDatabase: "mcpp.refreshCompilationDatabase", + checkModuleSupport: "mcpp.checkModuleSupport", + showModuleGraph: "mcpp.showModuleGraph", + showLanguageServerLogs: "mcpp.showLanguageServerLogs", +} as const; + +/** The C++ Modules view and everything it forwards. */ +export const LANGUAGE_SERVER_COMMANDS = { + refreshState: "mcpp.languageServer.refreshState", + restart: "mcpp.languageServer.restart", + restartEngine: "mcpp.languageServer.restartEngine", + resetWorkspaceCache: "mcpp.languageServer.resetWorkspaceCache", + selectContext: "mcpp.languageServer.selectContext", + showModuleGraph: "mcpp.languageServer.showModuleGraph", + showLogs: "mcpp.languageServer.showLogs", + collectReport: "mcpp.languageServer.collectReport", + exportDiagnosticBundle: "mcpp.languageServer.exportDiagnosticBundle", + revealBundle: "mcpp.languageServer.revealBundle", + openLogFolder: "mcpp.languageServer.openLogFolder", + runBuildToolInTerminal: "mcpp.languageServer.runBuildToolInTerminal", + manageConflicts: "mcpp.languageServer.manageConflicts", + toggleInWorkspace: "mcpp.languageServer.toggleInWorkspace", + installTools: "mcpp.languageServer.installTools", + reviewChanges: "mcpp.languageServer.reviewChanges", +} as const; + +/** Cache statistics and the cleanup table. */ +export const CACHE_COMMANDS = { + refreshStats: "mcpp.refreshCacheStats", + showEntry: "mcpp.showCacheEntry", + cleanStale: "mcpp.cleanStaleArtifacts", + cleanProject: "mcpp.cleanProjectArtifacts", + collect: "mcpp.gcGlobalCache", + prune: "mcpp.pruneGlobalCache", + verify: "mcpp.verifyGlobalCache", + cleanLegacy: "mcpp.cleanLegacyCache", +} as const; + +/** + * The library-ecosystem view. The list and the detail page talk to their + * webviews over messages, so these are the only ids a menu can name. + */ +export const LIBRARY_COMMANDS = { + search: "mcpp.library.search", + openDetail: "mcpp.library.openDetail", + updateIndex: "mcpp.library.updateIndex", +} as const; + +/** Settings, diagnostics and the mcpp CLI entry points added in 0.5.0. */ +export const TOOL_COMMANDS = { + openSettings: "mcpp.openSettings", + openLanguageServerSettings: "mcpp.openMcpplsSettings", + selfCheck: "mcpp.selfCheck", +} as const; + +/** + * Ids kept for compatibility. A keybinding or a script that names one of these + * still works; the menu shows the new title. + */ +export const DEPRECATED_COMMANDS = { + configureClangd: "mcpp.configureClangd", +} as const; + +export type CommandId = + | (typeof CLI_COMMANDS)[keyof typeof CLI_COMMANDS] + | (typeof LEGACY_LANGUAGE_SERVER_COMMANDS)[keyof typeof LEGACY_LANGUAGE_SERVER_COMMANDS] + | (typeof LANGUAGE_SERVER_COMMANDS)[keyof typeof LANGUAGE_SERVER_COMMANDS] + | (typeof CACHE_COMMANDS)[keyof typeof CACHE_COMMANDS] + | (typeof LIBRARY_COMMANDS)[keyof typeof LIBRARY_COMMANDS] + | (typeof TOOL_COMMANDS)[keyof typeof TOOL_COMMANDS]; + +/** Every id the manifest contributes, in the order the palette shows them. */ +export function contributedCommandIds(): string[] { + return [ + ...Object.values(TOOL_COMMANDS), + ...Object.values(CLI_COMMANDS), + ...Object.values(CACHE_COMMANDS), + ...Object.values(LIBRARY_COMMANDS), + ...Object.values(LANGUAGE_SERVER_COMMANDS), + ...Object.values(LEGACY_LANGUAGE_SERVER_COMMANDS), + ...Object.values(DEPRECATED_COMMANDS), + ]; +} diff --git a/src/commands/menu.ts b/src/commands/menu.ts new file mode 100644 index 0000000..342690e --- /dev/null +++ b/src/commands/menu.ts @@ -0,0 +1,103 @@ +import { CACHE_COMMANDS, CLI_COMMANDS, LANGUAGE_SERVER_COMMANDS, LIBRARY_COMMANDS, TOOL_COMMANDS } from "./ids"; + +/** + * The status-bar menu, as **data**: entries are keyed by command id, the titles + * are resolved through `src/i18n/t.ts` when the menu is built, and the icon is a + * codicon id without the `$()` (the same convention `TreeNode.icon` uses). + * + * The groups are not decoration. They are what the menu is built from — the + * quick pick renders one separator per group — so an entry and its section can + * never disagree, and a section that ends up empty simply produces no separator. + * + * ## Why the icons are files + * + * `vscode.QuickPickItem.iconPath` accepts a `ThemeIcon`, but VS Code 1.132 + * discards its colour: `MainThreadQuickOpen.expandIconPath` turns the icon into a + * bare codicon class (`o.iconClass = asClassName(e)`), and the list renderer only + * ever paints that class in the row's ordinary foreground. A `Uri` is different — + * it becomes the row's `background-image`, painted as-is — so the only way to + * colour a quick pick icon is to *ship a coloured image*. + * + * That is what `iconColor` is for. It is a **palette word** (`blue`, `green`, …), + * not a theme-colour id like `TreeNode.iconColor`, because the colour is baked + * into the file and cannot follow the theme the way a `ThemeIcon` does. Each word + * resolves to the default light and dark value of the matching `charts.*` token, + * so the menu and the project tree agree; `media/quick-menu/---- + * .svg` is generated from `@vscode/codicons` by + * `tools/generate-quick-menu-icons.mjs`, and `test/commands/menu.test.ts` walks + * this table and stats every file so the two cannot drift apart. + * + * The palette groups rows by **what they do**, the same way the tree does: blue + * builds or adds, green runs or verifies, purple tests or collects, yellow + * manages the toolchain, red deletes, and `neutral` is for rows whose only job is + * to configure or report. + */ +export interface QuickMenuItem { + labelKey: string; + command: string; + /** A codicon id, e.g. `"tools"`. */ + icon: string; + /** A palette word; see the note above. */ + iconColor: QuickMenuColour; + group: "project" | "library" | "toolchain" | "cache" | "languageServer" | "settings"; +} + +/** The palette words `iconColor` may use; the generator owns their values. */ +export const QUICK_MENU_COLOURS = ["blue", "green", "purple", "yellow", "red", "neutral"] as const; + +export type QuickMenuColour = (typeof QUICK_MENU_COLOURS)[number]; + +/** The groups, in the order the menu shows them. */ +export const QUICK_MENU_GROUPS: ReadonlyArray<{ id: QuickMenuItem["group"]; labelKey: string }> = [ + { id: "project", labelKey: "Current mcpp project" }, + { id: "library", labelKey: "mcpp library ecosystem" }, + { id: "toolchain", labelKey: "mcpp toolchain management" }, + { id: "cache", labelKey: "Build cache" }, + { id: "languageServer", labelKey: "C++ Modules language service" }, + { id: "settings", labelKey: "Settings and diagnostics" }, +]; + +/** + * The asset name for one row's icon in one theme. + * + * The generator writes exactly these names; the test checks that every one of + * them exists, which is what keeps this convention and the files in step. + */ +export function quickMenuIconAsset(item: QuickMenuItem, theme: "dark" | "light"): string { + return `${item.icon}--${item.iconColor}--${theme}.svg`; +} + +export const quickMenuItems: readonly QuickMenuItem[] = [ + { labelKey: "Build", command: CLI_COMMANDS.build, icon: "tools", iconColor: "blue", group: "project" }, + { labelKey: "Run", command: CLI_COMMANDS.run, icon: "play", iconColor: "green", group: "project" }, + { labelKey: "Test", command: CLI_COMMANDS.test, icon: "beaker", iconColor: "purple", group: "project" }, + { labelKey: "Clean project artifacts", command: CACHE_COMMANDS.cleanProject, icon: "trash", iconColor: "red", group: "project" }, + + { labelKey: "Search and add a dependency", command: LIBRARY_COMMANDS.search, icon: "library", iconColor: "blue", group: "library" }, + { labelKey: "Refresh the mcpp Package Index", command: LIBRARY_COMMANDS.updateIndex, icon: "cloud-download", iconColor: "blue", group: "library" }, + + { labelKey: "Show toolchains", command: CLI_COMMANDS.showToolchains, icon: "chip", iconColor: "yellow", group: "toolchain" }, + { labelKey: "Install toolchain", command: CLI_COMMANDS.installToolchain, icon: "cloud-download", iconColor: "yellow", group: "toolchain" }, + { labelKey: "Select the global default toolchain", command: CLI_COMMANDS.selectDefaultToolchain, icon: "star-full", iconColor: "yellow", group: "toolchain" }, + + { labelKey: "Refresh cache statistics", command: CACHE_COMMANDS.refreshStats, icon: "refresh", iconColor: "green", group: "cache" }, + { labelKey: "Clean stale artifacts", command: CACHE_COMMANDS.cleanStale, icon: "trash", iconColor: "red", group: "cache" }, + { labelKey: "Collect the global cache to a budget", command: CACHE_COMMANDS.collect, icon: "archive", iconColor: "purple", group: "cache" }, + { labelKey: "Verify the shared cache", command: CACHE_COMMANDS.verify, icon: "verified", iconColor: "green", group: "cache" }, + + { labelKey: "C++ Modules: restart the language server", command: LANGUAGE_SERVER_COMMANDS.restart, icon: "debug-restart", iconColor: "neutral", group: "languageServer" }, + { labelKey: "C++ Modules: select the analysis context", command: LANGUAGE_SERVER_COMMANDS.selectContext, icon: "symbol-interface", iconColor: "neutral", group: "languageServer" }, + { labelKey: "C++ Modules: show the module graph", command: LANGUAGE_SERVER_COMMANDS.showModuleGraph, icon: "type-hierarchy", iconColor: "neutral", group: "languageServer" }, + { labelKey: "C++ Modules: open the log", command: LANGUAGE_SERVER_COMMANDS.showLogs, icon: "output", iconColor: "neutral", group: "languageServer" }, + { labelKey: "C++ Modules: capture the logs (report + bundle)", command: LANGUAGE_SERVER_COMMANDS.exportDiagnosticBundle, icon: "file-zip", iconColor: "neutral", group: "languageServer" }, + { labelKey: "C++ Modules: show the diagnostic report", command: LANGUAGE_SERVER_COMMANDS.collectReport, icon: "report", iconColor: "neutral", group: "languageServer" }, + { labelKey: "C++ Modules: open the log folder", command: LANGUAGE_SERVER_COMMANDS.openLogFolder, icon: "folder-opened", iconColor: "neutral", group: "languageServer" }, + { labelKey: "C++ Modules: show the last captured bundle", command: LANGUAGE_SERVER_COMMANDS.revealBundle, icon: "folder", iconColor: "neutral", group: "languageServer" }, + { labelKey: "C++ Modules: reset this workspace's cache", command: LANGUAGE_SERVER_COMMANDS.resetWorkspaceCache, icon: "trash", iconColor: "neutral", group: "languageServer" }, + + { labelKey: "Build and refresh the language service", command: CLI_COMMANDS.autoConfigureModules, icon: "refresh", iconColor: "blue", group: "settings" }, + { labelKey: "Environment self-check", command: TOOL_COMMANDS.selfCheck, icon: "heart", iconColor: "green", group: "settings" }, + { labelKey: "mcpp settings", command: TOOL_COMMANDS.openSettings, icon: "settings-gear", iconColor: "neutral", group: "settings" }, +]; + +export const quickMenuStatusText = "$(tools) mcpp"; diff --git a/src/config/access.ts b/src/config/access.ts new file mode 100644 index 0000000..862e465 --- /dev/null +++ b/src/config/access.ts @@ -0,0 +1,133 @@ +/** + * Typed reads and writes for `mcpp.*` settings. + * + * Every read goes through the registry, so a value that does not match its + * declaration (a hand-edited `settings.json`) is replaced by the declared + * default and reported once — never silently passed on to the rest of the code. + * + * Writes always name a target. The configuration panel and the commands choose + * it deliberately; nothing here writes `mcppls.*` or any other section. + */ + +import * as vscode from "vscode"; + +import { defaultValue, expectation, validateValue } from "./validate"; +import { SECTION, SETTINGS, setting, subKey, type SettingEntry } from "./registry"; + +export type ValueSource = "default" | "user" | "workspace" | "workspaceFolder" | "language" | "invalid"; + +export interface EffectiveValue { + value: T; + source: ValueSource; +} + +const warned = new Set(); + +function configuration(resource?: vscode.Uri): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration(SECTION, resource ?? null); +} + +/** + * Where the effective value comes from, for the panel's "current value" column + * and for the environment self-check. + */ +export function effective(key: string, resource?: vscode.Uri): EffectiveValue { + const entry = setting(key); + if (entry === undefined) { + return { value: undefined as unknown as T, source: "invalid" }; + } + const inspected = configuration(resource).inspect(entry.key.slice(SECTION.length + 1)); + const candidates: Array<[ValueSource, unknown]> = [ + ["workspaceFolder", inspected?.workspaceFolderValue], + ["workspace", inspected?.workspaceValue], + ["user", inspected?.globalValue], + ]; + for (const [source, raw] of candidates) { + if (raw === undefined) { + continue; + } + const result = validateValue(entry, raw); + if (result.ok) { + return { value: result.value as T, source }; + } + warnOnce(entry, source, raw); + return { value: defaultValue(entry), source: "invalid" }; + } + return { value: defaultValue(entry), source: "default" }; +} + +/** Shorthand for callers that only want the value. */ +export function read(key: string, resource?: vscode.Uri): T { + return effective(key, resource).value; +} + +function warnOnce(entry: SettingEntry, source: ValueSource, raw: unknown): void { + const signature = `${entry.key}@${source}`; + if (warned.has(signature)) { + return; + } + warned.add(signature); + void vscode.window.showWarningMessage( + `${entry.key}: ${JSON.stringify(raw)} is not a valid value (expected ${expectation(entry)}); using the default.`, + ); +} + +/** Test seam: forget which invalid values have been reported. */ +export function resetInvalidWarnings(): void { + warned.clear(); +} + +export type WriteTarget = "user" | "workspace" | "workspaceFolder"; + +function configurationTarget(target: WriteTarget): vscode.ConfigurationTarget { + switch (target) { + case "workspace": + return vscode.ConfigurationTarget.Workspace; + case "workspaceFolder": + return vscode.ConfigurationTarget.WorkspaceFolder; + default: + return vscode.ConfigurationTarget.Global; + } +} + +/** + * Write one setting. A `resource`-scoped setting must name the folder it + * applies to, otherwise VS Code would write it to the wrong place. + */ +export async function write( + key: string, + value: unknown, + target: WriteTarget, + resource?: vscode.Uri, +): Promise { + const entry = setting(key); + if (entry === undefined) { + throw new Error(`unknown mcpp setting: ${key}`); + } + if (entry.scope === "resource" && target !== "user" && resource === undefined) { + throw new Error(`${key} is resource-scoped; a folder URI is required`); + } + await configuration(resource).update(subKey(entry.key), value, configurationTarget(target)); +} + +/** Every setting whose effective value differs from its declared default. */ +export function changedSettings(resource?: vscode.Uri): Array<{ key: string; value: unknown; source: ValueSource }> { + const changed: Array<{ key: string; value: unknown; source: ValueSource }> = []; + for (const entry of SETTINGS) { + const current = effective(entry.key, resource); + if (current.source === "default" || current.source === "language") { + continue; + } + changed.push({ key: entry.key, value: current.value, source: current.source }); + } + return changed; +} + +/** Fires for any `mcpp.*` change; the panel and the views subscribe once. */ +export function onDidChange(listener: () => void): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration(SECTION)) { + listener(); + } + }); +} diff --git a/src/config/migrate.ts b/src/config/migrate.ts new file mode 100644 index 0000000..053fd34 --- /dev/null +++ b/src/config/migrate.ts @@ -0,0 +1,66 @@ +/** + * Renamed settings: the old name keeps working, and the user is offered the + * move once. + * + * This mirrors what the C++ Modules extension does for its own renames: read + * the alias, write nothing until the user agrees, and never remove the old + * value on their behalf. + */ + +import * as vscode from "vscode"; + +import { effective, write } from "./access"; +import { renamedKeys, subKey, SECTION } from "./registry"; + +export interface RenamedSetting { + from: string; + to: string; + value: unknown; +} + +/** Renames whose old name still holds a value the new name does not. */ +export function pendingRenames(resource?: vscode.Uri): RenamedSetting[] { + const pending: RenamedSetting[] = []; + const configuration = vscode.workspace.getConfiguration(SECTION, resource ?? null); + for (const { from, to } of renamedKeys()) { + const oldValue = configuration.get(subKey(from)); + if (oldValue === undefined) { + continue; + } + const current = effective(to, resource); + if (current.source !== "default") { + continue; + } + pending.push({ from, to, value: oldValue }); + } + return pending; +} + +/** + * Perform the move for every pending rename. The old key is left in place — + * removing a user's setting is their decision, and a stale alias is harmless + * because nothing reads it. + */ +export async function applyRenames( + pending: readonly RenamedSetting[], + target: "user" | "workspace", + resource?: vscode.Uri, +): Promise { + let moved = 0; + for (const rename of pending) { + try { + await write(rename.to, rename.value, target, resource); + moved += 1; + } catch { + // A rejected write (read-only settings, no folder) is not fatal: the user + // can still set the new key themselves. + } + } + return moved; +} + +/** Notification text, kept here so the panel and the activation path agree. */ +export function renamePrompt(pending: readonly RenamedSetting[]): string { + const names = pending.map((rename) => `${rename.from} → ${rename.to}`).join(", "); + return `mcpp: ${pending.length} setting(s) were renamed (${names}). Move them to the new names?`; +} diff --git a/src/config/panel.ts b/src/config/panel.ts new file mode 100644 index 0000000..86eeb9e --- /dev/null +++ b/src/config/panel.ts @@ -0,0 +1,403 @@ +/** + * The VS Code half of the configuration panel: `mcpp: Open the settings panel`. + * + * The panel is deliberately narrow. It is a **reading aid** for the 60+ settings + * in `data/config-registry.json`: it groups them, shows each one's effective + * value and where that value comes from, and explains when a change takes + * effect. It does not replace the Settings editor — every row has a link that + * opens the native one — and it names the boundary out loud: it writes + * `mcpp.*` only, never `mcppls.*`, which belongs to the C++ Modules extension. + * + * Safety rules this file is responsible for: + * + * - a key is written only if the registry declares it (so `mcpp.*` only); + * - a value is validated against the registry before it is written, so the + * panel cannot be the thing that puts a bad value in `settings.json`; + * - a `resource`-scoped setting is never written to a workspace level without a + * folder URI (VS Code has nowhere to put it); + * - a rejected write (a read-only level, a folder that went away) is swallowed: + * the panel re-sends the effective model, and the user sees the real state. + * + * `src/config/panelHtml.ts` owns the document; this module owns the data and the + * messages. The webview answers every action with `{ type: "model", model }`. + */ + +import * as path from "node:path"; +import * as vscode from "vscode"; + +import { TOOL_COMMANDS } from "../commands/ids"; +import { languagePreference, t } from "../i18n/t"; +import { localeFromEditorLanguage } from "../i18n/translate"; +import { MCPPLS_EXTENSION_ID } from "../mcppls/contract"; +import { WebviewDocument } from "../webview/document"; +import { effective, onDidChange, write, type WriteTarget } from "./access"; +import { + PANEL_UI, + decodePanelMessage, + renderPanelHtml, + type PanelModel, + type PanelPreset, + type PanelRow, + type PanelSection, +} from "./panelHtml"; +import { PRESETS, presetValues } from "./presets"; +import { GROUPS, SECTION, SETTINGS, setting, settingsInGroup, subKey, type SettingEntry } from "./registry"; +import { validateValue } from "./validate"; + +/** `@ext:` queries need `publisher.name`, which is not in any registry. */ +const EXTENSION_ID = "mcpp-community.mcpp-vscode"; +const PANEL_VIEW_TYPE = "mcpp.settingsPanel"; +const OPEN_SETTINGS = "workbench.action.openSettings"; +const MEDIA_DIRECTORY = "media"; +const STYLESHEET = "settings.css"; + +/** + * The slice of `vscode.ExtensionContext` this module needs. `extensionUri` is + * optional so the declared shape stays callable from tests and from hosts that + * do not have one; when it is missing the media directory is derived from + * `__dirname` (`dist/src/config/` → the extension root). + */ +export interface PanelHostContext { + subscriptions: { push(...items: unknown[]): unknown }; + extensionUri?: vscode.Uri; +} + +interface PanelSession { + panel: vscode.WebviewPanel; + resource?: vscode.Uri; + host: PanelHostContext; + /** + * The document and its per-panel nonce. The panel's html is assigned exactly + * once per open — every later update travels by `postMessage` — but the kit + * is still used so every webview host in this extension holds its document + * the same way (one nonce minting point, one assignment site). + */ + webviewDocument: WebviewDocument; +} + +/** One panel per window: reopening reveals and refreshes the existing one. */ +let session: PanelSession | undefined; + +/** + * Register `mcpp.openSettings`. The command takes no arguments; the panel's + * resource (the folder whose values it shows) follows the active editor when it + * is inside a workspace folder, otherwise the first folder. + */ +export function registerSettingsPanel(host: PanelHostContext): void { + host.subscriptions.push( + vscode.commands.registerCommand(TOOL_COMMANDS.openSettings, () => { + open(host); + }), + onDidChange(() => { + if (session !== undefined) { + postModel(session); + } + }), + { + dispose: () => { + session?.panel.dispose(); + session = undefined; + }, + }, + ); +} + +function open(host: PanelHostContext): void { + if (session !== undefined) { + session.panel.reveal(); + postModel(session); + return; + } + const resource = resourceUri(); + const panel = vscode.window.createWebviewPanel(PANEL_VIEW_TYPE, t("mcpp settings"), vscode.ViewColumn.Active, { + enableScripts: true, + localResourceRoots: [mediaRoot(host)], + retainContextWhenHidden: false, + }); + const active: PanelSession = { panel, resource, host, webviewDocument: new WebviewDocument(STYLESHEET) }; + session = active; + const model = buildModel(resource); + const html = renderPanelHtml(model, { + ...active.webviewDocument.assets(mediaRoot(host), panel.webview), + // The client script is inline and nonced; the CSP names no external script + // source, so this stays empty on purpose. + scriptUri: "", + }); + active.webviewDocument.paint(panel.webview, html); + panel.webview.onDidReceiveMessage((raw: unknown) => { + void handle(active, raw); + }); + panel.onDidDispose(() => { + if (session === active) { + session = undefined; + } + }); +} + +async function handle(active: PanelSession, raw: unknown): Promise { + const message = decodePanelMessage(raw); + if (message === undefined) { + return; + } + try { + switch (message.type) { + case "update": + await applyUpdate(active, message.key, message.value, message.target); + break; + case "reset": + await resetEverywhere(message.key, active.resource); + break; + case "preset": + await applyPreset(message.id, active.resource); + break; + case "openNative": + await vscode.commands.executeCommand(OPEN_SETTINGS, `@ext:${EXTENSION_ID} ${message.key}`); + break; + case "openMcpplsSettings": + await vscode.commands.executeCommand(OPEN_SETTINGS, `@ext:${MCPPLS_EXTENSION_ID}`); + break; + } + } catch { + // A write can fail for reasons that are the user's business, not the + // panel's: a read-only settings level, a folder that disappeared. The model + // sent below shows the value that is actually in effect. + } + postModel(active); +} + +async function applyUpdate( + active: PanelSession, + key: string, + value: unknown, + target: "user" | "workspace", +): Promise { + const entry = setting(key); + if (entry === undefined) { + return; + } + const validated = validateValue(entry, value); + if (!validated.ok || !writable(entry, target, active.resource)) { + return; + } + await write(entry.key, validated.value, target, active.resource); +} + +/** A resource-scoped setting needs a folder URI for any workspace-level write. */ +function writable(entry: SettingEntry, target: WriteTarget, resource?: vscode.Uri): boolean { + return !(entry.scope === "resource" && target !== "user" && resource === undefined); +} + +function configurationTarget(target: WriteTarget): vscode.ConfigurationTarget { + switch (target) { + case "workspace": + return vscode.ConfigurationTarget.Workspace; + case "workspaceFolder": + return vscode.ConfigurationTarget.WorkspaceFolder; + default: + return vscode.ConfigurationTarget.Global; + } +} + +/** + * Remove one level's override. `configuration.update(key, undefined, target)` is + * the only way to delete a value; leaving the level alone is not enough, because + * a workspace value would then keep shadowing the default. + */ +async function resetKey(key: string, target: WriteTarget, resource?: vscode.Uri): Promise { + const entry = setting(key); + if (entry === undefined || !writable(entry, target, resource)) { + return; + } + await vscode.workspace + .getConfiguration(SECTION, resource ?? null) + .update(subKey(key), undefined, configurationTarget(target)); +} + +/** "Reset" means "back to the default", so every level that could hold it is cleared. */ +async function resetEverywhere(key: string, resource?: vscode.Uri): Promise { + const targets: WriteTarget[] = + resource === undefined ? ["user", "workspace"] : ["user", "workspace", "workspaceFolder"]; + for (const target of targets) { + try { + await resetKey(key, target, resource); + } catch { + // An unwritable level is skipped; the others still get cleared. + } + } +} + +async function applyPreset(id: string, resource?: vscode.Uri): Promise { + // `defaults` carries no values by design; it is the one preset that resets. + if (id === "defaults") { + for (const entry of SETTINGS) { + await resetEverywhere(entry.key, resource); + } + return; + } + const target: WriteTarget = resource === undefined ? "user" : "workspace"; + for (const { key, value } of presetValues(id)) { + const entry = setting(key); + if (entry === undefined || !writable(entry, target, resource)) { + continue; + } + const validated = validateValue(entry, value); + if (!validated.ok) { + continue; + } + try { + await write(entry.key, validated.value, target, resource); + } catch { + // Keep applying the rest of the preset rather than stopping halfway. + } + } +} + +/** + * The whole model. Labels go through `t()`, so the panel follows the editor's + * language exactly like the runtime messages do; registry titles and + * descriptions are the same English strings `package.nls.*` carries, and fall + * back to English until their translations land in `data/i18n`. + */ +function buildModel(resource?: vscode.Uri): PanelModel { + const sections: PanelSection[] = GROUPS.map((group) => ({ + id: group.id, + title: t(group.title), + rows: settingsInGroup(group.id).map((entry) => buildRow(entry, resource)), + })).filter((section) => section.rows.length > 0); + const presets: PanelPreset[] = PRESETS.map((preset) => ({ + id: preset.id, + title: t(preset.title), + description: t(preset.description), + })); + const model: PanelModel = { sections, presets, ui: panelLabels() }; + const label = resource === undefined ? undefined : folderLabel(resource); + if (label !== undefined) { + model.resourceLabel = label; + } + return model; +} + +function buildRow(entry: SettingEntry, resource?: vscode.Uri): PanelRow { + const current = effective(entry.key, resource); + const row: PanelRow = { + key: entry.key, + title: t(entry.title), + description: t(entry.description), + type: entry.type, + value: current.value, + scope: entry.scope, + applies: entry.applies, + // `effective()` also reports `language`, which is a level, not a source the + // panel can show a button for; it reads as a plain user-level value. + source: current.source === "language" ? "default" : current.source, + tier: entry.tier, + since: entry.since, + }; + if (entry.enum !== undefined) { + // Enum values are identifiers (`auto`, `workspaceFolder`, `warning`); the + // registry has no separate label for them and inventing copy here would + // give the panel words the Settings editor does not use. + row.options = entry.enum.map((value) => ({ value, label: value })); + } + if (entry.minimum !== undefined) { + row.minimum = entry.minimum; + } + if (entry.maximum !== undefined) { + row.maximum = entry.maximum; + } + if (entry.deprecated === true) { + row.deprecated = true; + if (entry.deprecationMessage !== undefined) { + row.deprecationMessage = t(entry.deprecationMessage); + } + } + return row; +} + +/** + * Every visible string, resolved once per model. These are the literals + * `tools/l10n-check.mjs` holds to `data/i18n/zh-cn.json`. + */ +function panelLabels(): Record { + return { + [PANEL_UI.htmlLang]: htmlLanguage(), + [PANEL_UI.title]: t("mcpp settings"), + [PANEL_UI.boundary]: t( + "This panel changes mcpp-vscode settings only; the C++ Modules extension ({0}) keeps its own mcppls.* settings and this panel never writes them.", + MCPPLS_EXTENSION_ID, + ), + [PANEL_UI.openMcpplsSettings]: t("Open the C++ Modules settings"), + [PANEL_UI.resourceLabel]: t("Values shown for {0}"), + [PANEL_UI.search]: t("Search settings"), + [PANEL_UI.onlyModified]: t("Only modified"), + [PANEL_UI.showAdvanced]: t("Show advanced settings"), + [PANEL_UI.target]: t("Save settings to"), + [PANEL_UI.targetUser]: t("User settings"), + [PANEL_UI.targetWorkspace]: t("Workspace settings"), + [PANEL_UI.targetWorkspaceUnavailable]: t("No workspace folder is open"), + [PANEL_UI.presets]: t("Presets"), + [PANEL_UI.modified]: t("Changed from the default"), + [PANEL_UI.deprecated]: t("Deprecated"), + [PANEL_UI.invalid]: t("Invalid"), + [PANEL_UI.appliesNextBuild]: t("Takes effect on the next build"), + [PANEL_UI.appliesNextClean]: t("Takes effect on the next clean"), + [PANEL_UI.appliesViewReload]: t("Reload the window to see this"), + [PANEL_UI.sourceDefault]: t("Default"), + [PANEL_UI.sourceUser]: t("User"), + [PANEL_UI.sourceWorkspace]: t("Workspace"), + [PANEL_UI.sourceWorkspaceFolder]: t("Workspace folder"), + [PANEL_UI.sourceInvalid]: t("Invalid"), + [PANEL_UI.reset]: t("Reset"), + [PANEL_UI.resetInvalid]: t("Reset the invalid value to the default"), + [PANEL_UI.openNative]: t("Open in the Settings editor"), + [PANEL_UI.arrayHint]: t("One value per line"), + [PANEL_UI.noMatches]: t("No settings match the search"), + [PANEL_UI.toggleSection]: t("Toggle this section"), + }; +} + +/** `auto` is the editor's own language; `en`/`zh-cn` are the manual override. */ +function htmlLanguage(): string { + const preference = languagePreference(); + if (preference === "zh-cn") { + return "zh-cn"; + } + if (preference === "en") { + return "en"; + } + return localeFromEditorLanguage(vscode.env.language) === "zh-cn" ? "zh-cn" : "en"; +} + +function postModel(active: PanelSession): void { + void active.panel.webview.postMessage({ type: "model", model: buildModel(active.resource) }); +} + +/** + * The folder whose values the panel shows. An active editor inside a workspace + * folder wins (that is the project the user is looking at); otherwise the first + * folder. `undefined` means "no resource", and then only user-level writes are + * offered. + */ +function resourceUri(): vscode.Uri | undefined { + const active = vscode.window.activeTextEditor?.document.uri; + if (active !== undefined) { + const folder = vscode.workspace.getWorkspaceFolder(active); + if (folder !== undefined) { + return folder.uri; + } + } + return vscode.workspace.workspaceFolders?.[0]?.uri; +} + +function folderLabel(resource: vscode.Uri): string { + return vscode.workspace.getWorkspaceFolder(resource)?.name ?? path.basename(resource.fsPath); +} + +function mediaRoot(host: PanelHostContext): vscode.Uri { + const extensionUri = host.extensionUri ?? vscode.extensions.getExtension(EXTENSION_ID)?.extensionUri; + if (extensionUri !== undefined) { + return vscode.Uri.joinPath(extensionUri, MEDIA_DIRECTORY); + } + // `dist/src/config/` -> the extension root, where `media/` ships. + return vscode.Uri.file(path.join(__dirname, "..", "..", "..", MEDIA_DIRECTORY)); +} diff --git a/src/config/panelHtml.ts b/src/config/panelHtml.ts new file mode 100644 index 0000000..7af7456 --- /dev/null +++ b/src/config/panelHtml.ts @@ -0,0 +1,612 @@ +/** + * The configuration panel's document, as a pure function. + * + * Everything the panel shows is data: `src/config/panel.ts` resolves the + * registry and the current values into a `PanelModel`, and this module turns + * that into one self-contained HTML document. No `vscode`, no file system, no + * network — which is what makes the interesting parts (escaping, the strict + * CSP, the advanced/deprecated/invalid marking) unit-testable. + * + * House rules the tests pin down: + * + * - **Strict CSP.** `default-src 'none'` plus exactly three sources: the + * stylesheet, the nonced inline script, and the webview's own image source. + * There are no external resources and no inline `style=` attributes, so a + * theme change cannot be the only thing that makes a control usable. + * - **No hard-coded copy.** Every user-visible label is looked up in + * `PanelModel.ui`, which the caller has already localized; the renderer never + * invents English. A key the caller forgot degrades to the key itself rather + * than to a blank control. + * - **No unescaped interpolation.** Every value from the registry or from + * `settings.json` goes through `escapeHtml`; the client script only ever + * assigns through `textContent`, never `innerHTML`. + * - **State is text plus shape, not colour.** Source, deprecation and invalid + * state are rendered as labelled badges and hidden attributes; the stylesheet + * adds borders around them. Colour is decoration only, so high-contrast + * themes stay readable. + * + * The webview talks back with JSON `postMessage`s (see `decodePanelMessage`) + * and the host answers with `{ type: "model", model }`, which the client script + * applies in place. + */ + +import { format } from "../i18n/translate"; + +/** One choice of an enum setting. */ +export interface PanelOption { + value: string; + label: string; +} + +/** One setting, ready to render. Values and copy are already resolved. */ +export interface PanelRow { + key: string; + title: string; + description: string; + type: "boolean" | "string" | "number" | "array"; + value: boolean | string | number | string[]; + options?: PanelOption[]; + minimum?: number; + maximum?: number; + scope: "resource" | "window"; + applies: "immediate" | "next-build" | "next-clean" | "view-reload"; + source: "default" | "user" | "workspace" | "workspaceFolder" | "invalid"; + tier: "public" | "advanced"; + deprecated?: boolean; + deprecationMessage?: string; + since: string; +} + +/** One registry group, with its rows in registry order. */ +export interface PanelSection { + id: string; + title: string; + rows: PanelRow[]; +} + +/** One preset button. */ +export interface PanelPreset { + id: string; + title: string; + description: string; +} + +export interface PanelModel { + sections: PanelSection[]; + presets: PanelPreset[]; + /** Every label the panel shows, already localized by the caller. */ + ui: Record; + /** The workspace folder the values belong to, when the panel was opened from one. */ + resourceLabel?: string; +} + +export interface PanelAssets { + cspSource: string; + nonce: string; + styleUri: string; + scriptUri: string; +} + +/** + * The `ui` keys the renderer reads, so the caller and the renderer cannot drift + * apart on a string. `panel.ts` fills all of them through `t()`. + */ +export const PANEL_UI = { + htmlLang: "panel.htmlLang", + title: "panel.title", + boundary: "panel.boundary", + openMcpplsSettings: "panel.openMcpplsSettings", + resourceLabel: "panel.resourceLabel", + search: "panel.search", + onlyModified: "panel.onlyModified", + showAdvanced: "panel.showAdvanced", + target: "panel.target", + targetUser: "panel.target.user", + targetWorkspace: "panel.target.workspace", + targetWorkspaceUnavailable: "panel.target.workspaceUnavailable", + presets: "panel.presets", + modified: "panel.modified", + deprecated: "panel.deprecated", + invalid: "panel.invalid", + appliesNextBuild: "panel.applies.nextBuild", + appliesNextClean: "panel.applies.nextClean", + appliesViewReload: "panel.applies.viewReload", + sourceDefault: "panel.source.default", + sourceUser: "panel.source.user", + sourceWorkspace: "panel.source.workspace", + sourceWorkspaceFolder: "panel.source.workspaceFolder", + sourceInvalid: "panel.source.invalid", + reset: "panel.reset", + resetInvalid: "panel.resetInvalid", + openNative: "panel.openNative", + arrayHint: "panel.arrayHint", + noMatches: "panel.noMatches", + toggleSection: "panel.toggleSection", +} as const; + +const SOURCE_UI: Record = { + default: PANEL_UI.sourceDefault, + user: PANEL_UI.sourceUser, + workspace: PANEL_UI.sourceWorkspace, + workspaceFolder: PANEL_UI.sourceWorkspaceFolder, + invalid: PANEL_UI.sourceInvalid, +}; + +const APPLIES_UI: Partial> = { + "next-build": PANEL_UI.appliesNextBuild, + "next-clean": PANEL_UI.appliesNextClean, + "view-reload": PANEL_UI.appliesViewReload, +}; + +export type PanelUiLabel = (key: string) => string; + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * The CSP is an attribute value, but its single quotes are syntax: escaping + * them would turn `'none'` into `'none'` and make the document state a + * different policy than it means. Only the characters that could end the + * attribute or start a tag are escaped; `cspSource` and the nonce come from + * VS Code and from `crypto`, not from the registry. + */ +function escapeCsp(value: string): string { + return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} + +/** ` name="value"`, or nothing when the value is absent. */ +function attribute(name: string, value: string | number | undefined): string { + return value === undefined ? "" : ` ${name}="${escapeHtml(String(value))}"`; +} + +/** ` name`, or nothing. Boolean HTML attributes only. */ +function flag(name: string, on: boolean): string { + return on ? ` ${name}` : ""; +} + +/** The text of the `applies` hint, or `""` when the setting is immediate. */ +function appliesHint(row: PanelRow, label: PanelUiLabel): string { + const key = APPLIES_UI[row.applies]; + return key === undefined ? "" : label(key); +} + +function renderControl(row: PanelRow, label: PanelUiLabel): string { + const invalid = row.source === "invalid"; + const key = escapeHtml(row.key); + const aria = escapeHtml(row.title); + if (row.type === "boolean") { + return ( + `` + ); + } + if (row.type === "number") { + return ( + `` + ); + } + if (row.type === "array") { + const lines = Array.isArray(row.value) ? row.value.join("\n") : String(row.value); + return ( + `` + + `

${escapeHtml(label(PANEL_UI.arrayHint))}

` + ); + } + if (row.options !== undefined && row.options.length > 0) { + const options = row.options + .map( + (option) => + ``, + ) + .join(""); + return ( + `` + ); + } + return ( + `` + ); +} + +function renderRow(row: PanelRow, label: PanelUiLabel): string { + const invalid = row.source === "invalid"; + const modified = row.source !== "default"; + const deprecated = row.deprecated === true; + const deprecation = row.deprecationMessage ?? ""; + const hint = appliesHint(row, label); + const advanced = row.tier === "advanced"; + const resetTitle = invalid ? label(PANEL_UI.resetInvalid) : label(PANEL_UI.reset); + return [ + `
`, + `
`, + ` ${escapeHtml(row.title)}`, + ` ${escapeHtml(label(PANEL_UI.modified))}`, + ` ${escapeHtml(label(PANEL_UI.deprecated))}`, + ` ${escapeHtml(label(PANEL_UI.invalid))}`, + ` ${escapeHtml(label(SOURCE_UI[row.source]))}`, + `
`, + `

${escapeHtml(row.description)}

`, + `

${escapeHtml(deprecation)}

`, + `

${escapeHtml(hint)}

`, + `
${renderControl(row, label)}
`, + `
`, + ` `, + ` `, + `
`, + `
`, + ].join("\n"); +} + +function renderSection(section: PanelSection, label: PanelUiLabel): string { + const rows = section.rows.map((row) => renderRow(row, label)).join("\n"); + return [ + `
`, + `

`, + ` `, + `

`, + `
`, + rows, + `
`, + `
`, + ].join("\n"); +} + +function renderPresets(presets: readonly PanelPreset[], label: PanelUiLabel): string { + if (presets.length === 0) { + return ""; + } + const buttons = presets + .map( + (preset) => + ``, + ) + .join("\n "); + return [ + ``, + ].join("\n"); +} + +function renderToolbar(label: PanelUiLabel, hasResource: boolean): string { + return [ + `
`, + ` `, + ` `, + ` `, + ` `, + `
`, + ].join("\n"); +} + +/** + * A model embedded in the page's own script. `<` is escaped so a registry + * string (or a value from `settings.json`) can never close the script element. + */ +function embedJson(value: unknown): string { + return JSON.stringify(value) + .replace(/ 0; }); + } + return element.value; + } + + function writeControl(element, row) { + var kind = element.getAttribute("data-kind"); + if (kind === "boolean") { element.checked = row.value === true; } + else if (kind === "array") { element.value = (row.value || []).join("\\n"); } + else { element.value = row.value === undefined || row.value === null ? "" : String(row.value); } + element.disabled = row.source === "invalid"; + } + + function applyRow(row) { + var element = rowElements[row.key]; + if (!element) { return; } + element.setAttribute("data-source", row.source); + element.setAttribute("data-modified", row.source === "default" ? "false" : "true"); + setText(element.querySelector('[data-badge="source"]'), ui[SOURCE_KEYS[row.source]] || row.source); + setHidden(element.querySelector('[data-badge="modified"]'), row.source === "default"); + setHidden(element.querySelector('[data-badge="deprecated"]'), row.deprecated !== true); + setHidden(element.querySelector('[data-badge="invalid"]'), row.source !== "invalid"); + var deprecation = element.querySelector(".row-deprecation"); + var message = typeof row.deprecationMessage === "string" ? row.deprecationMessage : ""; + setText(deprecation, message); + setHidden(deprecation, message.length === 0); + var applies = element.querySelector(".row-applies"); + var appliesKey = APPLIES_KEYS[row.applies]; + var hint = appliesKey ? (ui[appliesKey] || "") : ""; + setText(applies, hint); + setHidden(applies, hint.length === 0); + var input = element.querySelector("[data-control]"); + if (input) { writeControl(input, row); } + } + + function rowVisible(key, element, query, onlyChanged, advanced) { + if (!advanced && element.getAttribute("data-tier") === "advanced") { return false; } + if (onlyChanged && element.getAttribute("data-modified") !== "true") { return false; } + if (query.length === 0) { return true; } + var title = element.querySelector(".row-title"); + var description = element.querySelector(".row-description"); + var haystack = key + " " + (title ? title.textContent : "") + " " + (description ? description.textContent : ""); + return haystack.toLowerCase().indexOf(query) >= 0; + } + + function filter() { + var query = searchInput ? searchInput.value.trim().toLowerCase() : ""; + var onlyChanged = onlyModifiedInput ? onlyModifiedInput.checked : false; + var advanced = showAdvancedInput ? showAdvancedInput.checked : false; + var anySection = false; + for (var index = 0; index < sectionElements.length; index += 1) { + var section = sectionElements[index]; + var rows = section.querySelectorAll("[data-row]"); + var anyRow = false; + for (var inner = 0; inner < rows.length; inner += 1) { + var element = rows[inner]; + var shown = rowVisible(element.getAttribute("data-row"), element, query, onlyChanged, advanced); + element.hidden = !shown; + if (shown) { anyRow = true; } + } + section.hidden = !anyRow; + if (anyRow) { anySection = true; } + } + if (emptyState) { emptyState.hidden = anySection; } + } + + function applyModel(next) { + if (!next) { return; } + state = next; + ui = next.ui || {}; + var sections = next.sections || []; + for (var index = 0; index < sections.length; index += 1) { + var rows = sections[index].rows || []; + for (var inner = 0; inner < rows.length; inner += 1) { applyRow(rows[inner]); } + } + var presets = next.presets || []; + for (var presetIndex = 0; presetIndex < presets.length; presetIndex += 1) { + var button = presetElements[presets[presetIndex].id]; + if (button) { + setText(button, presets[presetIndex].title); + if (typeof presets[presetIndex].description === "string") { button.title = presets[presetIndex].description; } + } + } + filter(); + } + + if (searchInput) { searchInput.addEventListener("input", filter); } + if (onlyModifiedInput) { onlyModifiedInput.addEventListener("change", filter); } + if (showAdvancedInput) { showAdvancedInput.addEventListener("change", filter); } + + document.addEventListener("change", function (event) { + var element = event.target; + if (!element || typeof element.getAttribute !== "function") { return; } + if (!element.hasAttribute("data-control")) { return; } + var row = element.closest("[data-row]"); + if (!row) { return; } + var value = readControl(element); + if (value === undefined) { return; } + post({ type: "update", key: row.getAttribute("data-row"), value: value, target: targetSelect ? targetSelect.value : "user" }); + }); + + document.addEventListener("click", function (event) { + var target = event.target; + if (!target || typeof target.closest !== "function") { return; } + var button = target.closest("button"); + if (!button) { return; } + var preset = button.getAttribute("data-preset"); + if (preset) { post({ type: "preset", id: preset }); return; } + if (button.hasAttribute("data-section-toggle")) { + var section = button.closest("[data-section]"); + if (section) { + var collapsed = section.getAttribute("data-collapsed") === "true"; + section.setAttribute("data-collapsed", collapsed ? "false" : "true"); + button.setAttribute("aria-expanded", collapsed ? "true" : "false"); + } + return; + } + var action = button.getAttribute("data-action"); + if (action === "reset" || action === "openNative") { + var row = button.closest("[data-row]"); + if (row) { + if (action === "reset") { post({ type: "reset", key: row.getAttribute("data-row") }); } + else { post({ type: "openNative", key: row.getAttribute("data-row") }); } + } + return; + } + if (button.id === "panel-open-mcppls") { post({ type: "openMcpplsSettings" }); } + }); + + window.addEventListener("message", function (event) { + var data = event.data; + if (data && data.type === "model" && data.model) { applyModel(data.model); } + }); + + filter(); + post({ type: "ready" }); +})();`; +} + +/** + * The whole document. `assets.scriptUri` is accepted for the contract but not + * used: the client script is inline and nonced, and the CSP names no external + * script source, so nothing else can be loaded. + */ +export function renderPanelHtml(model: PanelModel, assets: PanelAssets): string { + const label: PanelUiLabel = (key) => model.ui[key] ?? key; + const hasResource = model.resourceLabel !== undefined; + const sections = model.sections.map((section) => renderSection(section, label)).join("\n"); + const lang = model.ui[PANEL_UI.htmlLang] ?? ""; + const resource = + model.resourceLabel === undefined + ? "" + : `

${escapeHtml(format(label(PANEL_UI.resourceLabel), [model.resourceLabel]))}

`; + const csp = `default-src 'none'; style-src ${assets.cspSource}; script-src 'nonce-${assets.nonce}'; img-src ${assets.cspSource}`; + return ` + + + + + + +${escapeHtml(label(PANEL_UI.title))} + + +
+

${escapeHtml(label(PANEL_UI.title))}

+

${escapeHtml(label(PANEL_UI.boundary))}

+ ${resource} +
+${renderToolbar(label, hasResource)} +${renderPresets(model.presets, label)} +
+${sections} +
+ + + + +`; +} + +/** Values posted back by the webview, decoded and validated. */ +export type PanelMessage = + | { type: "update"; key: string; value: unknown; target: "user" | "workspace" } + | { type: "reset"; key: string } + | { type: "preset"; id: string } + | { type: "openNative"; key: string } + | { type: "openMcpplsSettings" } + | { type: "ready" }; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function nonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} + +/** + * Decode one `postMessage` payload. Webview input is untrusted: anything that + * is not exactly one of the six shapes is dropped, and the returned object is + * rebuilt so foreign fields never travel further. + * + * A settings key is always required and always non-empty; `update` additionally + * needs a write target and a `value` field (the value itself may be anything — + * `validate.ts` checks it against the registry before it is written). + */ +export function decodePanelMessage(raw: unknown): PanelMessage | undefined { + if (!isRecord(raw)) { + return undefined; + } + switch (raw.type) { + case "ready": + return { type: "ready" }; + case "openMcpplsSettings": + return { type: "openMcpplsSettings" }; + case "update": { + if (!nonEmptyString(raw.key) || !("value" in raw)) { + return undefined; + } + if (raw.target !== "user" && raw.target !== "workspace") { + return undefined; + } + return { type: "update", key: raw.key, value: raw.value, target: raw.target }; + } + case "reset": + return nonEmptyString(raw.key) ? { type: "reset", key: raw.key } : undefined; + case "preset": + return nonEmptyString(raw.id) ? { type: "preset", id: raw.id } : undefined; + case "openNative": + return nonEmptyString(raw.key) ? { type: "openNative", key: raw.key } : undefined; + default: + return undefined; + } +} diff --git a/src/config/presets.ts b/src/config/presets.ts new file mode 100644 index 0000000..21d3b2e --- /dev/null +++ b/src/config/presets.ts @@ -0,0 +1,110 @@ +/** + * Named bundles of setting values for the configuration panel. + * + * Pure: a preset is data, so the panel, the tests and the docs all read the same + * table. A preset only ever names keys that exist in the registry (enforced by + * `presetProblems()` and the unit tests) — a preset that silently does nothing is + * worse than no preset. + */ + +import { setting } from "./registry"; + +export interface Preset { + id: string; + /** English; the panel localizes this through `src/i18n/t.ts`. */ + title: string; + description: string; + /** Key -> value, applied in order. */ + values: Readonly>; +} + +export const PRESETS: readonly Preset[] = [ + { + id: "defaults", + title: "Defaults", + description: "Put every mcpp setting back to its shipped default.", + values: {}, + }, + { + id: "quiet", + title: "Quiet", + description: "No status-bar counters, no automatic refresh, no editor diagnostics from this extension.", + values: { + "mcpp.cache.statusBar": false, + "mcpp.cache.autoRefreshSeconds": 0, + "mcpp.languageService.stateRefreshSeconds": 0, + "mcpp.ui.notifications.success": "silent", + "mcpp.task.focusTerminal": false, + "mcpp.toml.diagnostics.unknownKey": "off", + "mcpp.toml.diagnostics.legacyKeys": "off", + "mcpp.buildScript.diagnostics": false, + }, + }, + { + id: "everything", + title: "Everything on", + description: "All observation and editing help enabled, including the ones that run mcpp a little more often.", + values: { + "mcpp.cache.statusBar": true, + "mcpp.cache.autoRefreshSeconds": 60, + "mcpp.cache.warnAboveGiB": 10, + "mcpp.languageService.readState": true, + "mcpp.languageService.stateRefreshSeconds": 30, + "mcpp.languageService.notifyOnDegraded": true, + "mcpp.toml.hover": true, + "mcpp.toml.navigation": true, + "mcpp.toml.diagnostics.enabled": true, + "mcpp.toml.diagnostics.unknownSection": "warning", + "mcpp.toml.diagnostics.unknownKey": "warning", + "mcpp.toml.diagnostics.planeSeparation": "warning", + "mcpp.toml.diagnostics.legacyKeys": "info", + "mcpp.buildScript.intelligence": true, + "mcpp.buildScript.diagnostics": true, + "mcpp.buildScript.snippets": true, + "mcpp.buildScript.imports.knownModules": true, + "mcpp.views.cache.topN": 10, + "mcpp.ui.notifications.success": "statusBar", + "mcpp.log.level": "info", + }, + }, + { + id: "diagnose", + title: "Diagnose", + description: "Turn the logging up and read the C++ Modules state, for when something is wrong.", + values: { + "mcpp.log.level": "debug", + "mcpp.languageService.readState": true, + "mcpp.languageService.stateRefreshSeconds": 15, + "mcpp.languageService.notifyOnDegraded": true, + "mcpp.diagnostics.selfCheckOnStartup": true, + "mcpp.toml.diagnostics.unknownSection": "warning", + "mcpp.toml.diagnostics.unknownKey": "warning", + }, + }, +]; + +export function preset(id: string): Preset | undefined { + return PRESETS.find((entry) => entry.id === id); +} + +/** The key/value pairs a preset would write. `defaults` writes nothing: see `resetKeys()`. */ +export function presetValues(id: string): Array<{ key: string; value: unknown }> { + const found = preset(id); + if (found === undefined) { + return []; + } + return Object.entries(found.values).map(([key, value]) => ({ key, value })); +} + +/** Registry keys a preset mentions that do not exist (must be empty). */ +export function presetProblems(): string[] { + const problems: string[] = []; + for (const entry of PRESETS) { + for (const key of Object.keys(entry.values)) { + if (setting(key) === undefined) { + problems.push(`preset ${entry.id} names unknown setting ${key}`); + } + } + } + return problems; +} diff --git a/src/config/registry.ts b/src/config/registry.ts new file mode 100644 index 0000000..2f73423 --- /dev/null +++ b/src/config/registry.ts @@ -0,0 +1,184 @@ +/** + * The settings registry: **the single source of truth** for everything this + * extension makes configurable. + * + * `data/config-registry.json` holds the declaration; `package.json`'s + * `contributes.configuration` is hand-written but held to it by + * `tools/check-config.mjs`, and `docs/settings.md` is generated from it. The + * configuration panel (`src/config/panel.ts`) renders it, and + * `mcpp: environment self-check` dumps the entries the user has changed. + * + * Shape rules are enforced by `tools/check-config.mjs` and by the unit tests: + * unique keys, every `group` present in `groups`, ascending `order` within a + * group, and a `default` that `validate.ts` accepts. + */ + +import registryJson from "../../data/config-registry.json"; + +export type SettingType = "boolean" | "string" | "number" | "array"; +export type SettingScope = "resource" | "window"; +export type SettingTier = "public" | "advanced"; +export type SettingApplies = "immediate" | "next-build" | "next-clean" | "view-reload"; + +export interface SettingEntry { + key: string; + type: SettingType; + default: boolean | string | number | string[]; + enum?: string[]; + minimum?: number; + maximum?: number; + scope: SettingScope; + group: string; + order: number; + tier: SettingTier; + applies: SettingApplies; + since: string; + deprecated?: boolean; + aliases?: string[]; + title: string; + description: string; + deprecationMessage?: string; +} + +export interface SettingGroup { + id: string; + order: number; + title: string; +} + +interface RegistryFile { + version: number; + groups: SettingGroup[]; + settings: SettingEntry[]; +} + +const registry = registryJson as unknown as RegistryFile; + +export const SETTINGS: readonly SettingEntry[] = registry.settings; +export const GROUPS: readonly SettingGroup[] = [...registry.groups].sort((a, b) => a.order - b.order); + +const BY_KEY = new Map(SETTINGS.map((entry) => [entry.key, entry])); +const GROUP_BY_ID = new Map(GROUPS.map((group) => [group.id, group])); + +/** The section every key lives under; VS Code's `getConfiguration` needs it. */ +export const SECTION = "mcpp"; + +/** `"mcpp.cache.staleDays"` -> `"cache.staleDays"`. */ +export function subKey(key: string): string { + return key.startsWith(`${SECTION}.`) ? key.slice(SECTION.length + 1) : key; +} + +export function setting(key: string): SettingEntry | undefined { + return BY_KEY.get(key); +} + +export function requireSetting(key: string): SettingEntry { + const entry = BY_KEY.get(key); + if (entry === undefined) { + throw new Error(`unknown mcpp setting: ${key}`); + } + return entry; +} + +export function groupOf(key: string): SettingGroup | undefined { + const entry = BY_KEY.get(key); + return entry === undefined ? undefined : GROUP_BY_ID.get(entry.group); +} + +/** Entries of one group, in their declared order. */ +export function settingsInGroup(groupId: string): SettingEntry[] { + return SETTINGS.filter((entry) => entry.group === groupId) + .slice() + .sort((a, b) => a.order - b.order); +} + +export function settingsByTier(tier: SettingTier): SettingEntry[] { + return SETTINGS.filter((entry) => entry.tier === tier); +} + +/** Keys this build mentions in the manifest, in registry order. */ +export function manifestKeys(): string[] { + return SETTINGS.map((entry) => entry.key); +} + +/** The key an old name now resolves to, or `undefined` when it is unknown. */ +export function aliasedKey(oldKey: string): string | undefined { + for (const entry of SETTINGS) { + if (entry.aliases?.includes(oldKey) === true) { + return entry.key; + } + } + return undefined; +} + +/** Old name -> current name, for the one-time migration prompt. */ +export function renamedKeys(): Array<{ from: string; to: string }> { + const pairs: Array<{ from: string; to: string }> = []; + for (const entry of SETTINGS) { + for (const alias of entry.aliases ?? []) { + pairs.push({ from: alias, to: entry.key }); + } + } + return pairs; +} + +/** + * The registry's own invariants. Returns a list of problems (empty when sound). + * + * The same rules live in `tools/check-config.mjs`, but having them here means a + * malformed `data/config-registry.json` is caught by the unit tests as well — + * and lets `activate()` log a single line instead of failing feature by feature. + */ +export function registryShapeProblems(): string[] { + const problems: string[] = []; + const groupIds = new Set(); + for (const group of GROUPS) { + if (groupIds.has(group.id)) { + problems.push(`duplicate group ${group.id}`); + } + groupIds.add(group.id); + if (typeof group.title !== "string" || group.title.length === 0) { + problems.push(`group ${group.id} has no title`); + } + } + const seen = new Set(); + const lastOrder = new Map(); + for (const entry of SETTINGS) { + if (!entry.key.startsWith(`${SECTION}.`)) { + problems.push(`${entry.key} is not a ${SECTION}.* key`); + } + if (seen.has(entry.key)) { + problems.push(`duplicate key ${entry.key}`); + } + seen.add(entry.key); + if (!groupIds.has(entry.group)) { + problems.push(`${entry.key} names undeclared group ${entry.group}`); + } + const previous = lastOrder.get(entry.group); + if (previous !== undefined && !(entry.order > previous)) { + problems.push(`${entry.group} order is not ascending at ${entry.key}`); + } + lastOrder.set(entry.group, entry.order); + if (entry.title.length === 0 || entry.description.length === 0) { + problems.push(`${entry.key} has an empty title or description`); + } + if (entry.enum !== undefined && !entry.enum.includes(entry.default as string)) { + problems.push(`${entry.key} default is not in its enum`); + } + if (entry.deprecated === true && entry.deprecationMessage === undefined) { + problems.push(`${entry.key} is deprecated without a deprecationMessage`); + } + } + return problems; +} + +/** Every setting key, ordered by group then by the group's own order. */ +export function groupedKeys(): string[] { + const keys: string[] = []; + for (const group of GROUPS) { + for (const entry of settingsInGroup(group.id)) { + keys.push(entry.key); + } + } + return keys; +} diff --git a/src/config/validate.ts b/src/config/validate.ts new file mode 100644 index 0000000..91ec337 --- /dev/null +++ b/src/config/validate.ts @@ -0,0 +1,93 @@ +/** + * Value validation for the settings registry. Pure: no `vscode`. + * + * Used in two places: `access.ts` when a value arrives from the editor (a + * hand-edited `settings.json` can hold anything), and the configuration panel + * before it writes. An invalid value never reaches the rest of the extension – + * the declared default is used and the user is told once. + */ + +import type { SettingEntry } from "./registry"; + +export type ValidationFailure = + | "type" + | "enum" + | "minimum" + | "maximum" + | "items"; + +export interface ValidationResult { + ok: boolean; + /** Present when `ok`: the coerced value. */ + value?: boolean | string | number | string[]; + /** Present when `!ok`: why it was rejected. */ + reason?: ValidationFailure; +} + +function typeOf(entry: SettingEntry): string { + return entry.type === "array" ? "array of strings" : entry.type; +} + +export function validateValue(entry: SettingEntry, raw: unknown): ValidationResult { + if (raw === undefined || raw === null) { + return { ok: false, reason: "type" }; + } + switch (entry.type) { + case "boolean": { + if (typeof raw !== "boolean") { + return { ok: false, reason: "type" }; + } + return { ok: true, value: raw }; + } + case "number": { + if (typeof raw !== "number" || !Number.isFinite(raw)) { + return { ok: false, reason: "type" }; + } + if (entry.minimum !== undefined && raw < entry.minimum) { + return { ok: false, reason: "minimum" }; + } + if (entry.maximum !== undefined && raw > entry.maximum) { + return { ok: false, reason: "maximum" }; + } + return { ok: true, value: raw }; + } + case "string": { + if (typeof raw !== "string") { + return { ok: false, reason: "type" }; + } + if (entry.enum !== undefined && !entry.enum.includes(raw)) { + return { ok: false, reason: "enum" }; + } + return { ok: true, value: raw }; + } + case "array": { + if (!Array.isArray(raw)) { + return { ok: false, reason: "type" }; + } + if (raw.some((item) => typeof item !== "string")) { + return { ok: false, reason: "items" }; + } + return { ok: true, value: [...raw] as string[] }; + } + default: + return { ok: false, reason: "type" }; + } +} + +/** The declared default, as a fresh value (arrays are copied). */ +export function defaultValue(entry: SettingEntry): T { + return (Array.isArray(entry.default) ? [...entry.default] : entry.default) as T; +} + +/** Human-readable expectation, for the "value rejected" notice. */ +export function expectation(entry: SettingEntry): string { + if (entry.enum !== undefined) { + return entry.enum.join(" | "); + } + if (entry.type === "number" && (entry.minimum !== undefined || entry.maximum !== undefined)) { + const low = entry.minimum ?? Number.NEGATIVE_INFINITY; + const high = entry.maximum ?? Number.POSITIVE_INFINITY; + return `${low} … ${high}`; + } + return typeOf(entry); +} diff --git a/src/configureOnly.ts b/src/configureOnly.ts deleted file mode 100644 index cf233ae..0000000 --- a/src/configureOnly.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { runProcess, type ProcessResult, type ProcessRunner } from "./process"; - -export const configureOnlyArguments = ["build", "--configure-only"] as const; -// 配置阶段可能解析工具链和依赖,但不能无限期占住 IDE 操作队列。 -export const configureOnlyTimeoutMs = 5 * 60_000; - -export function runConfigureOnly( - projectRoot: string, - executable = "mcpp", - runner: ProcessRunner = runProcess, -): Promise { - return runner( - executable, - [...configureOnlyArguments], - projectRoot, - { timeoutMs: configureOnlyTimeoutMs }, - ); -} diff --git a/src/extension.ts b/src/extension.ts index b4d875e..db251be 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,23 +1,73 @@ import * as vscode from "vscode"; -import { McppCliController } from "./cliController"; -import { CLI_COMMANDS, DEPRECATED_COMMANDS } from "./commands"; -import { findNearestMcppProject, type McppProjectDiscovery } from "./discovery"; -import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./inProject"; +import { McppCliController, discoveryBoundaryFromSettings } from "./cli/controller"; +import { runMcpp } from "./cli/process"; +import { parseProtocolInfo } from "./cli/protocol"; +import { buildSelfCheckText } from "./cli/selfCheck"; +import { CLI_COMMANDS, LEGACY_LANGUAGE_SERVER_COMMANDS, LIBRARY_COMMANDS, TOOL_COMMANDS } from "./commands/ids"; +import { findNearestMcppProject, type McppProjectDiscovery } from "./projects/discovery"; +import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./projects/context"; import { createLanguageServerBridge, type LanguageServerBridge, type LanguageServerCommandResult, -} from "./languageServer"; -import { computeMcppTomlCompletions } from "./mcppTomlCompletion"; +} from "./mcppls/bridge"; +import { compareVersions } from "./library/xpkg"; +import { CAPABILITIES, MCPPLS_EXTENSION_ID, VERIFIED_MCPPLS_MINIMUM, VERIFIED_MCPPLS_RANGE } from "./mcppls/contract"; +import { formatResult } from "./mcppls/messages"; +import { describeState } from "./mcppls/state"; +import { + languageServerEnabled, + languageServerInstalled, + languageServerVersion, + readLanguageServerState, +} from "./mcppls/stateSource"; +import { registerBuildScriptProviders } from "./buildscript/providers"; +import { registerTomlProviders } from "./toml/providers"; import { buildModuleSetupPlan, executeModuleSetup, moduleSetupConfirmation, type ModuleSetupDecision, type ModuleSetupStepResult, -} from "./moduleSetup"; -import type { TaskCompletion } from "./tasks"; +} from "./workflows/moduleSetup"; +import type { TaskCompletion } from "./cli/tasks"; +import { changedSettings, onDidChange as onConfigurationChanged, read } from "./config/access"; +import { applyRenames, pendingRenames, renamePrompt } from "./config/migrate"; +import { registerSettingsPanel } from "./config/panel"; +import { languagePreference, setLanguagePreference, t, type LanguagePreference } from "./i18n/t"; +import { readCacheSnapshot, registerCacheView } from "./cache/cacheView"; +import { registerLanguageServerCommands } from "./views/languageServerView"; +import { createLibraryDetailOpener } from "./library/detailPanel"; +import { loadSnapshot } from "./library/indexLocator"; +import { registerLibraryView } from "./library/libraryView"; +import { registerProjectView } from "./views/projectView"; + +/** + * The commands an extension declares in its own `package.json`, read without + * activating it. `undefined` means "no static information", which the capability + * probe treats as "assume it works until a call says otherwise". + */ +function declaredCommandsOf(id: string): readonly string[] | undefined { + const extension = vscode.extensions.getExtension(id); + const commands = (extension?.packageJSON as { contributes?: { commands?: Array<{ command?: string }> } }) + ?.contributes?.commands; + if (!Array.isArray(commands)) { + return undefined; + } + return commands + .map((entry) => entry.command) + .filter((command): command is string => typeof command === "string"); +} + +/** When the language service was last asked to reload, for the self-check. */ +let lastRefresh: { at: string; state: string; command?: string } | undefined; + +/** `mcpp.ui.language` decides which of our strings the user sees. */ +function applyLanguagePreference(): void { + setLanguagePreference(read("mcpp.ui.language")); +} + function findCurrentProject(): McppProjectDiscovery | undefined { const activeEditor = vscode.window.activeTextEditor; @@ -30,11 +80,19 @@ function findCurrentProject(): McppProjectDiscovery | undefined { if (workspaceFolder === undefined) { return undefined; } - return findNearestMcppProject(activeUri.fsPath, workspaceFolder.uri.fsPath); + return findNearestMcppProject( + activeUri.fsPath, + workspaceFolder.uri.fsPath, + discoveryBoundaryFromSettings(workspaceFolder.uri), + ); } for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { - const project = findNearestMcppProject(workspaceFolder.uri.fsPath); + const project = findNearestMcppProject( + workspaceFolder.uri.fsPath, + workspaceFolder.uri.fsPath, + discoveryBoundaryFromSettings(workspaceFolder.uri), + ); if (project !== undefined) { return project; } @@ -51,35 +109,41 @@ function outputText(output: vscode.OutputChannel, line: string): void { } function resultText(output: vscode.OutputChannel, result: LanguageServerCommandResult): void { - outputText(output, `[C++ Modules] ${result.message}`); + const formatted = formatResult(result); + outputText(output, `[C++ Modules] ${formatted.message}${formatted.hint === undefined ? "" : ` ${formatted.hint}`}`); if (result.state === "unavailable") { - void vscode.window.showWarningMessage(result.message, "安装扩展").then((choice) => { - if (choice === "安装扩展") { - void vscode.commands.executeCommand("workbench.extensions.search", "@id:sunrisepeak.mcpp-language-server"); + const install = t("Install extension"); + void vscode.window.showWarningMessage(formatted.message, install).then((choice) => { + if (choice === install) { + void vscode.commands.executeCommand("workbench.extensions.search", `@id:${MCPPLS_EXTENSION_ID}`); } }); - } else if (result.state === "failed") { - void vscode.window.showErrorMessage(result.message); + return; + } + if (formatted.severity === "warning") { + void vscode.window.showWarningMessage(formatted.message); + } else if (formatted.severity === "error") { + void vscode.window.showErrorMessage(formatted.message); } } function moduleSetupBlockedMessage(reason: Extract["reason"]): string { switch (reason) { case "untrusted": - return "当前工作区未受信任,不会执行 mcpp 或刷新 C++ 模块语言服务。请先信任工作区。"; + return t("This workspace is not trusted, so mcpp will not run and the C++ Modules language service will not refresh. Trust the workspace first."); case "busy": - return "已有 mcpp 操作正在运行,请等待完成后再试。"; + return t("An mcpp operation is already running; wait for it to finish and try again."); } } function taskCompletionText(completion: TaskCompletion): string { if (completion.state === "cancelled") { - return "构建已取消;C++ 模块语言服务未刷新。"; + return t("The build was cancelled; the C++ Modules language service was not refreshed."); } if (completion.state === "failed") { - return `mcpp 构建失败(退出码 ${completion.exitCode ?? "未知"});C++ 模块语言服务已重新读取现有构建描述。`; + return t("The mcpp build failed (exit code {0}); the C++ Modules language service re-read the existing build description.", completion.exitCode ?? t("unknown")); } - return "mcpp 构建完成,C++ 模块语言服务已刷新。"; + return t("The mcpp build finished; the C++ Modules language service has been refreshed."); } async function autoConfigureModulesWizard( @@ -102,12 +166,14 @@ async function autoConfigureModulesWizard( return; } const confirmation = moduleSetupConfirmation(); + // One definition: the label is shown *and* the reply is compared against it. + const confirmLabel = t("Confirm one-click setup"); const choice = await vscode.window.showWarningMessage( confirmation.message, { modal: true, detail: confirmation.detail }, - "确认一键配置", + confirmLabel, ); - if (choice !== "确认一键配置") { + if (choice !== confirmLabel) { return; } @@ -118,7 +184,7 @@ async function autoConfigureModulesWizard( return { stage: "build", state: "not-started", - detail: "未启动 mcpp 构建。", + detail: t("mcpp build was not started."), }; } return { @@ -133,7 +199,7 @@ async function autoConfigureModulesWizard( return { stage: "language-server", state: result.state === "completed" ? "succeeded" : "failed", - detail: result.state === "completed" ? undefined : result.message, + detail: result.state === "completed" ? undefined : formatResult(result).message, }; }, }); @@ -146,53 +212,20 @@ async function autoConfigureModulesWizard( } else if (outcome.state === "cancelled") { await vscode.window.showWarningMessage(taskCompletionText({ state: "cancelled" })); } else { - await vscode.window.showErrorMessage(outcome.steps.at(-1)?.detail ?? "C++ 模块语言服务配置失败。"); + await vscode.window.showErrorMessage(outcome.steps.at(-1)?.detail ?? t("Configuring the C++ Modules language service failed.")); } } -// mcpp.toml structural completion is deliberately local text analysis. It never executes mcpp or -// the language server, so it remains available in restricted workspaces. -const mcppTomlCompletionKinds = { - section: vscode.CompletionItemKind.Folder, - template: vscode.CompletionItemKind.Snippet, -} as const; - -const mcppTomlCompletionProvider: vscode.CompletionItemProvider = { - provideCompletionItems(document, position) { - if (!vscode.workspace.getConfiguration("mcpp", document.uri).get("tomlCompletion", true)) { - return undefined; - } - const lines: string[] = []; - for (let line = 0; line <= position.line; line += 1) { - lines.push(document.lineAt(line).text); - } - return computeMcppTomlCompletions(lines, position.line, position.character).map((suggestion) => { - const item = new vscode.CompletionItem( - suggestion.label, - mcppTomlCompletionKinds[suggestion.kind], - ); - item.detail = suggestion.detail; - if (suggestion.documentation !== undefined) { - item.documentation = new vscode.MarkdownString(suggestion.documentation); - } - if (suggestion.insertSnippet !== undefined) { - item.insertText = new vscode.SnippetString(suggestion.insertSnippet); - } - item.range = new vscode.Range( - position.line, - suggestion.range.startCharacter, - position.line, - suggestion.range.endCharacter, - ); - return item; - }); - }, -}; - export async function activate(extensionContext: vscode.ExtensionContext): Promise { const output = vscode.window.createOutputChannel("mcpp"); + + // Language first: everything below may want to speak to the user. + applyLanguagePreference(); + extensionContext.subscriptions.push(onConfigurationChanged(applyLanguagePreference)); + const bridge = createLanguageServerBridge({ extensionInstalled: (id) => vscode.extensions.getExtension(id) !== undefined, + declaredCommands: declaredCommandsOf, activateExtension: async (id) => { await vscode.extensions.getExtension(id)?.activate(); }, @@ -203,8 +236,8 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await operation(); } catch (error) { const message = error instanceof Error ? error.message : String(error); - outputText(output, `发生未预期错误:${message}`); - void vscode.window.showErrorMessage(`mcpp:${message}`); + outputText(output, t("Unexpected error: {0}", message)); + void vscode.window.showErrorMessage(t("mcpp: {0}", message)); } }; const invokeLanguageServer = ( @@ -233,7 +266,14 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (completion.state === "cancelled") { return; } - const result = await bridge.refreshLanguageServerAfterBuild(); + const mode = read("mcpp.languageService.refreshAfterBuild"); + if (mode === "off") { + lastRefresh = { at: new Date().toISOString(), state: "skipped (mcpp.languageService.refreshAfterBuild = off)" }; + return; + } + const result = + mode === "restart" ? await bridge.restartLanguageServer() : await bridge.refreshLanguageServerAfterBuild(); + lastRefresh = { at: new Date().toISOString(), state: result.state, command: result.command }; resultText(output, result); if (completion.state === "succeeded" && result.state === "completed") { await vscode.window.showInformationMessage(taskCompletionText(completion)); @@ -244,9 +284,145 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi const cliController = new McppCliController({ output, + extensionUri: extensionContext.extensionUri, currentProject: findCurrentProject, afterProjectTask, isTrusted: () => vscode.workspace.isTrusted, + languageServerSummary: () => { + const view = readLanguageServerState(); + return view.available ? view.state : undefined; + }, + }); + + // ── views ──────────────────────────────────────────────────────────────── + // Each view owns its own tree provider and its own commands; the project view + // reads the manifest, the cache view runs mcpp's read-only queries, and the + // C++ Modules view only forwards to mcppls. + const applyViewVisibility = (): void => { + // Literal keys, so the wiring gate can see them. + // `mcpp.views.enabled` is the master switch: with it off every view is hidden. + // Whether VS Code also drops the container's icon from the activity bar is + // up to VS Code itself — an extension cannot mark its own container + // hidden-when-empty (the setting's description says the same). The `when` + // clauses in package.json are negated (`!mcpp.sidebarHidden`) so the default + // state is visible — otherwise activation would never run to set the key back. + void vscode.commands.executeCommand("setContext", "mcpp.sidebarHidden", !read("mcpp.views.enabled")); + void vscode.commands.executeCommand("setContext", "mcpp.views.project", read("mcpp.views.project.show")); + void vscode.commands.executeCommand("setContext", "mcpp.views.library", read("mcpp.views.library.show")); + void vscode.commands.executeCommand("setContext", "mcpp.views.cache", read("mcpp.views.cache.show")); + }; + applyViewVisibility(); + extensionContext.subscriptions.push(onConfigurationChanged(applyViewVisibility)); + + // The language service is a block inside the project view, so its state source has + // to exist before the view is registered. + const languageService = registerLanguageServerCommands(extensionContext, { bridge, output }); + registerProjectView(extensionContext, { currentProject: findCurrentProject, languageService }); + // The library ecosystem: a sidebar view plus an editor-area package page. The + // detail page writes `mcpp.toml` through `mcpp add`, so it tells the view to + // refresh — no save event fires for a file written outside the editor. + const openLibraryDetail = createLibraryDetailOpener(extensionContext, { + mcppExecutable: () => cliController.mcppExecutable(findCurrentProject()), + projectRoot: () => findCurrentProject()?.root, + output, + isTrusted: () => vscode.workspace.isTrusted, + onDependenciesChanged: () => void library.refresh(), + }); + const library = registerLibraryView(extensionContext, { + projectRoot: () => findCurrentProject()?.root, + mcppExecutable: () => cliController.mcppExecutable(findCurrentProject()), + openDetail: openLibraryDetail, + output, + // The cross-registry search runs `mcpp search`, and `mcpp.path` is + // resource-scoped: an untrusted workspace must not name the program. + isTrusted: () => vscode.workspace.isTrusted, + }); + // `registerLibraryView` registers its own webview provider; these are only the + // entry points a menu or the project view can name. + extensionContext.subscriptions.push( + vscode.commands.registerCommand(LIBRARY_COMMANDS.search, async () => { + await vscode.commands.executeCommand("mcpp.library.focus"); + }), + vscode.commands.registerCommand(LIBRARY_COMMANDS.openDetail, async (id: unknown) => { + if (typeof id === "string" && id.length > 0) { + await openLibraryDetail(id); + } + }), + vscode.commands.registerCommand(LIBRARY_COMMANDS.updateIndex, async () => { + if (!vscode.workspace.isTrusted) { + void vscode.window.showWarningMessage( + t("This workspace is not trusted. mcpp commands may run external programs named by workspace settings; trust the workspace first."), + ); + return; + } + const project = findCurrentProject(); + // A refresh that reaches the network is worth a progress notification, and + // a refresh that succeeds has to *say so*: the command produced no visible + // change at all before, which reads exactly like a broken button. + const result = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: t("Refreshing the mcpp Package Index…") }, + () => + runMcpp(vscode.workspace.isTrusted, cliController.mcppExecutable(project), ["index", "update"], project?.root, { + timeoutMs: 300_000, + }), + ); + await library.refresh(); + if (result === undefined || result.exitCode !== 0) { + void vscode.window.showErrorMessage( + result === undefined + ? t("This workspace is not trusted. mcpp commands may run external programs named by workspace settings; trust the workspace first.") + : t("mcpp index update failed with exit code {0}", result.exitCode), + ); + return; + } + // `library.refresh()` has just filled the snapshot cache, so this second + // look is the count the view is showing, not another read of the index. + const snapshot = await loadSnapshot(project === undefined ? {} : { projectRoot: project.root }); + void vscode.window.showInformationMessage( + t( + "mcpp Package Index refreshed: {0} package(s) from {1} index folder(s).", + snapshot.entries.length, + snapshot.roots.length, + ), + ); + }), + ); + // There is no `mcpp.library.indexFound` context key and no `viewsWelcome` + // entry for the library view. `paneview.ts`'s base `ViewPane` answers + // `shouldShowWelcome() === false`; only the tree-shaped panes override it, so + // welcome content can never render inside a `"type": "webview"` view. The + // library view states its own empty case in the document instead, which is the + // only place VS Code will show it. + + registerCacheView(extensionContext, { + output, + currentProject: findCurrentProject, + mcppExecutable: (project) => cliController.mcppExecutable(project), + isTrusted: () => vscode.workspace.isTrusted, + }); + registerSettingsPanel(extensionContext); + + // Text analysis only: no mcpp process, so both stay available in a restricted + // workspace, which is exactly what the `limited` capability promises. + registerTomlProviders( + extensionContext, + () => read("mcpp.toml.completion"), + () => ({ + syntax: read("mcpp.toml.diagnostics.syntax"), + unknownSection: read("mcpp.toml.diagnostics.unknownSection"), + unknownKey: read("mcpp.toml.diagnostics.unknownKey"), + planeSeparation: read("mcpp.toml.diagnostics.planeSeparation"), + legacyKeys: read("mcpp.toml.diagnostics.legacyKeys"), + }), + () => read("mcpp.toml.diagnostics.enabled"), + () => read("mcpp.toml.hover"), + () => read("mcpp.toml.navigation"), + ); + registerBuildScriptProviders(extensionContext, () => { + if (!read("mcpp.buildScript.intelligence") || !read("mcpp.buildScript.diagnostics")) { + return "off"; + } + return read<"warning" | "info" | "off">("mcpp.buildScript.diagnostics.severity"); }); extensionContext.subscriptions.push( @@ -254,40 +430,35 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi manifestWatcher, inProjectContext, ...cliController.register(), - vscode.languages.registerCompletionItemProvider( - { language: "mcpp-toml" }, - mcppTomlCompletionProvider, - "[", - ), - vscode.commands.registerCommand(CLI_COMMANDS.configureLanguageServer, invokeLanguageServer( - () => bridge.selectContext(), - )), - vscode.commands.registerCommand(DEPRECATED_COMMANDS.configureClangd, invokeLanguageServer( - () => bridge.selectContext(), - )), - vscode.commands.registerCommand(CLI_COMMANDS.refreshCompilationDatabase, runGuarded(async () => { - await cliController.runProjectTask("build"); - })), - vscode.commands.registerCommand(CLI_COMMANDS.checkModuleSupport, invokeLanguageServer( - () => bridge.restartLanguageServer(), - )), - vscode.commands.registerCommand(CLI_COMMANDS.showModuleGraph, invokeLanguageServer( - () => bridge.showModuleGraph(), - )), - vscode.commands.registerCommand(CLI_COMMANDS.showLanguageServerLogs, invokeLanguageServer( - () => bridge.showLanguageServerLogs(), - )), + + // The ids from 0.4.x keep working. vscode.commands.registerCommand(CLI_COMMANDS.autoConfigureModules, runGuarded(async () => { const project = findCurrentProject(); if (project === undefined) { - await vscode.window.showWarningMessage("当前工作区没有找到 mcpp.toml。"); + await vscode.window.showWarningMessage(t("This workspace has no mcpp.toml.")); return; } await autoConfigureModulesWizard(bridge, cliController, output); })), + vscode.commands.registerCommand(LEGACY_LANGUAGE_SERVER_COMMANDS.refreshCompilationDatabase, runGuarded(async () => { + await cliController.runProjectTask("build"); + })), + + // Environment self-check: one copyable snapshot of everything a bug report needs. + vscode.commands.registerCommand(TOOL_COMMANDS.selfCheck, runGuarded(async () => { + await showSelfCheck(output, cliController, bridge, extensionContext.extension.packageJSON.version); + })), + vscode.workspace.onDidGrantWorkspaceTrust(() => cliController.refreshStatus()), ); + void offerSettingRenames(extensionContext); + void noteUnverifiedLanguageService(output); + + if (read("mcpp.diagnostics.selfCheckOnStartup")) { + void showSelfCheck(output, cliController, bridge, extensionContext.extension.packageJSON.version); + } + extensionContext.subscriptions.push( manifestWatcher.onDidCreate(() => cliController.refreshStatus()), manifestWatcher.onDidChange(() => cliController.refreshStatus()), @@ -295,6 +466,114 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi ); } +/** + * The 0.4.x keys that were renamed still hold values nothing reads. Offer to move + * them, once per workspace — and leave the old key in place either way, because + * deleting a user's setting is their decision. + */ +async function offerSettingRenames(context: vscode.ExtensionContext): Promise { + const pending = pendingRenames(vscode.window.activeTextEditor?.document.uri); + if (pending.length === 0 || context.workspaceState.get("mcpp.renamesOffered") === true) { + return; + } + + const toUser = t("Move them to my user settings"); + const toWorkspace = t("Move them to this workspace"); + const choice = await vscode.window.showInformationMessage(renamePrompt(pending), toUser, toWorkspace); + if (choice !== toUser && choice !== toWorkspace) { + // Dismissed is not answered: the marker is only written after a choice, so + // the offer comes back next session instead of being lost forever — a + // marker written before the prompt stranded exactly those users (external + // review P2-9). + return; + } + await context.workspaceState.update("mcpp.renamesOffered", true); + const moved = await applyRenames(pending, choice === toUser ? "user" : "workspace"); + void vscode.window.showInformationMessage(t("mcpp: moved {0} setting(s).", moved)); +} + +/** + * A version notice, never a gate. `extensionDependencies` cannot express a version + * range, so a user can legitimately end up with a build of the language service + * older than the one this extension was tested against; the capability probe means + * that still works, but it is worth one sentence in the log. + */ +async function noteUnverifiedLanguageService(output: vscode.OutputChannel): Promise { + const version = languageServerVersion(); + if (version === undefined) { + return; + } + // The same comparator the version matrix uses, against the contract's own + // minimum — the old hand-rolled comparison had dead `< 0` branches and a + // second, drifting copy of the floor (external review P1-3). + const below = compareVersions(version, VERIFIED_MCPPLS_MINIMUM) < 0; + if (below) { + output.appendLine( + t("{0} {1} is older than the verified range ({2}); the capability probe will hide what it cannot do.", MCPPLS_EXTENSION_ID, version, VERIFIED_MCPPLS_RANGE), + ); + } +} + +/** + * The self-check gathers, never guesses: anything it cannot read is reported as + * unknown rather than left blank. It is the first thing to ask for in a bug + * report, and it writes to the `mcpp` channel so it can be copied in one go. + */ +async function showSelfCheck( + output: vscode.OutputChannel, + cliController: McppCliController, + bridge: LanguageServerBridge, + extensionVersion: unknown, +): Promise { + const project = findCurrentProject(); + const executable = cliController.mcppExecutable(project); + const timeoutSeconds = read("mcpp.runtime.timeoutSeconds"); + // The untrusted-workspace promise in the manifest is "no mcpp command runs", + // and `mcpp.path` is resource-scoped, so the probe refuses rather than + // executing a workspace-named program; the report says so in plain words. + const probeResult = await runMcpp(vscode.workspace.isTrusted, executable, ["--protocol-version"], project?.root, { + timeoutMs: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined, + }); + const info = probeResult !== undefined && probeResult.exitCode === 0 ? parseProtocolInfo(probeResult.stdout) : undefined; + const state = readLanguageServerState(); + const capabilities = CAPABILITIES.map((entry) => ({ + key: entry.key, + state: bridge.isGone(entry.key) ? "missing" : bridge.isUnconfirmed(entry.key) ? "unconfirmed" : "available", + })); + const text = buildSelfCheckText({ + extensionVersion: typeof extensionVersion === "string" ? extensionVersion : "unknown", + vscodeVersion: vscode.version, + platform: `${process.platform}-${process.arch}`, + languagePreference: languagePreference(), + trusted: vscode.workspace.isTrusted, + workspaceRoots: (vscode.workspace.workspaceFolders ?? []).map((folder) => folder.uri.fsPath), + projectRoot: project?.root, + mcppPath: executable, + mcppProbe: + info === undefined + ? undefined + : { version: info.mcppVersion, envelopeMax: info.envelopeMax, kinds: Object.keys(info.kinds) }, + mcppls: { + installed: languageServerInstalled(), + version: languageServerVersion(), + enabled: languageServerEnabled(), + state: describeState(state), + capabilities, + }, + changedSettings: changedSettings(project === undefined ? undefined : vscode.Uri.file(project.root)), + lastRefresh: lastRefresh, + cache: await readCacheSnapshot(), + }); + output.appendLine(""); + output.appendLine("===== mcpp: environment self-check ====="); + output.appendLine(text); + if (probeResult === undefined) { + output.appendLine(t("The workspace is not trusted; mcpp itself was not probed.")); + } + output.appendLine("========================================"); + output.show(true); +} + export function deactivate(): void { // VS Code disposes everything registered in activate(). } diff --git a/src/i18n/t.ts b/src/i18n/t.ts new file mode 100644 index 0000000..f36c3af --- /dev/null +++ b/src/i18n/t.ts @@ -0,0 +1,81 @@ +/** + * Runtime strings, in one place, following the editor's language by default. + * + * The **English text is the key** (the same convention `vscode.l10n` uses), so a + * missing translation degrades to readable English rather than to an identifier. + * `data/i18n/zh-cn.json` carries the translations; `tools/generate-l10n.mjs` + * turns it into `l10n/bundle.l10n.zh-cn.json`, which is what makes `auto` work + * inside VS Code. + * + * `mcpp.ui.language` overrides the choice for **our** strings — runtime messages + * and our webviews. It cannot change the command palette or the Settings UI: + * VS Code resolves `package.nls.*` once at startup from its own locale. That + * limitation is documented in `docs/settings.md` and stated in the panel. + */ + +import type * as vscode from "vscode"; + +import zhCn from "../../data/i18n/zh-cn.json"; +import { format, translate, type Bundle, type LanguagePreference } from "./translate"; + +export type { LanguagePreference } from "./translate"; + +const BUNDLES: Readonly> = { + en: {}, + "zh-cn": zhCn as Bundle, +}; + +let preference: LanguagePreference = "auto"; + +/** The effective preference, for the environment self-check and the panel. */ +export function languagePreference(): LanguagePreference { + return preference; +} + +export function setLanguagePreference(value: LanguagePreference | undefined): void { + preference = value === "en" || value === "zh-cn" ? value : "auto"; +} + +export type Substitution = string | number | boolean; + +/** + * The editor's own `l10n`, loaded on demand. + * + * `t()` needs it only on the `auto` path, so requiring it lazily keeps this + * module importable from **pure** modules: the TOML completion tables are unit + * tested without an editor, and importing them must not fail on a missing + * `vscode`. Outside an editor host there is no locale to consult, and the + * English text — which is the key — is the correct answer: it is exactly what a + * missing translation degrades to. + */ +function editorL10n(): typeof vscode.l10n | undefined { + try { + return (require("vscode") as typeof vscode).l10n; + } catch { + return undefined; + } +} + +/** + * Resolve one runtime string. + * + * - `auto` asks VS Code, which consults `l10n/bundle.l10n..json` and + * falls back to the English text we passed in; + * - a manual preference reads our own bundle, so the escape hatch works even + * when the editor is in a third language. + */ +export function t(english: string, ...args: readonly Substitution[]): string { + if (preference === "auto") { + const l10n = editorL10n(); + return l10n === undefined ? format(english, args) : l10n.t(english, ...args); + } + return translate(preference, english, args, BUNDLES[preference]); +} + +/** Substitute into an already-translated template (webviews, generated HTML). */ +export { format }; + +/** The bundle this build ships, for `tools/l10n-check.mjs`. */ +export function translationBundle(): Bundle { + return BUNDLES["zh-cn"]; +} diff --git a/src/i18n/translate.ts b/src/i18n/translate.ts new file mode 100644 index 0000000..122b706 --- /dev/null +++ b/src/i18n/translate.ts @@ -0,0 +1,55 @@ +/** + * The language-independent half of `src/i18n/t.ts`. Pure: no `vscode`, so the + * resolution rules are unit-testable. + * + * English text is the key; a translation bundle maps it to the target language. + * A missing entry returns the English text, which is why a forgotten translation + * reads as English rather than as an identifier. + */ + +export type LanguagePreference = "auto" | "en" | "zh-cn"; +export type Bundle = Readonly>; + +/** `{0}`-style substitution, the same placeholder syntax `vscode.l10n` uses. */ +export function format(template: string, args: readonly unknown[]): string { + return template.replace(/\{(\d+)\}/g, (whole, index: string) => { + const value = args[Number.parseInt(index, 10)]; + return value === undefined ? whole : String(value); + }); +} + +/** + * Resolve one string for an explicit preference. `auto` is not handled here: + * that path belongs to VS Code, which owns the editor's locale. + */ +export function translate( + preference: Exclude, + english: string, + args: readonly unknown[], + bundle: Bundle, +): string { + if (preference === "en") { + return format(english, args); + } + const translated = bundle[english]; + return format(typeof translated === "string" && translated.length > 0 ? translated : english, args); +} + +/** `"zh-cn"` for any Chinese locale VS Code may report; `undefined` otherwise. */ +export function localeFromEditorLanguage(language: string | undefined): "en" | "zh-cn" | undefined { + if (language === undefined) { + return undefined; + } + return language.toLowerCase().startsWith("zh") ? "zh-cn" : "en"; +} + +/** Missing entries for a set of used strings, for `tools/l10n-check.mjs`. */ +export function missingTranslations(used: Iterable, bundle: Bundle): string[] { + const missing: string[] = []; + for (const key of used) { + if (typeof bundle[key] !== "string" || bundle[key].length === 0) { + missing.push(key); + } + } + return missing; +} diff --git a/src/ideWorkflow.ts b/src/ideWorkflow.ts deleted file mode 100644 index 954ae55..0000000 --- a/src/ideWorkflow.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { ProcessResult } from "./process"; - -export interface IdeConfigurationRequest { - projectRoot: string; - compilationDatabasePath: string; - trusted: boolean; - force?: boolean; - databaseValid: () => boolean; - configure: () => Promise; -} - -export type IdeConfigurationOutcome = - | { state: "configured"; compileCommands: string } - | { state: "existing"; compileCommands: string } - | { state: "failed"; compileCommands?: string; exitCode: number } - | { state: "untrusted"; compileCommands?: string }; - -/** - * 决定是否需要执行 mcpp build --configure-only;不在这里读写 VS Code 设置, - * 便于 Extension Host 和纯 Node 测试共享同一套生命周期边界。 - */ -export async function ensureIdeConfigured( - request: IdeConfigurationRequest, -): Promise { - const validBefore = request.databaseValid(); - if (!request.trusted) { - return { - state: "untrusted", - ...(validBefore ? { compileCommands: request.compilationDatabasePath } : {}), - }; - } - if (validBefore && !request.force) { - return { state: "existing", compileCommands: request.compilationDatabasePath }; - } - const result = await request.configure(); - const validAfter = request.databaseValid(); - if (result.exitCode === 0 && validAfter) { - return { state: "configured", compileCommands: request.compilationDatabasePath }; - } - return { - state: "failed", - ...(validAfter ? { compileCommands: request.compilationDatabasePath } : {}), - exitCode: result.exitCode, - }; -} diff --git a/src/languageServer.ts b/src/languageServer.ts deleted file mode 100644 index 509cde8..0000000 --- a/src/languageServer.ts +++ /dev/null @@ -1,80 +0,0 @@ -export const MCPPLS_EXTENSION_ID = "sunrisepeak.mcpp-language-server"; - -export const MCPPLS_COMMANDS = { - restart: "mcppls.restartServer", - selectContext: "mcppls.selectContext", - graph: "mcppls.showModuleGraph", - logs: "mcppls.showLogs", -} as const; - -export type LanguageServerCommandState = "completed" | "unavailable" | "failed"; - -export interface LanguageServerCommandResult { - state: LanguageServerCommandState; - message: string; -} - -export interface LanguageServerCommandExecutor { - extensionInstalled(id: string): boolean; - /** Activate the dependency before forwarding a command when VS Code has not activated it yet. */ - activateExtension?(id: string): Thenable; - executeCommand(command: string, ...args: unknown[]): Thenable; -} - -export interface LanguageServerBridge { - restartLanguageServer(): Promise; - refreshLanguageServerAfterBuild(): Promise; - selectContext(): Promise; - showModuleGraph(): Promise; - showLanguageServerLogs(): Promise; -} - -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -export function createLanguageServerBridge( - executor: LanguageServerCommandExecutor, -): LanguageServerBridge { - let refreshInFlight: Promise | undefined; - - async function invoke(command: string, successMessage: string): Promise { - if (!executor.extensionInstalled(MCPPLS_EXTENSION_ID)) { - return { - state: "unavailable", - message: `C++ 模块语言服务依赖未安装或已禁用:${MCPPLS_EXTENSION_ID}。`, - }; - } - try { - await executor.activateExtension?.(MCPPLS_EXTENSION_ID); - await executor.executeCommand(command); - return { state: "completed", message: successMessage }; - } catch (error) { - return { - state: "failed", - message: `C++ Modules 命令执行失败:${errorMessage(error)}`, - }; - } - } - - async function restartLanguageServer(): Promise { - return invoke(MCPPLS_COMMANDS.restart, "C++ 模块语言服务已重启。"); - } - - function refreshLanguageServerAfterBuild(): Promise { - if (refreshInFlight !== undefined) { - return refreshInFlight; - } - refreshInFlight = invoke(MCPPLS_COMMANDS.restart, "C++ 模块语言服务已刷新。") - .finally(() => { refreshInFlight = undefined; }); - return refreshInFlight; - } - - return { - restartLanguageServer, - refreshLanguageServerAfterBuild, - selectContext: () => invoke(MCPPLS_COMMANDS.selectContext, "已打开 C++ 模块上下文选择。"), - showModuleGraph: () => invoke(MCPPLS_COMMANDS.graph, "已打开 C++ 模块图。"), - showLanguageServerLogs: () => invoke(MCPPLS_COMMANDS.logs, "已打开 C++ Modules 日志。"), - }; -} diff --git a/src/library/addDependency.ts b/src/library/addDependency.ts new file mode 100644 index 0000000..d761851 --- /dev/null +++ b/src/library/addDependency.ts @@ -0,0 +1,142 @@ +/** + * `mcpp add @ [--dev]` — the only way this extension changes a + * project's dependencies. + * + * **`mcpp.toml` is never written here.** `mcpp add` is the official "add a + * dependency to mcpp.toml" command; it owns the file's formatting, comments and + * lockfile. Editing TOML from an extension is how a manifest gets corrupted. + * + * The version is **mandatory**, and that is mcpp's rule, not ours: a bare + * `mcpp add compat.argparse` is refused with + * `error: package version required: \`mcpp add compat.argparse@\` (M2 supports + * exact-version only)`. The caller therefore always passes the version it picked + * from `mcpp xpkg parse --json` (the greatest one for this platform), and this + * module refuses to run without one rather than producing an error the user + * cannot act on. + * + * Failures are values, not exceptions: the exit code and the stderr tail travel + * back to the caller, which shows them. Nothing here throws, so a failed add can + * never leave the detail page half-rendered. + */ + +import * as vscode from "vscode"; + +import { runMcpp } from "../cli/process"; +import { read } from "../config/access"; +import { t } from "../i18n/t"; +import { clampOutput } from "../util/text"; + +/** `mcpp add` may have to fetch an index or build the cache entry, so it gets a long budget. */ +const ADD_TIMEOUT_MS = 300_000; + +export interface AddDependencyDeps { + /** The configured `mcpp` path, or `mcpp`. */ + mcppExecutable: () => string; + /** The workspace folder holding `mcpp.toml`; `undefined` is a refusal, not a crash. */ + projectRoot: () => string | undefined; + /** Where the command and its output are logged, the same channel the CLI uses. */ + output?: vscode.OutputChannel; + /** `vscode.workspace.isTrusted`; an untrusted workspace may not be written to. */ + isTrusted: () => boolean; +} + +export interface AddDependencyRequest { + /** The package id, `ns.name`. */ + id: string; + /** The exact version; `undefined` is refused. */ + version?: string; + /** `--dev`, i.e. a `[dev-dependencies]` entry. */ + dev: boolean; +} + +export interface AddDependencyResult { + ok: boolean; + /** The argv that ran (empty when nothing ran), for the log and the UI. */ + argv: string[]; + exitCode: number; + /** A sentence for the detail page, already localized. */ + message: string; +} + +function failure(message: string, argv: readonly string[] = []): AddDependencyResult { + return { ok: false, argv: [...argv], exitCode: 1, message }; +} + +/** The argv of one add, without running it. The preview in the UI uses the same shape. */ +export function addDependencyArgv(request: { id: string; version: string; dev: boolean }): string[] { + return ["add", `${request.id}@${request.version}`, ...(request.dev ? ["--dev"] : [])]; +} + +/** + * Run one `mcpp add`, with a progress notification, and report what happened. + * + * Every refusal is explicit and happens **before** anything runs: no version, no + * `mcpp.toml`, an untrusted workspace. A run that fails returns the exit code and + * the last lines of stderr, which the detail page prints verbatim — the user gets + * mcpp's own words, not ours. + */ +export async function addDependency( + deps: AddDependencyDeps, + request: AddDependencyRequest, +): Promise { + const version = request.version?.trim() ?? ""; + if (request.id.trim().length === 0 || version.length === 0) { + const message = t("mcpp add needs an exact version: mcpp add {0}@.", request.id); + void vscode.window.showWarningMessage(message); + return failure(message); + } + const root = deps.projectRoot(); + if (root === undefined) { + const message = t("This workspace has no mcpp.toml."); + void vscode.window.showWarningMessage(message); + return failure(message); + } + if (!deps.isTrusted()) { + const message = t("This workspace is not trusted. mcpp commands that write are disabled until you trust it."); + void vscode.window.showWarningMessage(message); + return failure(message); + } + + const argv = addDependencyArgv({ id: request.id, version, dev: request.dev }); + const executable = deps.mcppExecutable(); + const result = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: t("Adding {0}…", request.id) }, + () => + runMcpp(deps.isTrusted(), executable, argv, root, { + timeoutMs: ADD_TIMEOUT_MS, + maxBufferMiB: read("mcpp.runtime.maxOutputMiB"), + }), + ); + if (result === undefined) { + // The seam refused: the guard above usually answers first with a friendlier + // message, so this is the defensive spelling of the same refusal. + const message = t("This workspace is not trusted. mcpp commands that write are disabled until you trust it."); + return failure(message); + } + + try { + deps.output?.appendLine(`\n[${new Date().toISOString()}] mcpp ${argv.join(" ")}`); + deps.output?.appendLine(`$ ${[executable, ...argv].join(" ")}`); + const stdout = clampOutput(result.stdout); + const stderr = clampOutput(result.stderr); + if (stdout.text.trim().length > 0) deps.output?.appendLine(stdout.text.trimEnd()); + if (stderr.text.trim().length > 0) deps.output?.appendLine(stderr.text.trimEnd()); + deps.output?.appendLine(`[exit ${result.exitCode}]`); + } catch { + // The channel can already be gone during shutdown. + } + + if (result.exitCode === 0) { + const message = t("Added {0} to mcpp.toml.", `${request.id}@${version}`); + void vscode.window.showInformationMessage(message); + return { ok: true, argv, exitCode: 0, message }; + } + + const detail = clampOutput(result.stderr.length > 0 ? result.stderr : result.stdout).text.trim(); + const message = + detail.length === 0 + ? t("mcpp add failed with exit code {0}.", result.exitCode) + : t("mcpp add failed with exit code {0}: {1}", result.exitCode, detail); + void vscode.window.showErrorMessage(message); + return { ok: false, argv, exitCode: result.exitCode, message }; +} diff --git a/src/library/detailHtml.ts b/src/library/detailHtml.ts new file mode 100644 index 0000000..1c6d292 --- /dev/null +++ b/src/library/detailHtml.ts @@ -0,0 +1,713 @@ +/** + * The package detail page's document, as a pure function (plan §12.1–§12.5). + * + * The sidebar answers "which package"; this page answers "what is it, how do I + * consume it, and how do I add it". It opens in the **editor area** (a + * `WebviewPanel`, see `src/library/detailPanel.ts`), because a 200 px sidebar + * cannot show a version matrix and a code block. + * + * The same house rules as the other webviews apply: strict CSP, every label from + * `DetailModel.ui`, every interpolation escaped, no inline `style=`, and no state + * that is only a colour. Everything shown is either authoritative + * (`mcpp xpkg parse --json`, `mcpp.toml`) or explicitly attributed (the example + * code comes from the index's own CI-built test projects, and says so). + * + * The example block is tokenized by `indexModel.tokenizeCppLine` — a deliberately + * small highlighter: comments, strings, the preprocessor directive, a short + * keyword list and punctuation. It is not a parser and is not meant to be. + */ + +import { + BADGE_UI, + surfaceLabel, + tokenizeCppLine, + type BadgeKey, + type Surface, +} from "./indexModel"; + +/** One platform's row in the version matrix. */ +export interface DetailVersionGroup { + platform: string; + versions: string[]; + /** The platform this extension host is running on. */ + current: boolean; +} + +/** One declared dependency, as far as it is cheaply known. */ +export interface DetailDependency { + id: string; + version?: string; + /** The version the project's `mcpp.lock` resolved, when it is known. */ + resolved?: string; +} + +/** One window of real example code. */ +export interface DetailSnippet { + file: string; + startLine: number; + lines: string[]; + usageLine: number; +} + +/** The outcome of the last `mcpp add`, shown in place. */ +export interface DetailResult { + /** `pending` is the client's own "I asked the host" line, replaced by the answer. */ + state: "ok" | "error" | "pending"; + message: string; +} + +/** The workspace's own answer about one package. */ +export interface DetailInstalled { + version: string; + /** Declared under `[dev-dependencies]`. */ + dev: boolean; +} + +export interface DetailModel { + /** Everything is already localized by the caller. */ + ui: Record; + id: string; + name: string; + description?: string; + licenses: string[]; + repo?: string; + registry: string; + surface?: Surface; + surfaces: Surface[]; + badges: BadgeKey[]; + /** The version matrix, current platform first. */ + versions: DetailVersionGroup[]; + /** Every version the current platform has, greatest first. */ + currentVersions: string[]; + /** What `mcpp add` would use by default. */ + latest?: string; + /** + * The version this workspace already asks for — `mcpp.toml` first, then + * `mcpp.lock` — so the page can say "you have this one" and offer the switch + * instead of a blind add. Absent when the project does not depend on the + * package, or when the dependency names no version (a path or git entry). + */ + installed?: DetailInstalled; + standard?: string; + dependencies: DetailDependency[]; + includeDirs: string[]; + targets: string[]; + snippets: DetailSnippet[]; + /** The example project the snippets come from, when there is one. */ + exampleProject?: string; + /** + * How a reader brings the package into code (§22): the example project's + * real `import …;` / `#include …` lines when there are any, else the honest + * synthetic form for the surface. Empty when the surface is not importable. + */ + usage: string[]; + /** The index site's package page; omitted for a registry that has no site. */ + indexUrl?: string; + /** `mcpp add {0}@{1}` / with `--dev`, so the preview and the run agree. */ + commandTemplate: string; + commandDevTemplate: string; + /** Set when `mcpp xpkg parse` could not be read; the page still renders. */ + parseNotice?: string; + /** The data source, in plain words, for the footer. */ + dataSource: string; + result?: DetailResult; +} + +export interface DetailAssets { + cspSource: string; + nonce: string; + styleUri: string; +} + +/** The `ui` keys the renderer reads, so the caller and the renderer cannot drift. */ +export const DETAIL_UI = { + htmlLang: "detail.htmlLang", + title: "detail.title", + overview: "detail.overview", + license: "detail.license", + repo: "detail.repo", + openRepo: "detail.openRepo", + registry: "detail.registry", + surface: "detail.surface", + surfaceExternal: "detail.surface.external", + standard: "detail.standard", + versions: "detail.versions", + versionsAll: "detail.versions.all", + versionsCurrent: "detail.versions.current", + versionsNone: "detail.versions.none", + versionsPick: "detail.versions.pick", + dependencies: "detail.dependencies", + dependenciesNone: "detail.dependencies.none", + dependenciesHint: "detail.dependencies.hint", + resolved: "detail.resolved", + code: "detail.code", + codeNone: "detail.code.none", + codeSource: "detail.code.source", + codeProject: "detail.code.project", + add: "detail.add", + addDev: "detail.addDev", + switchTo: "detail.switchTo", + alreadyAdded: "detail.alreadyAdded", + installed: "detail.installed", + opening: "detail.opening", + addLatest: "detail.addLatest", + addNoVersion: "detail.add.noVersion", + command: "detail.command", + usage: "detail.usage", + copy: "detail.copy", + copied: "detail.copied", + copying: "detail.copying", + indexLink: "detail.indexLink", + badgeExamples: BADGE_UI.examples.key, + badgeCn: BADGE_UI.cn.key, + badgeOpenkalEcosystem: BADGE_UI["openkal-ecosystem"].key, + badgeOpenkalCompat: BADGE_UI["openkal-compat"].key, + badgeOpenkalPosix: BADGE_UI["openkal-posix"].key, + badgeOpenkalPlatform: BADGE_UI["openkal-platform"].key, + targets: "detail.targets", + includeDirs: "detail.includeDirs", +} as const; + +const BADGE_KEY_UI: Readonly> = { + examples: DETAIL_UI.badgeExamples, + cn: DETAIL_UI.badgeCn, + "openkal-ecosystem": DETAIL_UI.badgeOpenkalEcosystem, + "openkal-compat": DETAIL_UI.badgeOpenkalCompat, + "openkal-posix": DETAIL_UI.badgeOpenkalPosix, + "openkal-platform": DETAIL_UI.badgeOpenkalPlatform, +}; + +/** + * What the page says to the host. + * + * There is no `ready`: the document is already the whole model, and a page that + * announces its own load only invites the host to render it again — which is + * exactly the reload loop the *library sidebar* shipped with (see the note in + * `libraryHtml.ts`). This host happened to answer `ready` with an empty `return`, + * so the trap never fired here; it is gone all the same. + */ +export type DetailMessage = + | { type: "add"; version: string; dev: boolean } + | { type: "openUrl"; url: string } + | { type: "copy"; text: string }; + +export type UiLabel = (key: string) => string; + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function escapeCsp(value: string): string { + return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} + +function attribute(name: string, value: string | number | undefined): string { + return value === undefined ? "" : ` ${name}="${escapeHtml(String(value))}"`; +} + +function flag(name: string, on: boolean): string { + return on ? ` ${name}` : ""; +} + +/** `{0}`-style substitution into an already-localized ui string. */ +function fill(label: UiLabel, key: string, args: readonly (string | number)[]): string { + return label(key).replace(/\{(\d+)\}/g, (whole, index: string) => { + const value = args[Number(index)]; + return value === undefined ? whole : String(value); + }); +} + +function badgeLabels(badges: readonly BadgeKey[], label: UiLabel): string[] { + return badges.map((badge) => label(BADGE_KEY_UI[badge])); +} + +/** One source line, tokenized. The renderer escapes every token. */ +function renderLine(line: string, usage: boolean): string { + if (line.length === 0) { + return "\n"; + } + const tokens = tokenizeCppLine(line) + .map((token) => `${escapeHtml(token.text)}`) + .join(""); + return `${tokens}\n`; +} + +function renderSnippets(model: DetailModel, label: UiLabel): string { + if (model.snippets.length === 0) { + return `

${escapeHtml(label(DETAIL_UI.codeNone))}

`; + } + const blocks = model.snippets.map((snippet) => { + const lines = snippet.lines + .map((line, index) => renderLine(line, snippet.startLine + index === snippet.usageLine)) + .join(""); + const caption = fill(label, DETAIL_UI.codeSource, [snippet.file, snippet.startLine]); + return [ + `
${lines}
`, + `

${escapeHtml(caption)}

`, + ].join("\n"); + }); + return blocks.join("\n"); +} + +/** + * The version matrix, as the page's **selector**: each version is a button that + * aims the command at the top of the page at itself. + * + * A `${escapeHtml(label(DETAIL_UI.addDev))}`, + ...(model.repo === undefined ? [] : [link(model.repo, label(DETAIL_UI.openRepo))]), + ...(model.indexUrl === undefined ? [] : [link(model.indexUrl, label(DETAIL_UI.indexLink))]), + ` `, + // The copy button sits beside the command, not inside it: `command.textContent` + // is what a click copies, and a button inside the paragraph would be copied too. + `
`, + `

${escapeHtml(command)}

`, + ` `, + `
`, + disabled + ? `

${escapeHtml(label(DETAIL_UI.addNoVersion))}

` + : `

${escapeHtml(label(DETAIL_UI.addLatest))}

`, + ...usage, + `

${escapeHtml(model.result?.message ?? "")}

`, + `
`, + ] + .filter((line) => line.length > 0) + .join("\n"); +} + +/** + * The client. It posts two things — `add` and `openUrl` — and applies + * the host's `{type:"result"}` in place, so running `mcpp add` never rebuilds the + * document and never resets the version the reader picked. + */ +function clientScript(initialModel: string): string { + return `(function () { + "use strict"; + var api = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : undefined; + var state = ${initialModel}; + var devInput = document.getElementById("detail-dev"); + var command = document.getElementById("detail-command"); + var result = document.getElementById("detail-result"); + var addButton = document.getElementById("detail-add"); + var version = command ? (command.getAttribute("data-selected-version") || "") : ""; + var labels = (state && state.labels) || {}; + var installed = state && state.installed ? state.installed : ""; + + function post(message) { + if (api) { api.postMessage(message); } + } + + function fillTemplate(template, values) { + var out = template; + for (var index = 0; index < values.length; index += 1) { + out = out.split("{" + index + "}").join(String(values[index])); + } + return out; + } + + function updateCommand() { + if (!command) { return; } + var dev = devInput ? devInput.checked === true : false; + var template = dev + ? (command.getAttribute("data-template-dev") || "") + : (command.getAttribute("data-template") || ""); + command.setAttribute("data-selected-version", version); + command.textContent = fillTemplate(template, [state.id, version || "?"]); + } + + /** + * The button's label and enabled state follow the selection: adding a new + * package, switching the version of one that is already there, or nothing to do + * because this is the version the manifest already asks for. The inert state + * carries the data-installed attribute, which is what paints it the thinned + * green of a thing already done. + */ + function updateButton() { + if (!addButton) { return; } + if (!version) { + addButton.textContent = labels.add || ""; + addButton.disabled = true; + addButton.removeAttribute("data-installed"); + return; + } + if (installed && version === installed) { + addButton.textContent = labels.alreadyAdded || ""; + addButton.disabled = true; + addButton.setAttribute("data-installed", ""); + return; + } + addButton.textContent = installed + ? (labels.switchTo || "{0}").split("{0}").join(version) + : (labels.add || ""); + addButton.disabled = false; + addButton.removeAttribute("data-installed"); + } + + /** One version button is the selection; the rest are alternatives. */ + function selectVersion(next) { + version = next; + var buttons = document.querySelectorAll("[data-version]"); + for (var index = 0; index < buttons.length; index += 1) { + var button = buttons[index]; + if (button.getAttribute("data-version") === next) { + button.setAttribute("data-selected", ""); + } else { + button.removeAttribute("data-selected"); + } + } + updateCommand(); + updateButton(); + } + + function showResult(payload) { + if (!result) { return; } + result.hidden = false; + result.setAttribute( + "data-state", + payload.state === "ok" ? "ok" : payload.state === "pending" ? "pending" : "error", + ); + result.textContent = payload.message || ""; + } + + if (devInput) { devInput.addEventListener("change", updateCommand); } + + document.addEventListener("click", function (event) { + var target = event.target; + if (!target || typeof target.closest !== "function") { return; } + var link = target.closest("[data-open-url]"); + if (link) { + event.preventDefault(); + var url = link.getAttribute("data-open-url") || ""; + // Say something the instant the click lands: the host's answer replaces + // this line, and if it never arrives the reader can see that too. + showResult({ state: "pending", message: (labels.opening || "{0}").split("{0}").join(url) }); + post({ type: "openUrl", url: url }); + return; + } + var copy = target.closest("[data-copy-command], [data-copy]"); + if (copy) { + // The command's own text is what gets copied — the button beside it, not + // inside it, is why the command's textContent is exactly the command. + var text = copy.hasAttribute("data-copy-command") + ? (command ? String(command.textContent || "") : "") + : String(copy.getAttribute("data-copy") || ""); + if (!text) { return; } + showResult({ state: "pending", message: labels.copying || "" }); + post({ type: "copy", text: text }); + return; + } + var pick = target.closest("[data-version]"); + if (pick) { + selectVersion(pick.getAttribute("data-version") || ""); + return; + } + var add = target.closest("#detail-add"); + if (add && !add.disabled) { + if (!version) { return; } + post({ type: "add", version: version, dev: devInput ? devInput.checked === true : false }); + } + }); + + /** Move the "added" marker onto the version the project now asks for. */ + function markInstalled(next) { + installed = next; + var marks = document.querySelectorAll(".detail-installed"); + for (var index = 0; index < marks.length; index += 1) { marks[index].parentNode.removeChild(marks[index]); } + var buttons = document.querySelectorAll("[data-version]"); + for (var index2 = 0; index2 < buttons.length; index2 += 1) { + var button = buttons[index2]; + if (button.getAttribute("data-version") !== next) { + button.removeAttribute("data-installed"); + continue; + } + button.setAttribute("data-installed", ""); + var mark = document.createElement("span"); + mark.className = "detail-installed"; + mark.textContent = labels.installed || ""; + button.parentNode.insertBefore(mark, button.nextSibling); + } + updateButton(); + } + + window.addEventListener("message", function (event) { + var data = event.data; + if (data && data.type === "result" && data.result) { showResult(data.result); } + if (data && data.added && data.added.version) { markInstalled(data.added.version); } + }); + + updateCommand(); + updateButton(); +})();`; +} + +/** The little state the client needs; the page is already rendered from the rest. */ +function clientState(model: DetailModel): Record { + return { + id: model.id, + ...(model.latest === undefined ? {} : { latest: model.latest }), + // The three labels the button can wear, and the version the project already + // has: the client re-decides the label every time the selection changes, so + // the host cannot be the only one that knows what the button means. The + // `installed` word belongs here too — `markInstalled()` writes it next to + // the version button after a successful add, and an empty string there is + // a marker nobody can read (§22). + labels: { + add: model.ui[DETAIL_UI.add] ?? "", + switchTo: model.ui[DETAIL_UI.switchTo] ?? "", + alreadyAdded: model.ui[DETAIL_UI.alreadyAdded] ?? "", + opening: model.ui[DETAIL_UI.opening] ?? "", + installed: model.ui[DETAIL_UI.installed] ?? "", + copying: model.ui[DETAIL_UI.copying] ?? "", + }, + ...(model.installed === undefined ? {} : { installed: model.installed.version }), + }; +} + +/** A model embedded in the page's own script; `<` is escaped so it cannot close it. */ +function embedJson(value: unknown): string { + return JSON.stringify(value) + .replace(/ model.ui[key] ?? key; + const csp = `default-src 'none'; style-src ${assets.cspSource}; script-src 'nonce-${assets.nonce}'; img-src ${assets.cspSource}`; + const title = fill(label, DETAIL_UI.title, [model.id]); + const currentPlatform = model.versions.find((group) => group.current)?.platform; + const versionsHeading = + currentPlatform === undefined ? label(DETAIL_UI.versionsAll) : fill(label, DETAIL_UI.versions, [currentPlatform]); + return ` + + + + + + +${escapeHtml(title)} + + +
+

${escapeHtml(model.id)}

+ ${model.description === undefined ? "" : `

${escapeHtml(model.description)}

`} + ${renderMeta(model, label)} +
+
+ ${renderActions(model, label)} +
+

${escapeHtml(versionsHeading)}

+ ${renderVersions(model, label)} +
+
+

${escapeHtml(label(DETAIL_UI.dependencies))}

+ ${renderDependencies(model, label)} +
+
+

${escapeHtml(label(DETAIL_UI.code))}

+ ${renderSnippets(model, label)} + ${model.exampleProject === undefined ? "" : `

${escapeHtml(fill(label, DETAIL_UI.codeProject, [model.exampleProject]))}

`} +
+
+

${escapeHtml(label(DETAIL_UI.overview))}

+ ${renderExtras(model, label)} + ${model.parseNotice === undefined ? "" : `

${escapeHtml(model.parseNotice)}

`} +
+
+
${escapeHtml(model.dataSource)}
+ + + +`; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function nonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} + +/** + * Decode one `postMessage` payload. Webview input is untrusted: anything that is + * not one of the three shapes is dropped. `openUrl` is restricted to `https` + * here as well as in the host, so a compromised document cannot ask the host to + * open a `file:` or `command:` uri. + */ +export function decodeDetailMessage(raw: unknown): DetailMessage | undefined { + if (!isRecord(raw)) { + return undefined; + } + switch (raw.type) { + case "add": + return nonEmptyString(raw.version) && typeof raw.dev === "boolean" + ? { type: "add", version: raw.version, dev: raw.dev } + : undefined; + case "openUrl": + return typeof raw.url === "string" && raw.url.startsWith("https://") + ? { type: "openUrl", url: raw.url } + : undefined; + case "copy": + // Clipboard content, same trust level as the https-restricted url. The + // cap is 16 KiB — far above any command or usage line, low enough that a + // hostile document cannot park a novel in the clipboard — and everything + // else about the text is the user's own click. + return nonEmptyString(raw.text) && raw.text.length <= 16_384 + ? { type: "copy", text: raw.text } + : undefined; + default: + return undefined; + } +} diff --git a/src/library/detailPanel.ts b/src/library/detailPanel.ts new file mode 100644 index 0000000..b784b26 --- /dev/null +++ b/src/library/detailPanel.ts @@ -0,0 +1,531 @@ +/** + * The package detail page: a `WebviewPanel` in the **editor area** (plan §12.1). + * + * The sidebar answers "which package"; this page answers "what is it, how do I + * use it, how do I add it". It is where the authoritative reader finally runs: + * `mcpp xpkg parse --json` (see `src/library/xpkg.ts`), on + * demand, for the one package the reader opened — never once per row. + * + * Boundaries, in the same spirit as `src/cache/cachePanel.ts`: + * + * - **One panel per window.** Opening another package re-renders the existing + * panel instead of stacking a second one. + * - **A failure still renders.** A descriptor that cannot be parsed, an example + * that disappeared, or a failed `mcpp add` each produce a document with the + * reason in it. + * - **Nothing is written here.** The add button delegates to + * `src/library/addDependency.ts`, which runs `mcpp add`; `mcpp.toml` is mcpp's + * file. + */ + +import { readFileSync } from "node:fs"; +import * as path from "node:path"; +import * as vscode from "vscode"; + +import { runMcpp } from "../cli/process"; +import { read } from "../config/access"; +import { languagePreference, t } from "../i18n/t"; +import { localeFromEditorLanguage } from "../i18n/translate"; +import { WebviewDocument } from "../webview/document"; +import { addDependency } from "./addDependency"; +import { + badgesOf, + codeSnippets, + descriptorDependencies, + installedFor, + lockPackageVersions, + mergeSurfaces, + parseXpkgJson, + platformKey, + surfaceLabel, + syntheticUsageLines, + usageLinesFor, + type LibraryEntry, + type Surface, +} from "./indexModel"; +import { cachedDescriptorText, loadSnapshot, readDescriptorText, readExampleFiles } from "./indexLocator"; +import { + DETAIL_UI, + decodeDetailMessage, + renderDetailHtml, + type DetailInstalled, + type DetailModel, + type DetailVersionGroup, + type DetailResult, +} from "./detailHtml"; +import { compareVersions, versionGroups, type XpkgInfo } from "./xpkg"; + +const PANEL_VIEW_TYPE = "mcpp.libraryDetail"; +const MEDIA_DIRECTORY = "media"; +const STYLESHEET = "library.css"; + +/** `mcpp xpkg parse` is a local, read-only command; this is generous. */ +const PARSE_TIMEOUT_MS = 20_000; + +export interface DetailPanelDeps { + mcppExecutable: () => string; + projectRoot: () => string | undefined; + output: vscode.OutputChannel; + isTrusted: () => boolean; + /** + * Called after a successful `mcpp add`. `mcpp add` writes `mcpp.toml` on disk + * without the editor saving a document, so no save event fires; the sidebar's + * "已添加" state would otherwise stay stale. `extension.ts` wires this to the + * library view's `refresh()`. + */ + onDependenciesChanged?: () => void; +} + +/** Build the opener once; `extension.ts` passes `openLibraryDetail` to the view. */ +export function createLibraryDetailOpener( + context: vscode.ExtensionContext, + deps: DetailPanelDeps, +): (id: string) => Promise { + const session: DetailSession = { + context, + deps, + panel: undefined, + id: undefined, + busy: false, + webviewDocument: new WebviewDocument(STYLESHEET), + }; + context.subscriptions.push({ + dispose: () => { + session.panel?.dispose(); + session.panel = undefined; + }, + }); + return (id: string) => open(session, id); +} + +/** One panel per window, remembered so a second `open` reveals rather than stacks. */ +export interface DetailSession { + context: vscode.ExtensionContext; + deps: DetailPanelDeps; + panel: vscode.WebviewPanel | undefined; + id: string | undefined; + /** `mcpp add` in flight: a second click must not start a second command. */ + busy: boolean; + /** + * The document and its nonce, kept per session so re-opening the *same* + * package renders byte for byte the same document and does not reload the + * page (which would throw away the reader's scroll position). + */ + webviewDocument: WebviewDocument; +} + +/** The parse result per descriptor, so re-opening a package costs no process. */ +const parsed = new Map(); + +export async function open(session: DetailSession, id: string): Promise { + if (session.panel === undefined) { + const panel = vscode.window.createWebviewPanel( + PANEL_VIEW_TYPE, + t("Library"), + vscode.ViewColumn.Active, + { + enableScripts: true, + localResourceRoots: [vscode.Uri.joinPath(session.context.extensionUri, MEDIA_DIRECTORY)], + retainContextWhenHidden: false, + }, + ); + session.panel = panel; + panel.onDidDispose(() => { + session.panel = undefined; + session.id = undefined; + // The next open creates a brand-new webview: what was on screen says + // nothing about it, so the document comparison starts from nothing. + session.webviewDocument.invalidate(); + }); + panel.webview.onDidReceiveMessage((raw: unknown) => { + void handle(session, raw); + }); + } + const panel = session.panel; + session.id = id; + panel.title = id; + panel.reveal(vscode.ViewColumn.Active, false); + await render(session, id); +} + +async function handle(session: DetailSession, raw: unknown): Promise { + const message = decodeDetailMessage(raw); + if (message === undefined || session.panel === undefined) { + return; + } + switch (message.type) { + case "openUrl": { + // `decodeDetailMessage` already restricted this to https. + await openExternal(session, message.url); + return; + } + case "copy": { + // The command preview and the usage lines: both are text a reader pastes + // somewhere else, and a click that answers with nothing looks like a dead + // button (the lesson `openExternal` learned in §20.2). + try { + await vscode.env.clipboard.writeText(message.text); + postResult(session, { state: "ok", message: t("Copied to the clipboard.") }); + } catch (error) { + session.deps.output.appendLine( + `mcpp library: copying to the clipboard failed: ${error instanceof Error ? error.message : String(error)}`, + ); + postResult(session, { state: "error", message: t("Could not write the clipboard.") }); + } + return; + } + case "add": { + if (session.busy || session.id === undefined) { + return; + } + session.busy = true; + let result: DetailResult; + try { + const outcome = await addDependency( + { + mcppExecutable: session.deps.mcppExecutable, + projectRoot: session.deps.projectRoot, + output: session.deps.output, + isTrusted: session.deps.isTrusted, + }, + { id: session.id, version: message.version, dev: message.dev }, + ); + result = { state: outcome.ok ? "ok" : "error", message: outcome.message }; + if (outcome.ok) { + session.deps.onDependenciesChanged?.(); + } + } finally { + session.busy = false; + } + if (session.panel === undefined) { + return; + } + postResult(session, result, result.state === "ok" ? message.version : undefined); + return; + } + default: + return; + } +} + +/** + * Open a link, and say what happened. + * + * This used to be `void vscode.env.openExternal(…)`: the boolean it resolves to + * was dropped, so a host with no browser (or no way to reach one) answered a + * click with nothing at all — indistinguishable from a dead button. Now the page + * is told either way, a failure is copied to the clipboard so the click is still + * worth something, and the output channel keeps a record. + */ +async function openExternal(session: DetailSession, url: string): Promise { + let opened = false; + try { + opened = await vscode.env.openExternal(vscode.Uri.parse(url)); + } catch (error) { + session.deps.output?.appendLine( + `mcpp library: opening ${url} failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + if (opened) { + postResult(session, { state: "ok", message: t("Opened {0} in your browser.", url) }); + return; + } + session.deps.output?.appendLine(`mcpp library: VS Code could not open ${url}; copying it instead.`); + try { + await vscode.env.clipboard.writeText(url); + } catch { + // A clipboard that refuses is not worth a second error. + } + postResult(session, { + state: "error", + message: t("VS Code could not open {0}. The link is on your clipboard.", url), + }); +} + +/** + * The page's status line, in the same shape the add flow uses. + * + * `added` tells the page that the project now depends on that version, so it can + * move its "added" marker and re-label the button without rebuilding the + * document — which would throw away the reader's scroll position and their + * version selection, the one thing running `mcpp add` must not do. + */ +function postResult(session: DetailSession, result: DetailResult, added?: string): void { + void session.panel?.webview.postMessage({ + type: "result", + result, + ...(added === undefined ? {} : { added: { version: added } }), + }); +} + +async function render(session: DetailSession, id: string): Promise { + const panel = session.panel; + if (panel === undefined) { + return; + } + const model = await buildModel(session, id); + if (session.panel !== panel || session.id !== id) { + return; + } + const html = renderDetailHtml( + model, + session.webviewDocument.assets( + vscode.Uri.joinPath(session.context.extensionUri, MEDIA_DIRECTORY), + panel.webview, + ), + ); + session.webviewDocument.paint(panel.webview, html); +} + +async function buildModel(session: DetailSession, id: string): Promise { + const projectRoot = session.deps.projectRoot(); + const snapshot = await loadSnapshot(projectRoot === undefined ? {} : { projectRoot }); + const entry = snapshot.entries.find((candidate) => candidate.id === id); + // The root the descriptor lives under, matched by path: two roots may share a + // directory name, and the site link belongs to the one that holds the file. + const root = entry === undefined ? undefined : snapshot.roots.find((candidate) => entry.file.startsWith(candidate.path)); + const ui = labels(); + if (entry === undefined) { + return emptyModel(id, ui, t("This package is not in the local index any more.")); + } + const text = cachedDescriptorText(id) ?? (await readDescriptorText(entry.file)) ?? ""; + const platform = platformKey(process.platform); + // Keyed by the index revision as well as the path, so a refreshed index is not + // answered from a parse of the previous one. + const cacheKey = `${snapshot.revision}\u0000${entry.file}`; + let info = parsed.get(cacheKey); + if (!parsed.has(cacheKey)) { + info = await readXpkg(session, entry); + parsed.set(cacheKey, info); + } + + const versions = info === undefined ? entry.versions : info.versions; + const groups: DetailVersionGroup[] = versionGroups(versions, platform).map((group) => ({ + platform: group.platform, + versions: group.versions, + current: group.platform === platform, + })); + const currentVersions = platform === undefined ? [] : (versions[platform] ?? []); + const latest = currentVersions.length === 0 ? undefined : sortVersions(currentVersions)[0]; + + const exampleFiles = entry.example === undefined ? [] : await readExampleFiles(snapshot.roots, entry.example); + const snippets = codeSnippets(exampleFiles, { context: 2, maxSnippets: 3, maxLines: 20 }); + // §22: the example project's real `import`/`#include` lines when it states + // them, else the honest synthetic form for the surface the parse resolved. + const realUsage = usageLinesFor(exampleFiles, entry.id); + const usage = realUsage.length > 0 ? realUsage : syntheticUsageLines(mergeSurfaces(info, text), entry.id); + + // The lock answers what a build actually resolved for each declared edge — + // the same matching rule `installedFor` uses for this package itself. + const lock = lockPackageVersions(readInstalledLock(session)); + const dependencies = descriptorDependencies(text).map((dependency) => { + const short = dependency.id.slice(dependency.id.lastIndexOf(".") + 1); + const resolved = lock.find((entry) => entry.id === dependency.id || entry.id === short)?.version; + return resolved === undefined ? { ...dependency } : { ...dependency, resolved }; + }); + const model: DetailModel = { + ui, + id: entry.id, + name: entry.name, + ...(entry.description === undefined ? {} : { description: entry.description }), + licenses: entry.licenses, + ...(entry.repo === undefined ? {} : { repo: entry.repo }), + registry: entry.registry, + ...(entry.surface === undefined ? {} : { surface: entry.surface }), + surfaces: entry.surfaces, + badges: badgesOf(entry), + versions: groups, + currentVersions: sortVersions(currentVersions), + ...(latest === undefined ? {} : { latest }), + // §22: the page finally learns what the project already has. Without this + // the button said "Add" to a package mcpp.toml listed all along. + ...readInstalled(session, entry.id), + ...(info?.standard === undefined ? {} : { standard: info.standard }), + dependencies, + includeDirs: info?.includeDirs ?? [], + targets: (info?.targets ?? []).map((target) => target.name ?? "").filter((name) => name.length > 0), + snippets, + ...(entry.example === undefined ? {} : { exampleProject: entry.example.project }), + usage, + // One discreet link, and only when this root really is the index that + // publishes those package pages (see `IndexRoot.site`). + ...(root?.site === undefined ? {} : { indexUrl: `${root.site}/${entry.id}/` }), + commandTemplate: t("mcpp add {0}@{1}"), + commandDevTemplate: t("mcpp add {0}@{1} --dev"), + ...(info === undefined + ? { + parseNotice: session.deps.isTrusted() + ? t( + "mcpp xpkg parse could not read this descriptor; the versions below come from its text, which is less authoritative.", + ) + : t("This workspace is not trusted, so mcpp is not run; the versions below come from the descriptor's text."), + } + : {}), + dataSource: dataSource(entry, snapshot.roots.length), + }; + return model; +} + +/** Run the authoritative reader for one descriptor; never throws. */ +async function readXpkg(session: DetailSession, entry: LibraryEntry): Promise { + // `mcpp.path` is resource-scoped, so an untrusted workspace must not name + // the program: the refusal degrades to the descriptor's own text. + const result = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Window, title: t("Reading {0}…", entry.id) }, + () => + runMcpp(session.deps.isTrusted(), session.deps.mcppExecutable(), ["xpkg", "parse", entry.file, "--json"], session.deps.projectRoot(), { + timeoutMs: PARSE_TIMEOUT_MS, + maxBufferMiB: read("mcpp.runtime.maxOutputMiB"), + }), + ); + if (result === undefined) { + return undefined; + } + if (result.exitCode !== 0) { + session.deps.output.appendLine(`mcpp xpkg parse ${entry.file} failed with exit code ${result.exitCode}`); + if (result.stderr.trim().length > 0) { + session.deps.output.appendLine(result.stderr.trimEnd()); + } + return undefined; + } + return parseXpkgJson(result.stdout); +} + +function dataSource(entry: LibraryEntry, roots: number): string { + return t( + "Read offline from {0} in the {1} index ({2} index folder(s) found).", + entry.file, + entry.registry, + roots, + ); +} + +/** A model that still renders: the page says why it is empty instead of going blank. */ +function emptyModel(id: string, ui: Record, notice: string): DetailModel { + return { + ui, + id, + name: id, + licenses: [], + registry: "", + surfaces: [], + badges: [], + versions: [], + currentVersions: [], + dependencies: [], + includeDirs: [], + targets: [], + snippets: [], + usage: [], + commandTemplate: t("mcpp add {0}@{1}"), + commandDevTemplate: t("mcpp add {0}@{1} --dev"), + parseNotice: notice, + dataSource: t("No descriptor was read."), + }; +} + +/** + * The workspace's own answer about one package (§20.1, wired in §22): + * `mcpp.toml` first, `mcpp.lock` second, both read tolerantly — a missing or + * unreadable file is simply "not installed", never a broken page. The spread + * sets `installed` only when there is one. + */ +function readInstalled(session: DetailSession, id: string): { installed?: DetailInstalled } { + const root = session.deps.projectRoot(); + if (root === undefined) { + return {}; + } + const toml = readTextTolerantly(path.join(root, "mcpp.toml")); + const lock = readTextTolerantly(path.join(root, "mcpp.lock")); + const installed = installedFor(id, toml, lock); + return installed === undefined ? {} : { installed }; +} + +/** An unreadable or missing file reads as empty text; the view degrades, not breaks. */ +function readTextTolerantly(file: string): string { + try { + return readFileSync(file, "utf8"); + } catch { + return ""; + } +} + +/** The workspace's `mcpp.lock` text, empty when there is none to read. */ +function readInstalledLock(session: DetailSession): string { + const root = session.deps.projectRoot(); + return root === undefined ? "" : readTextTolerantly(path.join(root, "mcpp.lock")); +} + +/** Every visible string, resolved once per model. */ +function labels(): Record { + return { + [DETAIL_UI.htmlLang]: htmlLanguage(), + [DETAIL_UI.title]: t("Library package {0}"), + [DETAIL_UI.overview]: t("Build shape"), + [DETAIL_UI.license]: t("License"), + [DETAIL_UI.repo]: t("Repository"), + [DETAIL_UI.openRepo]: t("Open the repository"), + [DETAIL_UI.registry]: t("Registry"), + [DETAIL_UI.surface]: t("Use"), + [DETAIL_UI.surfaceExternal]: t("upstream mcpp.toml"), + [DETAIL_UI.standard]: t("Standard"), + [DETAIL_UI.versions]: t("Versions ({0})"), + [DETAIL_UI.versionsAll]: t("Versions"), + [DETAIL_UI.versionsCurrent]: t("this platform"), + [DETAIL_UI.versionsNone]: t("This index publishes no version for any platform."), + [DETAIL_UI.versionsPick]: t("Click a version to aim the command above at it."), + [DETAIL_UI.dependencies]: t("Dependencies"), + [DETAIL_UI.dependenciesNone]: t("This descriptor declares no dependencies."), + [DETAIL_UI.dependenciesHint]: t( + "What the descriptor declares. The resolved version lives in a project's mcpp.lock, not here.", + ), + [DETAIL_UI.resolved]: t("resolved {0}"), + [DETAIL_UI.code]: t("Example code"), + [DETAIL_UI.codeNone]: t("This package has no test project in the index, so there is no example to show."), + [DETAIL_UI.codeSource]: t("{0} · line {1}"), + [DETAIL_UI.codeProject]: t("Example project: {0} — built and run by the index's CI, not written for this page."), + [DETAIL_UI.add]: t("Add to mcpp.toml"), + [DETAIL_UI.addDev]: t("dev dependency"), + [DETAIL_UI.addLatest]: t("The version is required: mcpp accepts an exact version only."), + [DETAIL_UI.switchTo]: t("Switch to {0}"), + [DETAIL_UI.alreadyAdded]: t("Already added"), + [DETAIL_UI.installed]: t("added"), + [DETAIL_UI.opening]: t("Opening {0}…"), + [DETAIL_UI.addNoVersion]: t("This index publishes no version for this platform, so there is nothing to add."), + [DETAIL_UI.command]: t("Command"), + [DETAIL_UI.usage]: t("Bring it into your code"), + [DETAIL_UI.copy]: t("Copy"), + [DETAIL_UI.copied]: t("Copied to the clipboard."), + [DETAIL_UI.copying]: t("Copying…"), + [DETAIL_UI.indexLink]: t("Open on the index site"), + [DETAIL_UI.badgeExamples]: t("✓ Has examples"), + [DETAIL_UI.badgeCn]: t("China mirror"), + [DETAIL_UI.badgeOpenkalEcosystem]: t("openkal-ecosystem"), + [DETAIL_UI.badgeOpenkalCompat]: t("openkal-compat"), + [DETAIL_UI.badgeOpenkalPosix]: t("POSIX environment"), + [DETAIL_UI.badgeOpenkalPlatform]: t("uses platform interfaces"), + [DETAIL_UI.targets]: t("Targets"), + [DETAIL_UI.includeDirs]: t("Include directories"), + }; +} + +/** `auto` is the editor's own language; `en`/`zh-cn` are the manual override. */ +function htmlLanguage(): string { + const preference = languagePreference(); + if (preference === "zh-cn") { + return "zh-cn"; + } + if (preference === "en") { + return "en"; + } + return localeFromEditorLanguage(vscode.env.language) === "zh-cn" ? "zh-cn" : "en"; +} + +/** Kept next to the model so the surface vocabulary has one import site. */ +export { surfaceLabel }; +export type { Surface }; + +/** Greatest first — the order `xpkg.latestVersion` picks from. */ +function sortVersions(versions: readonly string[]): string[] { + return [...versions].sort((a, b) => compareVersions(b, a)); +} diff --git a/src/library/indexLocator.ts b/src/library/indexLocator.ts new file mode 100644 index 0000000..a29770a --- /dev/null +++ b/src/library/indexLocator.ts @@ -0,0 +1,559 @@ +/** + * Where the index lives, and how it is read **cheaply**. + * + * The catalog is a directory of Lua descriptors: + * `/.mcpp/registry/data//pkgs//.lua`. Three + * registries are installed on the reference machine (`mcpplibs` with 239 + * descriptors, `xim-pkgindex`, `xim-pkgindex-local`), and `mcpp index status` + * prints a table of them — which is deliberately **not** parsed: `--format json` + * is an unknown option there, and a second human-output parser is exactly what + * this project forbids. Globbing the data directory under `` gives the + * same answer without parsing anything. + * + * `` is `$MCPP_HOME`, then `~/.mcpp` — every one of them that holds a data + * directory — and when none does, whatever `mcpp self env --format json` + * reports, which is a machine shape and therefore allowed to be read. + * + * Two budgets shape this file: + * + * - **No process per package.** The list is read from the descriptor text + * (`src/library/indexModel.ts`); `mcpp xpkg parse --json` runs on demand, from + * the detail panel, never here. + * - **One read per index revision.** A snapshot is cached and invalidated by a + * cheap revision key (the `pkgs` directory's mtime, the `.mcpp-index-updated` + * marker and the descriptor count), so re-rendering the view after a filter + * change costs a few stats, not 239 file reads. + * + * A missing index is **not** an error: `locateIndexRoots()` returns `[]` and the + * caller renders the message this module hands it. + */ + +import { promises as fs } from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import * as vscode from "vscode"; + +import { runMcpp } from "../cli/process"; +import { read } from "../config/access"; +import { + descriptorEntry, + descriptorId, + declaredDependencies, + exampleCatalog, + openkalFacetFor, + parseDescriptorLua, + parseOpenkalJson, + parseSelfEnv, + platformKey, + type CodeFile, + type ExampleInput, + type ExampleRef, + type LibraryEntry, + type OpenkalIndex, +} from "./indexModel"; + +/** How many descriptor files a walk will read before it says the index is wrong. */ +const MAX_DESCRIPTORS = 20_000; + +/** + * How long `mcpp self env --format json` may take. It only runs when the cheap + * globs found nothing, and a hang there must not hold the view hostage. + */ +const SELF_ENV_TIMEOUT_MS = 15_000; + +/** The index site of the official C++ library index, and its package pages. */ +export const INDEX_SITE = "https://mcpplibs.github.io/mcpp-index/packages"; + +/** A directory that holds `pkgs/`, and the shared metadata beside it. */ +export interface IndexRoot { + /** The directory name, e.g. `mcpplibs`. */ + registry: string; + /** The index root: it contains `pkgs`, and may contain `tests/` and `.xpkgindex/`. */ + path: string; + /** `/pkgs`. */ + pkgs: string; + /** `/tests/examples` exists, so the example code can be shown. */ + hasExamples: boolean; + /** `/.xpkgindex/openkal-compat.json` exists. */ + hasOpenkal: boolean; + /** + * The package page base URL of the index site, when this root is the official + * index. + * + * Two shapes of the same index occur: the packed copy mcpp installs into + * `/.mcpp/registry/data/mcpplibs`, and a git checkout of `mcpp-index` + * (whose directory is named after the repository). The generator itself — + * `.xpkgindex/plugins/mcpp.py`, which only that repository carries — is the + * signal that this root is that index, so a checkout gets the deep link too. + */ + site?: string; +} + +/** Everything the view renders, read once per revision. */ +export interface LibrarySnapshot { + entries: LibraryEntry[]; + roots: IndexRoot[]; + revision: string; +} + +/** `~` is what a user types; the file system wants the home directory. */ +function expandHome(value: string): string { + if (value === "~") { + return os.homedir(); + } + if (value.startsWith("~/") || value.startsWith("~\\")) { + return path.join(os.homedir(), value.slice(2)); + } + return value; +} + +async function isDirectory(target: string): Promise { + try { + return (await fs.stat(target)).isDirectory(); + } catch { + return false; + } +} + +async function isFile(target: string): Promise { + try { + return (await fs.stat(target)).isFile(); + } catch { + return false; + } +} + +/** One index root, or `undefined` when the directory is not one. */ +async function rootAt(directory: string): Promise { + const pkgs = path.join(directory, "pkgs"); + if (!(await isDirectory(pkgs))) { + return undefined; + } + const registry = path.basename(directory); + const generator = await isFile(path.join(directory, ".xpkgindex", "plugins", "mcpp.py")); + return { + registry, + path: directory, + pkgs, + hasExamples: await isDirectory(path.join(directory, "tests", "examples")), + hasOpenkal: await isFile(path.join(directory, ".xpkgindex", "openkal-compat.json")), + ...(registry === "mcpplibs" || generator ? { site: INDEX_SITE } : {}), + }; +} + +/** + * The index roots to read. + * + * `mcpp.library.indexPath` wins when the user set it — it may name one index + * root (a directory with `pkgs/`), a `pkgs` directory itself, or a whole + * `/.mcpp/registry/data` directory of registries. With no setting, the + * default glob is the data directory under each known mcpp home. + * + * **Finding ``.** Two are tried, in order: `$MCPP_HOME` (the variable mcpp + * itself honours) and `~/.mcpp`. Both are listed when both hold something, and + * the view's footer names the registries it read, so a stale second home shows + * up as an extra source rather than as a wrong answer. When *neither* has a + * `registry/data` with anything in it, mcpp is asked directly — `mcpp self env + * --format json` reports `mcppHome`, which is the only authoritative answer for + * an install in a place nothing can guess. That probe costs one process **per + * session** and only runs when the cheap globs came up empty, so the ordinary + * machine never pays for it; it is also skipped in an untrusted workspace, + * because `mcpp.path` is a resource-scoped setting and a workspace must not + * choose a binary to run. + * + * `[]` means "no index found"; the caller turns that into the message rather + * than into an error, because a machine without the index is a normal machine. + * + * (`locateIndexRoots` is kept as an alias: the name reads better at the call + * site, the exported name is the one `extension.ts` uses.) + */ +export async function readIndexRoots(): Promise { + const configured = read("mcpp.library.indexPath"); + const candidates: string[] = []; + if (typeof configured === "string" && configured.trim().length > 0) { + const configured_path = path.resolve(expandHome(configured.trim())); + // A `pkgs` directory was named directly. + if (path.basename(configured_path) === "pkgs" && (await isDirectory(configured_path))) { + candidates.push(path.dirname(configured_path)); + } else { + const own = await rootAt(configured_path); + if (own !== undefined) { + candidates.push(own.path); + } else { + // A data directory: every registry inside it. + for (const entry of await readdirSafe(configured_path)) { + candidates.push(path.join(configured_path, entry)); + } + } + } + } else { + for (const data of await defaultDataDirectories()) { + for (const entry of await readdirSafe(data)) { + candidates.push(path.join(data, entry)); + } + } + } + + const roots: IndexRoot[] = []; + for (const candidate of candidates) { + const root = await rootAt(candidate); + if (root !== undefined && !roots.some((seen) => seen.path === root.path)) { + roots.push(root); + } + } + return roots.sort((a, b) => (a.registry < b.registry ? -1 : a.registry > b.registry ? 1 : 0)); +} + +/** Set once per session, so `mcpp self env` runs at most once. */ +let probedHome: string | undefined; +let probeAttempted = false; + +/** The homes mcpp may have used, most specific first, without duplicates. */ +function candidateHomes(): string[] { + const homes: string[] = []; + const fromEnvironment = process.env.MCPP_HOME?.trim(); + if (fromEnvironment !== undefined && fromEnvironment.length > 0) { + homes.push(fromEnvironment); + } + homes.push(path.join(os.homedir(), ".mcpp")); + if (probedHome !== undefined) { + homes.push(probedHome); + } + return [...new Set(homes)]; +} + +/** + * The `/registry/data` directories worth listing. + * + * The cheap answer first: a data directory that already holds something is the + * answer, and no process is started. Only when all of them are missing or empty + * is `mcpp self env` asked where its home is — and its answer is picked up by + * re-reading the candidate list, because the probe is what adds the home to it. + */ +async function defaultDataDirectories(): Promise { + const candidates = (): string[] => candidateHomes().map((home) => path.join(home, "registry", "data")); + const present: string[] = []; + for (const data of candidates()) { + if ((await readdirSafe(data)).length > 0) { + present.push(data); + } + } + if (present.length > 0) { + return present; + } + await mcppHomeFromCli(); + return [...new Set(candidates())]; +} + +/** + * `mcpp self env --format json`, for the one field that says where the index is. + * + * It runs at most once per session and never in an untrusted workspace. A + * missing or failing mcpp is not an error here: the caller falls back to the + * directories it already has, and the view says "no index" the way it does when + * there is none. + */ +async function mcppHomeFromCli(): Promise { + if (probeAttempted) { + return probedHome; + } + probeAttempted = true; + const executable = read("mcpp.path").trim(); + // `mcpp.path` is resource-scoped, so the probe goes through the trust-gated + // seam; a refusal is the same "no answer" as a failing mcpp. + const result = await runMcpp( + vscode.workspace.isTrusted, + executable.length === 0 ? "mcpp" : executable, + ["self", "env", "--format", "json"], + undefined, + { timeoutMs: SELF_ENV_TIMEOUT_MS, maxBufferMiB: 1 }, + ); + if (result === undefined || result.exitCode !== 0) { + return undefined; + } + const env = parseSelfEnv(result.stdout); + if (env === undefined) { + return undefined; + } + if (env.mcppHome !== undefined) { + probedHome = env.mcppHome; + } else if (env.registry !== undefined) { + // `registry` is `/registry`; the home is what the other paths hang off. + probedHome = path.dirname(env.registry); + } + return probedHome; +} + +export { readIndexRoots as locateIndexRoots }; + +/** A directory listing, or nothing when it cannot be read. */ +async function readdirSafe(directory: string): Promise { + try { + return await fs.readdir(directory); + } catch { + return []; + } +} + +/** Every `*.lua` under `/pkgs`, depth first, with a hard cap. */ +async function descriptorFiles(pkgs: string): Promise { + const out: string[] = []; + const walk = async (directory: string): Promise => { + if (out.length >= MAX_DESCRIPTORS) { + return; + } + let entries; + try { + entries = await fs.readdir(directory, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (out.length >= MAX_DESCRIPTORS) { + return; + } + const full = path.join(directory, entry.name); + if (entry.isDirectory()) { + await walk(full); + } else if (entry.isFile() && entry.name.endsWith(".lua")) { + out.push(full); + } + } + }; + await walk(pkgs); + return out.sort(); +} + +/** + * A cheap key that changes when the index does. + * + * The `pkgs` directory's own mtime catches an added or removed letter + * directory; `.mcpp-index-updated` is written whenever mcpp refreshes the index, + * which catches a changed descriptor body. The descriptor count is included so a + * tree that stops being updated still costs one full read when its size changes. + */ +async function revisionOf(roots: readonly IndexRoot[]): Promise { + const parts: string[] = []; + for (const root of roots) { + const pkgs = await statSafe(root.pkgs); + const marker = await statSafe(path.join(root.path, ".mcpp-index-updated")); + const letters = await readdirSafe(root.pkgs); + parts.push(`${root.registry}:${pkgs?.mtimeMs ?? 0}:${marker?.mtimeMs ?? 0}:${letters.length}`); + } + return parts.join("|"); +} + +async function statSafe(target: string): Promise<{ mtimeMs: number; size: number } | undefined> { + try { + const stat = await fs.stat(target); + return { mtimeMs: stat.mtimeMs, size: stat.size }; + } catch { + return undefined; + } +} + +/** + * The example projects of one index root, read as manifests plus the *names* of + * their test files. The file contents are read later, only for the package the + * reader actually opened. + */ +async function readExamples(root: IndexRoot): Promise { + if (!root.hasExamples) { + return []; + } + const base = path.join(root.path, "tests", "examples"); + const out: ExampleInput[] = []; + for (const project of await readdirSafe(base)) { + const directory = path.join(base, project); + const manifest = path.join(directory, "mcpp.toml"); + let text: string; + try { + text = await fs.readFile(manifest, "utf8"); + } catch { + continue; + } + const tests = path.join(directory, "tests"); + const sources: string[] = []; + for (const file of (await readdirSafe(tests)).sort()) { + if (file.endsWith(".cpp") || file.endsWith(".cppm") || file.endsWith(".cc")) { + sources.push(path.relative(root.path, path.join(tests, file)).split(path.sep).join("/")); + } + } + if (sources.length > 0) { + out.push({ project, text, sources }); + } + } + return out; +} + +/** The openkal measurement, merged across roots; the first one wins per member. */ +async function readOpenkal(roots: readonly IndexRoot[]): Promise { + const members: OpenkalIndex["members"] = {}; + let measured: string | undefined; + let found = false; + for (const root of roots) { + if (!root.hasOpenkal) { + continue; + } + let text: string; + try { + text = await fs.readFile(path.join(root.path, ".xpkgindex", "openkal-compat.json"), "utf8"); + } catch { + continue; + } + const parsed = parseOpenkalJson(text); + if (parsed === undefined) { + continue; + } + found = true; + measured = measured ?? parsed.measured; + for (const [name, member] of Object.entries(parsed.members)) { + if (members[name] === undefined) { + members[name] = member; + } + } + } + return found ? { members, ...(measured === undefined ? {} : { measured }) } : undefined; +} + +/** The package ids the workspace's own `mcpp.toml` declares. */ +async function readAdded(projectRoot: string | undefined): Promise> { + if (projectRoot === undefined) { + return new Set(); + } + try { + const text = await fs.readFile(path.join(projectRoot, "mcpp.toml"), "utf8"); + return new Set(declaredDependencies(text)); + } catch { + return new Set(); + } +} + +interface Cache { + revision: string; + snapshot: LibrarySnapshot; + /** Descriptor text, kept so the detail page does not re-read what the list did. */ + texts: Map; +} + +let cache: Cache | undefined; + +/** + * The list, read from disk only when the revision changed. + * + * `projectRoot` is the workspace folder whose `mcpp.toml` decides the "already + * declared" state; it is part of the cache key, because switching workspaces + * changes the answer without changing the index. + */ +export async function loadSnapshot(options: { projectRoot?: string; platform?: string } = {}): Promise { + const roots = await readIndexRoots(); + const revision = `${await revisionOf(roots)}|${options.projectRoot ?? ""}`; + if (cache !== undefined && cache.revision === revision) { + return cache.snapshot; + } + if (roots.length === 0) { + const empty: LibrarySnapshot = { entries: [], roots, revision }; + cache = { revision, snapshot: empty, texts: new Map() }; + return empty; + } + + const platform = options.platform ?? platformKey(process.platform); + const added = await readAdded(options.projectRoot); + const entries: LibraryEntry[] = []; + const texts = new Map(); + /** Example refs, keyed by package id, from every root that has examples. */ + const exampleRefs = new Map(); + for (const root of roots) { + for (const [id, ref] of exampleCatalog(await readExamples(root))) { + if (!exampleRefs.has(id)) { + exampleRefs.set(id, ref); + } + } + } + const openkal = await readOpenkal(roots); + + for (const root of roots) { + for (const file of await descriptorFiles(root.pkgs)) { + let text: string; + try { + text = await fs.readFile(file, "utf8"); + } catch { + continue; + } + const fileName = path.basename(file); + const fields = parseDescriptorLua(text); + // `descriptorEntry` decides the identity; the example and openkal lookups + // below must use that same id, so it is computed the same way here. + const id = descriptorId(fields, fileName).id; + const example = exampleRefs.get(id); + const facet = openkalFacetFor(openkal, id); + const entry = descriptorEntry({ + fileName, + registry: root.registry, + file, + text, + platform, + added: added.has(id), + ...(example === undefined ? {} : { example }), + ...(facet === undefined ? {} : { openkal: facet }), + }); + entries.push(entry); + texts.set(entry.id, text); + } + } + + entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); + const snapshot: LibrarySnapshot = { entries, roots, revision }; + cache = { revision, snapshot, texts }; + return snapshot; +} + +/** The descriptor text of one package, from the cache when possible. */ +export async function readDescriptorText(file: string): Promise { + try { + return await fs.readFile(file, "utf8"); + } catch { + return undefined; + } +} + +/** The cached text for a package id, when the last snapshot read it. */ +export function cachedDescriptorText(id: string): string | undefined { + return cache?.texts.get(id); +} + +/** + * The example files behind a package: real code from + * `tests/examples//tests`, which CI builds and runs. + * + * The ref's `paths` are relative to the index root, so the root is found by + * matching the prefix. Missing files are skipped, never invented. + */ +export async function readExampleFiles(roots: readonly IndexRoot[], example: ExampleRef): Promise { + const files: CodeFile[] = []; + for (const root of roots) { + if (!root.hasExamples) { + continue; + } + for (const relative of example.paths) { + const absolute = path.join(root.path, relative); + if (!absolute.startsWith(root.path)) { + continue; + } + try { + files.push({ path: relative, text: await fs.readFile(absolute, "utf8") }); + } catch { + // A file that disappeared between the listing and the read. + } + } + if (files.length > 0) { + return files; + } + } + return files; +} + +/** Test seam: forget the cached snapshot. */ +export function resetSnapshotCache(): void { + cache = undefined; +} diff --git a/src/library/indexModel.ts b/src/library/indexModel.ts new file mode 100644 index 0000000..3df6488 --- /dev/null +++ b/src/library/indexModel.ts @@ -0,0 +1,1520 @@ +/** + * The library view's model: everything that can be decided without an editor. + * + * The index is a directory of Lua descriptors on disk + * (`/.mcpp/registry/data//pkgs//.lua`). The list + * phase has a hard budget: **no process per package** — 239 `mcpp` spawns would + * freeze the sidebar — so the catalog fields (`namespace`, `name`, + * `description`, `licenses`, `repo`, whether a `CN` mirror exists) and the + * version numbers are read out of the Lua **text**, tolerantly, and anything + * that does not fit a single-line shape is simply left out. Every authoritative + * value comes from `mcpp xpkg parse --json` instead (`src/library/xpkg.ts`), + * which runs only when a row is expanded or the detail page opens. + * + * The vocabulary is the index site generator's, not ours + * (`.xpkgindex/plugins/mcpp.py`): the usage label is one of the four `SURFACES`, + * the badges are the generator's own badges, and the openkal facets are + * **measurements** recorded in `.xpkgindex/openkal-compat.json` — never + * descriptor fields, so they are read from that file or not shown at all. + * + * This module is `vscode`-free on purpose (`test/architecture.test.ts`): the + * escaping, the tolerant parsing, the version ordering and the example lookup are + * all unit-testable without an editor host. + */ + +import { + SURFACE_TEXT, + SURFACES, + latestVersion, + parseXpkgJson, + platformKey, + surfacesOf, + textNamesBinaryTarget, + versionGroups, + versionsFor, + type Surface, + type XpkgInfo, +} from "./xpkg"; + +// ─────────────────────────────────────────────────────────────── vocabulary ── + +/** + * The packages that make up openkal — the site generator's `OPENKAL_FAMILY`, + * copied verbatim (it is a list of **names**, not prose, so it is not + * translated). `openkal-compat` needs no list: it is read from the measurement + * file. A rename upstream is a one-line change here. + */ +export const OPENKAL_FAMILY: readonly string[] = [ + "mcpplibs.openkal", + "mcpplibs.openkal-kit", + "mcpplibs.openkal-linux", + "mcpplibs.openkal-macos", + "mcpplibs.openkal-windows", + "mcpplibs.openkal-uefi", + "mcpplibs.openkal-opensbi", + "mcpplibs.openkal-emscripten", + "mcpplibs.openkal-libc", + "mcpplibs.openkal-musl", + "mcpplibs.openkal-llvm-runtime", + "mcpplibs.std-freestanding-alloc-kal", +]; + +/** Every badge the view may show, in the order it shows them (§12.5). */ +export type BadgeKey = + | "examples" + | "cn" + | "openkal-ecosystem" + | "openkal-compat" + | "openkal-posix" + | "openkal-platform"; + +/** The `ui` key and English fallback for each badge. */ +export const BADGE_UI: Readonly> = { + examples: { key: "library.badge.examples", fallback: "✓ Has examples" }, + cn: { key: "library.badge.cn", fallback: "China mirror" }, + "openkal-ecosystem": { key: "library.badge.openkalEcosystem", fallback: "openkal-ecosystem" }, + "openkal-compat": { key: "library.badge.openkalCompat", fallback: "openkal-compat" }, + "openkal-posix": { key: "library.badge.openkalPosix", fallback: "POSIX environment" }, + "openkal-platform": { key: "library.badge.openkalPlatform", fallback: "uses platform interfaces" }, +}; + +/** The current platform, as `mcpp` names it. Re-exported so callers need one import. */ +export { platformKey }; + +// ──────────────────────────────────────────────────────────────── Lua text ── + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Drop Lua comments without touching string literals. + * + * It matters: descriptors are heavily commented, and a comment that mentions + * `kind = "bin"` or a `.cppm` path must not become a fact about the package. + * Line comments (`-- …`), long comments (`--[[ … ]]`, `--[=[ … ]=]`) and the + * long strings they can be confused with are all handled; `"` and `'` literals + * are copied through with their escapes. + */ +export function stripLuaComments(text: string): string { + let out = ""; + let index = 0; + while (index < text.length) { + const char = text[index]; + if (char === '"' || char === "'") { + const quote = char; + out += char; + index += 1; + while (index < text.length) { + const inner = text[index]; + out += inner; + index += 1; + if (inner === "\\") { + if (index < text.length) { + out += text[index]; + index += 1; + } + continue; + } + if (inner === quote) { + break; + } + } + continue; + } + if (char === "-" && text[index + 1] === "-") { + const long = /^\[(=*)\[/.exec(text.slice(index + 2)); + if (long !== null) { + const close = `]${long[1]}]`; + const end = text.indexOf(close, index + 2 + long[0].length); + index = end === -1 ? text.length : end + close.length; + continue; + } + const newline = text.indexOf("\n", index); + index = newline === -1 ? text.length : newline; + continue; + } + out += char; + index += 1; + } + return out; +} + +/** + * The body of the `{ … }` that starts at `open` (the index of the brace), or + * `undefined` when the braces do not balance. String literals are skipped so a + * `}` inside a URL cannot close the block early. + */ +export function bracedBody(text: string, open: number): { body: string; end: number } | undefined { + if (text[open] !== "{") { + return undefined; + } + let depth = 0; + let index = open; + while (index < text.length) { + const char = text[index]; + if (char === '"' || char === "'") { + const quote = char; + index += 1; + while (index < text.length && text[index] !== quote) { + index += text[index] === "\\" ? 2 : 1; + } + index += 1; + continue; + } + if (char === "{") { + depth += 1; + } else if (char === "}") { + depth -= 1; + if (depth === 0) { + return { body: text.slice(open + 1, index), end: index + 1 }; + } + } + index += 1; + } + return undefined; +} + +/** The `{ … }` assigned to `key`, searching the whole text. */ +export function keyBlock(text: string, key: string): string | undefined { + const pattern = new RegExp(`(?:^|[^\\w.])${key}\\s*=\\s*\\{`, "m"); + const match = pattern.exec(text); + if (match === null) { + return undefined; + } + const open = match.index + match[0].length - 1; + return bracedBody(text, open)?.body; +} + +/** One `key = value` pair at the top level of a Lua table body. */ +export interface LuaEntry { + key: string; + /** A string literal's contents, or the raw body of a `{ … }` value. */ + value?: string; + body?: string; +} + +/** + * The assignments at the **top level** of a table body, in source order. + * + * Only the top level, deliberately: `package` contains the whole `xpm` table, + * and a `name = "x"` nested inside a manifest target is not the package name. + * A value that is neither a one-line string nor a brace block is skipped — the + * caller then omits the field rather than guessing at it. + */ +export function topLevelEntries(body: string): LuaEntry[] { + const entries: LuaEntry[] = []; + let depth = 0; + let index = 0; + while (index < body.length) { + const char = body[index]; + if (char === '"' || char === "'") { + const quote = char; + index += 1; + while (index < body.length && body[index] !== quote) { + index += body[index] === "\\" ? 2 : 1; + } + index += 1; + continue; + } + if (char === "{" || char === "(") { + depth += 1; + index += 1; + continue; + } + if (char === "}" || char === ")") { + depth -= 1; + index += 1; + continue; + } + if (depth === 0) { + const quoted = /^\[["']([^"']*)["']\]\s*=\s*/.exec(body.slice(index)); + const bare = quoted === null ? /^([A-Za-z_]\w*)\s*=\s*/.exec(body.slice(index)) : null; + const match = quoted ?? bare; + if (match !== null) { + const key = quoted === null ? bare![1] : quoted[1]; + let cursor = index + match[0].length; + while (cursor < body.length && /\s/.test(body[cursor])) { + cursor += 1; + } + if (body[cursor] === "{") { + const block = bracedBody(body, cursor); + if (block === undefined) { + return entries; + } + entries.push({ key, body: block.body }); + index = block.end; + continue; + } + if (body[cursor] === '"' || body[cursor] === "'") { + const quote = body[cursor]; + let end = cursor + 1; + let value = ""; + while (end < body.length && body[end] !== quote) { + if (body[end] === "\\") { + value += body[end + 1] ?? ""; + end += 2; + continue; + } + value += body[end]; + end += 1; + } + // A value that spans a line break is not the single-line shape the + // catalog fields are written in; it is omitted rather than guessed at. + if (!value.includes("\n") && !value.includes("\r")) { + entries.push({ key, value }); + } + index = end + 1; + continue; + } + // A number, a boolean or an expression: not a catalog field. + const lineEnd = body.indexOf("\n", cursor); + index = lineEnd === -1 ? body.length : lineEnd; + continue; + } + } + index += 1; + } + return entries; +} + +/** Every `"…"` literal in a table body, in order — the shape of `licenses`. */ +export function stringLiterals(body: string): string[] { + const out: string[] = []; + for (const match of body.matchAll(/"((?:[^"\\]|\\.)*)"/g)) { + out.push(match[1].replace(/\\(.)/g, "$1")); + } + return out; +} + +/** The catalog fields of one descriptor, as far as its text states them. */ +export interface DescriptorFields { + namespace?: string; + name?: string; + description?: string; + licenses: string[]; + repo?: string; +} + +/** + * Parse a descriptor's Lua text tolerantly. Never throws, and never invents a + * value: a field written across lines, escaped unusually, or nested somewhere + * unexpected is left out, and the caller falls back to the file name and says + * the descriptor could not be read. + */ +export function parseDescriptorLua(text: string): DescriptorFields { + const clean = stripLuaComments(text); + const body = keyBlock(clean, "package") ?? clean; + const entries = topLevelEntries(body); + const fields: DescriptorFields = { licenses: [] }; + for (const entry of entries) { + if (entry.body !== undefined) { + if (entry.key === "licenses") { + fields.licenses = stringLiterals(entry.body).filter((value) => value.length > 0); + } + continue; + } + if (entry.value === undefined || entry.value.length === 0) { + continue; + } + if (entry.key === "namespace") { + fields.namespace = entry.value; + } else if (entry.key === "name") { + fields.name = entry.value; + } else if (entry.key === "description") { + fields.description = entry.value; + } else if (entry.key === "repo") { + fields.repo = entry.value; + } + } + return fields; +} + +/** A `CN` mirror url — the badge the site derives from the descriptor's urls. */ +export function hasCnMirror(text: string): boolean { + const clean = stripLuaComments(text); + // Both shapes occur: `CN = "…"` and `["CN"] = "…"`. + return /\bCN\b\s*=/.test(clean) || /\[\s*["']CN["']\s*\]\s*=/.test(clean); +} + +/** `name` and `namespace` as the file name encodes them (`ns.name.lua`, or `name.lua`). */ +export function idFromFileName(fileName: string): { namespace?: string; name: string } { + const base = fileName.replace(/\.lua$/i, ""); + // The separator is the *last* dot: `boost-ext.ut` is `boost-ext` + `ut`, and + // the one descriptor whose own fields are unreadable + // (`huxerui.huxerui.lua`) is `huxerui` + `huxerui`, not `huxerui` + `huxerui.huxerui`. + const dot = base.lastIndexOf("."); + if (dot <= 0) { + return { name: base }; + } + return { namespace: base.slice(0, dot), name: base.slice(dot + 1) }; +} + +/** + * The package id, `ns.name`, with the file name as the fallback (§9.3). + * + * `name` may be written **fully qualified**: `huxerui.huxerui.lua` declares + * `namespace = "huxerui"` and `name = "huxerui.huxerui"` on purpose (mcpp#278 + * INV-NAME — the split form "parses but can never be installed"), and `mcpp` + * itself reports the two halves separately. The prefix is therefore stripped + * again here, which is the difference between a usable id and `huxerui` three + * times over. + */ +export function descriptorId(fields: DescriptorFields, fileName: string): { id: string; namespace?: string; name: string } { + const fallback = idFromFileName(fileName); + let name = fields.name !== undefined && fields.name.length > 0 ? fields.name : fallback.name; + let namespace = fields.namespace !== undefined && fields.namespace.length > 0 ? fields.namespace : fallback.namespace; + const dot = name.lastIndexOf("."); + if (dot > 0) { + const head = name.slice(0, dot); + const tail = name.slice(dot + 1); + if (namespace === undefined || namespace === head) { + namespace = head; + } + name = tail; + } + return { id: namespace === undefined ? name : `${namespace}.${name}`, ...(namespace === undefined ? {} : { namespace }), name }; +} + +/** + * The list's cheap surface guess, from the descriptor text alone. + * + * The authoritative answer is `surfacesOf(xpkg parse --json)`; this exists + * because the list may not spawn a process per package. It asks the same + * questions in the same order as the site generator's `_surfaces`: a `.cppm` + * anywhere makes it a module, a `kind = "bin"` target makes it a tool, an + * inline `sources`/`include_dirs` manifest makes it a header package, and a + * descriptor with no inline manifest at all is a Form A `external`. + * + * `undefined` means the text does not say — the row then shows no usage label, + * and opening the package gets the authoritative answer. + */ +export function surfaceFromDescriptorText(text: string): Surface | undefined { + const clean = stripLuaComments(text); + if (/\.cppm"/.test(clean)) { + return "module"; + } + if (textNamesBinaryTarget(clean)) { + return "tool"; + } + if (/\b(?:sources|include_dirs)\s*=\s*\{/.test(clean)) { + return "header"; + } + if (/\btargets\s*=\s*\{/.test(clean)) { + // Targets but no sources and no headers: the text does not say how the + // package is consumed. The parse's own answer is used when it is available. + return undefined; + } + // No inline manifest at all is what a Form A descriptor looks like. + return /\b(?:package|xpm)\s*=/.test(clean) ? "external" : undefined; +} + +/** + * Merge what the parse says with what the text says, without ever letting the + * cheap guess override the authoritative reader. + * + * The **only** thing the text may add is a `tool` surface: `xpkg parse` prints + * target names, so a `kind = "bin"` is visible in the descriptor's own inline + * manifest but not in the command's output. Everything else comes from the parse + * when it succeeded — which is what keeps a feature-gated `.cppm` (ftxui) from + * being reported as a module just because the text mentions one. + */ +export function mergeSurfaces(info: XpkgInfo | undefined, text: string): Surface[] { + const fromParse = info === undefined ? [] : surfacesOf(info); + const out: Surface[] = [...fromParse]; + if (textNamesBinaryTarget(stripLuaComments(text)) && !out.includes("tool")) { + out.push("tool"); + } + return SURFACES.filter((surface) => out.includes(surface)); +} + +/** + * The version numbers the descriptor's own `xpm` table lists, per platform. + * + * A cheap list-phase stand-in for the authoritative `versions` in + * `mcpp xpkg parse --json`, which the detail page uses. It reads the + * `[""] =` keys of each platform block and, for the xlings-style + * `["latest"] = { ref = "1.6.43" }` alias, the `ref` it points at — but never + * returns `latest` itself as a version, because `mcpp add` accepts only an exact + * version. + */ +export function versionsFromDescriptorText(text: string): Record { + const clean = stripLuaComments(text); + const xpm = keyBlock(clean, "xpm"); + if (xpm === undefined) { + return {}; + } + const out: Record = {}; + for (const platform of topLevelEntries(xpm)) { + if (platform.body === undefined) { + continue; + } + const found: string[] = []; + const aliases: string[] = []; + for (const entry of topLevelEntries(platform.body)) { + // An entry with a `ref` is an alias (`["latest"] = { ref = "1.6.43" }`, + // and the xlings-style named variants): `mcpp add` accepts an exact + // version only, so the ref is the version and the alias name is dropped. + const ref = entry.body === undefined ? null : /\bref\s*=\s*"((?:[^"\\]|\\.)*)"/.exec(entry.body); + if (ref !== null) { + aliases.push(ref[1]); + continue; + } + // A version entry names an archive: a numeric key, or one whose body + // carries a `url`/`sha256`. That is what keeps structural keys — `deps`, + // `cxxflags`, `runtime` — out of the version list, while non-semver + // revisions (`b10069.2`, the llama.cpp checkpoint tags) stay in it. + if (/^\d/.test(entry.key) || (entry.body !== undefined && /\b(?:url|sha256)\s*=/.test(entry.body))) { + found.push(entry.key); + } + } + const versions = [...new Set([...found, ...aliases])]; + if (versions.length > 0) { + out[platform.key] = versions; + } + } + return out; +} + +// ───────────────────────────────────────────────────────────── the entries ── + +/** Where one example project demonstrates a package. */ +export interface ExampleRef { + project: string; + /** The first test file, relative to the index root; the site calls this `path`. */ + path: string; + paths: string[]; + count: number; +} + +/** One source file of an example project, already read. */ +export interface CodeFile { + path: string; + text: string; +} + +/** One index descriptor, as the view needs it. */ +export interface LibraryEntry { + id: string; + namespace?: string; + name: string; + description?: string; + licenses: string[]; + repo?: string; + /** The registry directory name, e.g. `mcpplibs`. */ + registry: string; + /** Absolute path of the descriptor. */ + file: string; + /** The list's cheap guess; the detail page replaces it with the authoritative list. */ + surface?: Surface; + surfaces: Surface[]; + hasCnMirror: boolean; + hasExamples: boolean; + openkal?: OpenkalFacet; + /** The example project that demonstrates this package, when the index has one. */ + example?: ExampleRef; + /** The greatest version this platform has, from the descriptor text. */ + version?: string; + versions: Record; + /** Declared in the workspace's `mcpp.toml`. */ + added: boolean; + /** Set when the descriptor text could not be read at all. */ + unreadable?: boolean; +} + +export interface DescriptorInput { + /** The descriptor's base name, used as the fallback identity. */ + fileName: string; + registry: string; + file: string; + text: string; + platform?: string; + example?: ExampleRef; + openkal?: OpenkalFacet; + added?: boolean; +} + +/** + * Assemble one entry from its text. The only place a descriptor becomes a row: + * the identity falls back to the file name, the surface to the cheap text + * guess, and every optional field is omitted rather than filled with a guess. + */ +export function descriptorEntry(input: DescriptorInput): LibraryEntry { + const fields = parseDescriptorLua(input.text); + const identity = descriptorId(fields, input.fileName); + const versions = versionsFromDescriptorText(input.text); + const surface = surfaceFromDescriptorText(input.text); + const entry: LibraryEntry = { + id: identity.id, + ...(identity.namespace === undefined ? {} : { namespace: identity.namespace }), + name: identity.name, + ...(fields.description === undefined ? {} : { description: fields.description }), + licenses: [...fields.licenses], + ...(fields.repo === undefined ? {} : { repo: fields.repo }), + registry: input.registry, + file: input.file, + ...(surface === undefined ? {} : { surface }), + surfaces: surface === undefined ? [] : [surface], + hasCnMirror: hasCnMirror(input.text), + hasExamples: input.example !== undefined, + version: latestVersion(versions, input.platform), + versions, + added: input.added === true, + }; + if (input.example !== undefined) { + entry.example = input.example; + } + const openkal = input.openkal; + if (openkal !== undefined && (openkal.level !== undefined || openkal.kind !== undefined)) { + entry.openkal = openkal; + } + if (fields.namespace === undefined && fields.name === undefined) { + // Nothing at all was readable: the row still exists, from its file name, + // and the UI says the descriptor could not be read. + entry.unreadable = true; + } + return entry; +} + +/** Replace the cheap text guess with the authoritative parse's answer. */ +export function withAuthoritative( + entry: LibraryEntry, + info: XpkgInfo | undefined, + text: string, + platform: string | undefined, +): LibraryEntry { + if (info === undefined) { + return entry; + } + const surfaces = mergeSurfaces(info, text); + const next: LibraryEntry = { + ...entry, + surfaces, + versions: info.versions, + version: latestVersion(info.versions, platform), + }; + if (surfaces[0] === undefined) { + delete next.surface; + } else { + next.surface = surfaces[0]; + } + return next; +} + +// ─────────────────────────────────────────────────────────────── examples ── + +/** + * The dependency ids a manifest declares, at any nesting depth. + * + * Mirrors the site generator's `_collect_dependencies`: a bare + * `tinyhttps = "…"` resolves in the index's default namespace, so it names both + * `tinyhttps` and `mcpplibs.tinyhttps`; `[dependencies.compat]` with + * `argparse = "3.2"` names `compat.argparse`; `[target.'cfg(linux)'.dependencies…]` + * is found too, because a dependency a platform gate hides is still the package + * that example demonstrates. + * + * The same reader serves two callers: the example index (one `mcpp.toml` per + * project under `tests/examples`) and the workspace's own `mcpp.toml`, which is + * how the "already declared" filter knows what is added. + */ +export function declaredDependencies(tomlText: string): string[] { + const out = new Set(); + const bare = new Set(); + const header = /^\s*\[([^\]]+)\]\s*$/; + let prefix = ""; + let collecting = false; + const lines = tomlText.split(/\r?\n/); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index].replace(/#.*$/, "").trim(); + const section = header.exec(line); + if (section !== null) { + const path = section[1] + .split(".") + .map((part) => part.trim().replace(/^['"]|['"]$/g, "")) + .filter((part) => part.length > 0); + const at = path.findIndex((part) => part === "dependencies" || part === "dev-dependencies"); + collecting = at !== -1; + prefix = at !== -1 && path.length > at + 1 ? path[at + 1] : ""; + continue; + } + if (!collecting) { + continue; + } + // A dependency table can be written inline (`compat = { argparse = "3.2" }`) + // or across several lines; both are accumulated before the keys are read. + let statement = line; + while (bracesOpen(statement) > 0 && index + 1 < lines.length) { + index += 1; + statement += ` ${lines[index].replace(/#.*$/, "").trim()}`; + } + if (statement.length === 0) { + continue; + } + const assignment = /^([A-Za-z0-9_.\-]+)\s*=\s*(.*)$/.exec(statement); + if (assignment === null) { + continue; + } + const key = assignment[1]; + const value = assignment[2].trim(); + if (value.startsWith("{")) { + // A namespace table: every key inside names a package in it. + for (const inner of value.matchAll(/([A-Za-z0-9_.\-]+)\s*=/g)) { + addDependency(out, bare, prefix.length > 0 ? `${prefix}.${key}.${inner[1]}` : `${key}.${inner[1]}`, false); + } + continue; + } + if (!/^[A-Za-z_][\w\-]*$/.test(key)) { + // `ns.name = "1.0"`: already a fully-qualified id. + addDependency(out, bare, key, false); + continue; + } + // Under `[dependencies.compat]` the key is a bare name in that namespace. + addDependency(out, bare, prefix.length > 0 ? `${prefix}.${key}` : key, prefix.length === 0); + } + for (const name of bare) { + out.add(name); + out.add(`mcpplibs.${name}`); + } + // Sorted: the answer is a set, and a stable order keeps callers and tests honest. + return [...out].sort(); +} + +/** One dependency name, plus the unqualified form when it may resolve by default. */ +function addDependency(out: Set, bare: Set, id: string, unqualified: boolean): void { + if (unqualified) { + bare.add(id); + } + out.add(id); +} + +/** How many `{` are still open on one (possibly concatenated) TOML line. */ +function bracesOpen(text: string): number { + let depth = 0; + let index = 0; + while (index < text.length) { + const char = text[index]; + if (char === '"' || char === "'") { + const quote = char; + index += 1; + while (index < text.length && text[index] !== quote) { + index += text[index] === "\\" ? 2 : 1; + } + index += 1; + continue; + } + if (char === "{") { + depth += 1; + } else if (char === "}") { + depth -= 1; + } + index += 1; + } + return depth; +} + +// ─────────────────────────────────────────────── what the project already has ── + +/** One dependency a workspace manifest declares, with the constraint it names. */ +export interface DeclaredDependencyEntry { + id: string; + /** + * The constraint text (`3.2`, `^1.0`) when the value is a plain string. + * A table value — `{ path = "…" }`, a git source — names no version, and the + * project's lock file is the next place to look. + */ + version?: string; + /** Declared under `[dev-dependencies]` (or a feature only tests pull in). */ + dev: boolean; +} + +/** + * The version-aware sibling of `declaredDependencies` (§22): the detail page + * asks "which version does this project already ask for", which an id-only + * list cannot answer. Same walk, same shapes — bare keys stay bare here + * (`argparse` is not expanded to `compat.argparse`), because the matching rule + * of the only caller (`installedFor`) compares against both forms anyway. + */ +export function declaredDependencyEntries(tomlText: string): DeclaredDependencyEntry[] { + const out: DeclaredDependencyEntry[] = []; + const seen = new Set(); + const push = (id: string, version: string | undefined, dev: boolean): void => { + if (id.length === 0 || seen.has(`${dev ? "d" : "r"}:${id}`)) { + return; + } + seen.add(`${dev ? "d" : "r"}:${id}`); + out.push(version === undefined ? { id, dev } : { id, version, dev }); + }; + const header = /^\s*\[([^\]]+)\]\s*$/; + let dev = false; + let prefix = ""; + let collecting = false; + const lines = tomlText.split(/\r?\n/); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index].replace(/#.*$/, "").trim(); + const section = header.exec(line); + if (section !== null) { + const path = section[1] + .split(".") + .map((part) => part.trim().replace(/^['"]|['"]$/g, "")) + .filter((part) => part.length > 0); + const at = path.findIndex((part) => part === "dependencies" || part === "dev-dependencies"); + collecting = at !== -1; + dev = path[at] === "dev-dependencies"; + prefix = at !== -1 && path.length > at + 1 ? path[at + 1] : ""; + continue; + } + if (!collecting) { + continue; + } + let statement = line; + while (bracesOpen(statement) > 0 && index + 1 < lines.length) { + index += 1; + statement += ` ${lines[index].replace(/#.*$/, "").trim()}`; + } + if (statement.length === 0) { + continue; + } + const assignment = /^([A-Za-z0-9_.\-]+)\s*=\s*(.*)$/.exec(statement); + if (assignment === null) { + continue; + } + const key = assignment[1]; + const value = assignment[2].trim(); + const version = /^"([^"]*)"$/.exec(value); + if (version !== null) { + push(prefix.length > 0 ? `${prefix}.${key}` : key, version[1], dev); + continue; + } + if (value.startsWith("{")) { + // Three shapes live inside braces. A namespace table + // (`compat = { argparse = "3.2" }`) names `compat.argparse` with that + // version. A source table (`counters = { path = "../counters" }`, git + // tables) names `counters` with no version at all; a `version` key + // inside one is honoured, the other source fields are not facts about + // the package. + const SOURCE_KEYS = new Set(["path", "git", "url", "branch", "rev", "tag"]); + const pairs = [...value.matchAll(/([A-Za-z0-9_.\-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^,}]+))/g)] + .map((match) => ({ key: match[1], value: (match[2] ?? match[3] ?? match[4] ?? "").trim() })) + .filter((pair) => pair.key !== undefined); + const named = pairs.filter((pair) => !SOURCE_KEYS.has(pair.key) && pair.key !== "version"); + if (named.length > 0) { + for (const pair of named) { + push( + prefix.length > 0 ? `${prefix}.${key}.${pair.key}` : `${key}.${pair.key}`, + pair.value.length > 0 ? pair.value : undefined, + dev, + ); + } + } else { + const tableVersion = pairs.find((pair) => pair.key === "version" && pair.value.length > 0)?.value; + push(prefix.length > 0 ? `${prefix}.${key}` : key, tableVersion, dev); + } + continue; + } + push(prefix.length > 0 ? `${prefix}.${key}` : key, undefined, dev); + } + return out; +} + +/** One package a build resolved, straight out of `mcpp.lock`. */ +export interface LockPackage { + id: string; + version: string; +} + +/** + * `mcpp.lock` (format `version = 2`): one `[package.""]` table per + * resolved package, with `namespace` and `version` beside it — measured, not + * assumed, against a real lock written by `mcpp 2026.9.30.2`. The file's own + * header says the rest: dev-dependencies are excluded, and only index-resolved + * packages appear, so a path or git dependency is never an answer here. + */ +export function lockPackageVersions(lockText: string): LockPackage[] { + const out: LockPackage[] = []; + const header = /^\s*\[package\."?([A-Za-z0-9_.\-]+)"?\]\s*$/; + let namespace: string | undefined; + let version: string | undefined; + let name: string | undefined; + const flush = (): void => { + if (name !== undefined && version !== undefined) { + // The key is the short name (`[package."openkal"]` + namespace). A + // fully-qualified key also occurs (`[package."mcpplibs.cmdline"]`): when + // the namespace is only the name's own head, the name already is the id. + let id = + namespace === undefined || namespace.length === 0 ? name : `${namespace}.${name}`; + const dot = name.lastIndexOf("."); + if (namespace !== undefined && dot > 0 && namespace === name.slice(0, dot)) { + id = name; + } + out.push({ id, version }); + } + namespace = undefined; + version = undefined; + name = undefined; + }; + for (const raw of lockText.split(/\r?\n/)) { + const line = raw.replace(/#.*$/, "").trim(); + if (line.length === 0) { + continue; + } + const section = header.exec(line); + if (section !== null) { + flush(); + name = section[1]; + continue; + } + if (/^\s*\[/.test(line)) { + // Some other table: whatever half-read package there was is over. + flush(); + continue; + } + if (name === undefined) { + continue; + } + const assignment = /^([A-Za-z_][\w]*)\s*=\s*"([^"]*)"/.exec(line); + if (assignment === null) { + continue; + } + if (assignment[1] === "namespace") { + namespace = assignment[2]; + } else if (assignment[1] === "version") { + version = assignment[2]; + } + } + flush(); + return out; +} + +/** The project's own answer about one package (§20.1, wired in §22). */ +export interface InstalledDependency { + /** The constraint the manifest asks for, or the version a build resolved. */ + version: string; + /** Declared under `[dev-dependencies]`. */ + dev: boolean; +} + +/** + * Whether the project already depends on `id`, and on which version: + * `mcpp.toml` first — the matching rule is §20.1's, the key equals the id or + * is its last segment, because `mcpp add compat.argparse` writes the short + * `argparse = "3.2"` — then `mcpp.lock`, for a dependency whose manifest value + * named no version. `undefined` is a real answer: the project does not have it. + */ +export function installedFor( + id: string, + tomlText: string, + lockText: string, +): InstalledDependency | undefined { + const short = id.slice(id.lastIndexOf(".") + 1); + for (const entry of declaredDependencyEntries(tomlText)) { + if ((entry.id === id || entry.id === short) && entry.version !== undefined) { + return { version: entry.version, dev: entry.dev }; + } + } + for (const locked of lockPackageVersions(lockText)) { + if (locked.id === id || locked.id === short) { + return { version: locked.version, dev: false }; + } + } + return undefined; +} + +/** One example project's manifest and its test files, already read. */ +export interface ExampleInput { + project: string; + text: string; + /** Paths relative to the index root, in the order the caller found them. */ + sources: string[]; +} + +/** + * package id → the example project that consumes it. + * + * The site generator's `_scan_examples` contract: `{project, path, paths[], + * count}`, keyed by every dependency id the project's manifest names. Several + * projects may demonstrate one package; the first (sorted) wins, which is what + * the generator does too. + */ +export function exampleCatalog(inputs: readonly ExampleInput[]): Map { + const catalog = new Map(); + const sorted = [...inputs].sort((a, b) => (a.project < b.project ? -1 : a.project > b.project ? 1 : 0)); + for (const input of sorted) { + if (input.sources.length === 0) { + continue; + } + const ref: ExampleRef = { + project: input.project, + path: input.sources[0], + paths: [...input.sources], + count: input.sources.length, + }; + for (const id of declaredDependencies(input.text)) { + if (!catalog.has(id)) { + catalog.set(id, ref); + } + } + } + return catalog; +} + +/** The `import x.y;` / `#include ` lines, in file order. */ +export interface UsageLine { + file: string; + /** 1-based, so it can be shown beside the code. */ + line: number; + text: string; +} + +const IMPORT_LINE = /^\s*import\s+[A-Za-z_][\w.]*\s*;/; +const INCLUDE_LINE = /^\s*#include\s*[<"][^>"]+[>"]/; + +/** The interface lines the site files a package under a `SURFACE` by. */ +export function usageLines(files: readonly CodeFile[]): UsageLine[] { + const out: UsageLine[] = []; + for (const file of files) { + file.text.split(/\r?\n/).forEach((text, index) => { + if (IMPORT_LINE.test(text) || INCLUDE_LINE.test(text)) { + out.push({ file: file.path, line: index + 1, text: text.replace(/\s+$/, "") }); + } + }); + } + return out; +} + +/** Regex metacharacters are literal in a package name, never a pattern. */ +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * The `import` / `#include` lines a reader of **this** package writes, out of + * an example project's real code (§22): a line counts when it names the + * package — the dotted id or its short name as a word — so `import std;` and + * the example's own other dependencies stay out. Deduplicated, capped at + * four: this is a hint beside the command, not a second code section. + */ +export function usageLinesFor(files: readonly CodeFile[], id: string): string[] { + const short = id.slice(id.lastIndexOf(".") + 1); + const dotted = new RegExp(`\\b${escapeRegExp(id)}\\b`); + const bare = new RegExp(`\\b${escapeRegExp(short)}\\b`); + const out: string[] = []; + for (const line of usageLines(files)) { + if ((dotted.test(line.text) || bare.test(line.text)) && !out.includes(line.text)) { + out.push(line.text); + if (out.length >= 4) { + break; + } + } + } + return out; +} + +/** + * The usage lines to show when no example project states the real ones (§22). + * + * The module name follows the **dotted id** — `import nlohmann.json;`, in that + * descriptor's own words (`export module nlohmann.json;`) — except the default + * `mcpplibs.` namespace, whose packages export their short name: measured in a + * real project (`openkal = "0.12.0"` in the manifest, `import openkal.types;` + * in the sources) and in the cmdline descriptor. A header package's real path + * lives inside the upstream archive and is unknowable offline, so the index + * site's own muted placeholder answers (`#include `) rather than an + * invented path. `tool` and `external` packages are not imported at all; their + * surface badge already says what they are. + */ +export function syntheticUsageLines(surfaces: readonly Surface[], id: string): string[] { + if (surfaces.includes("module")) { + const short = id.slice(id.lastIndexOf(".") + 1); + const moduleName = id.startsWith("mcpplibs.") ? short : id; + return [`import ${moduleName};`]; + } + if (surfaces.includes("header")) { + return [SURFACE_TEXT.header.usage]; + } + return []; +} + +/** A window of an example file around one interface line. */ +export interface CodeSnippet { + file: string; + /** 1-based line number of the first line of `lines`. */ + startLine: number; + lines: string[]; + /** The 1-based line the interface line is on. */ + usageLine: number; +} + +export interface SnippetOptions { + /** Lines of context before and after the interface line. */ + context?: number; + maxSnippets?: number; + maxLines?: number; +} + +/** + * The example code to show: one window per interface line, with overlapping + * windows merged, capped so a package with many test files cannot push the + * detail page's buttons off the screen. This is the *real* code from + * `tests/examples//tests/*.cpp` — CI builds and runs it. + */ +export function codeSnippets(files: readonly CodeFile[], options: SnippetOptions = {}): CodeSnippet[] { + const context = Math.max(0, options.context ?? 2); + const maxSnippets = Math.max(1, options.maxSnippets ?? 3); + const maxLines = Math.max(1, options.maxLines ?? 24); + const snippets: CodeSnippet[] = []; + for (const file of files) { + const lines = file.text.split(/\r?\n/); + const anchors: number[] = []; + lines.forEach((text, index) => { + if (IMPORT_LINE.test(text) || INCLUDE_LINE.test(text)) { + anchors.push(index); + } + }); + for (const anchor of anchors) { + const from = Math.max(0, anchor - context); + const to = Math.min(lines.length, anchor + context + 1); + const last = snippets[snippets.length - 1]; + if (last !== undefined && last.file === file.path && from <= last.startLine - 1 + last.lines.length) { + // Overlaps or touches the previous window: extend it instead of + // repeating its lines. + const merged = lines.slice(last.startLine - 1, to); + last.lines = merged.slice(0, maxLines); + continue; + } + snippets.push({ + file: file.path, + startLine: from + 1, + lines: lines.slice(from, Math.min(to, from + maxLines)), + usageLine: anchor + 1, + }); + if (snippets.length >= maxSnippets) { + return snippets; + } + } + } + return snippets; +} + +// ─────────────────────────────────────────────────────────── highlighting ── + +export type TokenKind = "plain" | "comment" | "string" | "preprocessor" | "keyword" | "punctuation"; + +export interface Token { + kind: TokenKind; + text: string; +} + +/** + * A deliberately small C++ tokenizer for the example block: enough for token + * *colours*, not a parser. Comments, string and character literals, the + * preprocessor directive, a short keyword list and punctuation; everything else + * is plain text. The renderer escapes each token, so no token can become markup. + */ +const KEYWORDS = new Set([ + "import", "export", "module", "include", "define", "ifdef", "ifndef", "endif", + "namespace", "using", "template", "typename", "class", "struct", "enum", "public", + "private", "protected", "static", "inline", "const", "constexpr", "consteval", + "auto", "void", "bool", "char", "int", "long", "short", "unsigned", "signed", + "float", "double", "return", "if", "else", "for", "while", "switch", "case", + "break", "continue", "true", "false", "nullptr", "new", "delete", "try", "catch", + "throw", "this", "operator", "noexcept", "override", "final", "co_await", + "co_return", "co_yield", "requires", "concept", "friend", "virtual", "explicit", +]); + +const PUNCTUATION = new Set([";", "(", ")", "{", "}", "[", "]", ",", "<", ">", ":", "*", "&", "=", "#", ".", "+", "-", "/", "!", "|", "%", "^", "~", "?"]); + +export function tokenizeCppLine(line: string): Token[] { + const tokens: Token[] = []; + let plain = ""; + const flush = (): void => { + if (plain.length > 0) { + tokens.push({ kind: "plain", text: plain }); + plain = ""; + } + }; + const push = (kind: TokenKind, text: string): void => { + flush(); + tokens.push({ kind, text }); + }; + let index = 0; + while (index < line.length) { + const char = line[index]; + if (char === "/" && line[index + 1] === "/") { + push("comment", line.slice(index)); + break; + } + if (char === '"' || char === "'") { + const quote = char; + let end = index + 1; + while (end < line.length) { + if (line[end] === "\\") { + end += 2; + continue; + } + if (line[end] === quote) { + end += 1; + break; + } + end += 1; + } + push("string", line.slice(index, Math.min(end, line.length))); + index = end; + continue; + } + if (/[A-Za-z_]/.test(char)) { + let end = index; + while (end < line.length && /[\w]/.test(line[end])) { + end += 1; + } + const word = line.slice(index, end); + if (KEYWORDS.has(word)) { + push("keyword", word); + } else { + plain += word; + } + index = end; + continue; + } + if (PUNCTUATION.has(char)) { + push("punctuation", char); + index += 1; + continue; + } + plain += char; + index += 1; + } + flush(); + return tokens; +} + +// ────────────────────────────────────────────────────────────── openkal ── + +export interface OpenkalTarget { + kind?: string; + status?: string; +} + +export interface OpenkalMember { + packages: string[]; + portable: boolean; + targets: Record; +} + +export interface OpenkalIndex { + members: Record; + measured?: string; +} + +/** + * `.xpkgindex/openkal-compat.json`, decoded. The file is a *measurement* log + * (`tests/openkal/compat.py` writes it), not a descriptor field, which is why + * nothing about it is inferred from the Lua text. A missing or malformed file + * yields `undefined`, and the view then shows no openkal badge at all. + */ +export function parseOpenkalJson(text: string): OpenkalIndex | undefined { + let value: unknown; + try { + value = JSON.parse(text); + } catch { + return undefined; + } + if (!isRecord(value) || !isRecord(value.members)) { + return undefined; + } + const members: Record = {}; + for (const [name, raw] of Object.entries(value.members)) { + if (!isRecord(raw)) { + continue; + } + const targets: Record = {}; + if (isRecord(raw.targets)) { + for (const [target, entry] of Object.entries(raw.targets)) { + if (!isRecord(entry)) { + continue; + } + targets[target] = { + ...(typeof entry.kind === "string" ? { kind: entry.kind } : {}), + ...(typeof entry.status === "string" ? { status: entry.status } : {}), + }; + } + } + members[name] = { + packages: Array.isArray(raw.packages) ? raw.packages.filter((id): id is string => typeof id === "string") : [], + portable: raw.portable !== false, + targets, + }; + } + return { + members, + ...(typeof value.measured === "string" ? { measured: value.measured } : {}), + }; +} + +const OPENKAL_RANK: Readonly> = { fails: 0, builds: 1, runs: 2 }; + +/** The openkal facet a package is filed under, or nothing. */ +export interface OpenkalFacet { + level?: "ecosystem" | "compat"; + kind?: "posix" | "platform"; +} + +/** + * The facet for one package id, mirroring the generator's `_openkal_level` / + * `_openkal_kind`: + * + * - `openkal-ecosystem` from the family list; + * - `openkal-compat` only when some measured target **runs** (a package measured + * only to build, or to fail, is filed under neither — the site says so in as + * many words); + * - `platform` wins over `posix`, because the package-level kind is the + * strictest measured target's answer, not a claim about every target. + */ +export function openkalFacetFor(index: OpenkalIndex | undefined, id: string): OpenkalFacet | undefined { + if (index === undefined) { + return undefined; + } + const facet: OpenkalFacet = {}; + if (OPENKAL_FAMILY.includes(id)) { + facet.level = "ecosystem"; + } + const targets: OpenkalTarget[] = []; + let best = -1; + for (const member of Object.values(index.members)) { + if (!member.packages.includes(id)) { + continue; + } + for (const target of Object.values(member.targets)) { + targets.push(target); + const rank = OPENKAL_RANK[target.status ?? ""] ?? -1; + if (rank > best) { + best = rank; + } + } + } + if (facet.level === undefined && best === 2) { + facet.level = "compat"; + } + if (targets.some((target) => target.kind === "platform")) { + facet.kind = "platform"; + } else if (targets.some((target) => target.kind === "posix")) { + facet.kind = "posix"; + } + if (facet.level === undefined && facet.kind === undefined) { + return undefined; + } + return facet; +} + +/** The badges one entry earns, in the site's order (§12.5). */ +export function badgesOf(input: { + hasExamples?: boolean; + hasCnMirror?: boolean; + openkal?: OpenkalFacet; +}): BadgeKey[] { + const out: BadgeKey[] = []; + if (input.hasExamples === true) { + out.push("examples"); + } + if (input.hasCnMirror === true) { + out.push("cn"); + } + if (input.openkal?.level === "ecosystem") { + out.push("openkal-ecosystem"); + } else if (input.openkal?.level === "compat") { + out.push("openkal-compat"); + } + if (input.openkal?.kind === "posix") { + out.push("openkal-posix"); + } else if (input.openkal?.kind === "platform") { + out.push("openkal-platform"); + } + return out; +} + +/** One dependency a descriptor declares for itself. */ +export interface DescriptorDependency { + id: string; + version?: string; +} + +/** + * The dependencies a descriptor's own inline manifest names. + * + * Two shapes occur in this index: a table keyed by package id + * (`deps = { ["compat.vulkan"] = "1.4.357.3" }`) and, in the xlings-style + * descriptors, a list of `ns:name@version` strings. Only the **declared** edge is + * read — the resolved version lives in a project's `mcpp.lock`, which is not this + * package's file, so it is never invented here. + */ +export function descriptorDependencies(text: string): DescriptorDependency[] { + const clean = stripLuaComments(text); + const body = keyBlock(clean, "deps"); + if (body === undefined) { + return []; + } + const out: DescriptorDependency[] = []; + const seen = new Set(); + const push = (id: string, version: string | undefined): void => { + const key = id.trim(); + if (key.length === 0 || seen.has(key) || out.length >= 100) { + return; + } + seen.add(key); + out.push(version === undefined || version.length === 0 ? { id: key } : { id: key, version }); + }; + for (const entry of topLevelEntries(body)) { + if (entry.value !== undefined) { + push(entry.key, entry.value); + continue; + } + const version = entry.body === undefined ? null : /\bversion\s*=\s*"([^"]*)"/.exec(entry.body); + push(entry.key, version === null ? undefined : version[1]); + } + if (out.length === 0) { + // The xlings-style list form: `deps = { "xim:gtk4@4.16.13" }`. Only entries + // that carry the `ns:name@version` shape count; a bare string is not a + // dependency id in this index. + for (const literal of stringLiterals(body)) { + const at = literal.lastIndexOf("@"); + if (at > 0 && literal.slice(0, at).includes(":")) { + push(literal.slice(0, at), literal.slice(at + 1)); + } else if (literal.includes(":")) { + push(literal, undefined); + } + } + } + return out; +} + +// ──────────────────────────────────────────────────── cross-registry search ── + +/** One hit from `mcpp search `. */ +export interface SearchHit { + id: string; + description?: string; + version?: string; +} + +/** + * The one place this extension reads **human** output, and it is behind a + * setting that is off by default (plan §10.2, risk table "第二处解析人类输出", + * upstream request U.8 `mcpp search --format json`). + * + * `mcpp search argparse` prints one hit per line: + * + * ```text + * compat:argparse argparse — header-only argument parser for modern C++ (3.2) + * ``` + * + * The reader is deliberately forgiving — a line that does not match is dropped, + * and an empty result is an empty list, never an error. The caller keeps the + * local index results either way, so a change in this format degrades to "no + * extra hits" instead of to a broken view. + */ +export function parseSearchOutput(text: string): SearchHit[] { + const hits: SearchHit[] = []; + const seen = new Set(); + for (const raw of text.split(/\r?\n/)) { + const line = raw.trim(); + if (line.length === 0) { + continue; + } + const match = /^([A-Za-z0-9_.\-]+)[:.]([A-Za-z0-9_.\-]+)\s+(.*)$/.exec(line); + if (match === null) { + continue; + } + const id = `${match[1]}.${match[2]}`; + if (seen.has(id)) { + continue; + } + seen.add(id); + const tail = match[3].trim(); + const version = /\(([^()]*)\)\s*$/.exec(tail); + const description = (version === null ? tail : tail.slice(0, version.index)).trim(); + hits.push({ + id, + ...(description.length === 0 ? {} : { description }), + ...(version === null || version[1].trim().length === 0 ? {} : { version: version[1].trim() }), + }); + if (hits.length >= 100) { + break; + } + } + return hits; +} + +// ─────────────────────────────────────────────────────── mcpp self env ── + +/** The shape of `mcpp self env --format json`, as far as this extension reads it. */ +export interface SelfEnv { + /** `$MCPP_HOME`, the directory everything else hangs off. */ + mcppHome?: string; + /** `/registry`, when mcpp states it. */ + registry?: string; +} + +/** + * `mcpp self env --format json`, read for the one field that matters. + * + * This is a **machine** shape, not human text, so it is held to the detection + * rule `src/cli/protocol.ts` states — presence *and* type of `schemaVersion`, + * plus the `kind` this reader owns — and anything else is refused rather than + * guessed at. The library view falls back to its own globs when this returns + * `undefined`, so a version of mcpp that renames a field costs a fallback and + * never a crash. + */ +export function parseSelfEnv(text: string): SelfEnv | undefined { + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + return undefined; + } + if (!isRecord(parsed) || typeof parsed.schemaVersion !== "number" || parsed.kind !== "mcpp.env") { + return undefined; + } + const data = parsed.data; + if (!isRecord(data)) { + return undefined; + } + const home = data.mcppHome; + const registry = data.registry; + return { + ...(typeof home === "string" && home.trim().length > 0 ? { mcppHome: home.trim() } : {}), + ...(typeof registry === "string" && registry.trim().length > 0 ? { registry: registry.trim() } : {}), + }; +} + +// ───────────────────────────────────────────────────────────────── search ── + +/** The fields a search looks at — never the whole Lua text. */ +export function searchText(entry: LibraryEntry): string { + return [entry.id, entry.description ?? "", entry.licenses.join(" "), entry.repo ?? "", entry.registry].join(" ").toLowerCase(); +} + +export function matchesQuery(entry: LibraryEntry, query: string): boolean { + const needle = query.trim().toLowerCase(); + if (needle.length === 0) { + return true; + } + const haystack = searchText(entry); + return needle.split(/\s+/).every((part) => haystack.includes(part)); +} + +/** The surface's label, localized by the caller and falling back to the vocabulary. */ +export function surfaceLabel(surface: Surface, label: (key: string) => string): string { + const text = SURFACE_TEXT[surface]; + if (text.uiKey === undefined) { + return text.label; + } + const localized = label(text.uiKey); + return localized === undefined || localized.length === 0 || localized === text.uiKey ? text.label : localized; +} + +/** + * Decode `mcpp xpkg parse --json` output for a caller that has the descriptor's + * text too. Kept here so the vscode halves import one library module; the + * parsing itself lives in `xpkg.ts`. + */ +export { parseXpkgJson, versionGroups, versionsFor, surfacesOf, SURFACE_TEXT, SURFACES }; +export type { Surface, XpkgInfo }; diff --git a/src/library/libraryHtml.ts b/src/library/libraryHtml.ts new file mode 100644 index 0000000..99feba8 --- /dev/null +++ b/src/library/libraryHtml.ts @@ -0,0 +1,445 @@ +/** + * The library sidebar's document, as a pure function (plan §9.3, §10.2, §12). + * + * `src/library/libraryView.ts` resolves the index into a `LibraryModel`; this + * module turns that into one self-contained HTML document. No `vscode`, no file + * system, no network — which is what makes the interesting parts (escaping, the + * strict CSP, the filter/search behaviour, the clamped description) unit + * testable. + * + * House rules, the same ones `src/config/panelHtml.ts` pins down: + * + * - **Strict CSP.** `default-src 'none'` plus exactly three sources: the + * stylesheet, the nonced inline script and the webview's own image source. + * There are no external resources and no inline `style=` attributes. + * - **No hard-coded copy.** Every user-visible label is looked up in + * `LibraryModel.ui`, which the caller has already localized; a key the caller + * forgot degrades to the English fallback beside it, not to a blank control. + * - **No unescaped interpolation.** Every value that reaches the markup goes + * through `escapeHtml`; the client script only ever writes through + * `textContent` and `setAttribute`, never `innerHTML`. + * - **State is text plus shape, not colour.** "已添加" is a labelled badge and a + * `data-added` attribute; the stylesheet may colour it, but the row reads the + * same in a high-contrast theme. + * + * The one deliberate performance choice: the search box + * are applied **in the client**, on rows that are already in the document, so + * typing never round-trips through the extension host and never moves the caret. + * The messages still go back (`search`, `filter`) so the host knows what the + * reader is looking at; the host re-renders only when the *data* changes — + * a refresh, the cross-registry toggle, or a new index revision. + */ + +import { BADGE_UI, SURFACE_TEXT, surfaceLabel, type BadgeKey, type Surface } from "./indexModel"; + +/** One result row, already resolved for display. */ +export interface LibraryRow { + id: string; + namespace?: string; + name: string; + /** The greatest version this platform has, or `undefined`. */ + version?: string; + surface?: Surface; + /** Every surface the descriptor supports, not only the leading one. */ + surfaces: Surface[]; + badges: BadgeKey[]; + /** + * The lower-cased text a query is matched against, built by the host through + * `indexModel.searchText`. It travels in the row so the client's filter and the + * host's `visibleEntries` cannot drift apart on what "matches" means. + */ + haystack: string; + description?: string; + /** Declared in the workspace's `mcpp.toml`. */ + added: boolean; + /** The descriptor's own fields could not be read; identity comes from the file name. */ + unreadable: boolean; + /** Came from `mcpp search`, not from the local index. */ + crossRegistry?: boolean; +} + +export interface LibraryModel { + /** Everything is already localized by the caller. */ + ui: Record; + rows: LibraryRow[]; + query: string; + networkSearch: boolean; + /** + * The setting the toggle writes. Carried in the model so the client and the + * host cannot disagree about which key the control drives. + */ + networkSearchSetting: string; + /** The data source, in plain words, for the footer. */ + dataSource: string; + /** `{0}` visible of `{1}` known, already localized; the header count uses it. */ + countTemplate: string; + /** Rows before filtering; the count line shows it against what is visible. */ + total: number; + /** Set when there is nothing to show, already localized. */ + notice?: string; +} + +export interface LibraryAssets { + cspSource: string; + nonce: string; + styleUri: string; +} + +/** + * The `ui` keys the renderer reads, so the caller and the renderer cannot drift + * apart on a string. `libraryView.ts` fills all of them through `t()`. + */ +export const LIBRARY_UI = { + htmlLang: "library.htmlLang", + title: "library.title", + search: "library.search", + networkSearch: "library.networkSearch", + networkSearchHint: "library.networkSearch.hint", + versionLatest: "library.version.latest", + surfaceExternal: SURFACE_TEXT.external.uiKey ?? "library.surface.external", + badgeExamples: BADGE_UI.examples.key, + badgeCn: BADGE_UI.cn.key, + badgeOpenkalEcosystem: BADGE_UI["openkal-ecosystem"].key, + badgeOpenkalCompat: BADGE_UI["openkal-compat"].key, + badgeOpenkalPosix: BADGE_UI["openkal-posix"].key, + badgeOpenkalPlatform: BADGE_UI["openkal-platform"].key, + added: "library.added", + unreadable: "library.unreadable", + crossRegistry: "library.crossRegistry", + noResults: "library.noResults", + noIndex: "library.noIndex", + dataSource: "library.dataSource", + count: "library.count", + open: "library.open", + refresh: "library.refresh", +} as const; + +/** One badge key's `ui` key, so the renderer has a single lookup table. */ +const BADGE_KEY_UI: Readonly> = { + examples: LIBRARY_UI.badgeExamples, + cn: LIBRARY_UI.badgeCn, + "openkal-ecosystem": LIBRARY_UI.badgeOpenkalEcosystem, + "openkal-compat": LIBRARY_UI.badgeOpenkalCompat, + "openkal-posix": LIBRARY_UI.badgeOpenkalPosix, + "openkal-platform": LIBRARY_UI.badgeOpenkalPlatform, +}; + +/** + * What the document says to the host. + * + * There deliberately **no `ready` message**. The document is the data — every + * row, badge and count is already in it — so a "I have loaded" announcement can + * only invite the host to render the same thing again. It did exactly that, and + * because assigning `webview.html` reloads the document while every render mints + * a fresh CSP nonce, the page never matched the previous one: the view reloaded + * itself forever (flicker, unclickable rows, a pegged CPU). The host now also + * refuses to re-assign an identical document — both rules live in + * `src/webview/document.ts` (`WebviewDocument`) and `src/webview/render.ts` + * (`documentNeedsRender`) — so this cannot come back by accident. + */ +export type LibraryMessage = + | { type: "refresh" } + | { type: "search"; query: string } + | { type: "networkSearch"; enabled: boolean } + | { type: "open"; id: string }; + +export type UiLabel = (key: string) => string; + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * The CSP is an attribute value, but its single quotes are syntax: escaping + * them would make the document state a different policy than it means. Only the + * characters that could end the attribute or start a tag are escaped. + */ +function escapeCsp(value: string): string { + return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} + +/** ` name="value"`, or nothing when the value is absent. */ +function attribute(name: string, value: string | number | undefined): string { + return value === undefined ? "" : ` ${name}="${escapeHtml(String(value))}"`; +} + +/** ` name`, or nothing. Boolean HTML attributes only. */ +function flag(name: string, on: boolean): string { + return on ? ` ${name}` : ""; +} + +/** `{0}`-style substitution into an already-localized ui string. */ +function fill(label: UiLabel, key: string, args: readonly (string | number)[]): string { + return label(key).replace(/\{(\d+)\}/g, (whole, index: string) => { + const value = args[Number(index)]; + return value === undefined ? whole : String(value); + }); +} + +/** The badge labels, in `BADGE_UI` order. */ +export function badgeLabels(badges: readonly BadgeKey[], label: UiLabel): string[] { + return badges.map((badge) => label(BADGE_KEY_UI[badge])); +} + +function renderRow(row: LibraryRow, label: UiLabel): string { + const badges = badgeLabels(row.badges, label); + const openTitle = fill(label, LIBRARY_UI.open, [row.id]); + const surface = row.surface === undefined ? undefined : surfaceLabel(row.surface, label); + const version = + row.version === undefined ? "" : `${escapeHtml(fill(label, LIBRARY_UI.versionLatest, [row.version]))}`; + const secondary = [ + surface === undefined ? "" : `${escapeHtml(surface)}`, + ...badges.map((badge) => `${escapeHtml(badge)}`), + row.added ? `${escapeHtml(label(LIBRARY_UI.added))}` : "", + row.unreadable ? `${escapeHtml(label(LIBRARY_UI.unreadable))}` : "", + row.crossRegistry === true ? `${escapeHtml(label(LIBRARY_UI.crossRegistry))}` : "", + ] + .filter((part) => part.length > 0) + .join(""); + const description = row.description === undefined ? "" : row.description; + return [ + `
  • `, + ` `, + `
  • `, + ] + .filter((line) => line.length > 0) + .join("\n"); +} + +/** + * The search box, and the network toggle under it. + * + * There used to be a wrapped row of filter chips between them — one per + * namespace, one per surface, plus "Added" — which on a real index is three + * lines of buttons above the list, and the list is the view. The search box + * already matches the namespace (it is part of a row's haystack), so the + * taxonomy the chips offered is still one keystroke away; the "Added" state is + * on the row itself, as a badge. + */ +function renderToolbar(model: LibraryModel, label: UiLabel): string { + return [ + `
    `, + ` `, + ` `, + ` `, + `
    `, + ].join("\n"); +} + +function renderList(model: LibraryModel, label: UiLabel): string { + if (model.rows.length === 0) { + return `

    ${escapeHtml(model.notice ?? label(LIBRARY_UI.noResults))}

    `; + } + return [ + `
      `, + model.rows.map((row) => renderRow(row, label)).join("\n"), + `
    `, + ``, + ].join("\n"); +} + +/** + * The client. Dependency-free, no template literals of its own, and it only + * writes through `textContent`/`setAttribute`. + * + * Search is local: a row carries the text the search box matches against as a + * `data-haystack` attribute, and the query decides its `hidden` flag. The host is + * told the query, and answers with a whole new document only when the data + * behind it changed. + */ +function clientScript(initialModel: string): string { + return `(function () { + "use strict"; + var api = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : undefined; + var state = ${initialModel}; + var searchInput = document.getElementById("library-search"); + var networkInput = document.getElementById("library-network"); + var list = document.getElementById("library-list"); + var empty = document.getElementById("library-empty"); + var countElement = document.getElementById("library-count"); + + function post(message) { + if (api) { api.postMessage(message); } + } + + // Every whitespace-separated word has to appear in the row's haystack, which + // the host builds from the id, the name, the description and the usage labels. + function rowMatches(row, query) { + if (query.length === 0) { return true; } + var haystack = row.getAttribute("data-haystack") || ""; + var parts = query.split(/\\s+/); + for (var index = 0; index < parts.length; index += 1) { + if (parts[index].length > 0 && haystack.indexOf(parts[index]) < 0) { return false; } + } + return true; + } + + function apply() { + var query = searchInput ? searchInput.value.trim().toLowerCase() : ""; + var any = false; + var visible = 0; + if (list) { + var rows = list.querySelectorAll("[data-row]"); + for (var index = 0; index < rows.length; index += 1) { + var shown = rowMatches(rows[index], query); + rows[index].parentNode.hidden = !shown; + if (shown) { any = true; visible += 1; } + } + list.hidden = !any; + } + if (empty) { empty.hidden = any || !list; } + if (countElement) { + var template = state && state.countTemplate ? state.countTemplate : ""; + var total = state && state.total ? state.total : 0; + countElement.textContent = template.split("{0}").join(String(visible)).split("{1}").join(String(total)); + } + } + + if (searchInput) { + searchInput.addEventListener("input", function () { + apply(); + post({ type: "search", query: searchInput.value }); + }); + } + if (networkInput) { + networkInput.addEventListener("change", function () { + post({ type: "networkSearch", enabled: networkInput.checked === true }); + }); + } + document.addEventListener("click", function (event) { + var target = event.target; + if (!target || typeof target.closest !== "function") { return; } + var refresh = target.closest("[data-action]"); + if (refresh && refresh.getAttribute("data-action") === "refresh") { + post({ type: "refresh" }); + return; + } + var row = target.closest("[data-row]"); + if (row) { post({ type: "open", id: row.getAttribute("data-row") }); } + }); + apply(); +})();`; +} + +/** + * The little state the client needs, not the whole model. + * + * The rows are already in the document and the badges are already resolved, so + * embedding the model again would double the page for nothing. `<` is escaped in + * `embedJson`, so even this cannot close the script element. + */ +function clientState(model: LibraryModel): Record { + return { + countTemplate: model.countTemplate, + total: model.total, + query: model.query, + }; +} + +/** A model embedded in the page's own script; `<` is escaped so it cannot close it. */ +function embedJson(value: unknown): string { + return JSON.stringify(value) + .replace(/ model.ui[key] ?? key; + const csp = `default-src 'none'; style-src ${assets.cspSource}; script-src 'nonce-${assets.nonce}'; img-src ${assets.cspSource}`; + const visible = model.rows.filter((row) => rowMatchesClient(model, row)).length; + const count = fill(label, LIBRARY_UI.count, [visible, model.total]); + return ` + + + + + + +${escapeHtml(label(LIBRARY_UI.title))} + + +
    +

    ${escapeHtml(label(LIBRARY_UI.title))}

    +

    ${escapeHtml(count)}

    +
    +${renderToolbar(model, label)} +
    +${renderList(model, label)} +
    +
    + ${escapeHtml(model.dataSource)} + +
    + + + +`; +} + +/** + * The rows the initial query keeps. The live search is the client's + * `rowMatches`; this mirrors it for the header count, so the count the reader + * first sees matches the list beneath it. + */ +function rowMatchesClient(model: LibraryModel, row: LibraryRow): boolean { + const query = model.query.trim().toLowerCase(); + if (query.length === 0) { + return true; + } + return query.split(/\s+/).every((part) => part.length === 0 || row.haystack.includes(part)); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function nonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} + +/** + * Decode one `postMessage` payload. Webview input is untrusted: anything that is + * not exactly one of the four shapes is dropped, and the returned object is + * rebuilt so foreign fields never travel further. + */ +export function decodeLibraryMessage(raw: unknown): LibraryMessage | undefined { + if (!isRecord(raw)) { + return undefined; + } + switch (raw.type) { + case "refresh": + return { type: "refresh" }; + case "search": + return typeof raw.query === "string" ? { type: "search", query: raw.query } : undefined; + case "networkSearch": + return typeof raw.enabled === "boolean" ? { type: "networkSearch", enabled: raw.enabled } : undefined; + case "open": + return nonEmptyString(raw.id) ? { type: "open", id: raw.id } : undefined; + default: + return undefined; + } +} diff --git a/src/library/libraryView.ts b/src/library/libraryView.ts new file mode 100644 index 0000000..b156fcb --- /dev/null +++ b/src/library/libraryView.ts @@ -0,0 +1,462 @@ +/** + * The library sidebar: the `mcpp.library` view. + * + * The *model* is `src/library/libraryHtml.ts` plus `src/library/indexModel.ts`, + * both pure and unit-tested; the *reading* is `src/library/indexLocator.ts`, + * which caches per index revision. This file is the seam: it owns the webview's + * lifetime, the `t()` literals, the settings and the messages. + * + * Three deliberate boundaries: + * + * - **The list never spawns a process per package and never touches the + * network.** The descriptors are read as text; `mcpp xpkg parse --json` runs + * only from the detail page. The one exception is `mcpp search`, behind + * `mcpp.library.networkSearch`, which is **off by default** and whose output + * reader is the tolerant best-effort one in `indexModel.parseSearchOutput` + * (plan §10.2; upstream request U.8). + * - **One render, applied in place.** Filtering happens in the document; the + * host re-renders only when the data changed. A `webview.html` assignment + * would otherwise steal the caret on every keystroke. + * - **A failure still renders.** No index, no descriptors or a failed read all + * produce a document with a sentence in it, never a blank sidebar. + */ + +import * as vscode from "vscode"; + +import { runMcpp } from "../cli/process"; +import { read, write } from "../config/access"; +import { languagePreference, t } from "../i18n/t"; +import { localeFromEditorLanguage } from "../i18n/translate"; +import { WebviewDocument } from "../webview/document"; +import { badgesOf, searchText, parseSearchOutput, type LibraryEntry } from "./indexModel"; +import { loadSnapshot, type IndexRoot, type LibrarySnapshot } from "./indexLocator"; +import { + LIBRARY_UI, + decodeLibraryMessage, + renderLibraryHtml, + type LibraryModel, + type LibraryRow, +} from "./libraryHtml"; + +/** The view id `package.json` declares. */ +export const LIBRARY_VIEW_ID = "mcpp.library"; + +const MEDIA_DIRECTORY = "media"; +const STYLESHEET = "library.css"; + +/** The delay before a cross-registry search runs: one per pause, not per keystroke. */ +const SEARCH_DEBOUNCE_MS = 400; +const SEARCH_TIMEOUT_MS = 20_000; + +export interface LibraryViewDeps { + /** + * The workspace folder whose `mcpp.toml` decides which packages are "already + * declared". `undefined` is a normal state: the list then simply has none. + */ + projectRoot: () => string | undefined; + /** The configured executable, for the cross-registry search only. */ + mcppExecutable: () => string; + /** Opens the editor-area detail page for one package id. */ + openDetail: (id: string) => Promise | void; + /** Where `mcpp search` is logged; the same channel the other commands use. */ + output?: vscode.OutputChannel; + /** `vscode.workspace.isTrusted`; `mcpp search` never runs without it. */ + isTrusted: () => boolean; +} + +/** + * Register the view: the provider, its settings/save listeners and the + * `registerWebviewViewProvider` call that makes VS Code ask it for a document. + * + * That last call is the whole point — without it the view has no document at + * all, and every `refresh()` below is a no-op against `this.view === undefined`. + * It lives here, next to the provider it hands over, exactly like + * `registerCachePanel`; `extension.ts` only wires the two entry-point commands. + * + * `retainContextWhenHidden` is not supported by a webview view, which is why the + * render is idempotent: the document is rebuilt from the snapshot every time the + * view becomes visible again. + */ +export function registerLibraryView(context: vscode.ExtensionContext, deps: LibraryViewDeps): LibraryViewHandle { + const provider = new LibraryViewProvider(context, deps); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(LIBRARY_VIEW_ID, provider, { + webviewOptions: { retainContextWhenHidden: false }, + }), + provider, + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("mcpp.library")) { + void provider.refresh(); + } + }), + // `mcpp.toml` decides the "已添加" state, so saving it re-renders the list. + vscode.workspace.onDidSaveTextDocument((document) => { + if (document.uri.path.endsWith("/mcpp.toml") || document.uri.path.endsWith("\\mcpp.toml")) { + void provider.refresh(); + } + }), + ); + return { + refresh: () => provider.refresh(), + dispose: () => provider.dispose(), + }; +} + +/** + * What `extension.ts` keeps a reference to. + * + * `refresh()` is how a dependency change made **outside the editor** — `mcpp add` + * run by the detail page, or `mcpp index update` — reaches the list, because no + * document is saved in that path and therefore no save event fires. The provider + * itself is not handed back: `registerLibraryView` has already registered it, + * and a caller that wants to reach into the view would be reaching past the two + * methods below. + */ +export interface LibraryViewHandle { + refresh(): Promise; + dispose(): void; +} + +class LibraryViewProvider implements vscode.WebviewViewProvider, vscode.Disposable { + private view: vscode.WebviewView | undefined; + private snapshot: LibrarySnapshot | undefined; + private query = ""; + /** Cross-registry hits for the current query, from `mcpp search`. */ + private extra: LibraryRow[] = []; + private searchNote: string | undefined; + private searchTimer: NodeJS.Timeout | undefined; + private rendering: Promise | undefined; + /** + * The document on screen and the one CSP nonce it may be built with. Both + * live in `WebviewDocument`: the nonce is per view (a nonce per render would + * make every render a *different* document and defeat the comparison, which + * is how the reload loop started), and `paint()` refuses to re-assign a + * document that has not changed, because an assignment reloads the view. + */ + private readonly webviewDocument = new WebviewDocument(STYLESHEET); + + public constructor( + private readonly context: vscode.ExtensionContext, + private readonly deps: LibraryViewDeps, + ) {} + + public dispose(): void { + if (this.searchTimer !== undefined) { + clearTimeout(this.searchTimer); + this.searchTimer = undefined; + } + } + + public resolveWebviewView(view: vscode.WebviewView): void { + this.view = view; + // A resolved view is a fresh, empty webview; see the note in `onDidDispose`. + this.webviewDocument.invalidate(); + view.webview.options = { + enableScripts: true, + localResourceRoots: [vscode.Uri.joinPath(this.context.extensionUri, MEDIA_DIRECTORY)], + }; + view.webview.onDidReceiveMessage((raw: unknown) => { + void this.handle(raw); + }); + view.onDidDispose(() => { + if (this.view === view) { + this.view = undefined; + // The next `resolveWebviewView` gets a brand-new, empty webview: what was + // pushed to the old one says nothing about it, so the comparison in + // `paint()` must start from nothing again. + this.webviewDocument.invalidate(); + } + }); + // First paint from the cached snapshot, so the view is never blank while the + // index is being read. + this.paint(); + void this.refresh(); + } + + /** Re-read the index (from the cache when the revision is unchanged) and paint. */ + public refresh(): Promise { + if (this.rendering !== undefined) { + return this.rendering; + } + const projectRoot = this.deps.projectRoot(); + this.rendering = loadSnapshot(projectRoot === undefined ? {} : { projectRoot }) + .then((snapshot) => { + this.snapshot = snapshot; + }) + .catch((error: unknown) => { + this.deps.output?.appendLine(`mcpp library: ${error instanceof Error ? error.message : String(error)}`); + this.snapshot = { entries: [], roots: [], revision: "error" }; + }) + .then(() => { + this.rendering = undefined; + this.paint(); + }); + return this.rendering; + } + + private async handle(raw: unknown): Promise { + const message = decodeLibraryMessage(raw); + if (message === undefined) { + return; + } + switch (message.type) { + case "refresh": + await this.refresh(); + return; + case "search": + this.query = message.query; + this.scheduleSearch(); + return; + case "networkSearch": + await this.setNetworkSearch(message.enabled); + return; + case "open": + await this.deps.openDetail(message.id); + return; + default: + return; + } + } + + /** `mcpp.library.networkSearch`: persisted, then honoured on the next query. */ + private async setNetworkSearch(enabled: boolean): Promise { + this.extra = []; + this.searchNote = undefined; + try { + await write("mcpp.library.networkSearch", enabled, "user"); + } catch (error) { + this.deps.output?.appendLine( + `mcpp library: could not write mcpp.library.networkSearch: ${error instanceof Error ? error.message : String(error)}`, + ); + this.searchNote = t("The setting could not be written; the toggle was not saved."); + } + if (enabled) { + await this.runCrossRegistrySearch(); + } else { + this.paint(); + } + } + + private scheduleSearch(): void { + if (this.searchTimer !== undefined) { + clearTimeout(this.searchTimer); + } + if (!this.networkSearch()) { + this.extra = []; + this.searchNote = undefined; + return; + } + this.searchTimer = setTimeout(() => { + this.searchTimer = undefined; + void this.runCrossRegistrySearch(); + }, SEARCH_DEBOUNCE_MS); + } + + private networkSearch(): boolean { + return read("mcpp.library.networkSearch") === true; + } + + /** + * The cross-registry tier: `mcpp search `. + * + * Best effort by design (§10.2): it may use the network, its output is human + * text (upstream request U.8 asks for `--format json`), and any failure keeps + * the local results and *says so* rather than failing silently. Only ids the + * local index does not already have are added. + */ + private async runCrossRegistrySearch(): Promise { + const query = this.query.trim(); + if (query.length < 2) { + this.extra = []; + this.searchNote = undefined; + this.paint(); + return; + } + // `mcpp search` runs the configured executable and `mcpp.path` is + // resource-scoped: an untrusted workspace must not name the program, so the + // tier refuses and says so instead of silently showing nothing extra. + const result = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Window, title: t("Searching all registries…") }, + () => + runMcpp(this.deps.isTrusted(), this.deps.mcppExecutable(), ["search", query], this.deps.projectRoot(), { + timeoutMs: SEARCH_TIMEOUT_MS, + maxBufferMiB: read("mcpp.runtime.maxOutputMiB"), + }), + ); + if (result === undefined) { + this.extra = []; + this.searchNote = t("The workspace is not trusted; the other registries were not searched."); + this.paint(); + return; + } + if (result.exitCode !== 0) { + this.extra = []; + this.searchNote = t( + "Searching all registries failed (exit {0}); showing the local index only.", + result.exitCode, + ); + this.paint(); + return; + } + const known = new Set((this.snapshot?.entries ?? []).map((entry) => entry.id)); + this.extra = parseSearchOutput(result.stdout) + .filter((hit) => !known.has(hit.id)) + .map((hit) => ({ + id: hit.id, + name: hit.id.includes(".") ? hit.id.slice(hit.id.indexOf(".") + 1) : hit.id, + ...(hit.id.includes(".") ? { namespace: hit.id.slice(0, hit.id.indexOf(".")) } : {}), + ...(hit.version === undefined ? {} : { version: hit.version }), + surfaces: [], + badges: [], + haystack: `${hit.id} ${hit.description ?? ""}`.toLowerCase(), + ...(hit.description === undefined ? {} : { description: hit.description }), + added: false, + unreadable: false, + crossRegistry: true, + })); + this.searchNote = + this.extra.length === 0 + ? t("No further packages were found in the other registries.") + : t("{0} further package(s) came from the other registries.", this.extra.length); + this.paint(); + } + + private labels(): Record { + return { + [LIBRARY_UI.htmlLang]: htmlLanguage(), + [LIBRARY_UI.title]: t("Library"), + [LIBRARY_UI.search]: t("Search packages"), + [LIBRARY_UI.networkSearch]: t("Search all registries"), + [LIBRARY_UI.networkSearchHint]: t( + "Off by default: this tier runs mcpp search, which may use the network and is read from human output on a best-effort basis. Failures fall back to the local index.", + ), + [LIBRARY_UI.versionLatest]: t("latest {0}"), + [LIBRARY_UI.surfaceExternal]: t("upstream mcpp.toml"), + [LIBRARY_UI.badgeExamples]: t("✓ Has examples"), + [LIBRARY_UI.badgeCn]: t("China mirror"), + [LIBRARY_UI.badgeOpenkalEcosystem]: t("openkal-ecosystem"), + [LIBRARY_UI.badgeOpenkalCompat]: t("openkal-compat"), + [LIBRARY_UI.badgeOpenkalPosix]: t("POSIX environment"), + [LIBRARY_UI.badgeOpenkalPlatform]: t("uses platform interfaces"), + [LIBRARY_UI.added]: t("Added"), + [LIBRARY_UI.unreadable]: t("Descriptor not readable"), + [LIBRARY_UI.noResults]: t("No package matches this search."), + [LIBRARY_UI.noIndex]: t( + "No mcpp index was found. Run mcpp: Refresh the mcpp Package Index once, or set mcpp.library.indexPath to an index checkout.", + ), + [LIBRARY_UI.dataSource]: t("Data source"), + [LIBRARY_UI.count]: t("{0} of {1} packages"), + [LIBRARY_UI.open]: t("Open the detail page for {0}"), + [LIBRARY_UI.crossRegistry]: t("other registry"), + [LIBRARY_UI.refresh]: t("Refresh"), + }; + } + + /** + * Put the current model on screen — but only when it says something new. + * + * `webview.html = …` reloads the document, so pushing an identical one would + * throw away the scroll position and the half-typed query for nothing. With + * a per-view nonce, "identical" means identical: the same model renders byte + * for byte the same document, and a render that produced it is dropped in + * `WebviewDocument.paint()`. + */ + private paint(): void { + const view = this.view; + if (view === undefined) { + return; + } + const document = renderLibraryHtml( + this.model(), + this.webviewDocument.assets(vscode.Uri.joinPath(this.context.extensionUri, MEDIA_DIRECTORY), view.webview), + ); + this.webviewDocument.paint(view.webview, document); + } + + private model(): LibraryModel { + const snapshot = this.snapshot; + const ui = this.labels(); + if (snapshot === undefined) { + return this.emptyModel(ui, t("Reading the index…"), t("Reading the index…")); + } + const entries = snapshot.entries; + const rows = [...entries.map(rowOf), ...this.extra]; + const notice = + snapshot.roots.length === 0 + ? ui[LIBRARY_UI.noIndex] + : entries.length === 0 + ? t("The index folders contain no descriptors.") + : ui[LIBRARY_UI.noResults]; + return { + ui, + rows, + query: this.query, + networkSearch: this.networkSearch(), + networkSearchSetting: "mcpp.library.networkSearch", + dataSource: this.dataSource(snapshot.roots, entries.length), + countTemplate: ui[LIBRARY_UI.count], + total: rows.length, + ...(rows.length === 0 ? { notice } : {}), + }; + } + + private emptyModel(ui: Record, notice: string, dataSource: string): LibraryModel { + return { + ui, + rows: [], + query: "", + networkSearch: this.networkSearch(), + networkSearchSetting: "mcpp.library.networkSearch", + dataSource, + countTemplate: ui[LIBRARY_UI.count], + total: 0, + notice, + }; + } + + /** The footer, in plain words: where the rows came from and whether it is offline. */ + private dataSource(roots: readonly IndexRoot[], total: number): string { + const registries = roots.map((root) => root.registry).join(", "); + const base = + roots.length === 0 + ? t("No local index was read.") + : t( + "{0} descriptor(s) from {1} local index folder(s): {2}. Read offline.", + total, + roots.length, + registries, + ); + const sentence = `${t("Data source")}: ${base}`; + return this.searchNote === undefined ? sentence : `${sentence} ${this.searchNote}`; + } +} + +/** One entry as the row the document renders. */ +function rowOf(entry: LibraryEntry): LibraryRow { + return { + id: entry.id, + haystack: searchText(entry), + ...(entry.namespace === undefined ? {} : { namespace: entry.namespace }), + name: entry.name, + ...(entry.version === undefined ? {} : { version: entry.version }), + ...(entry.surface === undefined ? {} : { surface: entry.surface }), + surfaces: entry.surfaces, + badges: badgesOf(entry), + ...(entry.description === undefined ? {} : { description: entry.description }), + added: entry.added, + unreadable: entry.unreadable === true, + }; +} + + +/** `auto` is the editor's own language; `en`/`zh-cn` are the manual override. */ +function htmlLanguage(): string { + const preference = languagePreference(); + if (preference === "zh-cn") { + return "zh-cn"; + } + if (preference === "en") { + return "en"; + } + return localeFromEditorLanguage(vscode.env.language) === "zh-cn" ? "zh-cn" : "en"; +} diff --git a/src/library/xpkg.ts b/src/library/xpkg.ts new file mode 100644 index 0000000..f0355e6 --- /dev/null +++ b/src/library/xpkg.ts @@ -0,0 +1,434 @@ +/** + * `mcpp xpkg parse --json`, decoded — as a pure module. + * + * The index's Lua descriptors are read cheaply and tolerantly by + * `src/library/indexModel.ts` (a few single-line catalog fields, no process). + * Everything **authoritative** comes from here instead: `mcpp`'s own resolver + * is the only thing that knows which platform a version belongs to, whether a + * descriptor is Form A, and what a target's `kind` is. That command is run on + * demand — when a row is expanded or the detail page opens — never once per + * package (233 spawns would freeze the view). + * + * The document is the flat shape `mcpp` prints, verified against + * `mcpp 2026.9.30.2` and all 239 `mcpplibs` descriptors: + * + * ```json + * {"namespace":"compat","name":"argparse", + * "versions":{"linux":["3.2"],"macosx":["3.2"],"windows":["3.2"]}, + * "standard":"c++23","import_std":false, + * "sources":["mcpp_generated/argparse_anchor.c"], + * "include_dirs":["<archive>/include"], + * "generated_files":[{"path":"…","bytes":52}], + * "generated_contents":{"mcpp_generated/argparse_anchor.c":"…"}, + * "targets":["argparse"],"unknown_keys":[]} + * ``` + * + * Two facts this module is built around, both measured rather than assumed: + * + * - a **Form A** descriptor answers with only `namespace`, `name`, `versions` + * and `form: "A"` — it deliberately carries no build information, because the + * upstream archive ships its own manifest. That is the index's `external` + * surface ("上游 mcpp.toml"), and it is the only honest answer offline. + * - `targets` is an **array of names** in every descriptor here (239/239). The + * site generator's `targets` *kinds* come from the upstream manifest, which + * `xpkg parse` does not expose. So a `kind` is honoured when the JSON happens + * to carry one and is not invented when it does not. + */ + +/** + * The consumer-facing usage label, exactly the index site's `SURFACES` table + * (`.xpkgindex/plugins/mcpp.py`): what a reader does with the package, not how + * it is built. An earlier design invented shape letters here; those were wrong. + */ +export type Surface = "module" | "header" | "tool" | "external"; + +/** `SURFACES` order, which is also the order the chips and badges are shown in. */ +export const SURFACES: readonly Surface[] = ["module", "header", "tool", "external"]; + +/** + * The label and the example line for each surface. + * + * `import` / `#include` / `tool` are code, not prose — the site generator emits + * them untranslated in every language, so they are constants here. Only + * `external` is a sentence, and it is looked up through `uiKey` so the caller + * can localize it. `usage` is the muted placeholder the site shows when a + * descriptor names no module or header of its own. + */ +export const SURFACE_TEXT: Readonly< + Record +> = { + module: { label: "import", usage: "import x.y;" }, + header: { label: "#include", usage: "#include " }, + tool: { label: "tool", usage: "$ tool" }, + external: { label: "upstream mcpp.toml", usage: "import x.y;", uiKey: "library.surface.external" }, +}; + +/** One `targets[]` entry. `mcpp` prints bare names; the site's kinds are optional. */ +export interface XpkgTarget { + name?: string; + kind?: string; +} + +/** The decoded `mcpp xpkg parse --json` document. Every list is present. */ +export interface XpkgInfo { + namespace: string; + name: string; + /** `form: "A"` means the descriptor names no build shape of its own. */ + form?: string; + versions: Record; + standard?: string; + importStd?: boolean; + sources: string[]; + includeDirs: string[]; + generatedFiles: string[]; + /** Only the paths; the contents themselves are of no use to a reader. */ + generatedContents: string[]; + targets: XpkgTarget[]; + unknownKeys: string[]; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stringList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0); +} + +/** A `versions` object; anything else becomes an empty table. */ +function versionTable(value: unknown): Record { + if (!isRecord(value)) { + return {}; + } + const out: Record = {}; + for (const [platform, list] of Object.entries(value)) { + const versions = stringList(list); + if (versions.length > 0) { + out[platform] = versions; + } + } + return out; +} + +function targetList(value: unknown): XpkgTarget[] { + if (!Array.isArray(value)) { + return []; + } + const out: XpkgTarget[] = []; + for (const entry of value) { + if (typeof entry === "string" && entry.length > 0) { + out.push({ name: entry }); + continue; + } + if (isRecord(entry)) { + const name = typeof entry.name === "string" ? entry.name : undefined; + const kind = typeof entry.kind === "string" ? entry.kind : undefined; + out.push({ ...(name === undefined ? {} : { name }), ...(kind === undefined ? {} : { kind }) }); + } + } + return out; +} + +/** + * Decode one already-parsed JSON value. Anything that is not the documented + * document — a different `kind`, a resolver error envelope, a hand-edited file — + * returns `undefined`, which the callers render as "cannot read this descriptor" + * rather than as a package with no versions. + */ +export function parseXpkgJsonValue(value: unknown): XpkgInfo | undefined { + if (!isRecord(value)) { + return undefined; + } + const name = value.name; + if (typeof name !== "string" || name.length === 0) { + return undefined; + } + const namespace = typeof value.namespace === "string" ? value.namespace : ""; + const generatedContents = isRecord(value.generated_contents) ? Object.keys(value.generated_contents) : []; + return { + namespace, + name, + ...(typeof value.form === "string" && value.form.length > 0 ? { form: value.form } : {}), + versions: versionTable(value.versions), + ...(typeof value.standard === "string" && value.standard.length > 0 ? { standard: value.standard } : {}), + ...(typeof value.import_std === "boolean" ? { importStd: value.import_std } : {}), + sources: stringList(value.sources), + includeDirs: stringList(value.include_dirs), + generatedFiles: generatedFilePaths(value.generated_files), + generatedContents, + targets: targetList(value.targets), + unknownKeys: stringList(value.unknown_keys), + }; +} + +/** `generated_files` is either a list of paths or a list of `{path, bytes}`. */ +function generatedFilePaths(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + const out: string[] = []; + for (const entry of value) { + if (typeof entry === "string" && entry.length > 0) { + out.push(entry); + } else if (isRecord(entry) && typeof entry.path === "string" && entry.path.length > 0) { + out.push(entry.path); + } + } + return out; +} + +/** + * Decode the command's stdout. A non-JSON answer (an error message on stdout, a + * partial write) returns `undefined`; the caller keeps the descriptor's own + * fields and says the authoritative read failed. + */ +export function parseXpkgJson(text: string): XpkgInfo | undefined { + const trimmed = text.trim(); + if (trimmed.length === 0) { + return undefined; + } + try { + return parseXpkgJsonValue(JSON.parse(trimmed)); + } catch { + return undefined; + } +} + +/** `.cppm` anywhere in a source or a generated file makes the package modular. */ +function isModular(info: XpkgInfo): boolean { + return ( + info.generatedContents.some((path) => path.endsWith(".cppm")) || + info.sources.some((source) => source.includes(".cppm")) || + info.generatedFiles.some((path) => path.endsWith(".cppm")) + ); +} + +/** A target the site files under the `tool` surface. */ +function hasBinaryTarget(info: XpkgInfo): boolean { + return info.targets.some((target) => /^(bin|binary)$/i.test(target.kind ?? "")); +} + +/** + * Every surface a descriptor's **parsed fields** support, in the order the site + * generator emits them (`_surfaces`): a module package may also offer its + * header, a package may ship both a library and a binary. + * + * A Form A descriptor answers `["external"]` and nothing else: the fields that + * would justify a module or header claim are not in the descriptor, and the + * index site only fills them in by fetching the upstream manifest — which this + * extension deliberately never does. + * + * An empty array is a real answer, not a failure, and it happens when a + * descriptor's fields name targets but neither a module nor a header. The site's + * generator degrades exactly that case to `external`; saying nothing is more + * honest than calling an inline Form B descriptor "upstream mcpp.toml". The row + * then shows no usage label, and the detail page still shows everything known. + * (The seven `compat.*-runtime` anchor packages are *not* that case: they carry a + * generated `.c` source, which is the header surface the generator gives a Form B + * package whose interfaces no module wraps.) + */ +export function surfacesOf(info: XpkgInfo): Surface[] { + if (info.form !== undefined) { + return ["external"]; + } + const out: Surface[] = []; + const modular = isModular(info); + if (modular) { + out.push("module"); + } + // `include_dirs` is not by itself a public header surface: on a module package + // it can exist only so the wrapper's own `#include` resolves. The site + // generator therefore keeps the header surface only for a package that is not + // modular (or that demonstrates an include line, which needs the example scan). + if (!modular && (info.includeDirs.length > 0 || info.sources.length > 0)) { + out.push("header"); + } + if (hasBinaryTarget(info)) { + out.push("tool"); + } + return out; +} + +/** The surface a row leads with, or `undefined` when nothing is claimed. */ +export function surfaceOf(info: XpkgInfo): Surface | undefined { + return surfacesOf(info)[0]; +} + +/** Does the descriptor's own text name a `kind = "bin"` target? */ +export function textNamesBinaryTarget(text: string): boolean { + return /\bkind\s*=\s*"(?:bin|binary)"/.test(text); +} + +/** + * The site's per-platform version groups, in a stable order: the current + * platform first, then the rest as `mcpp` printed them. Used by the detail + * page's version matrix, where the current platform must be the one a reader + * finds first. + */ +export function versionGroups( + versions: Record, + currentPlatform: string | undefined, +): Array<{ platform: string; versions: string[] }> { + const platforms = Object.keys(versions); + const ordered = [ + ...(currentPlatform !== undefined && platforms.includes(currentPlatform) ? [currentPlatform] : []), + ...platforms.filter((platform) => platform !== currentPlatform), + ]; + return ordered.map((platform) => ({ platform, versions: [...versions[platform]] })); +} + +/** + * The current platform as `mcpp` names it: `linux` / `macosx` / `windows`. + * `undefined` for a platform the index has no vocabulary for, so the caller + * shows the matrix rather than guessing which column is "mine". + */ +export function platformKey(nodePlatform: string): string | undefined { + switch (nodePlatform) { + case "linux": + return "linux"; + case "darwin": + return "macosx"; + case "win32": + return "windows"; + default: + return undefined; + } +} + +/** + * Version order, total and deterministic. + * + * mcpp's versions are dotted numbers (`3.2`, `1.6.43`, `0.2.4`), sometimes with + * a packaging revision (`22.1.8.1`), a prerelease tail (`1.0.0-rc1`) or a + * non-semver tag the upstream project publishes (`b10069.2`, the llama.cpp + * checkpoint revisions). The comparison follows the shape a reader expects: + * + * - the dotted core first, segment by segment, numerically when both segments + * are numbers (`1.10` is after `1.9`); + * - build metadata (`+…`) is ignored; + * - a prerelease (`-rc1`) sorts **below** its release, as semver says; + * - a numeric segment outranks a non-numeric one, which is what makes the + * `"latest"` alias some xlings descriptors publish lose to the concrete + * version beside it — the only answer `mcpp add` accepts. + */ +export function compareVersions(a: string, b: string): number { + const left = splitVersion(a); + const right = splitVersion(b); + const core = compareSegments(left.core, right.core); + if (core !== 0) { + return core; + } + if (left.pre === undefined && right.pre === undefined) { + return 0; + } + if (left.pre === undefined) { + return 1; + } + if (right.pre === undefined) { + return -1; + } + return compareSegments(left.pre, right.pre); +} + +function splitVersion(version: string): { core: string[]; pre?: string[] } { + const withoutBuild = version.split("+")[0]; + const dash = withoutBuild.indexOf("-"); + if (dash === -1) { + return { core: withoutBuild.split(".") }; + } + return { core: withoutBuild.slice(0, dash).split("."), pre: withoutBuild.slice(dash + 1).split(".") }; +} + +function compareSegments(left: readonly string[], right: readonly string[]): number { + const length = Math.max(left.length, right.length); + for (let index = 0; index < length; index += 1) { + const one = left[index]; + const two = right[index]; + if (one === undefined) { + return two === undefined ? 0 : -1; + } + if (two === undefined) { + return 1; + } + const oneNumber = /^\d+$/.test(one); + const twoNumber = /^\d+$/.test(two); + if (oneNumber && twoNumber) { + const difference = Number(one) - Number(two); + if (difference !== 0) { + return difference < 0 ? -1 : 1; + } + continue; + } + if (oneNumber !== twoNumber) { + return oneNumber ? 1 : -1; + } + const natural = compareNatural(one, two); + if (natural !== 0) { + return natural; + } + } + return 0; +} + +/** `rc2` before `rc10`: within an alphanumeric identifier, digit runs are numbers. */ +function compareNatural(left: string, right: string): number { + const one = left.match(/\d+|\D+/g) ?? []; + const two = right.match(/\d+|\D+/g) ?? []; + const length = Math.max(one.length, two.length); + for (let index = 0; index < length; index += 1) { + const a = one[index]; + const b = two[index]; + if (a === undefined) { + return b === undefined ? 0 : -1; + } + if (b === undefined) { + return 1; + } + const aNumber = /^\d+$/.test(a); + const bNumber = /^\d+$/.test(b); + if (aNumber && bNumber) { + const difference = Number(a) - Number(b); + if (difference !== 0) { + return difference < 0 ? -1 : 1; + } + continue; + } + if (a !== b) { + return a < b ? -1 : 1; + } + } + return 0; +} + +/** The list's versions, greatest first. */ +export function sortVersions(versions: readonly string[]): string[] { + return [...versions].sort((a, b) => compareVersions(b, a)); +} + +/** + * The version `mcpp add` must be given: the greatest one this platform has. + * + * `undefined` means the index knows no version for the current platform, and the + * UI says so instead of running a command mcpp would reject. + */ +export function latestVersion(versions: Record, platform: string | undefined): string | undefined { + if (platform === undefined) { + return undefined; + } + const list = versions[platform]; + if (list === undefined || list.length === 0) { + return undefined; + } + return sortVersions(list)[0]; +} + +/** Every version this platform has, greatest first; `[]` when it has none. */ +export function versionsFor(versions: Record, platform: string | undefined): string[] { + if (platform === undefined) { + return []; + } + return sortVersions(versions[platform] ?? []); +} diff --git a/src/mcppTomlCompletion.ts b/src/mcppTomlCompletion.ts deleted file mode 100644 index 016af8f..0000000 --- a/src/mcppTomlCompletion.ts +++ /dev/null @@ -1,223 +0,0 @@ -// mcpp.toml 的代码补全查询层(结构补全版)。 -// -// 范围:段头结构建议 + 开放词汇段的写法模板。每条建议携带显式替换范围。 -// 依赖包名/版本等动态数据补全与静态字段键/枚举补全均不在本版——前者等上游 -// 批量 catalog 接口,后者等版本化 manifest schema(见设计 issue #8 与 -// mcpp RFC #379)。 -// -// 本模块不依赖 vscode API;上下文来自 mcppTomlParser 的 contextAt(容错解析)。 - -import { - contextAt, - type ReplaceRange, - type SectionResolution, -} from "./mcppTomlParser"; - -export type McppTomlSuggestionKind = "section" | "template"; - -export interface McppTomlSuggestion { - label: string; - kind: McppTomlSuggestionKind; - detail: string; - documentation?: string; - /** 插入文本;含 $1 等 snippet 占位符。缺省时插入 label。 */ - insertSnippet?: string; - /** 替换范围(光标所在行的起止列)。 */ - range: ReplaceRange; -} - -export interface SectionHeaderSpec { - group: string; - label: string; - /** snippet 形式的段头(含 ${1:...} 占位)。 */ - header: string; - detail: string; -} - -// 段头结构清单:TOML 结构语法,非字段语义。出处:mcpp 文档 02/03/05/06 -// 与 src/manifest/toml.cppm 的段清单(契约测试用真实 mcpp 逐段验证)。 -export const SECTION_HEADERS: readonly SectionHeaderSpec[] = [ - { group: "package", label: "[package]", header: "[package]", detail: "包元数据" }, - { group: "lib", label: "[lib]", header: "[lib]", detail: "库根模块约定" }, - { group: "build", label: "[build]", header: "[build]", detail: "构建配置" }, - { group: "generated_files", label: "[generated_files]", header: "[generated_files]", detail: "生成文件(路径 → 内容)" }, - { group: "dependencies", label: "[dependencies]", header: "[dependencies]", detail: "运行时依赖" }, - { group: "dev-dependencies", label: "[dev-dependencies]", header: "[dev-dependencies]", detail: "开发/测试依赖" }, - { group: "build-dependencies", label: "[build-dependencies]", header: "[build-dependencies]", detail: "构建期依赖(仅构建期拉取,运行时不可见)" }, - { group: "workspace", label: "[workspace]", header: "[workspace]", detail: "工作空间成员声明" }, - { group: "workspace.dependencies", label: "[workspace.dependencies]", header: "[workspace.dependencies]", detail: "集中声明依赖版本,成员用 workspace = true 继承" }, - { group: "features", label: "[features]", header: "[features]", detail: "feature 定义" }, - { group: "feature-deps", label: "[feature-deps.]", header: "[feature-deps.${1:name}]", detail: "由 feature 拉取的可选依赖" }, - { group: "capabilities", label: "[capabilities]", header: "[capabilities]", detail: "capability 绑定(provider 选择)" }, - { group: "targets", label: "[targets.]", header: "[targets.${1:name}]", detail: "构建目标" }, - { group: "profile", label: "[profile.]", header: "[profile.${1:name}]", detail: "构建档案" }, - { group: "runtime", label: "[runtime]", header: "[runtime]", detail: "主机运行时能力" }, - { group: "resources", label: "[resources]", header: "[resources]", detail: "编译进产物的元数据与资产(仅 PE 目标)" }, - { group: "toolchain", label: "[toolchain]", header: "[toolchain]", detail: "编译器工具链简写" }, - { group: "xlings", label: "[xlings]", header: "[xlings]", detail: "构建环境(xlings 供给)" }, - { group: "xlings.workspace", label: "[xlings.workspace]", header: "[xlings.workspace]", detail: "固定工具版本" }, - { group: "target", label: "[target.]", header: "[target.${1:x86_64-linux-gnu}]", detail: "按目标三元组的配置" }, - { group: "pack", label: "[pack]", header: "[pack]", detail: "mcpp pack 打包配置" }, - { group: "pack.bundle-project", label: "[pack.bundle-project]", header: "[pack.bundle-project]", detail: "vendored 过滤策略微调" }, - { group: "indices", label: "[indices]", header: "[indices]", detail: "项目级索引重定向" }, - { group: "tools.overrides", label: "[tools.overrides]", header: "[tools.overrides]", detail: "host 工具二进制覆盖" }, - { group: "language", label: "[language]", header: "[language]", detail: "旧版兼容字段;新项目请用 [package].standard" }, -]; - -/** 依赖类段(键位置给依赖写法模板)。 */ -const DEPENDENCY_GROUPS: ReadonlySet = new Set([ - "dependencies", - "dev-dependencies", - "build-dependencies", - "workspace.dependencies", - "feature-deps", -]); - -interface TemplateSpec { - label: string; - detail: string; - documentation?: string; - insertSnippet: string; -} - -const DEPENDENCY_TEMPLATES: readonly TemplateSpec[] = [ - { - label: 'name = "version"', - detail: "SemVer 版本依赖", - documentation: "默认 caret 约束(^);也支持 ~、= 与 \">=1.0, <2.0\" 范围组合。", - insertSnippet: '${1:name} = "${2:1.0.0}"', - }, - { - label: "name = { path = ... }", - detail: "路径依赖(本地开发)", - insertSnippet: '${1:name} = { path = "${2:../mylib}" }', - }, - { - label: "name = { git = ..., tag = ... }", - detail: "Git 依赖(tag / branch / rev 三选一)", - insertSnippet: '${1:name} = { git = "${2:https://github.com/user/repo.git}", tag = "${3:v1.0.0}" }', - }, - { - label: "name = { version = ..., features = [...] }", - detail: "长式 dep spec:请求该依赖的 feature", - insertSnippet: '${1:name} = { version = "${2:1.0}", features = ["${3:feature}"] }', - }, - { - label: "name = { version = ..., tools = [...] }", - detail: "依赖产出的 host 工具(须为该包的 bin target)", - insertSnippet: '${1:name} = { version = "${2:1.0}", tools = ["${3:protoc}"] }', - }, -]; - -const FEATURE_TEMPLATES: readonly TemplateSpec[] = [ - { label: "name = [...]", detail: "数组简写:仅隐含 feature", insertSnippet: "${1:name} = [${2}]" }, - { label: "name = { defines = [...] }", detail: "表形式:激活时贡献包自有宏", insertSnippet: '${1:name} = { defines = ["${2:MACRO}"] }' }, - { label: "name = { requires = [...] }", detail: "表形式:需要 capability", insertSnippet: '${1:name} = { requires = ["${2:blas}"] }' }, - { label: "name = { sources = [...] }", detail: "表形式:feature 门控的源 glob", insertSnippet: '${1:name} = { sources = ["${2:src/simd/**}"] }' }, -]; - -const GENERATED_FILE_TEMPLATES: readonly TemplateSpec[] = [ - { - label: '"path" = "content"', - detail: "生成文件(相对路径 → 内容,进指纹)", - insertSnippet: '"${1:src/gen/wrap.cppm}" = """\n${2:}\n"""', - }, -]; - -const CAPABILITY_TEMPLATES: readonly TemplateSpec[] = [ - { - label: 'capability = "provider"', - detail: "capability 绑定(等价于 --cap)", - insertSnippet: '${1:blas} = "${2:compat.openblas}"', - }, -]; - -const XLINGS_WORKSPACE_TEMPLATES: readonly TemplateSpec[] = [ - { label: 'tool = "version"', detail: "固定 xlings 工具版本", insertSnippet: '${1:node} = "${2:24.19.0}"' }, -]; - -const TOOLS_OVERRIDES_TEMPLATES: readonly TemplateSpec[] = [ - { - label: '"pkg:tool" = "path"', - detail: "用已有二进制覆盖 host 工具(跳过构建)", - insertSnippet: '"${1:compat.protobuf:protoc}" = "${2:/usr/bin/protoc}"', - }, -]; - -const TEMPLATES_BY_GROUP: Record = { - "features": FEATURE_TEMPLATES, - "generated_files": GENERATED_FILE_TEMPLATES, - "capabilities": CAPABILITY_TEMPLATES, - "xlings.workspace": XLINGS_WORKSPACE_TEMPLATES, - "tools.overrides": TOOLS_OVERRIDES_TEMPLATES, -}; - -function sectionHeaderSuggestions(range: ReplaceRange): McppTomlSuggestion[] { - return SECTION_HEADERS.map((section) => ({ - label: section.label, - kind: "section", - detail: section.detail, - insertSnippet: section.header, - range, - })); -} - -function templateSuggestions(templates: readonly TemplateSpec[], range: ReplaceRange): McppTomlSuggestion[] { - return templates.map((template) => ({ - label: template.label, - kind: "template", - detail: template.detail, - documentation: template.documentation, - insertSnippet: template.insertSnippet, - range, - })); -} - -/** - * 计算 mcpp.toml 在指定位置的补全建议(结构补全:段头 + 写法模板)。 - */ -export function computeMcppTomlCompletions( - lines: readonly string[], - line: number, - character: number, -): McppTomlSuggestion[] { - const context = contextAt(lines, line, character); - - if (context.kind === "section-header") { - // mcpp manifest 不使用 TOML 数组表([[...]]);[[ 内不提供建议, - // 避免把用户意图的数组表悄悄替换成普通段 [x](未知段会被 mcpp 静默忽略)。 - if (context.isArray) { - return []; - } - // parser 的替换范围从段名 token 开始;段头建议插入的是完整 "[xxx]", - // 需要把范围扩展到本行的 "[",避免留下 "[["。仅当 "[" 是行内首个 - // 非空白字符时才扩展(section-header 上下文正常都满足,防御奇怪输入)。 - const lineText = (lines[line] ?? "").replace(/\r$/, ""); - const bracket = lineText.indexOf("["); - const firstNonWs = lineText.search(/\S/); - const range = bracket >= 0 && bracket === firstNonWs - ? { startCharacter: bracket, endCharacter: context.replaceRange.endCharacter } - : context.replaceRange; - return sectionHeaderSuggestions(range); - } - - if (context.kind === "key") { - const { section, containerPath, replaceRange } = context; - // 文档顶部(尚无段头):提示段头。未知段:不提供建议 - // (附录 A:不支持包自定义 toml 键)。 - if (section.kind === "top") { - return sectionHeaderSuggestions(replaceRange); - } - if (section.kind !== "known" || containerPath.length > 0) { - return []; - } - if (DEPENDENCY_GROUPS.has(section.group)) { - return templateSuggestions(DEPENDENCY_TEMPLATES, replaceRange); - } - const templates = TEMPLATES_BY_GROUP[section.group]; - return templates === undefined ? [] : templateSuggestions(templates, replaceRange); - } - - // 值位置:自由格式值不瞎猜(版本候选等动态数据层落地后再说)。 - return []; -} diff --git a/src/mcppls/bridge.ts b/src/mcppls/bridge.ts new file mode 100644 index 0000000..55a7738 --- /dev/null +++ b/src/mcppls/bridge.ts @@ -0,0 +1,106 @@ +/** + * The one place this extension talks to the C++ Modules extension. + * + * It forwards commands and nothing else: no LSP client, no reading of + * `mcppls.*` settings, no writing of them, no parsing of its logs. The + * capability table (`./contract.ts`) says what may be forwarded and how much + * confirmation each action needs; `./capabilities.ts` decides whether it exists. + * + * Results are **structured, not phrased**: a caller that has a user in front of + * it turns them into words through `./messages.ts`. That keeps this module free + * of `vscode` and therefore unit-testable, and it keeps the strings translatable + * without threading a translator through the probe logic. + */ + +import { CapabilityRegistry, type CapabilityEnvironment, type InvokeResult } from "./capabilities"; +import { capability } from "./contract"; + +export { MCPPLS_EXTENSION_ID } from "./contract"; + +export type LanguageServerCommandState = InvokeResult["state"]; +export type LanguageServerCommandResult = InvokeResult; +export { CapabilityRegistry } from "./capabilities"; +export type { CapabilityEnvironment } from "./capabilities"; + +export interface LanguageServerBridge { + /** The probe, exposed so the environment self-check can print the whole table. */ + readonly capabilities: CapabilityRegistry; + /** Generic entry point for the view and the menu. */ + invoke(key: string, ...args: unknown[]): Promise; + /** True when the user should not be offered this capability. */ + isGone(key: string): boolean; + /** True when it exists but the static read could not confirm it. */ + isUnconfirmed(key: string): boolean; + /** How much confirmation the action needs. */ + dangerOf(key: string): "none" | "confirm" | "destructive"; + + refreshLanguageServerAfterBuild(): Promise; + restartLanguageServer(): Promise; + restartEngine(): Promise; + resetWorkspaceCache(): Promise; + selectContext(): Promise; + showModuleGraph(): Promise; + showLanguageServerLogs(): Promise; + collectReport(): Promise; + exportDiagnosticBundle(): Promise; + runBuildToolInTerminal(): Promise; + manageConflicts(restore?: boolean): Promise; + toggleInWorkspace(enable: boolean): Promise; + installCommandLineTools(): Promise; + reviewChanges(clear?: boolean): Promise; +} + +/** Methods that map one-to-one onto a capability, so the bridge stays a table. */ +const FORWARDED = { + restartLanguageServer: "restartServer", + restartEngine: "restartEngine", + resetWorkspaceCache: "resetCache", + selectContext: "selectContext", + showModuleGraph: "moduleGraph", + showLanguageServerLogs: "logs", + collectReport: "report", + exportDiagnosticBundle: "diagnosticBundle", + runBuildToolInTerminal: "runBuildTool", + installCommandLineTools: "installTools", +} as const; + +export function createLanguageServerBridge(environment: CapabilityEnvironment): LanguageServerBridge { + const capabilities = new CapabilityRegistry(environment); + let refreshInFlight: Promise | undefined; + + const invoke = (key: string, ...args: unknown[]): Promise => + capabilities.invoke(key, ...args); + + /** + * Builds are frequent; a second refresh while one is in flight would restart + * the language server twice for one edit. The single-flight promise is shared, + * so both callers see the same outcome. + */ + function refreshLanguageServerAfterBuild(): Promise { + if (refreshInFlight !== undefined) { + return refreshInFlight; + } + refreshInFlight = invoke("refresh").finally(() => { + refreshInFlight = undefined; + }); + return refreshInFlight; + } + + const forwarded = Object.fromEntries( + Object.entries(FORWARDED).map(([method, key]) => [method, () => invoke(key)]), + ) as Record Promise>; + + return { + capabilities, + invoke, + isGone: (key) => capabilities.isGone(key), + isUnconfirmed: (key) => capabilities.isUnconfirmed(key), + dangerOf: (key) => capability(key)?.danger ?? "none", + refreshLanguageServerAfterBuild, + ...forwarded, + // The two commands whose *argument* selects the direction. + manageConflicts: (restore = false) => invoke("manageConflicts", restore), + toggleInWorkspace: (enable) => invoke("toggleInWorkspace", enable), + reviewChanges: (clear = false) => invoke("review", clear), + }; +} diff --git a/src/mcppls/capabilities.ts b/src/mcppls/capabilities.ts new file mode 100644 index 0000000..2a9f64f --- /dev/null +++ b/src/mcppls/capabilities.ts @@ -0,0 +1,212 @@ +/** + * Capability probing and invocation for the C++ Modules extension. + * + * Pure: the only VS Code-shaped things it needs arrive through + * `CapabilityEnvironment`, which is exactly what the extension host supplies and + * what the tests fake. + * + * Why probing is two-level: + * + * - **Static** — `packageJSON.contributes.commands` says which ids mcppls + * declares. It costs nothing and never runs anything. A command the static + * read cannot see is *greyed out*, not hidden, so a wrong read cannot remove a + * feature. + * - **Runtime** — the first real use calls the command once and classifies the + * failure. `command '…' not found` means the capability is gone for this + * session; anything else is a failure of that one call and does not remove the + * feature. + * + * We deliberately do **not** probe by calling every command at activation: + * `selectContext`, `showModuleGraph` and `showLogs` are UI commands with side + * effects, and probing them would open pickers and panels. `commands.getCommands()` + * is not used either — whether it lists a contributed-but-unactivated command is + * not guaranteed across VS Code versions. + */ + +import { CAPABILITIES, MCPPLS_EXTENSION_ID, capability } from "./contract"; + +export interface CapabilityEnvironment { + /** `vscode.extensions.getExtension(id) !== undefined`. */ + extensionInstalled(id: string): boolean; + /** Declared command ids, from the extension's `package.json`. */ + declaredCommands?(id: string): readonly string[] | undefined; + /** Activate the dependency before the first forward, when VS Code has not. */ + activateExtension?(id: string): Thenable; + executeCommand(command: string, ...args: unknown[]): Thenable; +} + +export type CapabilityState = "available" | "declared" | "undeclared" | "missing" | "unavailable"; + +export interface CapabilityStatus { + key: string; + state: CapabilityState; + /** The command chosen for this capability, when there is one. */ + command?: string; +} + +export type InvokeState = "completed" | "unavailable" | "missing" | "failed"; + +export interface InvokeResult { + state: InvokeState; + capabilityKey: string; + /** The command that ran, when one ran. */ + command?: string; + /** Error text from the failed call, verbatim. */ + error?: string; + /** + * Whatever the command answered with, when it answered with anything. + * + * A few mcppls commands return a path — `mcppls.exportDiagnosticBundle` + * resolves to the zip it wrote (`exportDiagnosticBundle(): Promise` upstream) — and that is the only reliable way to point at the + * file afterwards. Most commands return nothing, and the caller must treat this + * as unknown: it is passed on untouched, never parsed. + */ + value?: unknown; +} + +/** `command 'x' not found` in the several spellings VS Code has used. */ +const NOT_FOUND = /command\s+(?:['"`][^'"`]+['"`]\s+)?not found|not registered|no such command/i; + +export function classifyCommandError(error: unknown): "missing" | "failed" { + const message = error instanceof Error ? error.message : String(error); + return NOT_FOUND.test(message) ? "missing" : "failed"; +} + +export class CapabilityRegistry { + private readonly statuses = new Map(); + + public constructor(private readonly environment: CapabilityEnvironment) { + this.probe(); + } + + /** Re-read the static declaration; called when extensions are installed, enabled or updated. */ + public invalidate(): void { + this.statuses.clear(); + this.probe(); + } + + public status(key: string): CapabilityStatus { + return this.statuses.get(key) ?? { key, state: "unavailable" }; + } + + /** Capabilities whose first command is believed usable right now. */ + public availableKeys(): string[] { + return [...this.statuses.values()] + .filter((status) => status.state === "available" || status.state === "declared") + .map((status) => status.key); + } + + /** True when the user should not be offered the capability. */ + public isGone(key: string): boolean { + const state = this.status(key).state; + return state === "unavailable" || state === "missing"; + } + + /** True when the capability exists but the static read could not confirm it: grey out, do not hide. */ + public isUnconfirmed(key: string): boolean { + return this.status(key).state === "undeclared"; + } + + private probe(): void { + const installed = this.environment.extensionInstalled(MCPPLS_EXTENSION_ID); + const declared = installed ? this.environment.declaredCommands?.(MCPPLS_EXTENSION_ID) : undefined; + for (const entry of CAPABILITIES) { + if (entry.kind === "readState") { + // State comes from `extension.exports`, which cannot be inspected without + // activating; the reader decides and reports for itself. + this.statuses.set(entry.key, { key: entry.key, state: installed ? "declared" : "unavailable" }); + continue; + } + if (!installed) { + this.statuses.set(entry.key, { key: entry.key, state: "unavailable" }); + continue; + } + if (declared === undefined) { + // No static information: assume the first command works until a call says otherwise. + this.statuses.set(entry.key, { key: entry.key, state: "declared", command: entry.commands[0] }); + continue; + } + const command = entry.commands.find((candidate) => declared.includes(candidate)); + this.statuses.set( + entry.key, + command === undefined + ? { key: entry.key, state: "undeclared" } + : { key: entry.key, state: "declared", command }, + ); + } + } + + /** + * Run a capability, activating the dependency first. Returns the command that + * ran so the caller can name it in a log line. + */ + public async invoke(key: string, ...args: unknown[]): Promise { + const entry = capability(key); + if (entry === undefined) { + return { state: "failed", capabilityKey: key, error: `unknown capability ${key}` }; + } + if (!this.environment.extensionInstalled(MCPPLS_EXTENSION_ID)) { + return { state: "unavailable", capabilityKey: key }; + } + const known = this.status(key); + if (known.state === "missing") { + // Remembered from an earlier call: do not ask VS Code again. + return { state: "missing", capabilityKey: key }; + } + if (known.state === "unavailable") { + return { state: "unavailable", capabilityKey: key }; + } + + // Candidate chain: prefer the first command the probe could confirm. + const status = known; + const candidate = status.command ?? entry.commands[0]; + if (candidate === undefined) { + return { state: "failed", capabilityKey: key, error: `capability ${key} has no command` }; + } + + try { + await this.environment.activateExtension?.(MCPPLS_EXTENSION_ID); + const value = await this.environment.executeCommand(candidate, ...args); + this.statuses.set(key, { key, state: "available", command: candidate }); + return { + state: "completed", + capabilityKey: key, + command: candidate, + ...(value === undefined ? {} : { value }), + }; + } catch (error) { + const failure = classifyCommandError(error); + if (failure === "missing") { + // This command does not exist in the installed version: try the next + // candidate once, then remember. + const next = entry.commands.find((other) => other !== candidate); + if (next !== undefined) { + try { + const value = await this.environment.executeCommand(next, ...args); + this.statuses.set(key, { key, state: "available", command: next }); + return { + state: "completed", + capabilityKey: key, + command: next, + ...(value === undefined ? {} : { value }), + }; + } catch (second) { + if (classifyCommandError(second) === "missing") { + this.statuses.set(key, { key, state: "missing" }); + return { state: "missing", capabilityKey: key, command: candidate }; + } + return { state: "failed", capabilityKey: key, command: next, error: messageOf(second) }; + } + } + this.statuses.set(key, { key, state: "missing" }); + return { state: "missing", capabilityKey: key, command: candidate }; + } + return { state: "failed", capabilityKey: key, command: candidate, error: messageOf(error) }; + } + } +} + +function messageOf(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/src/mcppls/contract.ts b/src/mcppls/contract.ts new file mode 100644 index 0000000..67bbffa --- /dev/null +++ b/src/mcppls/contract.ts @@ -0,0 +1,260 @@ +/** + * The C++ Modules language service we depend on, as **data**. + * + * Everything this extension knows about `sunrisepeak.mcpp-language-server` + * (mcppls) lives here: its extension id, the commands we forward, and how badly + * each one needs confirming. No `vscode` import, so the table is unit-testable + * and `tools/` can read it. + * + * Two rules this file exists to keep: + * + * 1. **Our own commands never use the `mcppls.` prefix.** mcppls's S3 spec + * (S3-5.6-3) forbids a client from registering a command id the server + * advertises: `vscode-languageclient` registers those itself, and a collision + * stops the language client from starting. See `.agents/docs/mcppls-integration.md`. + * 2. **Nothing here writes an `mcppls.*` setting.** The only settings we ever + * touch are our own `mcpp.*`. The two commands that change mcppls state + * (`turnOn/turnOffInWorkspace`) are mcppls's own, and the user triggers them. + */ + +export const MCPPLS_EXTENSION_ID = "sunrisepeak.mcpp-language-server"; + +/** + * The version range this build was written against. Used for a **notice** only: + * a command that disappears is detected by calling it, not by reading a version, + * because a version number cannot predict a rename. + * + * The minimum is a fact in its own right (the comparison in `extension.ts` + * reads it through `compareVersions`), and the range string is its display + * form — one source of truth, two spellings (external review P1-3). + */ +export const VERIFIED_MCPPLS_MINIMUM = "0.0.4"; +export const VERIFIED_MCPPLS_RANGE = `>=${VERIFIED_MCPPLS_MINIMUM}`; + +export type CapabilityKind = "forward" | "readState"; + +/** How much ceremony an action needs before it runs. */ +export type Danger = "none" | "confirm" | "destructive"; + +export interface Capability { + key: string; + kind: CapabilityKind; + /** English; the UI resolves it through `src/i18n/t.ts`. */ + title: string; + /** Candidate command ids, most preferred first. Empty for `readState`. */ + commands: readonly string[]; + /** Always false: losing mcppls must never disable mcpp's own features. */ + required: boolean; + danger: Danger; + /** Shown when the capability is gone; says what still works. */ + degradedHint: string; + /** Extra sentence for a destructive action, spelling out what it does *not* touch. */ + confirmHint?: string; +} + +export const CAPABILITIES: readonly Capability[] = [ + { + key: "refresh", + kind: "forward", + title: "Refresh the C++ Modules build description", + commands: ["mcppls.reloadBuildDescription", "mcppls.restartServer"], + required: false, + danger: "none", + degradedHint: + "The build finished, but the language service could not be refreshed. Run \"C++ Modules: Restart Language Server\" from the Command Palette.", + }, + { + key: "selectContext", + kind: "forward", + title: "Select the C++ Modules analysis context", + commands: ["mcppls.selectContext"], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not offer a context picker; analysis uses the default context.", + }, + { + key: "moduleGraph", + kind: "forward", + title: "Show the module graph", + commands: ["mcppls.showModuleGraph"], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not offer a module graph.", + }, + { + key: "logs", + kind: "forward", + title: "Show the C++ Modules log", + commands: ["mcppls.showLogs"], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not offer a log command; look for its output channel in the Output view.", + }, + { + key: "readState", + kind: "readState", + title: "C++ Modules state", + commands: [], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not expose its state; only the actions below are available.", + }, + { + key: "restartServer", + kind: "forward", + title: "Restart the C++ Modules language server", + commands: ["mcppls.restartServer"], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not offer a restart command.", + }, + { + key: "restartEngine", + kind: "forward", + title: "Restart the C++ semantic engine", + commands: ["mcppls.restartClangd"], + required: false, + danger: "confirm", + degradedHint: "Installed C++ Modules does not offer an engine restart.", + confirmHint: "The semantic engine restarts; module preparation starts over and may take a while.", + }, + { + key: "resetCache", + kind: "forward", + title: "Reset this workspace's C++ Modules cache", + commands: ["mcppls.resetWorkspaceCache"], + required: false, + danger: "destructive", + degradedHint: "Installed C++ Modules does not offer a cache reset.", + confirmHint: + "This discards the language server's model cache for this workspace and prepares it again, which can take minutes. mcpp's own build cache and the project's target/ directory are not touched.", + }, + { + key: "logsDirectory", + // Verified upstream: `mcppls.revealCacheDirectory(which)` takes `'logs'` for + // `paths.logDirectory` and anything else for the workspace cache root + // (`editors/vscode/src/commands.ts` -> `revealCacheDirectory`), and the log + // directory is `/logs` (`src/orchestrator/workspace.cpp`). + // The argument is passed by the caller, so the chain stays one command. + kind: "forward", + title: "Open the C++ Modules log folder", + commands: ["mcppls.revealCacheDirectory"], + required: false, + danger: "none", + degradedHint: + "Installed C++ Modules does not offer a reveal command; its log lives in the mcppls cache directory.", + }, + { + key: "report", + kind: "forward", + title: "Collect a C++ Modules diagnostic report", + commands: ["mcppls.collectReport"], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not offer a report command.", + }, + { + key: "diagnosticBundle", + kind: "forward", + title: "Export a C++ Modules diagnostic bundle", + commands: ["mcppls.exportDiagnosticBundle"], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not offer a diagnostic bundle.", + }, + { + key: "runBuildTool", + kind: "forward", + title: "Run the build tool in a terminal", + commands: ["mcppls.runBuildToolInTerminal"], + required: false, + danger: "confirm", + degradedHint: "Installed C++ Modules does not offer this action.", + confirmHint: "The build tool runs in an integrated terminal, with its normal side effects.", + }, + { + key: "manageConflicts", + kind: "forward", + title: "Manage other C++ language features", + commands: ["mcppls.turnOffOtherCppFeatures", "mcppls.restoreOtherCppFeatures"], + required: false, + danger: "confirm", + degradedHint: "Installed C++ Modules does not offer conflict handling.", + confirmHint: "This changes settings belonging to other C++ extensions, at their own keys. C++ Modules performs the change.", + }, + { + key: "toggleInWorkspace", + kind: "forward", + title: "Enable or disable C++ Modules in this workspace", + commands: ["mcppls.turnOffInWorkspace", "mcppls.turnOnInWorkspace"], + required: false, + danger: "confirm", + degradedHint: "Installed C++ Modules does not offer a per-workspace switch.", + confirmHint: "This writes mcppls.enable, a setting owned by the C++ Modules extension.", + }, + { + key: "installTools", + kind: "forward", + title: "Install the C++ Modules command line tools", + commands: ["mcppls.installCommandLineTools"], + required: false, + danger: "confirm", + degradedHint: "Installed C++ Modules does not offer a command line tools installer.", + confirmHint: "This may install system packages the language server needs.", + }, + { + key: "review", + kind: "forward", + title: "Review workspace changes", + commands: ["mcppls.review.run", "mcppls.review.clear"], + required: false, + danger: "none", + degradedHint: "Installed C++ Modules does not offer its review commands here.", + }, +]; + +const BY_KEY = new Map(CAPABILITIES.map((capability) => [capability.key, capability])); + +export function capability(key: string): Capability | undefined { + return BY_KEY.get(key); +} + +/** The commands mcppls must declare for a capability to be usable at all. */ +export function commandsOf(key: string): readonly string[] { + return BY_KEY.get(key)?.commands ?? []; +} + +/** + * The capability table's own invariants (must be empty). + * + * The "our ids never use the mcppls prefix" rule is checked where our ids live + * (`src/commands/ids.ts`); this covers the forward table itself. + */ +export function capabilityProblems(): string[] { + const problems: string[] = []; + const seen = new Set(); + for (const entry of CAPABILITIES) { + if (seen.has(entry.key)) { + problems.push(`duplicate capability ${entry.key}`); + } + seen.add(entry.key); + if (entry.required) { + problems.push(`${entry.key} is marked required; losing the language service must not disable mcpp features`); + } + if (entry.kind === "forward" && entry.commands.length === 0) { + problems.push(`forward capability ${entry.key} has no command`); + } + if (entry.kind === "readState" && entry.commands.length > 0) { + problems.push(`readState capability ${entry.key} must not name a command`); + } + for (const command of entry.commands) { + if (!command.startsWith("mcppls.")) { + problems.push(`${entry.key} forwards ${command}, which is not an mcppls. command`); + } + } + if (entry.danger !== "none" && entry.confirmHint === undefined) { + problems.push(`${entry.key} needs confirmation but has no confirmHint`); + } + } + return problems; +} diff --git a/src/mcppls/messages.ts b/src/mcppls/messages.ts new file mode 100644 index 0000000..7d0282e --- /dev/null +++ b/src/mcppls/messages.ts @@ -0,0 +1,61 @@ +/** + * Turning a structured bridge result into something a person reads. + * + * Split out of `./bridge.ts` so the bridge stays free of `vscode` (and testable) + * while these strings still go through `src/i18n/t.ts`. Every English string here + * is a key: `tools/l10n-check.mjs` fails the build if one has no Chinese entry. + */ + +import { t } from "../i18n/t"; +import type { LanguageServerCommandResult } from "./bridge"; +import { MCPPLS_EXTENSION_ID, capability } from "./contract"; + +export type ResultSeverity = "info" | "warning" | "error"; + +export interface FormattedResult { + severity: ResultSeverity; + message: string; + /** The capability's own advice, when it is gone. */ + hint?: string; +} + +function titleOf(key: string): string { + return capability(key)?.title ?? key; +} + +export function formatResult(result: LanguageServerCommandResult): FormattedResult { + const title = titleOf(result.capabilityKey); + switch (result.state) { + case "completed": + return { severity: "info", message: t("{0}: done.", title) }; + case "unavailable": + return { + severity: "warning", + message: t("The C++ Modules extension ({0}) is not installed or is disabled.", MCPPLS_EXTENSION_ID), + }; + case "missing": + return { + severity: "warning", + message: t("{0}: the installed C++ Modules does not offer this action.", title), + hint: capability(result.capabilityKey)?.degradedHint, + }; + default: + return { + severity: "error", + message: t("{0} failed: {1}", title, result.error ?? t("unknown error")), + }; + } +} + +/** Confirmation text for an action that needs one, or `undefined` when it does not. */ +export function confirmationFor(key: string, value?: unknown): string | undefined { + const entry = capability(key); + if (entry === undefined || entry.danger === "none") { + return undefined; + } + const parts = [entry.confirmHint ?? entry.title]; + if (typeof value === "boolean") { + parts.push(value ? t("Direction: enable.") : t("Direction: disable.")); + } + return parts.join(" "); +} diff --git a/src/mcppls/state.ts b/src/mcppls/state.ts new file mode 100644 index 0000000..a931e62 --- /dev/null +++ b/src/mcppls/state.ts @@ -0,0 +1,229 @@ +/** + * What the C++ Modules extension reports about itself, normalised. + * + * Pure: it takes the object mcppls's `activate()` returned and answers with a + * shape this extension can render, or with `available: false`. + * + * **This is a best-effort channel, not a contract.** mcppls's own name for that + * object is `TestApi`; the *contents* we read (`CxxModulesStatus`) are specified + * by S3, but whether the object is exposed at all is not promised. So every read + * is defensive: a missing function, a thrown error, an unknown `state` or a + * field of the wrong type all end in `available: false` and the view simply + * shows less. Nothing here may throw. + * + * The one thing worth calling out: an issue carries an optional `command` — S3's + * own fix for it. We surface that rather than guessing a remedy. + */ + +export interface McpplsIssue { + code: string; + message: string; + /** S3's own remedy, when it offers one. */ + command?: { command: string; arguments?: unknown[]; title?: string }; +} + +export interface McpplsEngineStatus { + name: string; + version: string; + role: string; + state: string; +} + +export interface McpplsStateView { + available: boolean; + /** Why it is unavailable, for the log; never shown as an error. */ + reason?: string; + version?: string; + active?: boolean; + enabled?: boolean; + state?: McpplsState; + project?: { root: string; source: string; level?: number; tier?: number }; + profile?: { kind: string; compiler?: string; stdlib: string; target: string; standard?: string }; + engine?: { name: string; version: string }; + engines?: McpplsEngineStatus[]; + progress?: { done: number; total: number }; + issues?: McpplsIssue[]; + notices?: McpplsIssue[]; + onlineRun?: { outcome: string; message: string; at: string }; +} + +export const MCPPLS_STATES = ["starting", "loading", "preparing", "ready", "degraded", "error"] as const; +export type McpplsState = (typeof MCPPLS_STATES)[number]; + +export interface McpplsMeta { + version?: string; + active?: boolean; + enabled?: boolean; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function text(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +function finiteNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function knownState(value: unknown): McpplsState | undefined { + return typeof value === "string" && (MCPPLS_STATES as readonly string[]).includes(value) + ? (value as McpplsState) + : undefined; +} + +function issue(value: unknown): McpplsIssue | undefined { + if (!isRecord(value)) { + return undefined; + } + const code = text(value.code); + const message = text(value.message); + if (code === undefined || message === undefined) { + return undefined; + } + const rawCommand = value.command; + const command = + isRecord(rawCommand) && text(rawCommand.command) !== undefined + ? { + command: text(rawCommand.command) as string, + arguments: Array.isArray(rawCommand.arguments) ? rawCommand.arguments : undefined, + title: text(rawCommand.title), + } + : undefined; + return { code, message, command }; +} + +function issues(value: unknown): McpplsIssue[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + const parsed = value.map(issue).filter((entry): entry is McpplsIssue => entry !== undefined); + return parsed.length === 0 ? undefined : parsed; +} + +/** + * Normalise the object mcppls's `activate()` returned. + * + * `lastStatus()` is read through `Function.prototype.call` on the object itself + * so a getter or a prototype method behaves the same way. + */ +export function readStateFromExports(exports: unknown, meta: McpplsMeta = {}): McpplsStateView { + const base: McpplsStateView = { + available: false, + version: meta.version, + active: meta.active, + enabled: meta.enabled, + }; + if (!isRecord(exports)) { + return { ...base, reason: "mcppls exposes no API object" }; + } + const reader = (exports as Record).lastStatus; + if (typeof reader !== "function") { + return { ...base, reason: "mcppls's API object has no lastStatus()" }; + } + let raw: unknown; + try { + raw = (reader as () => unknown).call(exports); + } catch (error) { + return { ...base, reason: `lastStatus() threw: ${error instanceof Error ? error.message : String(error)}` }; + } + if (!isRecord(raw)) { + // `undefined` is normal before the first status arrives; say so, do not complain. + return { ...base, reason: raw === undefined ? "no status yet" : "lastStatus() returned an unexpected shape" }; + } + const state = knownState(raw.state); + if (state === undefined) { + return { ...base, reason: `unknown state ${JSON.stringify(raw.state)}` }; + } + + const view: McpplsStateView = { ...base, available: true, state }; + + const project = raw.project; + if (isRecord(project)) { + const root = text(project.root); + const source = text(project.source); + if (root !== undefined && source !== undefined) { + view.project = { + root, + source, + level: finiteNumber(project.level), + tier: finiteNumber(project.tier), + }; + } + } + + const profile = raw.profile; + if (isRecord(profile)) { + const kind = text(profile.kind); + const stdlib = text(profile.stdlib); + const target = text(profile.target); + if (kind !== undefined && stdlib !== undefined && target !== undefined) { + view.profile = { kind, stdlib, target, compiler: text(profile.compiler), standard: text(profile.standard) }; + } + } + + const engine = raw.engine; + if (isRecord(engine)) { + const name = text(engine.name); + const version = text(engine.version); + if (name !== undefined && version !== undefined) { + view.engine = { name, version }; + } + } + + if (Array.isArray(raw.engines)) { + const engines = raw.engines + .map((entry): McpplsEngineStatus | undefined => { + if (!isRecord(entry)) return undefined; + const name = text(entry.name); + const version = text(entry.version); + const role = text(entry.role); + const state = text(entry.state); + return name !== undefined && version !== undefined && role !== undefined && state !== undefined + ? { name, version, role, state } + : undefined; + }) + .filter((entry): entry is McpplsEngineStatus => entry !== undefined); + if (engines.length > 0) { + view.engines = engines; + } + } + + const progress = raw.progress; + if (isRecord(progress)) { + const done = finiteNumber(progress.done); + const total = finiteNumber(progress.total); + if (done !== undefined && total !== undefined) { + view.progress = { done, total }; + } + } + + view.issues = issues(raw.issues); + view.notices = issues(raw.notices); + + const onlineRun = raw.onlineRun; + if (isRecord(onlineRun)) { + const outcome = text(onlineRun.outcome); + const message = text(onlineRun.message); + const at = text(onlineRun.at); + if (outcome !== undefined && message !== undefined && at !== undefined) { + view.onlineRun = { outcome, message, at }; + } + } + + return view; +} + +/** One line for the environment self-check; never throws. */ +export function describeState(view: McpplsStateView): string { + if (!view.available) { + return `unavailable (${view.reason ?? "unknown reason"})`; + } + const parts = [view.state ?? "unknown"]; + if (view.project !== undefined) parts.push(`project ${view.project.source}`); + if (view.engine !== undefined) parts.push(`engine ${view.engine.name} ${view.engine.version}`); + if (view.issues !== undefined) parts.push(`${view.issues.length} issue(s)`); + return parts.join(" · "); +} diff --git a/src/mcppls/stateSource.ts b/src/mcppls/stateSource.ts new file mode 100644 index 0000000..a63e92d --- /dev/null +++ b/src/mcppls/stateSource.ts @@ -0,0 +1,71 @@ +/** + * The `vscode`-facing half of the C++ Modules state read. + * + * `state.ts` normalises what it is given; this file fetches it. It is the only + * place that touches `extension.exports`, and it is deliberately defensive: the + * object is mcppls's test API, not a contract, so every failure becomes + * `available: false` with a reason and never an exception. + * + * `mcpp.languageService.readState` can switch the whole read off. When it is off + * the gate below returns before `extension.exports` is touched at all — not a + * partial read, no read — and the view falls back to the degraded content it + * always has for an installed-but-unreadable mcppls. + */ + +import * as vscode from "vscode"; + +import { read } from "../config/access"; +import { MCPPLS_EXTENSION_ID } from "./contract"; +import { readStateFromExports, type McpplsStateView } from "./state"; + +/** Why a state read did not happen. The view prints it verbatim. */ +export function stateUnavailableReason(reason: "disabled" | "missing"): string { + return reason === "disabled" + ? "reading the C++ Modules state is turned off (mcpp.languageService.readState)" + : `mcppls ${MCPPLS_EXTENSION_ID} is not installed`; +} + +function dependency(): vscode.Extension | undefined { + return vscode.extensions.getExtension(MCPPLS_EXTENSION_ID); +} + +export function languageServerInstalled(): boolean { + return dependency() !== undefined; +} + +export function languageServerVersion(): string | undefined { + const version = (dependency()?.packageJSON as { version?: string } | undefined)?.version; + return typeof version === "string" ? version : undefined; +} + +/** `mcppls.enable`, read-only: the setting belongs to the other extension. */ +export function languageServerEnabled(): boolean { + return vscode.workspace.getConfiguration("mcppls").get("enable", true) !== false; +} + +export function readLanguageServerState(): McpplsStateView { + const extension = dependency(); + const meta = { + version: languageServerVersion(), + active: extension?.isActive, + enabled: languageServerEnabled(), + }; + // The gate: nothing below this line runs when the setting is off, so + // `extension.exports` is never read. `read()` falls back to the declared + // default (true) when the registry has no value for the key. + if (read("mcpp.languageService.readState") === false) { + return { available: false, reason: stateUnavailableReason("disabled"), ...meta }; + } + if (extension === undefined) { + return { available: false, reason: stateUnavailableReason("missing"), ...meta }; + } + try { + return readStateFromExports(extension.exports, meta); + } catch (error) { + return { + available: false, + reason: `reading mcppls state threw: ${error instanceof Error ? error.message : String(error)}`, + ...meta, + }; + } +} diff --git a/src/mcppls/timers.ts b/src/mcppls/timers.ts new file mode 100644 index 0000000..f915951 --- /dev/null +++ b/src/mcppls/timers.ts @@ -0,0 +1,80 @@ +/** + * The one interval timer two views share. + * + * Both `mcpp.cache.autoRefreshSeconds` and + * `mcpp.languageService.stateRefreshSeconds` mean the same thing: re-read while + * the view is visible, stop when it is not, and never let a second read overlap + * one that is still running. Keeping the mechanism here — free of `vscode`, so + * `node:test` can drive it with a fake clock — means the two call sites own only + * the decision of *what* to refresh. + * + * The caller disposes the timer with the extension; `dispose()` is idempotent. + */ + +export interface PollTimerOptions { + periodMs: number; + /** The refresh itself. Runs between ticks; the timer never awaits it. */ + tick: () => void; + /** Injected for tests; defaults to `setInterval`/`clearInterval`. */ + setIntervalFn?: (handler: () => void, periodMs: number) => unknown; + clearIntervalFn?: (handle: unknown) => void; +} + +export class PollTimer { + private readonly setIntervalFn: (handler: () => void, periodMs: number) => unknown; + private readonly clearIntervalFn: (handle: unknown) => void; + private handle: unknown; + private periodMs: number; + + public constructor(private readonly options: PollTimerOptions) { + this.setIntervalFn = + options.setIntervalFn ?? ((handler, periodMs) => setInterval(handler, periodMs)); + this.clearIntervalFn = options.clearIntervalFn ?? ((handle) => clearInterval(handle as NodeJS.Timeout)); + this.periodMs = options.periodMs; + } + + /** True while a period is armed, so the caller can assert it did not leak. */ + public get active(): boolean { + return this.handle !== undefined; + } + + /** The period currently armed, in milliseconds. */ + public get period(): number { + return this.periodMs; + } + + /** + * Arm, re-arm or stop. A non-positive or unreadable period stops the timer, so + * "0 = off" holds no matter what the settings file contains. + */ + public start(periodMs: number): void { + this.periodMs = periodMs; + this.stop(); + if (!Number.isFinite(periodMs) || periodMs <= 0) { + return; + } + this.handle = this.setIntervalFn(() => { + try { + this.options.tick(); + } catch { + // A refresh that throws must not kill the timer or the host. + } + }, periodMs); + } + + public stop(): void { + if (this.handle === undefined) { + return; + } + try { + this.clearIntervalFn(this.handle); + } catch { + // Already cleared, or the host is shutting down. + } + this.handle = undefined; + } + + public dispose(): void { + this.stop(); + } +} diff --git a/src/process.ts b/src/process.ts deleted file mode 100644 index e6740cf..0000000 --- a/src/process.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; - -const execFileAsync = promisify(execFile); - -export interface ProcessResult { - exitCode: number; - stdout: string; - stderr: string; -} - -export interface ProcessRunOptions { - timeoutMs?: number; -} - -export type ProcessRunner = ( - executable: string, - args: string[], - cwd?: string, - options?: ProcessRunOptions, -) => Promise; - -export async function runProcess( - executable: string, - args: string[], - cwd?: string, - options: ProcessRunOptions = {}, -): Promise { - try { - const result = await execFileAsync(executable, args, { - cwd, - encoding: "utf8", - maxBuffer: 16 * 1024 * 1024, - timeout: options.timeoutMs, - }); - return { - exitCode: 0, - stdout: result.stdout, - stderr: result.stderr, - }; - } catch (error) { - const processError = error as NodeJS.ErrnoException & { - stdout?: string; - stderr?: string; - code?: number | string; - }; - return { - exitCode: typeof processError.code === "number" ? processError.code : 1, - stdout: processError.stdout ?? "", - stderr: processError.stderr ?? (typeof processError.message === "string" ? processError.message : ""), - }; - } -} diff --git a/src/inProject.ts b/src/projects/context.ts similarity index 50% rename from src/inProject.ts rename to src/projects/context.ts index 7b901e1..7b9f590 100644 --- a/src/inProject.ts +++ b/src/projects/context.ts @@ -1,6 +1,13 @@ export const IN_PROJECT_CONTEXT_KEY = "mcpp.inProject"; export const MCPP_MANIFEST_GLOB = "**/mcpp.toml"; +/** + * `mcpp.task.editorTitleButtons`: the second half of the editor/title `when` + * clause. `mcpp.inProject` says "this file belongs to an mcpp project"; this key + * lets the user turn the Run/Test buttons off without hiding them for everyone. + */ +export const EDITOR_TITLE_BUTTONS_CONTEXT_KEY = "mcpp.editorTitleButtons"; + export interface DisposableLike { dispose(): unknown; } @@ -17,6 +24,22 @@ export async function updateInProjectContext(env: InProjectEnvironment): Promise return inProject; } +export interface EditorTitleButtonsEnvironment { + enabled(): boolean; + setContextValue(key: string, value: boolean): PromiseLike; +} + +/** + * Apply `mcpp.task.editorTitleButtons` to the context key `package.json` gates + * the editor/title buttons with. The caller re-runs it on a configuration + * change; like every other context write it is fire-and-forget. + */ +export async function updateEditorTitleButtonsContext(env: EditorTitleButtonsEnvironment): Promise { + const enabled = env.enabled(); + await env.setContextValue(EDITOR_TITLE_BUTTONS_CONTEXT_KEY, enabled); + return enabled; +} + export function registerInProjectContext(env: InProjectEnvironment): { dispose(): unknown } { void updateInProjectContext(env); const disposables = env.subscribe(() => void updateInProjectContext(env)); diff --git a/src/discovery.ts b/src/projects/discovery.ts similarity index 53% rename from src/discovery.ts rename to src/projects/discovery.ts index c7a08ff..2f680f4 100644 --- a/src/discovery.ts +++ b/src/projects/discovery.ts @@ -6,6 +6,20 @@ export interface McppProjectDiscovery { manifestPath: string; } +/** + * How far the upward walk may go. `workspaceFolder` is the shipped behaviour; + * `filesystem` also reaches a project outside the opened folder + * (`mcpp.project.discoveryBoundary`). The walk itself never reads a setting — + * the `vscode` layer resolves the boundary and passes it in, which keeps this + * module pure and testable. + */ +export type DiscoveryBoundary = "workspaceFolder" | "filesystem"; + +/** `mcpp.project.discoveryBoundary`; anything unknown is the safe default. */ +export function discoveryBoundaryOf(value: unknown): DiscoveryBoundary { + return value === "filesystem" ? "filesystem" : "workspaceFolder"; +} + function isPathWithin(candidate: string, root: string): boolean { const relative = path.relative(root, candidate); return relative === "" || ( @@ -18,9 +32,13 @@ function isPathWithin(candidate: string, root: string): boolean { export function findNearestMcppProject( startPath: string, workspaceRoot?: string, + boundary: DiscoveryBoundary = "workspaceFolder", ): McppProjectDiscovery | undefined { let current = path.resolve(startPath); - const boundary = workspaceRoot === undefined ? undefined : path.resolve(workspaceRoot); + // `filesystem` drops the boundary entirely and keeps walking to the root. + const stop = boundary === "filesystem" || workspaceRoot === undefined + ? undefined + : path.resolve(workspaceRoot); try { if (statSync(current).isFile()) { @@ -30,7 +48,7 @@ export function findNearestMcppProject( // A newly-created workspace path may not exist yet; treat it as a directory. } - if (boundary !== undefined && !isPathWithin(current, boundary)) { + if (stop !== undefined && !isPathWithin(current, stop)) { return undefined; } @@ -39,7 +57,7 @@ export function findNearestMcppProject( if (existsSync(manifestPath)) { return { root: current, manifestPath }; } - if (boundary !== undefined && current === boundary) { + if (stop !== undefined && current === stop) { return undefined; } const parent = path.dirname(current); diff --git a/src/projects/summary.ts b/src/projects/summary.ts new file mode 100644 index 0000000..3ecc978 --- /dev/null +++ b/src/projects/summary.ts @@ -0,0 +1,111 @@ +/** + * A project summary read out of `mcpp.toml`, for the project view. + * + * Deliberately shallow and line-oriented: the view shows an identity line and + * the toolchain/targets, so it needs a handful of scalars, not a manifest model. + * Anything it cannot read stays `undefined` and the view simply omits it — the + * authoritative reader is mcpp itself, and `mcpp: Environment Self-check` shows + * what mcpp says. + * + * Pure: no `vscode`, no `mcpp` execution. + */ + +import type { ProjectSummary, TargetSummary } from "../views/models"; + +/** `[section]` or `[section.sub]`, ignoring quotes inside dotted names. */ +function headerOf(line: string): string | undefined { + const match = /^\s*\[\s*([^\]]+?)\s*\]\s*(?:#.*)?$/.exec(line); + if (match === null) { + return undefined; + } + const name = match[1].replace(/^['"]|['"]$/g, "").trim(); + return name.length === 0 ? undefined : name; +} + +/** `key = "value"` where the value is a plain string or a bare word. */ +function scalar(body: string, key: string): string | undefined { + const pattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}\\s*=\\s*(.+?)\\s*(?:#.*)?$`); + const match = pattern.exec(body); + if (match === null) { + return undefined; + } + return unquote(match[1]); +} + +function unquote(raw: string): string | undefined { + const trimmed = raw.trim(); + const quoted = /^"(.*)"$/.exec(trimmed) ?? /^'(.*)'$/.exec(trimmed); + const value = quoted === null ? trimmed : quoted[1]; + return value.length === 0 ? undefined : value; +} + +/** Section names whose scalars we read, matched by prefix. */ +const IDENTITY = "package"; +const TOOLCHAIN = "toolchain"; +const TARGETS = "targets."; + +export function readProjectSummary(root: string, lines: readonly string[]): ProjectSummary { + let section: string | undefined; + let name: string | undefined; + let version: string | undefined; + let standard: string | undefined; + let profile: string | undefined; + let toolchainSpec: string | undefined; + let target: string | undefined; + const targets: TargetSummary[] = []; + let currentTarget: { name: string; kind?: string } | undefined; + + const flushTarget = (): void => { + if (currentTarget !== undefined) { + targets.push({ name: currentTarget.name, kind: currentTarget.kind ?? "target" }); + currentTarget = undefined; + } + }; + + for (const line of lines) { + const header = headerOf(line); + if (header !== undefined) { + flushTarget(); + section = header; + if (header.startsWith(TARGETS)) { + currentTarget = { name: header.slice(TARGETS.length).replace(/^['"]|['"]$/g, "") }; + } + continue; + } + if (section === IDENTITY) { + // `[package]` only: a `[target.'cfg(..)'.package]` is a different plane. + name ??= scalar(line, "name"); + version ??= scalar(line, "version"); + standard ??= scalar(line, "standard"); + profile ??= scalar(line, "default_profile"); + } else if (section === TOOLCHAIN) { + toolchainSpec ??= scalar(line, "spec") ?? scalar(line, "family"); + } else if (section === "build") { + profile ??= scalar(line, "profile"); + } else if (currentTarget !== undefined) { + currentTarget.kind ??= scalar(line, "kind"); + } + } + flushTarget(); + + // `[target.]` names the triple; `[targets.]` names an artifact. + for (const line of lines) { + const match = /^\s*\[\s*target\s*\.\s*(?:'([^']+)'|"([^"]+)"|([^\]]+))\s*\]\s*$/.exec(line); + if (match !== null) { + target = (match[1] ?? match[2] ?? match[3] ?? "").trim() || undefined; + break; + } + } + + // Keys are omitted rather than set to undefined, so a summary compares cleanly. + const summary: ProjectSummary = { root }; + if (name !== undefined) summary.name = name; + if (version !== undefined) summary.version = version; + if (standard !== undefined) summary.standard = standard; + if (profile !== undefined) summary.profile = profile; + if (toolchainSpec !== undefined) summary.toolchainSpec = toolchainSpec; + if (target !== undefined) summary.target = target; + if (targets.length > 0) summary.targets = targets; + if (lines.some((line) => headerOf(line) === "test")) summary.hasTests = true; + return summary; +} diff --git a/src/toml/completion.ts b/src/toml/completion.ts new file mode 100644 index 0000000..876d2e6 --- /dev/null +++ b/src/toml/completion.ts @@ -0,0 +1,523 @@ +// mcpp.toml 的代码补全查询层(schema 驱动版)。 +// +// 范围:段头结构建议 + 已知段的键/枚举值补全 + 开放词汇段的写法模板。每条建议 +// 携带显式替换范围。段/键/枚举来自 `data/toml-schema.json`(src/toml/schema.ts), +// 与诊断共用同一份快照:快照没有的段不会出现在段头表里,快照标记 `openKeys` +// 的段(键由用户自选)不出键建议。 +// +// 依赖版本补全(`mcpp.toml.indexCompletion`,默认关):本模块只负责 +// 「光标是不是在依赖版本值位置」与「把候选变成建议」两件纯事;执行 +// `mcpp search`、超时、会话缓存、信任/离线判定都在 src/toml/providers.ts, +// 解析人类输出在 src/cli/search.ts。本模块不依赖 vscode API:`t()` 只在 +// `auto` 路径上按需加载编辑器 API,纯单测环境下退化为英文 key。 + +import type { PackageVersion } from "../cli/search"; +import { t } from "../i18n/t"; +import { + contextAt, + parseMcppToml, + resolveSection, + type ReplaceRange, +} from "./parser"; +import { SCHEMA, sectionByName, type TomlKey, type TomlSection } from "./schema"; + +export type McppTomlSuggestionKind = "section" | "template" | "version"; + +export interface McppTomlSuggestion { + label: string; + kind: McppTomlSuggestionKind; + detail: string; + documentation?: string; + /** 插入文本;含 $1 等 snippet 占位符。缺省时插入 label。 */ + insertSnippet?: string; + /** 替换范围(光标所在行的起止列)。 */ + range: ReplaceRange; +} + +export interface SectionHeaderSpec { + group: string; + label: string; + /** snippet 形式的段头(含 ${1:...} 占位)。 */ + header: string; + /** + * 明细文案。存的是英文 key 的**取用函数**,每个补全请求才 `t()` 一次,所以 + * 改 `mcpp.ui.language` 无需重载窗口,且本模块在无编辑器的单测里也能加载。 + */ + detail: () => string; +} + +// 段头明细表:只补充「写法」(label/snippet/detail),不再是段清单本身。 +// 段清单来自 schema;这里按 group 名匹配,schema 新出现的段用 plane 兜底。 +// 出处:mcpp 文档 02/03/05/06 与 src/manifest/toml.cppm 的段清单(契约测试用 +// 真实 mcpp 逐段验证)。 +export const SECTION_HEADERS: readonly SectionHeaderSpec[] = [ + { group: "package", label: "[package]", header: "[package]", detail: () => t("Package metadata") }, + { group: "lib", label: "[lib]", header: "[lib]", detail: () => t("Library root-module convention") }, + { group: "build", label: "[build]", header: "[build]", detail: () => t("Build configuration") }, + { group: "generated_files", label: "[generated_files]", header: "[generated_files]", detail: () => t("Generated files (path → content)") }, + { group: "dependencies", label: "[dependencies]", header: "[dependencies]", detail: () => t("Runtime dependencies") }, + { group: "dev-dependencies", label: "[dev-dependencies]", header: "[dev-dependencies]", detail: () => t("Development/test dependencies") }, + { group: "build-dependencies", label: "[build-dependencies]", header: "[build-dependencies]", detail: () => t("Build-time dependencies (pulled at build time only, invisible at run time)") }, + { group: "workspace", label: "[workspace]", header: "[workspace]", detail: () => t("Workspace member declarations") }, + { group: "workspace.dependencies", label: "[workspace.dependencies]", header: "[workspace.dependencies]", detail: () => t("Declare dependency versions centrally; members inherit them with workspace = true") }, + { group: "features", label: "[features]", header: "[features]", detail: () => t("Feature definitions") }, + { group: "feature-deps", label: "[feature-deps.]", header: "[feature-deps.${1:name}]", detail: () => t("Optional dependencies pulled in by a feature") }, + { group: "capabilities", label: "[capabilities]", header: "[capabilities]", detail: () => t("Capability bindings (provider selection)") }, + { group: "targets", label: "[targets.]", header: "[targets.${1:name}]", detail: () => t("Build targets") }, + { group: "profile", label: "[profile.]", header: "[profile.${1:name}]", detail: () => t("Build profiles") }, + { group: "runtime", label: "[runtime]", header: "[runtime]", detail: () => t("Host runtime capabilities") }, + { group: "resources", label: "[resources]", header: "[resources]", detail: () => t("Metadata and assets compiled into the product (PE targets only)") }, + { group: "toolchain", label: "[toolchain]", header: "[toolchain]", detail: () => t("Compiler toolchain shorthand") }, + { group: "xlings", label: "[xlings]", header: "[xlings]", detail: () => t("Build environment (supplied by xlings)") }, + { group: "xlings.workspace", label: "[xlings.workspace]", header: "[xlings.workspace]", detail: () => t("Pin tool versions") }, + { group: "target", label: "[target.]", header: "[target.${1:x86_64-linux-gnu}]", detail: () => t("Configuration per target triple") }, + { group: "pack", label: "[pack]", header: "[pack]", detail: () => t("mcpp pack packaging configuration") }, + { group: "pack.bundle-project", label: "[pack.bundle-project]", header: "[pack.bundle-project]", detail: () => t("Fine-tuning of the vendored filtering policy") }, + { group: "indices", label: "[indices]", header: "[indices]", detail: () => t("Project-level index redirection") }, + { group: "tools.overrides", label: "[tools.overrides]", header: "[tools.overrides]", detail: () => t("Host tool binary overrides") }, + { group: "language", label: "[language]", header: "[language]", detail: () => t("Legacy compatibility field; new projects should use [package].standard") }, +]; + +/** 依赖类段(键位置给依赖写法模板)。 */ +const DEPENDENCY_GROUPS: ReadonlySet = new Set([ + "dependencies", + "dev-dependencies", + "build-dependencies", + "workspace.dependencies", + "feature-deps", +]); + +interface TemplateSpec { + label: string; + /** 同 `SectionHeaderSpec.detail`:取用时才 `t()`,语言切换无需重载。 */ + detail: () => string; + documentation?: () => string; + insertSnippet: string; +} + +const DEPENDENCY_TEMPLATES: readonly TemplateSpec[] = [ + { + label: 'name = "version"', + detail: () => t("SemVer version dependency"), + documentation: () => t("Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported."), + insertSnippet: '${1:name} = "${2:1.0.0}"', + }, + { + label: "name = { path = ... }", + detail: () => t("Path dependency (local development)"), + insertSnippet: '${1:name} = { path = "${2:../mylib}" }', + }, + { + label: "name = { git = ..., tag = ... }", + detail: () => t("Git dependency (one of tag / branch / rev)"), + insertSnippet: '${1:name} = { git = "${2:https://github.com/user/repo.git}", tag = "${3:v1.0.0}" }', + }, + { + label: "name = { version = ..., features = [...] }", + detail: () => t("Long dep spec: request a feature of that dependency"), + insertSnippet: '${1:name} = { version = "${2:1.0}", features = ["${3:feature}"] }', + }, + { + label: "name = { version = ..., tools = [...] }", + detail: () => t("Host tools produced by the dependency (must be a bin target of that package)"), + insertSnippet: '${1:name} = { version = "${2:1.0}", tools = ["${3:protoc}"] }', + }, +]; + +const FEATURE_TEMPLATES: readonly TemplateSpec[] = [ + { label: "name = [...]", detail: () => t("Array shorthand: implies the feature only"), insertSnippet: "${1:name} = [${2}]" }, + { label: "name = { defines = [...] }", detail: () => t("Table form: contributes the package's own macros when activated"), insertSnippet: '${1:name} = { defines = ["${2:MACRO}"] }' }, + { label: "name = { requires = [...] }", detail: () => t("Table form: requires a capability"), insertSnippet: '${1:name} = { requires = ["${2:blas}"] }' }, + { label: "name = { sources = [...] }", detail: () => t("Table form: feature-gated source globs"), insertSnippet: '${1:name} = { sources = ["${2:src/simd/**}"] }' }, +]; + +const GENERATED_FILE_TEMPLATES: readonly TemplateSpec[] = [ + { + label: '"path" = "content"', + detail: () => t("Generated file (relative path → content, part of the fingerprint)"), + insertSnippet: '"${1:src/gen/wrap.cppm}" = """\n${2:}\n"""', + }, +]; + +const CAPABILITY_TEMPLATES: readonly TemplateSpec[] = [ + { + label: 'capability = "provider"', + detail: () => t("Capability binding (equivalent to --cap)"), + insertSnippet: '${1:blas} = "${2:compat.openblas}"', + }, +]; + +const XLINGS_WORKSPACE_TEMPLATES: readonly TemplateSpec[] = [ + { label: 'tool = "version"', detail: () => t("Pin the xlings tool version"), insertSnippet: '${1:node} = "${2:24.19.0}"' }, +]; + +const TOOLS_OVERRIDES_TEMPLATES: readonly TemplateSpec[] = [ + { + label: '"pkg:tool" = "path"', + detail: () => t("Override a host tool with an existing binary (skips the build)"), + insertSnippet: '"${1:compat.protobuf:protoc}" = "${2:/usr/bin/protoc}"', + }, +]; + +const TEMPLATES_BY_GROUP: Record = { + "features": FEATURE_TEMPLATES, + "generated_files": GENERATED_FILE_TEMPLATES, + "capabilities": CAPABILITY_TEMPLATES, + "xlings.workspace": XLINGS_WORKSPACE_TEMPLATES, + "tools.overrides": TOOLS_OVERRIDES_TEMPLATES, +}; + +/** 手写明细按 group 索引;schema 段只借它取 label/snippet/detail。 */ +const HEADER_SPEC_BY_GROUP: ReadonlyMap = new Map( + SECTION_HEADERS.map((spec) => [spec.group, spec]), +); + +interface HeaderEntry { + label: string; + header: string; + detail: string; + documentation?: string; +} + +/** + * 段头清单:以 schema 为准(§3.3.2)。schema 为空(快照未生成)时才退回手写 + * 清单,保证补全在缺数据时仍可用。手写明细按段名匹配以保留既有 label/snippet/ + * 文案;schema 新收录的段用平面名兜底。 + */ +function headerEntries(): HeaderEntry[] { + if (SCHEMA.sections.length === 0) { + return SECTION_HEADERS.map((spec) => ({ + label: spec.label, + header: spec.header, + detail: spec.detail(), + })); + } + // The snapshot describes the schema mcpp validates; the curated list also covers + // tables mcpp accepts but the snapshot does not model (for example + // `[workspace.dependencies]`). Merge them, preferring the curated wording, so + // schema-driven detail never costs a writable table its suggestion. + const entries: HeaderEntry[] = SCHEMA.sections.map((section) => { + const spec = HEADER_SPEC_BY_GROUP.get(section.name); + const entry: HeaderEntry = { + label: spec?.label ?? section.header, + header: spec?.header ?? section.header, + detail: spec?.detail() ?? `plane: ${section.plane}`, + }; + if (section.deprecatedBy) { + entry.documentation = `Deprecated; use \`${section.deprecatedBy}\`.`; + } + return entry; + }); + const suggested = new Set(entries.map((entry) => entry.header)); + for (const spec of SECTION_HEADERS) { + if (suggested.has(spec.header)) { + continue; + } + entries.push({ label: spec.label, header: spec.header, detail: spec.detail() }); + } + return entries; +} + +function headerSuggestions(range: ReplaceRange): McppTomlSuggestion[] { + return headerEntries().map((entry) => { + const suggestion: McppTomlSuggestion = { + label: entry.label, + kind: "section", + detail: entry.detail, + insertSnippet: entry.header, + range, + }; + if (entry.documentation !== undefined) { + suggestion.documentation = entry.documentation; + } + return suggestion; + }); +} + +function templateSuggestions(templates: readonly TemplateSpec[], range: ReplaceRange): McppTomlSuggestion[] { + return templates.map((template) => ({ + label: template.label, + kind: "template", + detail: template.detail(), + documentation: template.documentation?.(), + insertSnippet: template.insertSnippet, + range, + })); +} + +/** 类型的占位值:与 §3.3.2 的键补全约定一致。 */ +function keyPlaceholder(key: TomlKey): string { + switch (key.type) { + case "boolean": + return "true"; + case "number": + return "0"; + case "array": + return "[]"; + case "enum": + return JSON.stringify(key.values?.[0] ?? ""); + default: + return '""'; + } +} + +/** `detail` 展示类型;枚举展开取值,有默认值再补默认值。 */ +function keyDetail(key: TomlKey): string { + let detail = key.type; + if (key.values !== undefined && key.values.length > 0) { + detail += `: ${key.values.join(" | ")}`; + } + if (key.default !== undefined) { + detail += ` (default: ${JSON.stringify(key.default)})`; + } + return detail; +} + +function keyDocumentation(key: TomlKey): string | undefined { + const lines: string[] = []; + if (key.note !== undefined && key.note !== "") { + lines.push(key.note); + } + if (key.since !== undefined) { + lines.push(`Since ${key.since}.`); + } + if (key.legacy === true) { + lines.push("Legacy key."); + } + return lines.length === 0 ? undefined : lines.join(" "); +} + +function keySuggestions( + section: TomlSection, + used: ReadonlySet, + range: ReplaceRange, +): McppTomlSuggestion[] { + const keys = section.keys ?? []; + return keys + .filter((key) => !used.has(key.key)) + .map((key) => { + const suggestion: McppTomlSuggestion = { + label: key.key, + kind: "template", + detail: keyDetail(key), + insertSnippet: `${key.key} = ${keyPlaceholder(key)}`, + range, + }; + const documentation = keyDocumentation(key); + if (documentation !== undefined) { + suggestion.documentation = documentation; + } + return suggestion; + }); +} + +/** + * 同段中光标之前已出现的顶层键。用容错解析器遍历节点树:`[targets.]` 这类 + * 行表按 parser 的组归属聚合,因此同组的不同行表会互相剔除已用键。 + */ +function keysUsedBefore(lines: readonly string[], line: number, group: string): Set { + const used = new Set(); + let current: string | undefined; + for (const node of parseMcppToml(lines).nodes) { + if (node.type === "section") { + const resolution = resolveSection(node.segments.map((segment) => segment.name)); + current = resolution.kind === "known" ? resolution.group : undefined; + continue; + } + if (current !== group || node.range.startLine >= line) { + continue; + } + const first = node.keyPath[0]; + if (first !== undefined) { + used.add(first.name); + } + } + return used; +} + +/** 值位置的枚举键:点分键按完整路径查,找不到退回首段(与诊断一致)。 */ +function enumKeyOf(section: TomlSection | undefined, keyPath: readonly string[]): TomlKey | undefined { + if (section?.keys === undefined || keyPath.length === 0) { + return undefined; + } + const joined = keyPath.join("."); + const key = + section.keys.find((entry) => entry.key === joined) ?? + section.keys.find((entry) => entry.key === keyPath[0]); + return key?.type === "enum" ? key : undefined; +} + +function enumValueSuggestions( + key: TomlKey, + insideString: boolean, + range: ReplaceRange, +): McppTomlSuggestion[] { + return (key.values ?? []).map((value) => ({ + label: value, + kind: "template", + detail: `value of ${key.key}`, + // 字符串内只替换内容;裸值位置补上引号,枚举值在 manifest 里都是字符串。 + insertSnippet: insideString ? value : JSON.stringify(value), + range, + })); +} + +/** + * 计算 mcpp.toml 在指定位置的补全建议(段头 + 键 + 枚举值 + 写法模板)。 + */ +export function computeMcppTomlCompletions( + lines: readonly string[], + line: number, + character: number, +): McppTomlSuggestion[] { + const context = contextAt(lines, line, character); + + if (context.kind === "section-header") { + // mcpp manifest 不使用 TOML 数组表([[...]]);[[ 内不提供建议, + // 避免把用户意图的数组表悄悄替换成普通段 [x](未知段会被 mcpp 静默忽略)。 + if (context.isArray) { + return []; + } + // parser 的替换范围从段名 token 开始;段头建议插入的是完整 "[xxx]", + // 需要把范围扩展到本行的 "[",避免留下 "[["。仅当 "[" 是行内首个 + // 非空白字符时才扩展(section-header 上下文正常都满足,防御奇怪输入)。 + const lineText = (lines[line] ?? "").replace(/\r$/, ""); + const bracket = lineText.indexOf("["); + const firstNonWs = lineText.search(/\S/); + const range = bracket >= 0 && bracket === firstNonWs + ? { startCharacter: bracket, endCharacter: context.replaceRange.endCharacter } + : context.replaceRange; + return headerSuggestions(range); + } + + if (context.kind === "key") { + const { section, containerPath, replaceRange } = context; + // 文档顶部(尚无段头):提示段头。未知段:不提供建议 + // (附录 A:不支持包自定义 toml 键)。 + if (section.kind === "top") { + return headerSuggestions(replaceRange); + } + if (section.kind !== "known" || containerPath.length > 0) { + return []; + } + if (DEPENDENCY_GROUPS.has(section.group)) { + return templateSuggestions(DEPENDENCY_TEMPLATES, replaceRange); + } + const schemaSection = sectionByName(section.group); + // openKeys 段的键由用户自选(如 [toolchain] 的平台名),不能给固定词表。 + if (schemaSection?.openKeys !== true && schemaSection?.keys !== undefined) { + const used = keysUsedBefore(lines, line, section.group); + return keySuggestions(schemaSection, used, replaceRange); + } + const templates = TEMPLATES_BY_GROUP[section.group]; + return templates === undefined ? [] : templateSuggestions(templates, replaceRange); + } + + // 值位置:只有已知枚举键才出候选,其余自由格式值不瞎猜。 + if (context.section.kind === "known") { + const schemaSection = sectionByName(context.section.group); + const key = enumKeyOf(schemaSection, context.keyPath); + if (key !== undefined) { + return enumValueSuggestions(key, context.insideString, context.replaceRange); + } + } + return []; +} + +/** 光标所在的依赖版本值位置:属于哪个依赖、替换范围、是否已在字符串内。 */ +export interface DependencyVersionContext { + /** 依赖名(值所属的键,如 `zlib`)。 */ + name: string; + /** 光标已在引号内(只替换内容,不再补引号)。 */ + insideString: boolean; + /** 替换范围:简写覆盖 `= ` 之后的值,长式在字符串内只覆盖已输入内容。 */ + range: ReplaceRange; +} + +/** + * 判断光标是否落在依赖的**版本值**上(方案 §3.3.2 的 `toml.indexCompletion` + * 位置)。只认两种写法: + * + * `zlib = "1.2|"` 简写版本 + * `zlib = { version = "1.2|" }` 长式 dep spec 的 version 字段 + * + * `git` / `path` / `features` 的值不是版本,返回 undefined,绝不据此查询索引。 + */ +export function dependencyVersionContextAt( + lines: readonly string[], + line: number, + character: number, +): DependencyVersionContext | undefined { + const context = contextAt(lines, line, character); + if (context.kind !== "value" || context.section.kind !== "known") { + return undefined; + } + if (!DEPENDENCY_GROUPS.has(context.section.group) || context.keyPath.length === 0) { + return undefined; + } + const last = context.keyPath[context.keyPath.length - 1]; + if (context.keyPath.length > 1 && last !== "version") { + return undefined; + } + // A version is a string, or a value not typed yet. An inline table, an array + // or a bare non-version token is a different dep-spec field; offering versions + // there would replace the wrong thing. + const kind = context.valueKind; + if (kind !== undefined && kind !== "string" && kind !== "unknown") { + return undefined; + } + return { + name: context.keyPath[0], + insideString: context.insideString, + range: context.replaceRange, + }; +} + +/** 把索引候选变成补全建议;字符串内只替换内容,裸值位置补上引号。 */ +export function indexVersionSuggestions( + context: DependencyVersionContext, + versions: readonly PackageVersion[], +): McppTomlSuggestion[] { + return versions.map((entry) => { + const suggestion: McppTomlSuggestion = { + label: entry.version, + kind: "version", + detail: + entry.summary === undefined ? `version of ${context.name}` : `${context.name}: ${entry.summary}`, + insertSnippet: context.insideString ? entry.version : JSON.stringify(entry.version), + range: context.range, + }; + return suggestion; + }); +} + +/** 取索引候选:按依赖名查询,失败与超时都由调用方降级为空数组。 */ +export type IndexVersionResolver = (name: string) => Promise; + +/** + * 静态补全 + 依赖版本补全的合并入口。静态层有建议就返回它;否则若光标在依赖 + * 版本值上且调用方给了 resolver,就询问索引。resolver 抛异常时静默降级为 + * 「无候选」,绝不冒泡成错误。 + */ +export async function computeMcppTomlCompletionsWithIndex( + lines: readonly string[], + line: number, + character: number, + resolveIndex?: IndexVersionResolver, +): Promise { + const direct = computeMcppTomlCompletions(lines, line, character); + if (direct.length > 0 || resolveIndex === undefined) { + return direct; + } + const context = dependencyVersionContextAt(lines, line, character); + if (context === undefined) { + return direct; + } + let versions: readonly PackageVersion[]; + try { + versions = await resolveIndex(context.name); + } catch { + return direct; + } + return indexVersionSuggestions(context, versions); +} diff --git a/src/toml/diagnostics.ts b/src/toml/diagnostics.ts new file mode 100644 index 0000000..1f0095e --- /dev/null +++ b/src/toml/diagnostics.ts @@ -0,0 +1,629 @@ +// mcpp.toml 的纯文本诊断层。 +// +// 输入是文件的物理行 + 严重度配置,输出是 1 基行列(列按 UTF-16 码元计数, +// 与 VS Code 的 Diagnostic 一致)的诊断列表。本模块不依赖 vscode API, +// 可在 node --test 下直接测试。 +// +// 它**不是**一个 TOML 校验器:只理解一个保守子集——段头、`key = value`、 +// 行注释、单/双引号字符串、三引号多行字符串,以及可跨行的数组/内联表—— +// 目的是抓住明显写坏的行与 mcpp 专属的平面/废弃规则,而不是重实现 TOML。 +// 子集之外的一切一律不报(宁可漏报,不可误报)。下面每个规则都注明它停在哪里。 +// +// 规则与严重度(默认值见 data/toml-schema.json 的 rules): +// 1 syntax 明显的语法破坏 +// 2 unknown-section 段不在 schema 中 +// 3 unknown-key 已知段中的未知键 +// 4 plane-separation 工具当依赖 / 包当工具 +// 5 mcpp-floor [package].mcpp 不是 ">=" 形式 +// 6 legacy-key legacy 段/键 +// 7 array-table 数组表 [[...]](mcpp 只接受少数几处,见下) + +import { sectionByName, type TomlKey, type TomlSection } from "./schema"; + +export type Severity = "error" | "warning" | "info" | "off"; + +export interface TomlDiagnostic { + code: string; + message: string; + severity: Exclude; + /** 1 基行号。 */ + line: number; + /** 1 基起始列(含)。 */ + startCharacter: number; + /** 1 基结束列(不含)。 */ + endCharacter: number; + relatedKey?: string; +} + +export interface DiagnosticSettings { + syntax: Severity; + unknownSection: Severity; + unknownKey: Severity; + planeSeparation: Severity; + legacyKeys: Severity; +} + +/** + * `mcppFloor` 与 `arrayTable` 在设置面上还没有独立开关(§4 的 registry 尚未 + * 收录),因此默认是 error;一旦设置面补齐,传入同名字段即可覆盖,无需改签名。 + */ +type ExtendedSettings = DiagnosticSettings & { + mcppFloor?: Severity; + arrayTable?: Severity; +}; + +/** 稳定的诊断码。补全/快速修复按它分发。 */ +export const DIAGNOSTIC_CODES = { + syntax: "mcpp.manifest.syntax", + unknownSection: "mcpp.manifest.unknownSection", + unknownKey: "mcpp.manifest.unknownKey", + planeSeparation: "mcpp.manifest.planeSeparation", + mcppFloor: "mcpp.manifest.mcppFloor", + legacyKey: "mcpp.manifest.legacyKey", + arrayTable: "mcpp.manifest.arrayTable", +} as const; + +type RuleName = keyof typeof DIAGNOSTIC_CODES; + +// ── TOML 子集的扫描状态 ───────────────────────────────────────────────────── +// mode: 当前是否在一个未闭合的三引号字符串里。 +// depth: 未闭合的 `[` / `{` 层数(数组与内联表可跨行)。 +interface ParseState { + mode: "normal" | "ml-double" | "ml-single"; + depth: number; +} + +/** text[i] 之前的反斜杠是否为奇数个(三引号串里的 \"\"\" 不结束)。 */ +function isEscaped(text: string, index: number): boolean { + let backslashes = 0; + for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) { + backslashes += 1; + } + return backslashes % 2 === 1; +} + +/** + * 扫描一行,更新跨行状态;返回该行中「真正的注释起始 `#`」的下标(没有则 -1)。 + * 引号内的 `#` 不是注释。这是本模块对 TOML 词法理解的边界:不处理转义还原、 + * 不处理 \uXXXX、不看值是否合法。 + */ +function scan(text: string, state: ParseState): number { + let i = 0; + while (i < text.length) { + if (state.mode === "ml-double") { + if (text.startsWith('"""', i) && !isEscaped(text, i)) { + state.mode = "normal"; + i += 3; + continue; + } + i += 1; + continue; + } + if (state.mode === "ml-single") { + if (text.startsWith("'''", i)) { + state.mode = "normal"; + i += 3; + continue; + } + i += 1; + continue; + } + + const ch = text[i]; + if (ch === "#") { + return i; + } + if (ch === '"') { + if (text.startsWith('"""', i)) { + state.mode = "ml-double"; + i += 3; + continue; + } + i += 1; + while (i < text.length) { + if (text[i] === "\\") { + i += 2; + continue; + } + if (text[i] === '"') { + i += 1; + break; + } + i += 1; + } + continue; + } + if (ch === "'") { + if (text.startsWith("'''", i)) { + state.mode = "ml-single"; + i += 3; + continue; + } + i += 1; + while (i < text.length) { + if (text[i] === "'") { + i += 1; + break; + } + i += 1; + } + continue; + } + if (ch === "[" || ch === "{") { + state.depth += 1; + i += 1; + continue; + } + if (ch === "]" || ch === "}") { + if (state.depth > 0) state.depth -= 1; + i += 1; + continue; + } + i += 1; + } + return -1; +} + +/** 去掉行尾注释(引号内的 `#` 保留),返回可解析的代码片段。 */ +function codeBeforeComment(text: string): string { + const stop = scan(text, { mode: "normal", depth: 0 }); + return stop < 0 ? text : text.slice(0, stop); +} + +/** 在引号之外查找 needle 的下标;找不到返回 -1。 */ +function indexOfUnquoted(text: string, needle: string): number { + let i = 0; + while (i < text.length) { + const ch = text[i]; + if (ch === '"' || ch === "'") { + const delim = ch.repeat(3); + if (!text.startsWith(delim, i)) { + i += 1; + while (i < text.length) { + if (ch === '"' && text[i] === "\\") { + i += 2; + continue; + } + if (text[i] === ch) { + i += 1; + break; + } + i += 1; + } + continue; + } + i += 3; + while (i < text.length) { + if (text.startsWith(delim, i) && !isEscaped(text, i)) { + i += 3; + break; + } + i += 1; + } + continue; + } + if (text.startsWith(needle, i)) { + return i; + } + i += 1; + } + return -1; +} + +/** 按点拆分段路径/点分键,引号内的点不拆。保留引号。 */ +function splitDots(text: string): string[] { + const parts: string[] = []; + let current = ""; + let quote: string | null = null; + for (let i = 0; i < text.length; i += 1) { + const ch = text[i]; + if (quote !== null) { + current += ch; + if (ch === quote) { + quote = null; + } + continue; + } + if (ch === '"' || ch === "'") { + quote = ch; + current += ch; + continue; + } + if (ch === ".") { + parts.push(current); + current = ""; + continue; + } + current += ch; + } + parts.push(current); + return parts; +} + +function isBareKey(text: string): boolean { + return /^[A-Za-z0-9_-]+$/.test(text); +} + +function isQuotedKey(text: string): boolean { + if (text.length < 2) { + return false; + } + const first = text[0]; + const last = text[text.length - 1]; + return (first === '"' && last === '"') || (first === "'" && last === "'"); +} + +/** `a.b` / `"a b"` / `c."d.e"` 是合法键;其余交给语法规则。 */ +function isValidKey(text: string): boolean { + const parts = splitDots(text); + return parts.length > 0 && parts.every((part) => isBareKey(part) || isQuotedKey(part)); +} + +/** 去掉一层引号;不是带引号的字符串则返回 undefined(不做转义还原)。 */ +function unquote(text: string): string | undefined { + if (text.length >= 2) { + const first = text[0]; + const last = text[text.length - 1]; + if ((first === '"' && last === '"') || (first === "'" && last === "'")) { + return text.slice(1, -1); + } + } + return undefined; +} + +// ── schema 侧的段解析 ─────────────────────────────────────────────────────── + +/** 精确匹配,失败时逐级去掉尾部段(`[target.'cfg(x)'.dependencies]` → `target`)。 */ +function lookupSection(name: string): TomlSection | undefined { + const exact = sectionByName(name); + if (exact !== undefined) { + return exact; + } + const parts = splitDots(name); + for (let i = parts.length - 1; i >= 1; i -= 1) { + const candidate = sectionByName(parts.slice(0, i).join(".")); + if (candidate !== undefined) { + return candidate; + } + } + return undefined; +} + +/** + * 键规则要用的段:`[targets.]`、`[profile.]`、`[target.]` 这三种 + * 「行表」继承基段的键表,更深一层(`[package.metadata.]`、 + * `[target..build]`、`[runtime.]`、`[toolchain.]`)的键 + * 属于另一套词表,不做未知键检查。 + */ +const ROW_KEY_SECTIONS: ReadonlySet = new Set(["targets", "profile", "target"]); + +function keySectionOf(headerName: string): TomlSection | undefined { + const exact = sectionByName(headerName); + if (exact !== undefined) { + return exact; + } + const parts = splitDots(headerName); + if (parts.length === 2 && ROW_KEY_SECTIONS.has(parts[0])) { + return sectionByName(parts[0]); + } + return undefined; +} + +/** 键在段里的元数据;点分键(`metadata.demo`)按首段查找。 */ +function keyInfoOf(section: TomlSection | undefined, key: string): TomlKey | undefined { + if (section?.keys === undefined) { + return undefined; + } + const parts = splitDots(key); + const direct = section.keys.find((entry) => entry.key === key); + if (direct !== undefined) { + return direct; + } + if (parts.length > 1) { + return section.keys.find((entry) => entry.key === parts[0]); + } + return undefined; +} + +// ── mcpp 专属规则用的判定 ─────────────────────────────────────────────────── + +const DEPENDENCY_TABLES: ReadonlySet = new Set([ + "dependencies", + "dev-dependencies", + "build-dependencies", +]); + +function lastSegment(name: string): string { + const parts = splitDots(name); + return parts[parts.length - 1]; +} + +function isDependencySection(name: string): boolean { + return DEPENDENCY_TABLES.has(lastSegment(name)); +} + +/** `[xlings]`、`[xlings.workspace]` 及其条件形式 `[target..xlings…]`。 */ +function isXlingsSection(name: string): boolean { + return splitDots(name).includes("xlings"); +} + +/** 工具载荷名:`xim:` / `xpm:` / `xlings:` / `xvm:` 前缀。 */ +function isToolPayloadName(key: string): boolean { + const bare = unquote(key) ?? key; + return /^(xim|xpm|xlings|xvm):/i.test(bare); +} + +/** mcpp 包身份形如 `namespace.name`(至少一个点),不是工具载荷。 */ +function isMcppPackageName(key: string): boolean { + const bare = unquote(key) ?? key; + if (isToolPayloadName(bare)) { + return false; + } + return /^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)+$/.test(bare); +} + +/** + * 值是否陈述了一个版本而不是一个路径。`"1.0"` / `{ version = "1.0" }` 是; + * `{ path = "../x" }`、`""`(平台键对象)、`{ linux = "" }` 不是。 + */ +function declaresVersion(valueText: string): boolean { + const text = valueText.trim(); + if (text.startsWith("{")) { + return /(^|[,{\s])version\s*=/.test(text) && !/(^|[,{\s])path\s*=/.test(text); + } + const bare = unquote(text); + if (bare === undefined || bare === "") { + return false; + } + return !bare.includes("/") && !bare.startsWith("."); +} + +/** + * mcpp 允许的数组表(`[[...]]`)。这份名单来自 toml.cppm 的 + * `kAllowedArraysOfTables`:除这些之外,任何 `[[...]]` 都是 mcpp 不接受的写法。 + * + * 注意:任务书写的是「`[[name]]`,mcpp 不使用」,但 mcpp 明确接受 + * `[[build.flags]]`、`[[features..flags]]`、`[[runtime.requirements]]` 等 + * 少数数组表,把它们报错就是误报——而本层的第一原则是不误报。 + */ +const ALLOWED_ARRAY_TABLES: ReadonlyArray> = [ + ["build", "flags"], + ["build", "sources"], + ["features", "*", "flags"], + ["runtime", "requirements"], + ["runtime", "artifacts"], + ["runtime", "deploy"], + ["target", "*", "build", "flags"], + ["target", "*", "build", "sources"], + ["xlings", "deps"], +]; + +function isAllowedArrayTable(name: string): boolean { + const parts = splitDots(name); + return ALLOWED_ARRAY_TABLES.some( + (pattern) => + pattern.length === parts.length && pattern.every((part, index) => part === "*" || part === parts[index]), + ); +} + +/** docs/04 §2.1:只有 `>=` 形式的下界被接受。 */ +function isMcppFloor(valueText: string): boolean { + const bare = unquote(valueText.trim()); + return bare !== undefined && /^>=\d+(\.\d+)+$/.test(bare); +} + +// ── 入口 ──────────────────────────────────────────────────────────────────── + +/** + * 分析一份 mcpp.toml。`lines` 是文件的物理行(含或不含 `\r` 都可以); + * 返回按 (line, startCharacter, code) 排序的稳定诊断列表。 + */ +export function analyseManifest( + lines: readonly string[], + settings: DiagnosticSettings, +): TomlDiagnostic[] { + const extended = settings as ExtendedSettings; + const severity: Record = { + syntax: settings.syntax, + unknownSection: settings.unknownSection, + unknownKey: settings.unknownKey, + planeSeparation: settings.planeSeparation, + mcppFloor: extended.mcppFloor ?? "error", + legacyKey: settings.legacyKeys, + arrayTable: extended.arrayTable ?? "error", + }; + + const diagnostics: TomlDiagnostic[] = []; + + function push( + rule: RuleName, + message: string, + line: number, + startCharacter: number, + endCharacter: number, + relatedKey?: string, + ): void { + const level = severity[rule]; + if (level === "off") { + return; + } + const diagnostic: TomlDiagnostic = { + code: DIAGNOSTIC_CODES[rule], + message, + severity: level, + line, + startCharacter, + endCharacter, + }; + if (relatedKey !== undefined) { + diagnostic.relatedKey = relatedKey; + } + diagnostics.push(diagnostic); + } + + const state: ParseState = { mode: "normal", depth: 0 }; + /** 当前段头的原始名字(未解析到 schema 的时刻也保留,用于平面规则)。 */ + let headerName: string | undefined; + /** 当前段解析到的 schema 段(键规则用;未知段为 undefined)。 */ + let section: TomlSection | undefined; + + for (let index = 0; index < lines.length; index += 1) { + const raw = lines[index].endsWith("\r") ? lines[index].slice(0, -1) : lines[index]; + const lineNumber = index + 1; + + // 多行字符串/数组的续行:整行属于上一个结构,不产生诊断。 + if (state.mode !== "normal" || state.depth > 0) { + scan(raw, state); + continue; + } + + const code = codeBeforeComment(raw); + const text = code.trim(); + if (text === "") { + scan(raw, state); + continue; + } + // 行首空白宽度。 + const lead = code.length - code.trimStart().length; + const lineStart = lead + 1; + const lineEnd = lead + text.length + 1; + + // ── 段头 ── + if (text.startsWith("[")) { + const arrayHeader = /^\[\[(.+?)\]\]$/.exec(text); + const plainHeader = /^\[(.+?)\]$/.exec(text); + if (arrayHeader === null && plainHeader === null) { + // `[` / `[foo` / `[foo] bar`:不是合法段头,也不是 key = value。 + push("syntax", `not a [section] header: ${text}`, lineNumber, lineStart, lineEnd); + scan(raw, state); + continue; + } + + const isArray = arrayHeader !== null; + const name = ((isArray ? arrayHeader : plainHeader)?.[1] ?? "").trim(); + headerName = name; + section = keySectionOf(name); + + if (isArray) { + // 规则 7:mcpp 只接受少数数组表;其余是用户写错的 `[[...]]`。 + if (!isAllowedArrayTable(name)) { + push( + "arrayTable", + `[[${name}]] is an array of tables, which mcpp does not use here`, + lineNumber, + lineStart, + lineEnd, + name, + ); + } + // 规则 2 明确忽略数组表。 + } else { + // 规则 2:未知段(允许点分名逐级回退到已知前缀段)。 + if (lookupSection(name) === undefined) { + push("unknownSection", `unknown section [${name}]`, lineNumber, lineStart, lineEnd, name); + } else if (section?.deprecatedBy) { + // 规则 6:段级 legacy。 + push( + "legacyKey", + `[${name}] is deprecated; use ${section.deprecatedBy}`, + lineNumber, + lineStart, + lineEnd, + section.deprecatedBy, + ); + } + } + scan(raw, state); + continue; + } + + // ── 键值行 ── + const equals = indexOfUnquoted(text, "="); + if (equals <= 0) { + push("syntax", `not a [section] header or key = value: ${text}`, lineNumber, lineStart, lineEnd); + scan(raw, state); + continue; + } + const key = text.slice(0, equals).trim(); + const value = text.slice(equals + 1).trim(); + const keyStart = lineStart; + const keyEnd = keyStart + key.length; + + if (!isValidKey(key)) { + push("syntax", `invalid key: ${key}`, lineNumber, keyStart, keyEnd); + scan(raw, state); + continue; + } + if (value === "") { + push("syntax", `key ${key} has no value`, lineNumber, keyStart, keyEnd, key); + scan(raw, state); + continue; + } + + const info = keyInfoOf(section, key); + + // 规则 3:已知段 + 有键表 + 不在表中(且该段不是自选键的段)。 + if (section?.keys !== undefined && section.openKeys !== true && info === undefined) { + const label = section.header; + push("unknownKey", `unknown key ${key} in ${label}`, lineNumber, keyStart, keyEnd, key); + } + + // 规则 6:键级 legacy(段本身已 deprecated 时只在段头报一次)。 + if (info?.legacy === true && !section?.deprecatedBy) { + const replacement = info.note ?? "no replacement documented"; + push( + "legacyKey", + `key ${key} is deprecated; ${replacement}`, + lineNumber, + keyStart, + keyEnd, + key, + ); + } + + // 规则 5:[package].mcpp 必须是 ">="。 + if (section?.name === "package" && (splitDots(key)[0] === "mcpp") && !isMcppFloor(value)) { + push( + "mcppFloor", + `[package] mcpp must be a release floor like ">=2026.9.28.3"`, + lineNumber, + keyStart, + keyEnd, + "mcpp", + ); + } + + // 规则 4:平面分离。 + const bareKey = unquote(key) ?? key; + if (isDependencySection(headerName ?? "") && isToolPayloadName(bareKey)) { + push( + "planeSeparation", + `${bareKey} is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]`, + lineNumber, + keyStart, + keyEnd, + bareKey, + ); + } else if (isXlingsSection(headerName ?? "") && isMcppPackageName(bareKey) && declaresVersion(value)) { + push( + "planeSeparation", + `${bareKey} is an mcpp package, not a tool payload; declare it in [dependencies]`, + lineNumber, + keyStart, + keyEnd, + bareKey, + ); + } + + scan(raw, state); + } + + diagnostics.sort( + (a, b) => + a.line - b.line || + a.startCharacter - b.startCharacter || + a.code.localeCompare(b.code) || + a.message.localeCompare(b.message), + ); + return diagnostics; +} diff --git a/src/toml/hover.ts b/src/toml/hover.ts new file mode 100644 index 0000000..1342499 --- /dev/null +++ b/src/toml/hover.ts @@ -0,0 +1,206 @@ +// mcpp.toml 的悬停查询层。 +// +// 输入是文件的物理行 + 0 基光标,输出纯数据 HoverInfo;provider 负责渲染成 +// MarkdownString(本模块不依赖 vscode API)。上下文来自 parser 的 contextAt, +// 与补全共用同一套容错解析,不另写 TOML 解析器。 +// +// 覆盖三类位置:段头、键、枚举键的值。其余位置一律 undefined——宁可不显示, +// 也不猜;任何畸形输入都不抛异常。 + +import { + contextAt, + resolveSection, + type ReplaceRange, + type TomlCursorContext, +} from "./parser"; +import { sectionByName, type TomlKey, type TomlSection } from "./schema"; + +export interface HoverInfo { + title: string; + body: string; + documentation?: string; +} + +function clamp(value: number, min: number, max: number): number { + if (!Number.isFinite(value)) { + return min; + } + return Math.min(Math.max(Math.trunc(value), min), max); +} + +/** 取光标所在行(越界与 CRLF 都容错)。 */ +function lineTextAt(lines: readonly string[], line: number): string { + if (!Array.isArray(lines) || lines.length === 0) { + return ""; + } + const index = clamp(line, 0, lines.length - 1); + return (lines[index] ?? "").replace(/\r$/, ""); +} + +/** 替换范围对应的原文(越界钳制)。 */ +function tokenAt(lineText: string, range: ReplaceRange): string { + const start = clamp(range.startCharacter, 0, lineText.length); + const end = clamp(range.endCharacter, start, lineText.length); + return lineText.slice(start, end); +} + +function unquote(text: string): string { + if (text.length >= 2) { + const first = text[0]; + const last = text[text.length - 1]; + if ((first === '"' && last === '"') || (first === "'" && last === "'")) { + return text.slice(1, -1); + } + } + return text; +} + +/** + * 段头路径 → schema 段:先精确匹配,再让 parser 的组映射处理参数化段 + * (`[targets.app]` → `targets`),最后逐级去掉尾部(与诊断的 lookupSection + * 同策略),使 `[features.a]` 这类行表也能落到基段。 + */ +function sectionForHeader(segments: readonly string[]): TomlSection | undefined { + if (segments.length === 0) { + return undefined; + } + const exact = sectionByName(segments.join(".")); + if (exact !== undefined) { + return exact; + } + const resolution = resolveSection(segments); + if (resolution.kind === "known") { + // parser 认识的组就是权威归属:schema 没收录它(如 [xlings.workspace])时 + // 不显示,而不是把键张冠李戴到基段上。 + return sectionByName(resolution.group); + } + for (let length = segments.length - 1; length >= 1; length -= 1) { + const found = sectionByName(segments.slice(0, length).join(".")); + if (found !== undefined) { + return found; + } + } + return undefined; +} + +function sectionHover(section: TomlSection): HoverInfo { + const lines: string[] = [`plane: \`${section.plane}\``]; + if (section.deprecatedBy) { + lines.push(`legacy: replaced by \`${section.deprecatedBy}\``); + } + const keyCount = section.keys?.length ?? 0; + lines.push( + section.keys === undefined + ? "keys: none declared in the snapshot" + : `keys: ${keyCount}${section.openKeys === true ? " (open: the user picks the names)" : ""}`, + ); + const info: HoverInfo = { title: section.header, body: lines.join("\n\n") }; + if (section.doc !== undefined) { + info.documentation = section.doc; + } + return info; +} + +function keyHover(key: TomlKey, section: TomlSection | undefined): HoverInfo { + const lines: string[] = [`type: \`${key.type}\``]; + if (key.values !== undefined && key.values.length > 0) { + lines.push(`values: ${key.values.map((value) => `\`${value}\``).join(" | ")}`); + } + if (key.default !== undefined) { + lines.push(`default: \`${JSON.stringify(key.default)}\``); + } + if (key.since !== undefined) { + lines.push(`since: ${key.since}`); + } + if (key.note !== undefined && key.note !== "") { + lines.push(key.note); + } + if (key.legacy === true) { + lines.push("legacy: this key is deprecated"); + } + if (key.unmodelled === true) { + lines.push("unmodelled: the type is inferred from the docs, not read from mcpp"); + } + const info: HoverInfo = { title: key.key, body: lines.join("\n\n") }; + if (section?.doc !== undefined) { + info.documentation = section.doc; + } + return info; +} + +function enumValueHover(key: TomlKey, value: string, section: TomlSection | undefined): HoverInfo { + const lines: string[] = [`enum value of \`${key.key}\``]; + if (key.values !== undefined && key.values.length > 0) { + lines.push(`one of: ${key.values.map((entry) => `\`${entry}\``).join(" | ")}`); + } + if (key.default !== undefined) { + lines.push(`default: \`${JSON.stringify(key.default)}\``); + } + if (key.note !== undefined && key.note !== "") { + lines.push(key.note); + } + const info: HoverInfo = { title: value === "" ? key.key : value, body: lines.join("\n\n") }; + if (section?.doc !== undefined) { + info.documentation = section.doc; + } + return info; +} + +/** + * 光标下的悬停内容;没有可显示的内容时返回 undefined。永不抛异常。 + */ +export function hoverAt( + lines: readonly string[], + line: number, + character: number, +): HoverInfo | undefined { + try { + const context: TomlCursorContext = contextAt(lines, line, character); + const text = lineTextAt(lines, line); + + if (context.kind === "section-header") { + const segments = [...context.segments]; + const token = tokenAt(text, context.replaceRange).trim(); + // 光标落在某个段路径 token 内时,context.segments 是它之前的段。 + if (token !== "") { + segments.push(unquote(token)); + } + const section = sectionForHeader(segments); + return section === undefined ? undefined : sectionHover(section); + } + + if (context.kind === "key") { + if (context.section.kind !== "known" || context.containerPath.length > 0) { + return undefined; + } + const section = sectionByName(context.section.group); + if (section?.keys === undefined) { + return undefined; + } + // 点分键的根段是 schema 里的键(`metadata.demo` → `metadata`)。 + const token = unquote(tokenAt(text, context.replaceRange).trim()); + const name = context.keyPrefix.length > 0 ? context.keyPrefix[0] : token; + const key = section.keys.find((entry) => entry.key === name); + return key === undefined ? undefined : keyHover(key, section); + } + + // 值位置:只有枚举键的值有可展示的语义。 + if (context.section.kind !== "known") { + return undefined; + } + const section = sectionByName(context.section.group); + if (section?.keys === undefined || context.keyPath.length === 0) { + return undefined; + } + const key = + section.keys.find((entry) => entry.key === context.keyPath.join(".")) ?? + section.keys.find((entry) => entry.key === context.keyPath[0]); + if (key?.type !== "enum") { + return undefined; + } + const value = unquote(tokenAt(text, context.replaceRange)).trim(); + return enumValueHover(key, value, section); + } catch { + return undefined; + } +} diff --git a/src/toml/navigation.ts b/src/toml/navigation.ts new file mode 100644 index 0000000..aa0b7f8 --- /dev/null +++ b/src/toml/navigation.ts @@ -0,0 +1,306 @@ +// mcpp.toml 的跳转查询层:只处理 manifest 内部的三种局部跳转。 +// +// workspace = true → [workspace.dependencies] 里的同名依赖键 +// path = "../x" → 那个 mcpp.toml 的 [package] 段头(行号由调用方解析) +// features = ["a"] → [features.a] 段头(光标所在的那个元素) +// +// 复用 parser 的节点树(parseMcppToml)定位光标下的键/值/数组元素;跨文件的 +// 文件系统语义由 `resolveManifest` 回调注入,本模块保持纯函数、不依赖 vscode, +// 也不自己读盘。任何畸形输入都不抛异常。 + +import { + parseMcppToml, + resolveSection, + type TomlDocument, + type TomlKeyValueNode, + type TomlSectionNode, + type TomlValueNode, +} from "./parser"; + +export interface ManifestLocation { + line: number; + startCharacter: number; + endCharacter: number; +} + +/** 依赖类段组:三种跳转都只在这些段里有意义(`[lib] path`、`[indices] path` 不是)。 */ +const DEPENDENCY_GROUPS: ReadonlySet = new Set([ + "dependencies", + "dev-dependencies", + "build-dependencies", + "workspace.dependencies", + "feature-deps", +]); + +const PACKAGE_HEADER = "[package]"; + +function clamp(value: number, min: number, max: number): number { + if (!Number.isFinite(value)) { + return min; + } + return Math.min(Math.max(Math.trunc(value), min), max); +} + +/** 光标是否落在范围内(端点含尾,与 parser 的判定一致)。 */ +function contains( + range: { startLine: number; startCharacter: number; endLine: number; endCharacter: number }, + line: number, + character: number, +): boolean { + if (line < range.startLine || line > range.endLine) { + return false; + } + if (line === range.startLine && character < range.startCharacter) { + return false; + } + if (line === range.endLine && character > range.endCharacter) { + return false; + } + return true; +} + +/** 光标所在候选:一条键值语句,外加它所属的顶层依赖键(内联表内的条目才有)。 */ +interface Candidate { + node: TomlKeyValueNode; + /** 顶层语句的点分键(`compat.zlib = { workspace = true }` → "compat.zlib")。 */ + ownerKey?: string; + sectionSegments: string[]; +} + +/** 内联表/数组可以嵌套:把所有可达的键值语句摊平,附带顶层依赖键。 */ +function collectNested( + value: TomlValueNode | undefined, + ownerKey: string, + sectionSegments: string[], + out: Candidate[], +): void { + if (value === undefined) { + return; + } + if (value.kind === "inlineTable" && value.entries !== undefined) { + for (const entry of value.entries) { + out.push({ node: entry, ownerKey, sectionSegments }); + collectNested(entry.value, ownerKey, sectionSegments, out); + } + return; + } + if (value.kind === "array" && value.elements !== undefined) { + for (const element of value.elements) { + collectNested(element, ownerKey, sectionSegments, out); + } + } +} + +function candidatesOf(doc: TomlDocument): Candidate[] { + const out: Candidate[] = []; + let sectionSegments: string[] = []; + for (const node of doc.nodes) { + if (node.type === "section") { + sectionSegments = node.segments.map((segment) => segment.name); + continue; + } + out.push({ node, sectionSegments }); + const key = node.keyPath.map((segment) => segment.name).join("."); + collectNested(node.value, key, sectionSegments, out); + } + return out; +} + +function groupOf(sectionSegments: readonly string[]): string | undefined { + if (sectionSegments.length === 0) { + return undefined; + } + const resolution = resolveSection(sectionSegments); + return resolution.kind === "known" ? resolution.group : undefined; +} + +/** 依赖名:内联表条目取顶层语句的键;`[dependencies.foo]` 取段名去掉首段。 */ +function dependencyName(candidate: Candidate): string { + if (candidate.ownerKey !== undefined && candidate.ownerKey !== "") { + return candidate.ownerKey; + } + if (candidate.sectionSegments.length >= 2) { + return candidate.sectionSegments.slice(1).join("."); + } + return ""; +} + +function locationOfKey(node: TomlKeyValueNode): ManifestLocation | undefined { + const first = node.keyPath[0]; + const last = node.keyPath[node.keyPath.length - 1]; + if (first === undefined || last === undefined) { + return undefined; + } + // 点分键(`compat.zlib`)整体选中,不只选第一段。 + return { + line: first.range.startLine, + startCharacter: first.range.startCharacter, + endCharacter: last.range.endCharacter, + }; +} + +function locationOfSection(node: TomlSectionNode): ManifestLocation { + return { + line: node.line, + startCharacter: node.range.startCharacter, + endCharacter: node.range.endCharacter, + }; +} + +/** `[workspace.dependencies]` 里名字匹配的那条键值语句。 */ +function findWorkspaceDependency( + doc: TomlDocument, + dependency: string, +): TomlKeyValueNode | undefined { + if (dependency === "") { + return undefined; + } + let inWorkspaceDependencies = false; + for (const node of doc.nodes) { + if (node.type === "section") { + inWorkspaceDependencies = groupOf(node.segments.map((segment) => segment.name)) === "workspace.dependencies"; + continue; + } + if (inWorkspaceDependencies && node.keyPath.map((segment) => segment.name).join(".") === dependency) { + return node; + } + } + return undefined; +} + +/** `[features.]` 段头(数组表不算)。 */ +function findFeatureSection(doc: TomlDocument, name: string): TomlSectionNode | undefined { + for (const node of doc.nodes) { + if (node.type !== "section" || node.isArray || node.segments.length !== 2) { + continue; + } + if (node.segments[0].name === "features" && node.segments[1].name === name) { + return node; + } + } + return undefined; +} + +function isRelativePath(value: string): boolean { + const text = value.trim(); + if (text === "") { + return false; + } + if (text.startsWith("/") || text.startsWith("\\") || text.startsWith("~")) { + return false; + } + if (/^[A-Za-z]:[\\/]/.test(text)) { + return false; + } + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) { + return false; + } + return true; +} + +function safeResolve( + resolveManifest: (relative: string) => number | undefined, + relative: string, +): number | undefined { + try { + const value = resolveManifest(relative); + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + return undefined; + } + return Math.trunc(value); + } catch { + return undefined; + } +} + +function locate( + candidate: Candidate, + line: number, + character: number, + doc: TomlDocument, + resolveManifest: (relative: string) => number | undefined, +): ManifestLocation | undefined { + const { node, sectionSegments } = candidate; + const onKey = node.keyPath.some((segment) => contains(segment.range, line, character)); + const onValue = node.value !== undefined && contains(node.value.range, line, character); + if (!onKey && !onValue) { + return undefined; + } + + const keyName = node.keyPath[node.keyPath.length - 1]?.name ?? ""; + const group = groupOf(sectionSegments); + const inDependencies = group !== undefined && DEPENDENCY_GROUPS.has(group); + + if (keyName === "workspace") { + const isTrue = node.value?.kind === "boolean" && node.value.text === "true"; + if (!isTrue || !inDependencies) { + return undefined; + } + const dependency = dependencyName(candidate); + const target = findWorkspaceDependency(doc, dependency); + return target === undefined ? undefined : locationOfKey(target); + } + + if (keyName === "path") { + if (!inDependencies || node.value?.kind !== "string") { + return undefined; + } + const relative = node.value.text ?? ""; + if (!isRelativePath(relative)) { + return undefined; + } + const resolved = safeResolve(resolveManifest, relative); + if (resolved === undefined) { + return undefined; + } + // 只有行号是可知的(另一个文件的内容不在本函数手里),所以选中该行的 + // [package] 字面量;缩进可能使起点略有偏差,位置仍落在段头行上。 + return { line: resolved, startCharacter: 0, endCharacter: PACKAGE_HEADER.length }; + } + + if (keyName === "features") { + if (node.value?.kind !== "array" || node.value.elements === undefined) { + return undefined; + } + const element = node.value.elements.find((entry) => contains(entry.range, line, character)); + if (element === undefined || element.kind !== "string") { + return undefined; + } + const name = (element.text ?? "").trim(); + if (name === "") { + return undefined; + } + const target = findFeatureSection(doc, name); + return target === undefined ? undefined : locationOfSection(target); + } + + return undefined; +} + +/** + * 光标位置的跳转目标;不在三种形态上时返回 undefined。永不抛异常。 + * + * `resolveManifest` 把相对路径解析为「那个 mcpp.toml 里 [package] 段头的行号 + * (0 基)」,不存在时返回 undefined——文件系统由调用方负责。 + */ +export function definitionAt( + lines: readonly string[], + line: number, + character: number, + resolveManifest: (relative: string) => number | undefined = () => undefined, +): ManifestLocation | undefined { + try { + const cursorLine = clamp(line, 0, Number.MAX_SAFE_INTEGER); + const cursorCharacter = clamp(character, 0, Number.MAX_SAFE_INTEGER); + const doc = parseMcppToml(lines); + for (const candidate of candidatesOf(doc)) { + const found = locate(candidate, cursorLine, cursorCharacter, doc, resolveManifest); + if (found !== undefined) { + return found; + } + } + return undefined; + } catch { + return undefined; + } +} diff --git a/src/mcppTomlParser.ts b/src/toml/parser.ts similarity index 99% rename from src/mcppTomlParser.ts rename to src/toml/parser.ts index 1276eca..9587c0e 100644 --- a/src/mcppTomlParser.ts +++ b/src/toml/parser.ts @@ -624,7 +624,8 @@ class Scanner { // 光标后方同行还有值 token(光标恰在 token 首字符、或在 = 与 token // 之间的空白上)时,替换范围要覆盖整个 token,否则补全插入后原文残留。 if (!this.atEol() && this.peek() !== "#") { - const ahead = this.scanValue(valuePath); // 光标落在 token 内时由 scanValue 捕获 + // 光标落在 token 内时由 scanValue 捕获。 + const ahead = this.scanValue(valuePath); this.captureValue(valuePath, ahead.kind, { replaceRange: Scanner.singleLineReplaceRange(ahead.range) ?? this.emptyReplaceRange(), }); @@ -681,7 +682,8 @@ class Scanner { if (open) { while (!this.eof()) { if (!multiline && this.atEol()) { - break; // 单行串跨行 → 未闭合 + // 单行串跨行 → 未闭合。 + break; } const ch = this.peek(); if (ch === "\\" && quote === '"') { @@ -788,7 +790,8 @@ class Scanner { const elementStart = this.pos(); elements.push(this.scanValue(keyPath)); if (this.samePos(this.pos(), elementStart)) { - this.advance(); // 进度保护 + // 进度保护。 + this.advance(); } } return { kind: "array", range: this.rangeFrom(start), open, elements }; @@ -825,7 +828,8 @@ class Scanner { entries.push(entry); } if (this.samePos(this.pos(), entryStart)) { - this.advance(); // 进度保护 + // 进度保护。 + this.advance(); } } return { kind: "inlineTable", range: this.rangeFrom(start), open, entries }; diff --git a/src/toml/providers.ts b/src/toml/providers.ts new file mode 100644 index 0000000..d474ecc --- /dev/null +++ b/src/toml/providers.ts @@ -0,0 +1,284 @@ +/** + * `mcpp.toml` editing: structural completion plus manifest diagnostics. + * + * Both are pure text analysis — no mcpp process, no language server — so they + * keep working in a restricted workspace, which is what + * `capabilities.untrustedWorkspaces: limited` promises. + * + * The one exception is dependency-version completion + * (`mcpp.toml.indexCompletion`, off by default): it runs `mcpp search`, which may + * use the network. Everything about *that* decision lives here — the two + * settings, workspace trust, the timeout and the session cache — while + * `./completion` stays pure and `../cli/search` stays the one place that parses + * mcpp's human-readable output. + */ + +import * as vscode from "vscode"; + +import { runMcpp } from "../cli/process"; +import { + indexCompletionRequest, + parseSearchOutput, + withDeadline, + type PackageVersion, +} from "../cli/search"; +import { read } from "../config/access"; +import { t } from "../i18n/t"; + +import { computeMcppTomlCompletionsWithIndex } from "./completion"; +import { analyseManifest, type DiagnosticSettings, type Severity } from "./diagnostics"; +import { hoverAt } from "./hover"; +import { definitionAt } from "./navigation"; + +export const MCPP_TOML_LANGUAGE = "mcpp-toml"; + +/** + * The session cache §3.3.2 asks for: at most one `mcpp search + * --all-versions` per package per session. Empty results are cached too — a + * package the index does not know must not be queried on every keystroke. + */ +const indexVersions = new Map(); + +let indexNoticeShown = false; + +/** The plan's one-time notice: this feature runs mcpp and may use the network. */ +function noticeIndexQuery(): void { + if (indexNoticeShown) { + return; + } + indexNoticeShown = true; + void vscode.window.showInformationMessage( + t("Completing dependency versions runs `mcpp search`, which may use the network."), + ); +} + +/** + * Versions for one dependency, or none. Every failure mode — the setting off, an + * untrusted workspace, a non-zero exit, a timeout, output the parser does not + * recognise — degrades to an empty list; nothing here surfaces an error. + */ +async function resolveIndexVersions( + document: vscode.TextDocument, + name: string, +): Promise { + const request = indexCompletionRequest(name, { + enabled: read("mcpp.toml.indexCompletion"), + trusted: vscode.workspace.isTrusted, + // VS Code exposes no offline flag and this extension never probes the + // network, so "offline" is discovered the only honest way: the query fails + // and degrades to no candidates. The term stays in `shouldSearch`'s contract + // (and its tests) so a future signal can fill it in without a shape change. + offline: false, + timeoutSeconds: read("mcpp.toml.indexCompletionTimeoutSeconds"), + }); + if (request === undefined) { + return []; + } + + // The cache is consulted *after* the switches, so turning the setting off (or + // losing trust) stops suggestions immediately instead of serving a hit. + const cached = indexVersions.get(name); + if (cached !== undefined) { + return cached; + } + noticeIndexQuery(); + + const folder = vscode.workspace.getWorkspaceFolder(document.uri); + const cwd = folder?.uri.fsPath ?? vscode.Uri.joinPath(document.uri, "..").fsPath; + const executable = (read("mcpp.path") ?? "").trim() || "mcpp"; + // The process gets the setting's timeout and the promise a short grace period, + // so a process that refuses to die still cannot hold the editor past it. + const result = await withDeadline( + runMcpp(vscode.workspace.isTrusted, executable, request.args, cwd, { timeoutMs: request.timeoutMs }), + request.timeoutMs + 250, + ); + const versions = + result === undefined || result.exitCode !== 0 ? [] : parseSearchOutput(result.stdout); + indexVersions.set(name, versions); + return versions; +} + +function linesOf(document: vscode.TextDocument): string[] { + const lines: string[] = []; + for (let line = 0; line < document.lineCount; line += 1) { + lines.push(document.lineAt(line).text); + } + return lines; +} + +/** + * The line index of `[package]` in a neighbouring manifest, for `path = "…"`. + * The file system belongs to the caller, which is why navigation takes a callback. + */ +function packageHeaderLine(uri: vscode.Uri, relative: string): number | undefined { + try { + const target = vscode.Uri.joinPath(uri, "..", relative, "mcpp.toml"); + const bytes = require("node:fs").readFileSync(target.fsPath, "utf8") as string; + const index = bytes.split(/\r?\n/).findIndex((line) => /^\s*\[\s*package\s*\]\s*$/.test(line)); + return index === -1 ? undefined : index; + } catch { + return undefined; + } +} + +function completionItemKind(kind: "section" | "template" | "version"): vscode.CompletionItemKind { + switch (kind) { + case "section": + return vscode.CompletionItemKind.Folder; + case "version": + return vscode.CompletionItemKind.Value; + default: + return vscode.CompletionItemKind.Snippet; + } +} + +function severityOf(value: Severity | undefined, fallback: Severity): Severity { + return value === "error" || value === "warning" || value === "info" || value === "off" ? value : fallback; +} + +export function registerTomlProviders( + context: vscode.ExtensionContext, + /** `mcpp.toml.completion`: structure suggestions on or off. */ + completionEnabled: () => boolean, + settings: () => DiagnosticSettings, + /** `mcpp.toml.diagnostics.enabled`. */ + enabled: () => boolean, + /** `mcpp.toml.hover` and `mcpp.toml.navigation`, which are independent switches. */ + hoverEnabled: () => boolean = () => true, + navigationEnabled: () => boolean = () => true, +): void { + context.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + { language: MCPP_TOML_LANGUAGE }, + { + async provideCompletionItems(document, position) { + if (!completionEnabled()) { + return undefined; + } + const lines: string[] = []; + for (let line = 0; line <= position.line; line += 1) { + lines.push(document.lineAt(line).text); + } + const suggestions = await computeMcppTomlCompletionsWithIndex( + lines, + position.line, + position.character, + (name) => resolveIndexVersions(document, name), + ); + return suggestions.map((suggestion) => { + const item = new vscode.CompletionItem(suggestion.label, completionItemKind(suggestion.kind)); + item.detail = suggestion.detail; + if (suggestion.documentation !== undefined) { + item.documentation = new vscode.MarkdownString(suggestion.documentation); + } + if (suggestion.insertSnippet !== undefined) { + item.insertText = new vscode.SnippetString(suggestion.insertSnippet); + } + item.range = new vscode.Range( + position.line, + suggestion.range.startCharacter, + position.line, + suggestion.range.endCharacter, + ); + return item; + }); + }, + }, + // `"` so a dependency version value is discoverable without Ctrl+Space; + // every other trigger position falls straight through to no suggestions. + "[", + '"', + ), + ); + + context.subscriptions.push( + vscode.languages.registerHoverProvider({ language: MCPP_TOML_LANGUAGE }, { + provideHover(document, position) { + if (!hoverEnabled()) { + return undefined; + } + const info = hoverAt(linesOf(document), position.line, position.character); + if (info === undefined) { + return undefined; + } + const contents = [new vscode.MarkdownString(`**${info.title}**`), new vscode.MarkdownString(info.body)]; + if (info.documentation !== undefined) { + contents.push(new vscode.MarkdownString(`[${t("Manifest reference")}](${info.documentation})`)); + } + return new vscode.Hover(contents); + }, + }), + vscode.languages.registerDefinitionProvider({ language: MCPP_TOML_LANGUAGE }, { + provideDefinition(document, position) { + if (!navigationEnabled()) { + return undefined; + } + const location = definitionAt(linesOf(document), position.line, position.character, (relative) => + packageHeaderLine(document.uri, relative), + ); + if (location === undefined) { + return undefined; + } + return new vscode.Location(document.uri, new vscode.Position(location.line, location.startCharacter)); + }, + }), + ); + + const collection = vscode.languages.createDiagnosticCollection("mcpp-manifest"); + context.subscriptions.push(collection); + + const refresh = (document: vscode.TextDocument): void => { + if (document.languageId !== MCPP_TOML_LANGUAGE) { + return; + } + if (!enabled()) { + collection.delete(document.uri); + return; + } + const lines: string[] = []; + for (let line = 0; line < document.lineCount; line += 1) { + lines.push(document.lineAt(line).text); + } + const diagnostics = analyseManifest(lines, settings()).map((entry) => { + const range = new vscode.Range( + Math.max(0, entry.line - 1), + entry.startCharacter, + Math.max(0, entry.line - 1), + entry.endCharacter, + ); + const diagnostic = new vscode.Diagnostic(range, entry.message, severityFor(entry.severity)); + diagnostic.code = entry.code; + diagnostic.source = "mcpp"; + return diagnostic; + }); + collection.set(document.uri, diagnostics); + }; + + for (const document of vscode.workspace.textDocuments) { + refresh(document); + } + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument(refresh), + vscode.workspace.onDidChangeTextDocument((event) => refresh(event.document)), + vscode.workspace.onDidCloseTextDocument((document) => collection.delete(document.uri)), + vscode.workspace.onDidChangeConfiguration(() => { + for (const document of vscode.workspace.textDocuments) { + refresh(document); + } + }), + ); +} + +function severityFor(severity: "error" | "warning" | "info"): vscode.DiagnosticSeverity { + switch (severity) { + case "error": + return vscode.DiagnosticSeverity.Error; + case "warning": + return vscode.DiagnosticSeverity.Warning; + default: + return vscode.DiagnosticSeverity.Information; + } +} + +/** The registry keys that shape manifest diagnostics, read in one place. */ +export { severityOf }; diff --git a/src/toml/schema.ts b/src/toml/schema.ts new file mode 100644 index 0000000..a27b93d --- /dev/null +++ b/src/toml/schema.ts @@ -0,0 +1,125 @@ +// mcpp.toml 快照的读取层。 +// +// 数据来自 `tools/generate-toml-schema.mjs` 生成的 `data/toml-schema.json` +// (贡献者期从 mcpp 仓库生成,运行期只读快照,不新增依赖)。本模块是纯数据 +// 访问,不依赖 vscode API:补全、悬停、诊断都从这里取段/键/枚举。 +// +// 快照里没有的段不会出现在这里,因此 mcpp 停止接受的段会自动消失——这正是 +// "段列表必须来自 toml.cppm" 的落点。 + +import raw from "../../data/toml-schema.json"; + +export interface TomlKey { + key: string; + type: "string" | "boolean" | "number" | "array" | "enum"; + values?: string[]; + default?: unknown; + since?: string; + legacy?: boolean; + note?: string; + /** 类型是推断而非读出来的:不要据此做严格的类型校验。 */ + unmodelled?: boolean; +} + +export interface TomlSection { + header: string; + name: string; + plane: string; + doc?: string; + deprecatedBy?: string | null; + keys?: TomlKey[]; + /** + * 该段的键由用户自选而非固定词表(如 `[toolchain]` 的平台名与 `bootstrap`), + * 诊断层不对它做未知键检查。生成器只对这类段写 true。 + */ + openKeys?: boolean; +} + +export interface TomlSchema { + sourceVersion: string; + sourceCommit: string; + sections: TomlSection[]; + rules: Array<{ id: string; severity: string }>; +} + +export const SCHEMA: TomlSchema = raw as unknown as TomlSchema; + +/** `[package]` / `package` → `package`;数组表 `[[x]]` 不是段,返回空串。 */ +function normaliseSectionName(headerOrName: string): string { + const text = headerOrName.trim(); + if (text.startsWith("[[")) { + return ""; + } + if (text.startsWith("[") && text.endsWith("]")) { + return text.slice(1, -1).trim(); + } + return text; +} + +/** 按段头(`"[package]"` 或 `"package"`)查找。数组表不匹配任何段。 */ +export function sectionByHeader(header: string): TomlSection | undefined { + const name = normaliseSectionName(header); + return name === "" ? undefined : sectionByName(name); +} + +/** 按段名精确查找。参数化段(`[targets.]`)由调用方用前缀匹配处理。 */ +export function sectionByName(name: string): TomlSection | undefined { + const wanted = normaliseSectionName(name); + if (wanted === "") { + return undefined; + } + return SCHEMA.sections.find((section) => section.name === wanted); +} + +/** 段内某个键的元数据;段不存在、段没有键表或键不在表中都返回 undefined。 */ +export function keyOf(sectionName: string, key: string): TomlKey | undefined { + const section = sectionByName(sectionName); + return section?.keys?.find((entry) => entry.key === key); +} + +/** + * `deprecatedBy` 形如 `[package].standard` / `[package]` / `package.standard`, + * 取出它指向的段名。 + */ +function deprecatedSectionName(reference: string): string { + const text = reference.trim(); + const bracketed = /^\[([^\]]+)\]/.exec(text); + if (bracketed !== null) { + return bracketed[1].split(".")[0].trim(); + } + return text.split(".")[0].trim(); +} + +/** + * 快照自身的结构问题:重复段头、空平面、以及指向不存在段的 deprecatedBy。 + * 生成器保证当前快照为空数组;测试把它当门禁。 + */ +export function sectionProblems(): string[] { + const problems: string[] = []; + const seen = new Set(); + for (const section of SCHEMA.sections) { + if (seen.has(section.header)) { + problems.push(`duplicate section header ${section.header}`); + } + seen.add(section.header); + + if (section.plane.trim() === "") { + problems.push(`section ${section.header} has an empty plane`); + } + + if (section.deprecatedBy) { + const target = deprecatedSectionName(section.deprecatedBy); + if (target === "" || sectionByName(target) === undefined) { + problems.push( + `${section.header} is deprecated by ${section.deprecatedBy}, which names no known section`, + ); + } + } + } + return problems; +} + +/** 快照来自哪个 mcpp 版本与提交(环境自检与诊断输出用)。 */ +export function schemaSource(): { version: string; commit: string } { + return { version: SCHEMA.sourceVersion, commit: SCHEMA.sourceCommit }; +} diff --git a/src/util/format.ts b/src/util/format.ts new file mode 100644 index 0000000..d4a03b9 --- /dev/null +++ b/src/util/format.ts @@ -0,0 +1,153 @@ +/** + * Byte / count formatting for the cache views and the status bar. + * + * Pure: no `vscode`, no i18n. The unit suffixes (B/KiB/MiB/GiB/TiB) are the same + * in every language this extension speaks, so they are not translated; the + * surrounding sentence is (`src/i18n/t.ts`). + */ + +export type NumberFormat = "binary" | "decimal"; + +const BINARY_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"] as const; +const DECIMAL_UNITS = ["B", "kB", "MB", "GB", "TB", "PB"] as const; + +function significant(value: number): string { + if (!Number.isFinite(value)) { + return "0"; + } + const abs = Math.abs(value); + if (abs >= 100) { + return value.toFixed(0); + } + if (abs >= 10) { + return value.toFixed(1); + } + return value.toFixed(2); +} + +/** + * `1500000` -> `"1.43 MiB"` (binary) / `"1.50 MB"` (decimal). + * + * Three significant digits, matching what the plan promises for the cache views: + * big numbers stay readable and small ones keep two decimals. + */ +export function formatBytes(bytes: number, format: NumberFormat = "binary"): string { + const units = format === "decimal" ? DECIMAL_UNITS : BINARY_UNITS; + const step = format === "decimal" ? 1000 : 1024; + let value = Number.isFinite(bytes) ? Math.max(0, bytes) : 0; + let index = 0; + while (value >= step && index < units.length - 1) { + value /= step; + index += 1; + } + return `${significant(value)} ${units[index]}`; +} + +/** `12345` -> `"12,345"`. Grouping follows the host locale. */ +export function formatCount(value: number): string { + return Number.isFinite(value) ? Math.round(value).toLocaleString() : "0"; +} + +/** Seconds since the Unix epoch -> `Date`, or `undefined` for a non-finite input. */ +export function fromUnixSeconds(seconds: number | undefined): Date | undefined { + if (seconds === undefined || !Number.isFinite(seconds) || seconds <= 0) { + return undefined; + } + return new Date(seconds * 1000); +} + +/** + * Numbers of whole days between two instants, floored at 0. + * Used by the cache age buckets: `<1d` / `1–7d` / `7–30d` / `>30d`. + */ +export function ageInDays(at: Date, now: Date): number { + const delta = now.getTime() - at.getTime(); + return delta <= 0 ? 0 : Math.floor(delta / 86_400_000); +} + +/** + * Bucket boundaries in days, ascending, from settings such as + * `["1d", "7d", "30d"]`. Unparsable entries are dropped; the result is sorted + * and de-duplicated so a hand-edited setting cannot produce overlapping buckets. + */ +export function parseAgeBuckets(values: readonly string[] | undefined): number[] { + const parsed = new Set(); + for (const raw of values ?? []) { + const match = /^\s*(\d+)\s*([dhwm]?)\s*$/.exec(raw); + if (match === null) { + continue; + } + const amount = Number.parseInt(match[1], 10); + const unit = match[2]; + const days = unit === "h" ? amount / 24 : unit === "w" ? amount * 7 : unit === "m" ? amount * 30 : amount; + const rounded = Math.max(0, Math.round(days)); + parsed.add(rounded); + } + if (parsed.size === 0) { + return [1, 7, 30]; + } + return [...parsed].sort((a, b) => a - b); +} + +/** Which bucket index `days` falls in; `boundaries.length` is the last (oldest) bucket. */ +export function bucketIndex(days: number, boundaries: readonly number[]): number { + for (let index = 0; index < boundaries.length; index += 1) { + if (days < boundaries[index]) { + return index; + } + } + return boundaries.length; +} + +export interface CacheEntrySize { + bytes: number; + accessed?: number; + complete?: boolean; +} + +export interface CacheProjection { + /** Entries removed, in LRU order, to reach the budget. */ + removed: CacheEntrySize[]; + kept: CacheEntrySize[]; + freedBytes: number; + remainingBytes: number; +} + +/** + * Simulate `mcpp cache gc --max-size `: drop least-recently-used entries + * until the total fits the budget. Entries without a usable `accessed` timestamp + * are treated as the most recent, so a projection never promises to free + * something mcpp would keep. + * + * This is an **estimate** — mcpp's own LRU also weighs entry completeness and + * its own bookkeeping, so the figure is offered as a preview only. + */ +export function projectGc(entries: readonly CacheEntrySize[], budgetBytes: number): CacheProjection { + const total = entries.reduce((sum, entry) => sum + Math.max(0, entry.bytes), 0); + if (budgetBytes >= total) { + return { removed: [], kept: [...entries], freedBytes: 0, remainingBytes: total }; + } + const ranked = entries + .map((entry, index) => ({ entry, index })) + .sort((a, b) => { + const left = a.entry.accessed ?? Number.MAX_SAFE_INTEGER; + const right = b.entry.accessed ?? Number.MAX_SAFE_INTEGER; + return left === right ? a.index - b.index : left - right; + }); + const removed: CacheEntrySize[] = []; + let remaining = total; + for (const { entry } of ranked) { + if (remaining <= budgetBytes) { + break; + } + removed.push(entry); + remaining -= Math.max(0, entry.bytes); + } + const removedSet = new Set(removed); + return { + removed, + kept: entries.filter((entry) => !removedSet.has(entry)), + freedBytes: total - remaining, + remainingBytes: remaining, + }; +} diff --git a/src/util/log.ts b/src/util/log.ts new file mode 100644 index 0000000..2119e4d --- /dev/null +++ b/src/util/log.ts @@ -0,0 +1,82 @@ +/** + * One place that decides whether a line reaches the `mcpp` output channel. + * + * `mcpp.log.level` is a **threshold**, not a filter list: `error` writes only + * errors, `warn` adds warnings, `info` is the declared default, and `debug` + * writes everything. Two rules keep the channel honest: + * + * - an error is never suppressed by any configured level — that is why a failed + * command's raw stdout/stderr is written at `error` by its caller; + * - an unknown configured value degrades to the default instead of silencing + * the channel, and an unknown line level is treated as the most verbose thing + * it could be. + * + * Pure and `vscode`-free: `createLogger` takes any `appendLine`-shaped sink and + * a level callback, so the policy can be asserted without an editor. + */ + +export type LogLevel = "error" | "warn" | "info" | "debug"; + +const ORDER: Readonly> = { + error: 0, + warn: 1, + info: 2, + debug: 3, +}; + +const DEFAULT_LEVEL: LogLevel = "info"; + +export function isLogLevel(value: unknown): value is LogLevel { + return value === "error" || value === "warn" || value === "info" || value === "debug"; +} + +/** Coerce a configured value: anything unknown becomes the declared default. */ +export function logLevelOf(value: unknown): LogLevel { + return isLogLevel(value) ? value : DEFAULT_LEVEL; +} + +/** + * True when a line at `level` should be written while `configured` is the + * effective `mcpp.log.level`. An error passes at every level. + */ +export function shouldLog(level: LogLevel, configured: unknown): boolean { + const threshold = ORDER[logLevelOf(configured)]; + const wanted = ORDER[isLogLevel(level) ? level : "debug"]; + return wanted <= threshold; +} + +/** The part of an `OutputChannel` this module needs. */ +export interface LogChannel { + appendLine(line: string): unknown; +} + +export interface Logger { + error(line: string): void; + warn(line: string): void; + info(line: string): void; + debug(line: string): void; +} + +/** + * A levelled writer over an output channel. The level is read per line, so a + * configuration change applies to the next line without re-creating the logger. + * A channel that has already been disposed is a no-op, never a crash. + */ +export function createLogger(channel: LogChannel, levelOf: () => unknown): Logger { + const write = (level: LogLevel, line: string): void => { + if (!shouldLog(level, levelOf())) { + return; + } + try { + channel.appendLine(line); + } catch { + // The output channel can be released during a window reload or shutdown. + } + }; + return { + error: (line) => write("error", line), + warn: (line) => write("warn", line), + info: (line) => write("info", line), + debug: (line) => write("debug", line), + }; +} diff --git a/src/util/text.ts b/src/util/text.ts new file mode 100644 index 0000000..ec3c0ae --- /dev/null +++ b/src/util/text.ts @@ -0,0 +1,79 @@ +/** + * Small text helpers shared by the CLI adapter and the views. Pure: no `vscode`. + */ + +/** `mcpp` colourises its status row; anything captured from a terminal may carry ANSI. */ +const ANSI_ESCAPE = /\u001b\[[0-?]*[ -/]*[@-~]/g; + +export function stripAnsi(text: string): string { + return text.replace(ANSI_ESCAPE, ""); +} + +export interface ClampedText { + text: string; + truncated: boolean; + /** Lines dropped from the front, so a caller can say "… N earlier lines". */ + droppedLines: number; +} + +export interface ClampOptions { + maxLines?: number; + maxChars?: number; +} + +/** + * Keep the **tail** of a long output and say so. Command output that matters + * (a failure, a list of stale directories) is at the end; the head is status + * narration. The full text is expected to be in the `mcpp` output channel. + */ +export function clampOutput(input: string, options: ClampOptions = {}): ClampedText { + const maxLines = options.maxLines ?? 400; + const maxChars = options.maxChars ?? 64 * 1024; + const normalised = stripAnsi(input).replace(/\r\n/g, "\n"); + const lines = normalised.split("\n"); + let truncated = false; + let droppedLines = 0; + let kept = lines; + if (lines.length > maxLines) { + droppedLines = lines.length - maxLines; + kept = lines.slice(droppedLines); + truncated = true; + } + let text = kept.join("\n"); + if (text.length > maxChars) { + text = text.slice(text.length - maxChars); + truncated = true; + } + return { text, truncated, droppedLines }; +} + +/** First line of a multi-line string, trimmed; used for one-line tree tooltips. */ +export function firstLine(text: string): string { + const index = text.indexOf("\n"); + return (index === -1 ? text : text.slice(0, index)).trim(); +} + +/** Escape a string for use as a `markdown` tooltip / webview text node. */ +export function escapeHtml(text: string): string { + return text + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +/** Split a comma-separated setting value (`"mcpp,cmake"`) into trimmed entries. */ +export function splitList(value: string | undefined): string[] { + return (value ?? "") + .split(",") + .map((part) => part.trim()) + .filter((part) => part.length > 0); +} + +/** `["-j","4"]` -> `"-j 4"`, quoting entries that contain whitespace. */ +export function formatArguments(args: readonly string[]): string { + return args + .map((arg) => (/\s/.test(arg) ? JSON.stringify(arg) : arg)) + .join(" "); +} diff --git a/src/views/languageServerView.ts b/src/views/languageServerView.ts new file mode 100644 index 0000000..6baa138 --- /dev/null +++ b/src/views/languageServerView.ts @@ -0,0 +1,345 @@ +/** + * The C++ Modules language service, as commands plus one state source. + * + * There is no view of its own any more: the four things worth seeing — state, + * provider, problems, actions — are folded into the project view's 「基本信息」 + * section, and this file feeds that block. Every button still forwards an mcppls + * command; this extension never reimplements language-service behaviour and never + * writes an `mcppls.*` setting itself. The block states its provider on its own + * line, so "who owns what" stays answerable after the fold. + * + * Confirmation policy comes from the capability table (`danger` + `confirmHint`), + * so a new dangerous action cannot be added without saying what it does. + * `mcpp.languageService.confirmResetCache` can add one more explicit step to the + * reset-cache action; it can never take the capability's own modal away, and + * `./viewPolicy.ts` is where that rule lives. + * + * Three settings also own the block's *timing*: `mcpp.languageService.readState` + * decides whether state is read at all, `…stateRefreshSeconds` whether it is + * re-read on an interval, and `…notifyOnDegraded` whether the one-time + * missing-dependency line is written. `mcpp.views.languageServer.show` decides + * whether the block appears in the project view at all. + */ + +import { existsSync } from "node:fs"; + +import * as vscode from "vscode"; + +import { LANGUAGE_SERVER_COMMANDS, LEGACY_LANGUAGE_SERVER_COMMANDS, TOOL_COMMANDS } from "../commands/ids"; +import { read } from "../config/access"; +import { t } from "../i18n/t"; +import type { LanguageServerBridge } from "../mcppls/bridge"; +import { capability } from "../mcppls/contract"; +import { confirmationFor, formatResult, type FormattedResult } from "../mcppls/messages"; +import { + languageServerEnabled, + languageServerInstalled, + languageServerVersion, + readLanguageServerState, +} from "../mcppls/stateSource"; +import { PollTimer } from "../mcppls/timers"; +import { refreshTimerDecision } from "../cache/cacheState"; +import type { LanguageServiceBlock } from "./models"; +import { degradedNoticeEnabled, resetCacheConfirmation } from "./viewPolicy"; + +/** The tick granularity; the settings are in seconds. */ +const REFRESH_TICK_MS = 100; + +export interface LanguageServerViewDeps { + bridge: LanguageServerBridge; + output: vscode.OutputChannel; +} + +/** + * The folded block's data source, handed to the project view. + * + * `input()` reads; `onDidChange` tells the project view when to rebuild; and + * `setVisible` is how the poller learns whether the project view is on screen at + * all. Keeping the timer here rather than in the project view means the setting + * that shapes it (`mcpp.languageService.stateRefreshSeconds`) is read next to the + * action it governs. + */ +export interface LanguageServerBlockSource { + /** The block's current input, or `undefined` while `mcpp.views.languageServer.show` is off. */ + input(): LanguageServiceBlock | undefined; + /** Fires whenever the state, or the setting that shows it, may have changed. */ + onDidChange(listener: () => void): vscode.Disposable; + /** The project view says whether its tree is visible; the poller follows it. */ + setVisible(visible: boolean): void; +} + +/** Ask, run, report — the same shape for every forwarded command. */ +export async function runLanguageServerCommand( + bridge: LanguageServerBridge, + output: vscode.OutputChannel, + key: string, + options: { args?: unknown[]; value?: unknown; extraConfirmation?: string } = {}, +): Promise<{ message: string; value?: unknown } | undefined> { + const confirmation = confirmationFor(key, options.value); + if (confirmation !== undefined) { + const run = t("Run"); + const choice = await vscode.window.showWarningMessage(confirmation, { modal: true }, run); + if (choice !== run) { + return undefined; + } + } + // A setting may ask once more; it never replaces the capability's own modal. + if (options.extraConfirmation !== undefined) { + const run = t("Run"); + const choice = await vscode.window.showWarningMessage( + options.extraConfirmation, + { modal: true }, + run, + ); + if (choice !== run) { + return undefined; + } + } + const result = await bridge.invoke(key, ...(options.args ?? [])); + const formatted: FormattedResult = formatResult(result); + try { + output.appendLine(`[C++ Modules] ${formatted.message}${formatted.hint === undefined ? "" : ` ${formatted.hint}`}`); + } catch { + // The channel can be gone during shutdown. + } + if (formatted.severity === "error") { + void vscode.window.showErrorMessage(formatted.message); + } else if (formatted.severity === "warning") { + void vscode.window.showWarningMessage(formatted.message); + } + return result.value === undefined + ? { message: formatted.message } + : { message: formatted.message, value: result.value }; +} + +/** + * The extra confirmation `mcpp.languageService.confirmResetCache` adds, or + * `undefined` when the capability's own `danger` level should be the only one. + * `bridge.dangerOf` is the capability table's answer, never a copy of it. + * + * The text is the capability's own `confirmHint`, so the second modal repeats + * exactly what the reset does — the setting buys a second explicit "yes", not a + * second explanation. It is resolved at render time, like every other capability + * string (`src/mcppls/messages.ts`). + */ +export function extraResetCacheConfirmation( + bridge: LanguageServerBridge, + confirmResetCache: boolean, +): string | undefined { + const decision = resetCacheConfirmation({ + danger: bridge.dangerOf("resetCache"), + confirmResetCache, + }); + return decision === "extra-modal" ? capability("resetCache")?.confirmHint : undefined; +} + +/** + * Registers every `mcpp.languageServer.*` command (and the forwarded mcppls + * actions behind them) and returns the state source the project view renders. + */ +export function registerLanguageServerCommands( + context: vscode.ExtensionContext, + deps: LanguageServerViewDeps, +): LanguageServerBlockSource { + const changed = new vscode.EventEmitter(); + context.subscriptions.push(changed); + + // Whether the project view's tree is on screen. The timer below follows it, so + // a collapsed sidebar does not keep asking mcppls for state. + let visible = false; + const refresh = (): void => { + changed.fire(); + }; + + const input = (): LanguageServiceBlock | undefined => { + // Literal key, so the wiring gate can see it. + if (read("mcpp.views.languageServer.show") === false) { + return undefined; + } + return { + show: true, + installed: languageServerInstalled(), + enabled: languageServerEnabled(), + version: languageServerVersion(), + state: readLanguageServerState(), + }; + }; + + // `mcpp.languageService.stateRefreshSeconds`, but only while the project view is + // visible and only while state is read at all (0 = off). + const timer = new PollTimer({ + periodMs: 0, + tick: (): void => { + if (visible) { + refresh(); + } + }, + }); + context.subscriptions.push(timer); + const applyTimer = (): void => { + const disabled = read("mcpp.languageService.readState") === false; + const seconds = read("mcpp.languageService.stateRefreshSeconds"); + const usable = !disabled && Number.isFinite(seconds) && seconds > 0; + const decision = refreshTimerDecision(usable ? seconds : 0, visible); + timer.start(decision.active ? decision.seconds * 1000 : 0); + }; + applyTimer(); + + const register = (id: string, handler: (...args: unknown[]) => Promise): void => { + context.subscriptions.push( + vscode.commands.registerCommand(id, async (...args: unknown[]) => { + try { + await handler(...args); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + deps.output.appendLine(t("C++ Modules command failed: {0}", message)); + void vscode.window.showErrorMessage(t("{0} failed: {1}", id, message)); + } + }), + ); + }; + + const forward = (id: string, key: string, args?: () => unknown[]): void => { + register(id, async () => { + const options: { args?: unknown[]; extraConfirmation?: string } = + args === undefined ? {} : { args: args() }; + if (key === "resetCache") { + const extra = extraResetCacheConfirmation( + deps.bridge, + read("mcpp.languageService.confirmResetCache"), + ); + if (extra !== undefined) { + options.extraConfirmation = extra; + } + } + await runLanguageServerCommand(deps.bridge, deps.output, key, options); + refresh(); + }); + }; + + register(LANGUAGE_SERVER_COMMANDS.refreshState, async () => { + refresh(); + }); + + /** + * Where the last bundle was written. + * + * `mcppls.exportDiagnosticBundle` resolves to the zip it wrote, and that path + * is the only reliable answer to "where is it?" — the directory is + * `/bundles` on the server's own terms, which this extension + * must not guess at. Remembered machine-wide, because the bundle directory is + * machine-wide too. + */ + const BUNDLE_PATH_KEY = "mcpp.languageServer.lastDiagnosticBundle"; + const bundleFrom = (value: unknown): string | undefined => { + const path = value instanceof vscode.Uri ? value.fsPath : typeof value === "string" ? value : undefined; + return path !== undefined && path.length > 0 ? path : undefined; + }; + + register(LANGUAGE_SERVER_COMMANDS.exportDiagnosticBundle, async () => { + const result = await runLanguageServerCommand(deps.bridge, deps.output, "diagnosticBundle"); + const bundle = bundleFrom(result?.value); + if (bundle !== undefined) { + await context.globalState.update(BUNDLE_PATH_KEY, bundle); + } + refresh(); + }); + + register(LANGUAGE_SERVER_COMMANDS.revealBundle, async () => { + const remembered = context.globalState.get(BUNDLE_PATH_KEY); + if (remembered === undefined || !existsSync(remembered)) { + const exportNow = t("Capture logs now"); + const choice = await vscode.window.showInformationMessage( + t("No diagnostic bundle has been written yet. Capturing the logs writes one zip with the report, the environment and the recent logs."), + exportNow, + ); + if (choice === exportNow) { + await vscode.commands.executeCommand(LANGUAGE_SERVER_COMMANDS.exportDiagnosticBundle); + } + return; + } + await vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(remembered)); + }); + + register(LANGUAGE_SERVER_COMMANDS.openLogFolder, async () => { + // Upstream owns the path: `revealCacheDirectory('logs')` asks the server's own + // detail for `paths.logDirectory` and reveals it, falling back to the output + // channel when the server cannot answer. Nothing here reconstructs a path. + await runLanguageServerCommand(deps.bridge, deps.output, "logsDirectory", { args: ["logs"] }); + }); + + forward(LANGUAGE_SERVER_COMMANDS.restart, "restartServer"); + forward(LANGUAGE_SERVER_COMMANDS.restartEngine, "restartEngine"); + forward(LANGUAGE_SERVER_COMMANDS.resetWorkspaceCache, "resetCache"); + forward(LANGUAGE_SERVER_COMMANDS.selectContext, "selectContext"); + forward(LANGUAGE_SERVER_COMMANDS.showModuleGraph, "moduleGraph"); + forward(LANGUAGE_SERVER_COMMANDS.showLogs, "logs"); + forward(LANGUAGE_SERVER_COMMANDS.collectReport, "report"); + // `exportDiagnosticBundle` is registered above rather than forwarded: its + // answer is the zip's path, and the caller has to keep it. + forward(LANGUAGE_SERVER_COMMANDS.runBuildToolInTerminal, "runBuildTool"); + forward(LANGUAGE_SERVER_COMMANDS.installTools, "installTools"); + forward(LANGUAGE_SERVER_COMMANDS.manageConflicts, "manageConflicts", () => [true]); + forward(LANGUAGE_SERVER_COMMANDS.reviewChanges, "review", () => [false]); + forward(LANGUAGE_SERVER_COMMANDS.toggleInWorkspace, "toggleInWorkspace", () => [!languageServerEnabled()]); + + // The legacy ids from 0.4.x keep working, pointing at the new actions. + register(LEGACY_LANGUAGE_SERVER_COMMANDS.configureLanguageServer, async () => { + await runLanguageServerCommand(deps.bridge, deps.output, "selectContext"); + }); + register("mcpp.configureClangd", async () => { + await runLanguageServerCommand(deps.bridge, deps.output, "selectContext"); + }); + register(LEGACY_LANGUAGE_SERVER_COMMANDS.checkModuleSupport, async () => { + await runLanguageServerCommand(deps.bridge, deps.output, "restartServer"); + }); + register(LEGACY_LANGUAGE_SERVER_COMMANDS.showModuleGraph, async () => { + await runLanguageServerCommand(deps.bridge, deps.output, "moduleGraph"); + }); + register(LEGACY_LANGUAGE_SERVER_COMMANDS.showLanguageServerLogs, async () => { + await runLanguageServerCommand(deps.bridge, deps.output, "logs"); + }); + + register(TOOL_COMMANDS.openLanguageServerSettings, async () => { + await vscode.commands.executeCommand("workbench.action.openSettings", "@ext:sunrisepeak.mcpp-language-server"); + }); + + context.subscriptions.push( + vscode.extensions.onDidChange(() => refresh()), + vscode.workspace.onDidChangeConfiguration((event) => { + if ( + event.affectsConfiguration("mcppls.enable") || + event.affectsConfiguration("mcpp.views.languageServer") || + event.affectsConfiguration("mcpp.languageService") + ) { + refresh(); + applyTimer(); + } + }), + ); + + // A capability that has gone missing is worth one sentence, once — unless + // `mcpp.languageService.notifyOnDegraded` says the user does not want it. + if (degradedNoticeEnabled(read("mcpp.languageService.notifyOnDegraded")) && !languageServerInstalled()) { + deps.output.appendLine( + t("The C++ Modules extension ({0}) is not installed or is disabled.", capability("refresh")?.key ?? ""), + ); + } + + return { + input, + onDidChange: (listener: () => void): vscode.Disposable => changed.event(listener), + setVisible: (next: boolean): void => { + if (visible === next) { + return; + } + visible = next; + applyTimer(); + // Becoming visible is exactly when a stale block must be re-read. + if (visible) { + refresh(); + } + }, + }; +} + diff --git a/src/views/models.ts b/src/views/models.ts new file mode 100644 index 0000000..95028d1 --- /dev/null +++ b/src/views/models.ts @@ -0,0 +1,878 @@ +/** + * The trees, as **data**. + * + * Labels are keys, not sentences: the builder stays free of `vscode` and of the + * current language, the tests assert stable keys, and `src/views/treeProvider.ts` + * resolves them through `src/i18n/t.ts` at render time. A label's arguments may + * themselves be labels, so a phrase like `C++23 · 87 source file(s)` is composed + * from two independently translatable pieces instead of one frozen sentence. + * + * The project view is two labelled sections: **Basics** (what this project is) + * and **Common commands** (what can be done to it). The C++ Modules block is + * folded into the first one, with the status line carrying the problem count, so + * a degraded language service is visible **without expanding anything**. The + * second section is the only place in the view whose rows are all commands, and + * it is the one that starts open: the sidebar should open on what can be done, + * with 「基本信息」 one click away. + * + * Everything the project view needs from disk lives here as well — the declared + * dependencies, `mcpp.lock`'s resolved versions and the source-file count — so + * the whole layout can be unit tested without an editor host. Nothing here + * imports `vscode`; `test/architecture.test.ts` enforces that. + */ + +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; + +import { parseMcppToml } from "../toml/parser"; + +export type LabelArgument = string | number | Label; + +export interface Label { + key: string; + args?: readonly LabelArgument[]; +} + +export interface TreeCommand { + command: string; + title: Label; + arguments?: readonly unknown[]; +} + +export interface TreeNode { + id: string; + label: Label; + description?: Label; + tooltip?: Label; + /** A codicon id without the `$()`, e.g. `"database"`. */ + icon?: string; + /** + * A `ThemeColor` id the icon is drawn in, e.g. `"charts.blue"`. + * + * Only ids the colour registry always carries are used (`charts.*`), and a + * theme that omits one leaves the icon in the normal foreground colour rather + * than making the row unreadable — colour is a hint here, never the state. + */ + iconColor?: string; + /** Consumed by `when` clauses in package.json for inline actions. */ + contextValue?: string; + /** + * Render expanded on first display. A node with children is collapsed unless + * this is set. 「常用命令」 starts open because it is the part of the view that + * gets used; 「基本信息」 starts closed, so the sidebar opens on what can be + * *done* rather than on what the project happens to be. + */ + expanded?: boolean; + command?: TreeCommand; + children?: readonly TreeNode[]; +} + +const plain = (text: string): Label => ({ key: text }); + +/** `a`, `b` -> `a · b`, nested so each side keeps its own translation. */ +function joined(parts: readonly Label[]): Label | undefined { + if (parts.length === 0) { + return undefined; + } + return parts.reduce((left, right): Label => ({ key: "{0} · {1}", args: [left, right] })); +} + +export interface TargetSummary { + name: string; + kind: string; +} + +/** One dependency declared in `mcpp.toml`, before `mcpp.lock` says what it resolved to. */ +export interface DependencyDeclaration { + /** `namespace.name` as written in the manifest. */ + name: string; + /** The declared version constraint, when the manifest states one. */ + version?: string; + /** Declared in `[dev-dependencies]`. */ + dev?: boolean; + /** A local path dependency. */ + path?: string; + /** A git dependency. */ + git?: string; +} + +export interface ProjectSummary { + root: string; + name?: string; + version?: string; + standard?: string; + profile?: string; + toolchainSpec?: string; + target?: string; + targets?: readonly TargetSummary[]; + hasTests?: boolean; + /** Declared in `[dependencies]` / `[dev-dependencies]`; absent when there are none. */ + dependencies?: readonly DependencyDeclaration[]; + /** Source files counted under the project root; absent when not measured. */ + sourceFiles?: number; + /** Set when `mcpp.toml` could not be read; the tree then says so instead of lying. */ + error?: string; +} + +/* ------------------------------------------------------------------ mcpp.toml */ + +/** The two manifest groups the project view reports. A dep is either runtime or dev. */ +const DEPENDENCY_GROUPS: Readonly> = { + dependencies: false, + "dev-dependencies": true, +}; + +function dependencyField(entry: DependencyDeclaration, key: string, value: string | undefined): void { + if (value === undefined || value.length === 0) { + return; + } + if (key === "version") { + entry.version = value; + } else if (key === "path") { + entry.path = value; + } else if (key === "git") { + entry.git = value; + } +} + +/** + * The declared dependencies, read with the fault-tolerant TOML scanner the + * completion provider already uses. + * + * Three shapes are recognised, because all three are legal mcpp.toml: + * `name = "1.0"`, `name = { path = "…" }` (including multi-line inline tables) + * and `[dependencies.name]` with the fields on following lines. `workspace` and + * `features` are deliberately ignored: neither changes what the row must say. + */ +export function readDependencies(lines: readonly string[]): DependencyDeclaration[] { + const found: DependencyDeclaration[] = []; + const byName = new Map(); + const ensure = (name: string, dev: boolean): DependencyDeclaration => { + const existing = byName.get(name); + if (existing !== undefined) { + return existing; + } + const entry: DependencyDeclaration = dev ? { name, dev: true } : { name }; + byName.set(name, entry); + found.push(entry); + return entry; + }; + + try { + let group: string | undefined; + let named: DependencyDeclaration | undefined; + for (const node of parseMcppToml(lines).nodes) { + if (node.type === "section") { + const segments = node.segments.map((segment) => segment.name); + const head = segments[0]; + if (head === undefined || DEPENDENCY_GROUPS[head] === undefined) { + group = undefined; + named = undefined; + continue; + } + group = head; + // `[dependencies.name]` names the dependency; its fields follow. + named = segments.length >= 2 ? ensure(segments.slice(1).join("."), DEPENDENCY_GROUPS[head] === true) : undefined; + continue; + } + if (group === undefined) { + continue; + } + const dev = DEPENDENCY_GROUPS[group] === true; + const key = node.keyPath.map((segment) => segment.name).join("."); + const value = node.value; + if (value === undefined) { + continue; + } + if (named !== undefined) { + dependencyField(named, key, value.text); + continue; + } + if (key.length === 0) { + continue; + } + const entry = ensure(key, dev); + if (value.kind === "string") { + dependencyField(entry, "version", value.text); + } else if (value.kind === "inlineTable") { + for (const field of value.entries ?? []) { + dependencyField(entry, field.keyPath.map((segment) => segment.name).join("."), field.value?.text); + } + } + } + } catch { + // A manifest being edited is not an error here; whatever was read stands. + } + return found; +} + +/* ------------------------------------------------------------------ mcpp.lock */ + +/** One `[package."…"]` entry of `mcpp.lock`: a resolved package, with no parent edge. */ +export interface LockPackage { + /** The table key as written: `openkal` or `compat.freetype`. */ + key: string; + namespace?: string; + version?: string; + source?: string; + hash?: string; +} + +const LOCK_HEADER = /^\[\s*package\s*\.\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+))\s*\]\s*$/; +const LOCK_FIELD = /^([A-Za-z0-9_-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^#\s]+))\s*(?:#.*)?$/; +const LOCK_FIELDS: ReadonlySet = new Set(["namespace", "version", "source", "hash"]); + +/** + * `mcpp.lock`, read tolerantly. + * + * The file is a flat TOML set — `version = 2` then one `[package."…"]` table per + * resolved package — and it deliberately carries **no parent/child edges**, + * which is why the view can only ever show two levels. Anything unrecognised is + * skipped and nothing here throws: a file being written is still a valid lock + * with fewer entries, not a broken view. + */ +export function parseLockfile(text: string): LockPackage[] { + const packages: LockPackage[] = []; + try { + let current: LockPackage | undefined; + for (const raw of text.split(/\r?\n/)) { + const line = raw.trim(); + if (line.length === 0 || line.startsWith("#")) { + continue; + } + const header = LOCK_HEADER.exec(line); + if (header !== null) { + const key = header[1] ?? header[2] ?? header[3] ?? ""; + if (key.length === 0) { + current = undefined; + continue; + } + current = { key }; + packages.push(current); + continue; + } + if (line.startsWith("[")) { + current = undefined; + continue; + } + if (current === undefined) { + continue; + } + const field = LOCK_FIELD.exec(line); + if (field === null || !LOCK_FIELDS.has(field[1])) { + continue; + } + const value = field[2] ?? field[3] ?? field[4]; + if (value === undefined || value.length === 0) { + continue; + } + if (field[1] === "namespace") current.namespace = value; + else if (field[1] === "version") current.version = value; + else if (field[1] === "source") current.source = value; + else current.hash = value; + } + } catch { + // Tolerant by construction: an unreadable lock is an empty lock. + } + return packages; +} + +/** {@link parseLockfile} for a path that may not exist. A missing lock is empty, not an error. */ +export function readLockfile(file: string): LockPackage[] { + try { + return parseLockfile(readFileSync(file, "utf8")); + } catch { + return []; + } +} + +/** + * The lock entry a declaration resolved to, matched by `namespace.name`. + * + * Real lock files are inconsistent about the table key: `[package."openkal"]` + * carries `namespace = "mcpplibs"` while `[package."compat.freetype"]` already + * folds the namespace into the key. Both resolve here; when the entry has no + * namespace of its own, the name is the only thing left to match on. + */ +export function resolveLockedPackage(entries: readonly LockPackage[], name: string): LockPackage | undefined { + const dot = name.lastIndexOf("."); + const namespace = dot > 0 ? name.slice(0, dot) : undefined; + const local = dot > 0 ? name.slice(dot + 1) : name; + for (const entry of entries) { + if (entry.key === name) { + return entry; + } + const entryLocal = entry.key.includes(".") ? entry.key.slice(entry.key.lastIndexOf(".") + 1) : entry.key; + if (entryLocal !== local) { + continue; + } + if (namespace === undefined || entry.namespace === undefined || entry.namespace === namespace) { + return entry; + } + } + return undefined; +} + +/* --------------------------------------------------------------- source files */ + +/** Extensions counted as source: module interfaces, translation units and headers. */ +const SOURCE_EXTENSIONS: ReadonlySet = new Set([ + ".c", + ".cc", + ".cpp", + ".cxx", + ".c++", + ".cppm", + ".ixx", + ".mpp", + ".h", + ".hh", + ".hpp", + ".hxx", + ".ipp", + ".inl", +]); + +/** Directories that hold build output or other people's code, never the project's own sources. */ +const SOURCE_SKIP_DIRS: ReadonlySet = new Set([ + "target", + "build", + "out", + "dist", + "node_modules", + ".git", + ".mcpp", + ".vscode", + ".cache", +]); + +const SOURCE_MAX_ENTRIES = 20_000; +const SOURCE_MAX_DEPTH = 8; + +/** + * How many source files this project has, counted under `root`. + * + * Bounded and non-throwing, like every other walk in this codebase: a huge or + * unreadable tree stops early rather than hanging the extension host, and the + * build directories are skipped so `target/` cannot flatter the number. The + * count is what makes the identity row say something about the project's size + * without running mcpp. + */ +export function countSourceFiles(root: string): number { + let count = 0; + let seen = 0; + const visit = (dir: string, depth: number): void => { + if (depth > SOURCE_MAX_DEPTH || seen >= SOURCE_MAX_ENTRIES) { + return; + } + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + seen += 1; + if (seen > SOURCE_MAX_ENTRIES) { + return; + } + if (entry.isSymbolicLink()) { + continue; + } + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (!SOURCE_SKIP_DIRS.has(entry.name)) { + visit(full, depth + 1); + } + } else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) { + count += 1; + } + } + }; + visit(root, 0); + return count; +} + +/* ---------------------------------------------------------------- keybindings */ + +export interface KeybindingHint { + key?: string; + mac?: string; +} + +export type KeyboardPlatform = "mac" | "other"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * The keybindings this extension contributes, keyed by command id, read from + * `package.json` rather than copied into the source. + * + * A hint in the tree that disagrees with the manifest is worse than no hint, so + * the row asks the manifest. `context.extension.packageJSON` is what the caller + * hands in; this function itself is pure and testable. + */ +export function keybindingsFromPackage(packageJSON: unknown): Readonly> { + const bindings: Record = {}; + if (!isRecord(packageJSON) || !isRecord(packageJSON.contributes)) { + return bindings; + } + const list = packageJSON.contributes.keybindings; + if (!Array.isArray(list)) { + return bindings; + } + for (const entry of list) { + if (!isRecord(entry) || typeof entry.command !== "string" || bindings[entry.command] !== undefined) { + continue; + } + const key = typeof entry.key === "string" && entry.key.length > 0 ? entry.key : undefined; + const mac = typeof entry.mac === "string" && entry.mac.length > 0 ? entry.mac : undefined; + if (key === undefined && mac === undefined) { + continue; + } + const hint: KeybindingHint = {}; + if (key !== undefined) hint.key = key; + if (mac !== undefined) hint.mac = mac; + bindings[entry.command] = hint; + } + return bindings; +} + +function keyToken(token: string, platform: KeyboardPlatform): string { + if (platform === "mac") { + switch (token) { + case "cmd": + case "meta": + case "super": + return "⌘"; + case "ctrl": + case "control": + return "⌃"; + case "alt": + case "option": + return "⌥"; + case "shift": + return "⇧"; + default: + return token.length === 1 ? token.toUpperCase() : token; + } + } + switch (token) { + case "cmd": + case "meta": + case "super": + return "Meta"; + case "ctrl": + case "control": + return "Ctrl"; + case "alt": + case "option": + return "Alt"; + case "shift": + return "Shift"; + default: + return token.length === 1 ? token.toUpperCase() : token; + } +} + +/** `cmd+alt+b` -> `⌘⌥B` on macOS, `ctrl+alt+b` -> `Ctrl+Alt+B` elsewhere. */ +export function formatKeybinding(hint: KeybindingHint | undefined, platform: KeyboardPlatform): string | undefined { + if (hint === undefined) { + return undefined; + } + const raw = platform === "mac" ? hint.mac ?? hint.key : hint.key ?? hint.mac; + if (raw === undefined || raw.trim().length === 0) { + return undefined; + } + const parts = raw + .split("+") + .map((part) => keyToken(part.trim().toLowerCase(), platform)) + .filter((part) => part.length > 0); + if (parts.length === 0) { + return undefined; + } + return platform === "mac" ? parts.join("") : parts.join("+"); +} + +/* ------------------------------------------------------------ language service */ + +/** The part of an mcppls issue the folded block renders. `clangd` is never among them. */ +export interface LanguageServiceIssue { + code: string; + message: string; + /** mcppls's own remedy for this issue, when it offers one. */ + command?: { command: string; arguments?: unknown[]; title?: string }; +} + +/** + * The folded C++ Modules block. + * + * Only the language service's own identity, its state and its problems are + * rendered: the engines, the semantic profile and the compilation database used + * to *define* the old view, and the spec is explicit that `clangd` must not + * appear anywhere in it (§8.2). + */ +export interface LanguageServiceBlock { + /** `mcpp.views.languageServer.show`; `false` removes the block from the tree. */ + show?: boolean; + installed: boolean; + enabled?: boolean; + version?: string; + state?: { + available: boolean; + reason?: string; + state?: string; + issues?: readonly LanguageServiceIssue[]; + }; +} + +/** The provider line the block always carries, so "who owns this" stays answerable. */ +export const LANGUAGE_SERVICE_PROVIDER = "sunrisepeak.mcpp-language-server"; + +interface ActionRow { + id: string; + label: string; + icon: string; + command: string; +} + +/** The four actions worth a permanent row; everything else folds into 「其它 N 项…」. */ +const LANGUAGE_SERVICE_COMMON_ACTIONS: readonly ActionRow[] = [ + { id: "project.languageService.action.restart", label: "Restart language service", icon: "debug-restart", command: "mcpp.languageServer.restart" }, + { id: "project.languageService.action.selectContext", label: "Select analysis context", icon: "symbol-interface", command: "mcpp.languageServer.selectContext" }, + { id: "project.languageService.action.graph", label: "Module graph", icon: "type-hierarchy", command: "mcpp.languageServer.showModuleGraph" }, + { id: "project.languageService.action.logs", label: "Open logs", icon: "output", command: "mcpp.languageServer.showLogs" }, +]; + +const LANGUAGE_SERVICE_MORE_ACTIONS: readonly ActionRow[] = [ + { id: "project.languageService.action.refreshState", label: "Refresh this view", icon: "refresh", command: "mcpp.languageServer.refreshState" }, + { id: "project.languageService.action.restartEngine", label: "Restart the semantic engine", icon: "debug-restart", command: "mcpp.languageServer.restartEngine" }, + { id: "project.languageService.action.resetCache", label: "Reset this workspace's cache", icon: "trash", command: "mcpp.languageServer.resetWorkspaceCache" }, + { id: "project.languageService.action.report", label: "Collect a diagnostic report", icon: "report", command: "mcpp.languageServer.collectReport" }, + { id: "project.languageService.action.bundle", label: "Export a diagnostic bundle", icon: "file-zip", command: "mcpp.languageServer.exportDiagnosticBundle" }, + { id: "project.languageService.action.runBuildTool", label: "Run the build tool in a terminal", icon: "terminal", command: "mcpp.languageServer.runBuildToolInTerminal" }, + { id: "project.languageService.action.settings", label: "Open the C++ Modules settings", icon: "settings-gear", command: "mcpp.openMcpplsSettings" }, +]; + +/* -------------------------------------------------------------- project view */ + +export interface ProjectTreeOptions { + /** `mcpp.lock`'s resolved packages, matched to the declarations by name. */ + lock?: readonly LockPackage[]; + /** The folded C++ Modules block; omitted means "no block". */ + languageService?: LanguageServiceBlock; + /** Keybindings read from the extension manifest. */ + keybindings?: Readonly>; + platform?: KeyboardPlatform; +} + +/** The project view: two labelled sections — what this is, then what to do with it. */ +export function buildProjectTree(project: ProjectSummary | undefined, options: ProjectTreeOptions = {}): TreeNode[] { + if (project === undefined) { + // An empty workspace gets the one thing that can be done in it, next to the + // sentence that says why the view is otherwise empty. 「初始化当前目录」 is + // deliberately absent: `mcpp new` refuses a destination that already exists + // (`scaffold/create.cppm`: `"'{}' already exists"`) and has no `--here`, so + // there is nothing to run for "make this folder a project" — see §17 of + // `.agents/docs/archive/2026-10-02-ui-ux-optimisation-plan.md`. + return [ + { + id: "project.none", + label: plain("No mcpp project in this workspace"), + icon: "info", + tooltip: plain("Open a folder containing mcpp.toml, or create a project with mcpp: New Project."), + }, + newProjectNode(), + ]; + } + if (project.error !== undefined) { + return [ + { + id: "project.error", + label: plain("mcpp.toml could not be read"), + description: { key: "{0}", args: [project.error] }, + icon: "error", + contextValue: "mcppProjectError", + }, + ]; + } + + const facts: Label[] = []; + if (project.standard !== undefined) { + facts.push({ key: "{0}", args: [project.standard] }); + } + if (project.sourceFiles !== undefined) { + facts.push({ key: "{0} source file(s)", args: [project.sourceFiles] }); + } + const identity: TreeNode = { + id: "project.package", + label: plain(project.name ?? "mcpp project"), + icon: "package", + tooltip: + project.version === undefined + ? { key: "{0}", args: [project.root] } + : { key: "{0} · version {1}", args: [project.root, project.version] }, + contextValue: "mcppProject", + }; + const identityFacts = joined(facts); + if (identityFacts !== undefined) { + identity.description = identityFacts; + } + + const basic: TreeNode[] = [ + identity, + ...(project.target === undefined + ? [] + : [{ id: "project.target", label: plain("Target"), description: { key: "{0}", args: [project.target] }, icon: "target" }]), + { + id: "project.toolchain", + label: plain("Toolchain"), + description: { key: "{0}", args: [project.toolchainSpec ?? "host default"] }, + icon: "chip", + }, + ...dependencyNodes(project, options), + ...languageServiceNodes(options.languageService), + ]; + + return [ + { id: "project.section.basic", label: plain("Basics"), icon: "info", children: basic }, + { id: "project.section.commands", label: plain("Common commands"), icon: "terminal", expanded: true, children: commandNodes(options) }, + ]; +} + +interface CommandRow { + id: string; + label: string; + icon: string; + /** See `TreeNode.iconColor`; the palette groups rows by what they act on. */ + iconColor: string; + command: string; +} + +/** + * The nine rows of 「常用命令」, in the order the prototype fixes them, with + * creation first: the section is what the view offers to *do*, and in a new + * workspace the only thing to do is make a project. + * + * The colours are a small, explainable palette grouped by **what the row acts + * on**, not by decoration: blue builds or adds, green runs or verifies, purple + * tests, red deletes, yellow manages the toolchain, and configuration stays in + * the ordinary foreground so it never competes with the actions. + * + * Deletion is `charts.red` and not `charts.orange` because `charts.orange` + * resolves to `minimap.findMatchHighlight` -> `editor.findMatchHighlightBackground`, + * which is `#EA5C00` at 33% alpha: as a glyph colour that is a washed-out smear + * in both themes. `charts.red` is `editorError.foreground`, a solid value in + * every theme. The quick menu uses the same words, one file per colour — see + * `src/commands/menu.ts`. + */ +const COMMON_COMMANDS: readonly CommandRow[] = [ + { id: "project.action.new", label: "New mcpp project…", icon: "new-folder", iconColor: "charts.blue", command: "mcpp.newProject" }, + { id: "project.action.build", label: "Build", icon: "tools", iconColor: "charts.blue", command: "mcpp.build" }, + { id: "project.action.run", label: "Run", icon: "play", iconColor: "charts.green", command: "mcpp.run" }, + { id: "project.action.test", label: "Test", icon: "beaker", iconColor: "charts.purple", command: "mcpp.test" }, + { id: "project.action.clean", label: "Clean", icon: "trash", iconColor: "charts.red", command: "mcpp.cleanProjectArtifacts" }, + { id: "project.action.toolchain", label: "Toolchain", icon: "chip", iconColor: "charts.yellow", command: "mcpp.showToolchains" }, + { id: "project.action.librarySearch", label: "Search and add a dependency…", icon: "library", iconColor: "charts.blue", command: "mcpp.library.search" }, + { id: "project.action.selfCheck", label: "Environment self-check", icon: "heart", iconColor: "charts.green", command: "mcpp.selfCheck" }, + { id: "project.action.settings", label: "Settings", icon: "settings-gear", iconColor: "", command: "mcpp.openSettings" }, +]; + +/** The creation row, built once so the empty state and the section agree. */ +function newProjectNode(): TreeNode { + const row = COMMON_COMMANDS[0]; + const label = plain(row.label); + return { + id: row.id, + label, + icon: row.icon, + iconColor: row.iconColor, + contextValue: "mcppProjectCommand", + command: { command: row.command, title: label }, + }; +} + +function commandNodes(options: ProjectTreeOptions): TreeNode[] { + return COMMON_COMMANDS.map((row): TreeNode => { + const hint = formatKeybinding(options.keybindings?.[row.command], options.platform ?? "other"); + const label = plain(row.label); + const node: TreeNode = { + id: row.id, + label, + icon: row.icon, + contextValue: "mcppProjectCommand", + command: { command: row.command, title: label }, + }; + if (row.iconColor.length > 0) { + node.iconColor = row.iconColor; + } + if (hint !== undefined) { + node.description = plain(hint); + } + return node; + }); +} + +function dependencyNodes(project: ProjectSummary, options: ProjectTreeOptions): TreeNode[] { + const declared = project.dependencies ?? []; + if (declared.length === 0) { + return []; + } + const lock = options.lock ?? []; + const dev = declared.filter((entry) => entry.dev === true).length; + return [ + { + id: "project.dependencies", + label: { key: "Dependencies ({0})", args: [declared.length] }, + ...(dev === 0 ? {} : { description: { key: "{0} dev", args: [dev] } }), + icon: "library", + tooltip: plain("Declared in mcpp.toml; the resolved version comes from mcpp.lock."), + expanded: true, + children: declared.map((entry) => dependencyNode(entry, lock)), + }, + ]; +} + +/** + * One declared dependency. + * + * Level 1 is the declaration, with its markers; level 2 is the single version + * `mcpp.lock` resolved, or `Resolved —` when the lock has nothing to say. There + * is deliberately no third level and no connector glyph: `mcpp.lock` is a flat + * set with no parent/child edges, so a deeper tree would be invented. + */ +function dependencyNode(declaration: DependencyDeclaration, lock: readonly LockPackage[]): TreeNode { + const resolved = resolveLockedPackage(lock, declaration.name); + const resolvedLabel: Label = + resolved?.version === undefined ? plain("Resolved —") : { key: "Resolved {0}", args: [resolved.version] }; + const marker = dependencyMarker(declaration); + const node: TreeNode = { + id: `project.dependency.${declaration.name}`, + label: plain(declaration.name), + description: marker === undefined ? resolvedLabel : { key: "{0} · {1}", args: [marker, resolvedLabel] }, + icon: declaration.path === undefined ? "package" : "folder", + }; + return node; +} + +function dependencyMarker(declaration: DependencyDeclaration): Label | undefined { + if (declaration.path !== undefined) { + return { key: "path · {0}", args: [declaration.path] }; + } + if (declaration.git !== undefined) { + return { key: "git · {0}", args: [declaration.git] }; + } + return declaration.dev === true ? plain("dev") : undefined; +} + +function languageServiceNodes(block: LanguageServiceBlock | undefined): TreeNode[] { + if (block === undefined || block.show === false) { + return []; + } + const issues = block.state?.issues ?? []; + const healthy = block.state?.available === true && block.state.state === "ready"; + const identity: Label = { key: "mcppls {0}", args: [block.version ?? "?"] }; + const node: TreeNode = { + id: "project.languageService", + label: plain("Language service"), + description: + issues.length === 0 + ? identity + : { key: "{0} · {1}", args: [identity, { key: "{0} problem(s)", args: [issues.length] }] }, + icon: healthy ? "pass" : "warning", + contextValue: "mcppLanguageService", + children: languageServiceChildren(block), + }; + if (block.state?.available === false && block.state.reason !== undefined) { + node.tooltip = plain(block.state.reason); + } + return [node]; +} + +/** + * The expanded block: state, provider, every problem, then the four common + * actions and one folded row for the rest. mcppls's engines and semantic profile + * are not read here at all — only the language service itself. + */ +function languageServiceChildren(block: LanguageServiceBlock): TreeNode[] { + if (!block.installed) { + return [ + { + id: "project.languageService.status", + label: plain("C++ Modules is not installed"), + icon: "warning", + command: { command: "mcpp.openMcpplsSettings", title: plain("Install C++ Modules") }, + }, + ]; + } + + const state = block.state; + const nodes: TreeNode[] = [ + languageServiceStatus(state), + { + id: "project.languageService.provider", + label: plain("Provided by"), + description: plain(LANGUAGE_SERVICE_PROVIDER), + icon: "beaker", + }, + ]; + + (state?.issues ?? []).forEach((issue, index) => { + nodes.push({ + id: `project.languageService.issue.${index}.${issue.code}`, + label: plain(issue.message), + description: plain(issue.code), + icon: "warning", + // S3 hands us the remedy; use it rather than inventing one. + command: + issue.command === undefined + ? undefined + : { + command: issue.command.command, + // mcppls's own title — a sentence we did not write, shown as-is. + title: plain(issue.command.title ?? "Fix"), + arguments: issue.command.arguments, + }, + }); + }); + + for (const action of LANGUAGE_SERVICE_COMMON_ACTIONS) { + nodes.push(actionNode(action)); + } + nodes.push({ + id: "project.languageService.action.more", + label: { key: "More ({0})…", args: [LANGUAGE_SERVICE_MORE_ACTIONS.length] }, + icon: "ellipsis", + children: LANGUAGE_SERVICE_MORE_ACTIONS.map((action) => actionNode(action)), + }); + return nodes; +} + +function actionNode(action: ActionRow): TreeNode { + const label = plain(action.label); + return { + id: action.id, + label, + icon: action.icon, + contextValue: "mcppProjectCommand", + command: { command: action.command, title: label }, + }; +} + +function languageServiceStatus(state: LanguageServiceBlock["state"]): TreeNode { + if (state === undefined || !state.available) { + return { + id: "project.languageService.status", + label: plain("Unavailable"), + description: plain(state?.reason ?? "not read yet"), + icon: "warning", + }; + } + return { + id: "project.languageService.status", + label: plain(state.state === "ready" ? "Ready" : state.state === "degraded" ? "Degraded" : state.state ?? "Unknown"), + icon: state.state === "ready" ? "pass" : "warning", + }; +} diff --git a/src/views/projectView.ts b/src/views/projectView.ts new file mode 100644 index 0000000..27e5239 --- /dev/null +++ b/src/views/projectView.ts @@ -0,0 +1,148 @@ +/** + * The project view: two labelled sections — 「基本信息」 and 「常用命令」. + * + * The first section is what the project is (identity, target, toolchain, the + * declared dependencies and the folded C++ Modules block); the second is what can + * be done to it. The summary is read from `mcpp.toml` and refreshed when the + * manifest changes; anything unreadable is omitted rather than guessed, and + * `mcpp: Environment Self-check` is the place that reports what mcpp itself + * resolves. + */ + +import { readFileSync } from "node:fs"; +import path from "node:path"; + +import * as vscode from "vscode"; + +import { readProjectSummary } from "../projects/summary"; +import { MCPP_MANIFEST_GLOB } from "../projects/context"; +import type { McppProjectDiscovery } from "../projects/discovery"; +import type { LanguageServerBlockSource } from "./languageServerView"; +import { + buildProjectTree, + countSourceFiles, + keybindingsFromPackage, + readDependencies, + readLockfile, + type KeyboardPlatform, + type LockPackage, + type ProjectSummary, +} from "./models"; +import { registerTreeView } from "./treeProvider"; + +export const PROJECT_VIEW_ID = "mcpp.project"; + +export interface ProjectViewDeps { + currentProject: () => McppProjectDiscovery | undefined; + /** + * The folded C++ Modules block. Omit it and the block is never rendered, which + * is also what happens when `mcpp.views.languageServer.show` is off. + */ + languageService?: LanguageServerBlockSource; +} + +/** `mcpp.lock` sits next to `mcpp.toml`, and changes without the manifest changing. */ +const LOCKFILE_NAME = "mcpp.lock"; +const LOCKFILE_GLOB = "**/mcpp.lock"; + +/** Read one manifest into a summary; a failure becomes `error`, never a throw. */ +export function summariseProject(project: McppProjectDiscovery | undefined): ProjectSummary | undefined { + if (project === undefined) { + return undefined; + } + try { + const text = readFileSync(project.manifestPath, "utf8"); + const lines = text.split(/\r?\n/); + const summary = readProjectSummary(project.root, lines); + const dependencies = readDependencies(lines); + return dependencies.length === 0 ? summary : { ...summary, dependencies }; + } catch (error) { + return { + root: project.root, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +interface ProjectSnapshot { + summary: ProjectSummary | undefined; + lock: LockPackage[]; +} + +export function registerProjectView(context: vscode.ExtensionContext, deps: ProjectViewDeps): void { + // Read from the manifest rather than copied into the source: a hint in the tree + // that disagrees with `package.json` is worse than no hint. + const keybindings = keybindingsFromPackage(context.extension.packageJSON); + const platform: KeyboardPlatform = process.platform === "darwin" ? "mac" : "other"; + const bridge = deps.languageService; + + // Counting source files walks the project, so it is not repeated for every + // active-editor change: the manifest watcher and a workspace-folder change ask + // for a fresh count, everything else reuses the last one. + let measured: { root: string; count: number } | undefined; + const withSources = (summary: ProjectSummary | undefined, fresh: boolean): ProjectSummary | undefined => { + if (summary === undefined || summary.error !== undefined) { + return summary; + } + if (fresh || measured === undefined || measured.root !== summary.root) { + measured = { root: summary.root, count: countSourceFiles(summary.root) }; + } + return { ...summary, sourceFiles: measured.count }; + }; + + const snapshot = (project: McppProjectDiscovery | undefined, freshSources: boolean): ProjectSnapshot => ({ + summary: withSources(summariseProject(project), freshSources), + lock: project === undefined ? [] : readLockfile(path.join(project.root, LOCKFILE_NAME)), + }); + + let current = snapshot(deps.currentProject(), true); + + const view = registerTreeView(PROJECT_VIEW_ID, () => + buildProjectTree(current.summary, { + lock: current.lock, + keybindings, + platform, + languageService: bridge?.input(), + }), + ); + context.subscriptions.push(view.disposable); + + const reload = (freshSources: boolean): void => { + current = snapshot(deps.currentProject(), freshSources); + view.provider.refresh(); + }; + + const manifestWatcher = vscode.workspace.createFileSystemWatcher(MCPP_MANIFEST_GLOB); + const lockWatcher = vscode.workspace.createFileSystemWatcher(LOCKFILE_GLOB); + context.subscriptions.push( + manifestWatcher, + lockWatcher, + manifestWatcher.onDidCreate(() => reload(true)), + manifestWatcher.onDidChange(() => reload(true)), + manifestWatcher.onDidDelete(() => reload(true)), + lockWatcher.onDidCreate(() => reload(false)), + lockWatcher.onDidChange(() => reload(false)), + lockWatcher.onDidDelete(() => reload(false)), + vscode.window.onDidChangeActiveTextEditor(() => reload(false)), + vscode.workspace.onDidChangeWorkspaceFolders(() => reload(true)), + vscode.workspace.onDidGrantWorkspaceTrust(() => reload(true)), + ); + + if (bridge !== undefined) { + const tree = view.view; + context.subscriptions.push( + bridge.onDidChange(() => reload(false)), + // The folded block's poller only runs while this tree is on screen. + tree.onDidChangeVisibility(() => bridge.setVisible(tree.visible)), + ); + bridge.setVisible(tree.visible); + } + + context.subscriptions.push( + vscode.commands.registerCommand("mcpp.internal.refreshProjectView", async () => { + reload(true); + }), + ); + + reload(true); +} diff --git a/src/views/treeProvider.ts b/src/views/treeProvider.ts new file mode 100644 index 0000000..67d5ce0 --- /dev/null +++ b/src/views/treeProvider.ts @@ -0,0 +1,111 @@ +/** + * The tree provider for the project view. + * + * The trees are described as data (`./models.ts`); this file is the only part + * that knows about `vscode.TreeItem`. Labels carry keys, so the translation + * happens here, at render time. + */ + +import * as vscode from "vscode"; + +import { t } from "../i18n/t"; +import type { Label, LabelArgument, TreeNode } from "./models"; + +/** `{ key, args }` -> a sentence in the user's language. */ +export function resolveLabel(label: Label | undefined): string { + if (label === undefined) { + return ""; + } + return t(label.key, ...(label.args ?? []).map(resolveArgument)); +} + +/** + * An argument is usually a value, but it may be another label: `C++23 · 87 + * source file(s)` is two translatable pieces, and the join happens here rather + * than in a builder that must stay free of the current language. + */ +function resolveArgument(argument: LabelArgument): string | number { + return typeof argument === "object" ? resolveLabel(argument) : argument; +} + +export function toTreeItem(node: TreeNode): vscode.TreeItem { + const collapsible = + node.children === undefined || node.children.length === 0 + ? vscode.TreeItemCollapsibleState.None + : node.expanded === true + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.Collapsed; + + const item = new vscode.TreeItem(resolveLabel(node.label), collapsible); + item.id = node.id; + item.description = node.description === undefined ? undefined : resolveLabel(node.description); + item.tooltip = node.tooltip === undefined ? undefined : resolveLabel(node.tooltip); + if (node.icon !== undefined) { + // A `ThemeColor` id is resolved by the host; an id a theme does not define + // leaves the icon in the normal foreground colour, which is why the model + // only ever names `charts.*`. + item.iconPath = + node.iconColor === undefined || node.iconColor.length === 0 + ? new vscode.ThemeIcon(node.icon) + : new vscode.ThemeIcon(node.icon, new vscode.ThemeColor(node.iconColor)); + } + item.contextValue = node.contextValue; + if (node.command !== undefined) { + item.command = { + command: node.command.command, + title: resolveLabel(node.command.title), + arguments: [...(node.command.arguments ?? [])], + }; + } + return item; +} + +/** + * A provider over a snapshot the caller recomputes whenever something changes. + * `refresh()` re-asks for the root nodes and fires the tree event. + * + * It is disposable so it can be handed straight to `createTreeView`'s + * `context.subscriptions.push`, next to the view it feeds. + */ +export class StaticTreeProvider implements vscode.TreeDataProvider, vscode.Disposable { + private readonly changed = new vscode.EventEmitter(); + + public readonly onDidChangeTreeData = this.changed.event; + + public constructor(private readonly roots: () => readonly TreeNode[]) {} + + public refresh(): void { + this.changed.fire(undefined); + } + + public dispose(): void { + this.changed.dispose(); + } + + public getTreeItem(element: TreeNode): vscode.TreeItem { + return toTreeItem(element); + } + + public getChildren(element?: TreeNode): TreeNode[] { + if (element === undefined) { + return [...this.roots()]; + } + return [...(element.children ?? [])]; + } +} + +/** + * Registers a view and hands back the provider, the view and its disposable. + * + * The `TreeView` is returned (rather than using `registerTreeDataProvider`) + * because a caller may need its visibility: the project view gates the C++ + * Modules poller on whether its tree is actually on screen. + */ +export function registerTreeView( + viewId: string, + roots: () => readonly TreeNode[], +): { provider: StaticTreeProvider; view: vscode.TreeView; disposable: vscode.Disposable } { + const provider = new StaticTreeProvider(roots); + const view = vscode.window.createTreeView(viewId, { treeDataProvider: provider }); + return { provider, view, disposable: view }; +} diff --git a/src/views/viewPolicy.ts b/src/views/viewPolicy.ts new file mode 100644 index 0000000..6dffc0d --- /dev/null +++ b/src/views/viewPolicy.ts @@ -0,0 +1,46 @@ +/** + * The C++ Modules view's settings, as decisions. + * + * Three settings shape that view, and each one is a policy question rather than + * a rendering detail, so the answers live here — free of `vscode` and therefore + * testable: + * + * - `mcpp.languageService.confirmResetCache` — does resetting the workspace cache + * ask a modal of its own, or does it rely on the capability table's `danger`? + * The answer may only ever *add* a confirmation; + * - `mcpp.languageService.notifyOnDegraded` — is the one-time "the dependency is + * gone" notice emitted at all? + * + * `src/mcppls/contract.ts` stays the single source of what each capability's + * danger is; `src/views/languageServerView.ts` stays the only place that talks to + * `vscode.window`. + */ + +export type LsConfirmation = "extra-modal" | "capability" | "none"; + +export interface ResetCacheConfirmInput { + /** The capability's own `danger` level, from `contract.ts`. */ + danger: "none" | "confirm" | "destructive"; + /** `mcpp.languageService.confirmResetCache`. */ + confirmResetCache: boolean; +} + +/** + * What the reset-cache command must ask before forwarding. + * + * Both settings default to `true`, so the shipped behaviour is unchanged: + * the destructive capability asks its modal, and this setting adds a second + * explicit step. Turning the setting off falls back to the capability's own + * level — it never removes a `destructive` modal. + */ +export function resetCacheConfirmation(input: ResetCacheConfirmInput): LsConfirmation { + if (!input.confirmResetCache) { + return input.danger === "none" ? "none" : "capability"; + } + return input.danger === "none" ? "none" : "extra-modal"; +} + +/** `mcpp.languageService.notifyOnDegraded`: emit the one-time dependency notice? */ +export function degradedNoticeEnabled(notifyOnDegraded: boolean | undefined): boolean { + return notifyOnDegraded !== false; +} diff --git a/src/webview/document.ts b/src/webview/document.ts new file mode 100644 index 0000000..cdd8005 --- /dev/null +++ b/src/webview/document.ts @@ -0,0 +1,64 @@ +/** + * One webview document: the CSP nonce and the render-only-on-change rule every + * webview host in this extension shares (library view, cache view, settings + * panel, package detail page). + * + * Assigning `webview.html` reloads the document — it throws away the scroll + * position, any half-typed input and the focus. Two properties make a host + * safe against pointless and looping reloads: + * + * 1. **The nonce is per view, not per render.** The CSP nonce is embedded in + * the document, so a fresh nonce per render makes every render a *different* + * document and no comparison can ever say "unchanged" — that is exactly how + * the library view once reloaded itself forever (flicker, unclickable rows, + * a pegged CPU). + * 2. **`paint()` assigns only when the document changed**, via + * `documentNeedsRender` in `./render`. + * + * A hidden `WebviewView` loses its context (`retainContextWhenHidden` is not + * supported), so the next resolve is a brand-new, empty webview: the host + * calls `invalidate()` on resolve and on dispose, and the comparison starts + * from nothing again. + */ +import { randomBytes } from "node:crypto"; +import * as vscode from "vscode"; + +import { documentNeedsRender } from "./render"; + +/** What an html renderer embeds: the CSP source, the script nonce, the stylesheet. */ +export interface WebviewAssets { + cspSource: string; + nonce: string; + styleUri: string; +} + +/** One webview's document state: its nonce and the html currently on screen. */ +export class WebviewDocument { + private readonly nonce = randomBytes(16).toString("base64"); + private current: string | undefined; + + /** @param stylesheet the stylesheet file, joined onto the media root. */ + constructor(private readonly stylesheet: string) {} + + /** The webview was (re)resolved or disposed: it is a fresh, empty document. */ + invalidate(): void { + this.current = undefined; + } + + assets(mediaRoot: vscode.Uri, webview: vscode.Webview): WebviewAssets { + return { + cspSource: webview.cspSource, + nonce: this.nonce, + styleUri: webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, this.stylesheet)).toString(), + }; + } + + /** Put the document on screen, but only when it says something new. */ + paint(webview: vscode.Webview, html: string): void { + if (!documentNeedsRender(this.current, html)) { + return; + } + this.current = html; + webview.html = html; + } +} diff --git a/src/webview/render.ts b/src/webview/render.ts new file mode 100644 index 0000000..0de9fe8 --- /dev/null +++ b/src/webview/render.ts @@ -0,0 +1,18 @@ +/** + * The pure half of the webview render kit. + * + * This module deliberately imports nothing — not even `vscode` — so the unit + * tests can state its semantics directly under `node --test`. + */ + +/** + * Whether a freshly rendered document should replace the one on screen. + * + * Assigning `webview.html` reloads the view, so an identical document must not + * be pushed. This is the guard that keeps a "render again" request from + * turning into a reload loop; it is the one rule of the kit that a unit test + * can state in one line. + */ +export function documentNeedsRender(rendered: string | undefined, next: string): boolean { + return rendered !== next; +} diff --git a/src/moduleSetup.ts b/src/workflows/moduleSetup.ts similarity index 88% rename from src/moduleSetup.ts rename to src/workflows/moduleSetup.ts index cc0557f..6810dde 100644 --- a/src/moduleSetup.ts +++ b/src/workflows/moduleSetup.ts @@ -1,3 +1,5 @@ +import { t } from "../i18n/t"; + export type ModuleSetupStage = "build" | "language-server"; export type ModuleSetupStepState = "succeeded" | "failed" | "cancelled" | "not-started"; export type ModuleSetupBlockedReason = "untrusted" | "busy"; @@ -26,8 +28,8 @@ export interface ModuleSetupConfirmation { export function moduleSetupConfirmation(): ModuleSetupConfirmation { return { - message: "是否构建当前 mcpp 工程并刷新 C++ 模块语言服务?", - detail: "将执行 mcpp build,并由 C++ Modules 扩展重新读取构建描述;不会修改工具链、mcpp.toml 或任何语言服务设置。", + message: t("Build the current mcpp project and refresh the C++ Modules language service?"), + detail: t("This runs mcpp build and has the C++ Modules extension re-read the build description; it does not change the toolchain, mcpp.toml or any language service setting."), }; } diff --git a/test/architecture.test.ts b/test/architecture.test.ts new file mode 100644 index 0000000..4b6c377 --- /dev/null +++ b/test/architecture.test.ts @@ -0,0 +1,141 @@ +import assert from "node:assert/strict"; +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +/** + * The architectural rule this codebase is built on: **pure modules never import + * `vscode`**. + * + * It is what makes the logic testable under plain `node --test`, and it is easy + * to break by accident — a convenience `vscode.window.showWarningMessage` in a + * policy helper, or an `import * as vscode` at the top of a translation module, + * silently takes the whole file out of the unit test's reach. So the rule is + * checked two ways: the import is absent from the source, and the module can + * actually be loaded outside an editor host. + */ + +/** The modules that must stay importable without VS Code. */ +const PURE_MODULES = [ + "src/i18n/t.ts", + "src/i18n/translate.ts", + "src/cli/clean.ts", + "src/cli/cache.ts", + "src/cli/artifacts.ts", + "src/cli/process.ts", + "src/cli/protocol.ts", + "src/cli/search.ts", + "src/cli/tasks.ts", + "src/cli/newProject.ts", + "src/cli/toolchain.ts", + "src/cli/labels.ts", + "src/cli/statusBar.ts", + "src/cli/selfCheck.ts", + "src/config/registry.ts", + "src/config/validate.ts", + "src/config/panelHtml.ts", + "src/mcppls/contract.ts", + "src/mcppls/state.ts", + "src/mcppls/messages.ts", + "src/mcppls/timers.ts", + "src/projects/discovery.ts", + "src/projects/summary.ts", + "src/toml/completion.ts", + "src/toml/diagnostics.ts", + "src/toml/parser.ts", + "src/toml/schema.ts", + "src/toml/hover.ts", + "src/toml/navigation.ts", + "src/views/models.ts", + "src/cache/cacheState.ts", + "src/library/indexModel.ts", + "src/library/xpkg.ts", + "src/library/libraryHtml.ts", + "src/library/detailHtml.ts", + "src/cache/cachePanelHtml.ts", + "src/views/viewPolicy.ts", + "src/buildscript/analysis.ts", + "src/buildscript/api.ts", + "src/buildscript/modules.ts", + "src/buildscript/settings.ts", + "src/buildscript/snippets.ts", + "src/util/format.ts", + "src/util/log.ts", + "src/util/text.ts", + "src/webview/render.ts", + "src/workflows/moduleSetup.ts", +]; + +const root = process.cwd(); + +test("a pure module does not import vscode", () => { + const offenders: string[] = []; + for (const relative of PURE_MODULES) { + const text = readFileSync(path.join(root, relative), "utf8"); + // `import type` is fine: it is erased at compile time and costs nothing at + // runtime, which is exactly how `src/i18n/t.ts` reads the l10n API lazily. + for (const match of text.matchAll(/^\s*import\s+(?!type\b)[^;]*from\s+"vscode";/gm)) { + offenders.push(`${relative}: ${match[0].trim()}`); + } + assert.ok(!/require\(\s*["']vscode["']\s*\)/.test(text.replace(/require\(\s*["']vscode["']\s*\)\s*as\s+typeof\s+vscode/g, "")), + `${relative} requires vscode at module scope`); + } + assert.deepEqual(offenders, [], `these pure modules must not import vscode:\n ${offenders.join("\n ")}`); +}); + +test("a pure module can actually be loaded without an editor host", async () => { + // The source check above proves the absence of an import; this proves the + // consequence, which is the property the unit tests depend on. + const failed: string[] = []; + for (const relative of PURE_MODULES) { + const compiled = path.join(root, "dist", relative.replace(/\.ts$/, ".js")); + try { + await import(compiled); + } catch (error) { + failed.push(`${relative}: ${error instanceof Error ? error.message : String(error)}`); + } + } + assert.deepEqual(failed, [], `these pure modules could not load outside VS Code:\n ${failed.join("\n ")}`); +}); + +test("the list is not so short that the rule stops meaning anything", () => { + assert.ok(PURE_MODULES.length >= 40, `only ${PURE_MODULES.length} modules are checked`); +}); + +test("every listed module exists", () => { + for (const relative of PURE_MODULES) { + readFileSync(path.join(root, relative), "utf8"); + } +}); + +test("runProcess stays inside src/cli — everything else runs mcpp through the trust-gated seam", () => { + // The external review's P0 (2026-10-03): `mcpp.path` is a resource-scoped + // setting, so any call site that skips the workspace-trust gate lets an + // untrusted workspace name the program that runs. The seam is `runMcpp`; + // `src/cli/` keeps `runProcess` because its controller wraps whole commands + // in `requireTrusted()` prompts of its own. + const offenders: string[] = []; + const walk = (directory: string): void => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const full = path.join(directory, entry.name); + if (entry.isDirectory()) { + walk(full); + continue; + } + if (!entry.name.endsWith(".ts")) { + continue; + } + const relative = path.relative(root, full); + if (relative.split(path.sep)[1] === "cli") { + continue; + } + const text = readFileSync(full, "utf8"); + const code = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); + if (/\brunProcess\b/.test(code)) { + offenders.push(relative); + } + } + }; + walk(path.join(root, "src")); + assert.deepEqual(offenders, [], "these files call runProcess directly; use runMcpp(trusted, …) instead"); +}); diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index c3ca208..d710dbd 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -1,10 +1,13 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; +import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import test from "node:test"; +import { contributedCommandIds } from "../src/commands/ids"; + interface PackageManifest { version?: string; + displayName?: string; description?: string; icon?: string; dependencies?: Record; @@ -15,13 +18,29 @@ interface PackageManifest { engines?: { vscode?: string }; extensionDependencies?: string[]; activationEvents?: string[]; - capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string } }; + capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string; restrictedConfigurations?: string[] } }; contributes?: { - commands?: Array<{ command: string; icon?: string }>; + commands?: Array<{ command: string; title?: string; category?: string; icon?: string }>; menus?: { "editor/title"?: Array<{ command: string; group?: string; when?: string }> }; - configuration?: { properties?: Record }; + configuration?: { title?: string; properties?: Record }; configurationDefaults?: Record; languages?: Array<{ id: string; aliases?: string[]; filenames?: string[]; configuration?: string }>; + viewsContainers?: { activitybar?: Array<{ id: string; title?: string; icon?: string }> }; + views?: Record< + string, + Array<{ + id: string; + name?: string; + description?: string; + when?: string; + type?: string; + visibility?: string; + initialSize?: number; + }> + >; + viewsWelcome?: Array<{ view: string; contents: string; when?: string }>; + colors?: Array<{ id: string; description?: string }>; + keybindings?: Array<{ command: string; key?: string; mac?: string; when?: string }>; grammars?: Array<{ language?: string; scopeName: string; injectTo?: string[]; path: string }>; }; } @@ -30,43 +49,61 @@ const root = path.resolve(process.cwd()); test("declares mcpp-language-server as the C++ modules language service", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.4.0"); - assert.equal(manifest.description, "mcpp 工程构建、工具链与 C++ Modules 语言服务集成"); + // The version lives in three places; they must agree or a release tag is a lie. + const lock = JSON.parse(readFileSync(path.join(root, "package-lock.json"), "utf8")) as { + version?: string; + packages?: Record; + }; + assert.match(manifest.version ?? "", /^\d+\.\d+\.\d+$/); + assert.equal(lock.version, manifest.version); + assert.equal(lock.packages?.[""]?.version, manifest.version); + // User-visible strings live in the nls bundle; the manifest only names them. + assert.equal(manifest.description, "%description%"); + assert.equal(manifest.displayName, "%displayName%"); assert.equal(manifest.engines?.vscode, "^1.91.0"); assert.deepEqual(manifest.extensionDependencies, ["sunrisepeak.mcpp-language-server"]); assert.ok(!manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); - assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); - assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); - assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.configureLanguageServer")); - assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.configureClangd")); // deprecated alias + // Since VS Code 1.74 every contributes.commands entry implies its own + // onCommand activation, so only the file- and folder-based triggers remain. + assert.deepEqual(manifest.activationEvents, [ + "workspaceContains:mcpp.toml", + "onLanguage:mcpp-toml", + "onLanguage:mcpp-build", + ]); assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited"); - assert.equal( - manifest.capabilities?.untrustedWorkspaces?.description, - "未受信任工作区仅启用本扩展的模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 mcpp CLI 或接管语言服务配置。", + assert.equal(manifest.capabilities?.untrustedWorkspaces?.description, "%untrustedWorkspaces.description%"); + // The `limited` promise says "no mcpp command runs" in an untrusted + // workspace; a resource-scoped `mcpp.path` can only be kept out of the + // workspace's reach by naming it here (external review P0, 2026-10-03). + assert.deepEqual(manifest.capabilities?.untrustedWorkspaces?.restrictedConfigurations, [ + "mcpp.path", + "mcpp.clangd.path", + ]); + // Same set, not necessarily the same order: the manifest's order is the + // palette's presentation order, which `ids.ts` has no business dictating. + assert.deepEqual( + (manifest.contributes?.commands?.map((command) => command.command) ?? []).slice().sort(), + contributedCommandIds().slice().sort(), + ); + assert.deepEqual( + manifest.contributes?.viewsContainers?.activitybar?.map((container) => container.id), + ["mcpp"], ); assert.deepEqual( - manifest.contributes?.commands?.map((command) => command.command), - [ - "mcpp.showMenu", - "mcpp.newProject", - "mcpp.build", - "mcpp.run", - "mcpp.test", - "mcpp.clean", - "mcpp.showToolchains", - "mcpp.installToolchain", - "mcpp.selectDefaultToolchain", - "mcpp.configureLanguageServer", - "mcpp.configureClangd", - "mcpp.refreshCompilationDatabase", - "mcpp.checkModuleSupport", - "mcpp.autoConfigureModules", - "mcpp.showModuleGraph", - "mcpp.showLanguageServerLogs", - ], + manifest.contributes?.views?.mcpp?.map((view) => view.id), + ["mcpp.project", "mcpp.library", "mcpp.cache"], + ); + assert.deepEqual( + manifest.contributes?.colors?.map((color) => color.id), + ["mcpp.cacheOkForeground", "mcpp.cacheStaleForeground"], ); assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.path"]); assert.ok(manifest.contributes?.configuration?.properties?.["mcpp.tomlCompletion"]); + assert.equal(manifest.contributes?.configuration?.title, "%mcpp.configuration.title%"); + for (const command of manifest.contributes?.commands ?? []) { + assert.match(command.title ?? "", /^%command\.[^%]+\.title%$/, `command ${command.command} title must be an nls key`); + assert.equal(command.category, "%category%"); + } assert.equal(manifest.dependencies?.["vscode-languageclient"], undefined); assert.equal(manifest.devDependencies?.["vscode-languageclient"], undefined); assert.deepEqual(manifest.contributes?.configurationDefaults?.["files.associations"], { @@ -78,10 +115,12 @@ test("declares mcpp-language-server as the C++ modules language service", () => }); test("一键向导只执行普通 build 并在之后刷新 C++ 模块语言服务", () => { - const controller = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const controller = readFileSync(path.join(root, "src/cli/controller.ts"), "utf8"); const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); + // Slice up to a marker that exists for its own sake, not as a test hook: the + // wizard is the last function before `activate`. const start = source.indexOf("async function autoConfigureModulesWizard"); - const end = source.indexOf("const mcppTomlCompletionKinds", start); + const end = source.indexOf("export async function activate(", start); assert.notEqual(start, -1); assert.notEqual(end, -1); const wizard = source.slice(start, end); @@ -100,11 +139,207 @@ test("一键向导只执行普通 build 并在之后刷新 C++ 模块语言服 assert.doesNotMatch(controller, /runAutomaticModuleSetup|executeAutomaticModuleSetupCommand/); }); +test("the promised keybindings are contributed and scoped to a project", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + const bindings = manifest.contributes?.keybindings ?? []; + assert.deepEqual( + bindings.map((binding) => binding.command), + ["mcpp.build", "mcpp.run", "mcpp.test", "mcpp.cleanProjectArtifacts", "mcpp.showMenu"], + ); + for (const binding of bindings) { + assert.match(binding.key ?? "", /^ctrl\+alt\+[a-z]$/); + assert.match(binding.mac ?? "", /^cmd\+alt\+[a-z]$/); + assert.equal(binding.when, "mcpp.inProject"); + } +}); + +test("each view is gated by its own visibility setting", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + // The master switch comes first, in negated form: with `mcpp.views.enabled` off + // every view is hidden and VS Code drops the container from the activity bar. + assert.deepEqual(manifest.contributes?.views?.mcpp?.map((view) => view.when), [ + "!mcpp.sidebarHidden && mcpp.views.project", + "!mcpp.sidebarHidden && mcpp.views.library", + "!mcpp.sidebarHidden && mcpp.views.cache", + ]); + assert.deepEqual(manifest.contributes?.views?.mcpp?.map((view) => view.type), [ + undefined, + "webview", + "webview", + ]); + // The cache view starts folded, so the sidebar opens on the library list; the + // two above it start open. `visibility` is the only way a view can say this — + // VS Code keeps whatever the user does to the header afterwards. + assert.deepEqual(manifest.contributes?.views?.mcpp?.map((view) => view.visibility), [ + undefined, + undefined, + "collapsed", + ]); + // `initialSize` becomes the view's split-view `weight`, and VS Code hands out + // the container height in proportion to it — defaulting every view to 20 + // (`computeInitialSizes()`: `dimension.height * (weight || 20) / total`). Twice + // the default is what makes the library take the bottom two thirds and leaves + // the tree above it its third, which is the layout the sidebar is for. + assert.deepEqual(manifest.contributes?.views?.mcpp?.map((view) => view.initialSize), [ + undefined, + 40, + undefined, + ]); +}); + +test("every webview view registers the provider that fills it", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + const webviews = (manifest.contributes?.views?.mcpp ?? []).filter((view) => view.type === "webview"); + assert.ok(webviews.length > 0, "this gate is pointless without a webview view"); + for (const view of webviews) { + // The view id is declared once, in the module that owns the view (other + // modules re-export the constant), and that module must also call + // `registerWebviewViewProvider`. Without it VS Code never asks the view for + // a document and every `refresh()` silently does nothing — which is exactly + // the bug this test exists for: the library view declared its id, exported a + // provider, wired its listeners, and never registered. + const declaration = new RegExp(`export const \\w*_VIEW_ID = "${view.id}"`); + const owners = sourceFiles().filter(([, text]) => declaration.test(text)); + assert.equal(owners.length, 1, `${view.id} must have exactly one declaring module, found ${owners.length}`); + assert.ok( + owners[0][1].includes("registerWebviewViewProvider("), + `${owners[0][0]} declares ${view.id} but never calls registerWebviewViewProvider`, + ); + } +}); + +test("the library view cannot reload itself in a loop", () => { + const view = readFileSync(path.join(root, "src/library", "libraryView.ts"), "utf8"); + const html = readFileSync(path.join(root, "src/library", "libraryHtml.ts"), "utf8"); + // Assigning `webview.html` reloads the document. The library document used to + // announce its own load with a `ready` message and the host answered it with a + // repaint, so the view reloaded itself forever: flicker, rows that could not be + // clicked, a pegged CPU. Both halves of that handshake are gone. + assert.doesNotMatch(html, /post\(\{\s*type:\s*"ready"\s*\}\)/, "the document must not announce its own load"); + assert.doesNotMatch(view, /case "ready"/, "the host must not answer a load with a re-render"); + assert.doesNotMatch(html, /\| \{ type: "ready" \}/); + // Two guards keep it shut: the render-only-on-change comparison, and a nonce + // that is per *view* — a nonce per render would make every render a different + // document, so the comparison could never say "unchanged". Both now live in + // the shared WebviewDocument kit, so the gate holds the host to the kit + // instead of to one implementation of it. Comments are stripped first: the + // prose explains the rule with the very assignment it forbids. + const viewCode = view.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); + assert.match(viewCode, /new WebviewDocument\(/, "the host must hold its document through the kit"); + assert.doesNotMatch(viewCode, /randomNonce\(\)/, "the host must not mint its own nonce"); + assert.doesNotMatch(viewCode, /webview\.html\s*=/, "the host must assign html only through the kit"); + // A freshly resolved view is a new, empty webview: the last document says + // nothing about it, so it must be forgotten when the old view goes away. + assert.match(view, /\.invalidate\(\);/); +}); + +test("every webview host holds its document through the kit", () => { + // The nonce and the html assignment are the two halves of the reload-loop + // accident (see the library gate above); since the kit extraction they are + // owned once. Every host — library, cache, settings panel, detail page — + // must go through `WebviewDocument`, and a nonce may be minted nowhere but + // the kit. + const hosts = [ + "src/library/libraryView.ts", + "src/cache/cachePanel.ts", + "src/config/panel.ts", + "src/library/detailPanel.ts", + ]; + for (const host of hosts) { + const text = readFileSync(path.join(root, host), "utf8"); + // Comments are stripped: the prose explains the rules with the very code + // it forbids. + const code = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); + assert.match(code, /new WebviewDocument\(/, `${host} must hold its document through the kit`); + assert.doesNotMatch(code, /\.html\s*=/, `${host} must assign html only through the kit`); + } + const offenders: string[] = []; + for (const [file, text] of sourceFiles()) { + if (file === path.join("src", "webview", "document.ts")) { + continue; + } + const code = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); + if (/randomBytes/.test(code)) { + offenders.push(file); + } + } + assert.deepEqual(offenders, [], "a webview nonce is minted outside src/webview/document.ts"); +}); + +test("no module drives a second language client", () => { + // The same rule the `package` job enforces, but runnable here: the CI grep used + // to match the bare name, and the compiled comments that explain why this + // extension does *not* use a language client made it fail on a clean build. + const offenders: string[] = []; + for (const [file, text] of sourceFiles()) { + const code = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); + if (/from\s+["']vscode-languageclient["']|require\(["']vscode-languageclient["']\)/.test(code)) { + offenders.push(file); + } + } + assert.deepEqual(offenders, [], "a language client is imported again"); +}); + +test("the committed snapshots record a clone-independent commit", () => { + // `git rev-parse --short` shortens to the shortest unique prefix *for that + // clone*, so the same mcpp produced a 7-character hash in a fresh CI checkout + // and 8 in a developer's — and the drift gate went red on an unchanged API. + for (const file of ["data/buildscript-api.json", "data/toml-schema.json"]) { + const snapshot = JSON.parse(readFileSync(path.join(root, file), "utf8")) as { + sourceVersion?: string; + sourceCommit?: string; + }; + assert.match(snapshot.sourceVersion ?? "", /^\d{4}\.\d+\.\d+/, `${file}: no mcpp version`); + assert.match( + snapshot.sourceCommit ?? "", + /^[0-9a-f]{40}$/, + `${file}: the provenance hash must be full length, not an abbreviation`, + ); + } +}); + +test("opening a link always produces an answer", () => { + // The comment above `openExternal` quotes the old call, so comments are + // stripped before the check — the point is the code, not the prose. + const panel = readFileSync(path.join(root, "src", "library", "detailPanel.ts"), "utf8").replace( + /\/\*[\s\S]*?\*\//g, + "", + ); + // `void vscode.env.openExternal(…)` drops the boolean it resolves to, so a host + // that cannot open a browser answered a click with nothing at all. The call is + // awaited, both outcomes are posted to the page, and a failure leaves the URL + // somewhere the reader can use it. + assert.doesNotMatch(panel, /void vscode\.env\.openExternal/); + assert.match(panel, /opened = await vscode\.env\.openExternal\(/); + assert.match(panel, /postResult\(session, \{ state: "ok", message: t\("Opened \{0\} in your browser\."/); + assert.match(panel, /postResult\(session, \{\s*state: "error",/); + assert.match(panel, /vscode\.env\.clipboard\.writeText\(url\)/); + // The client's own line, so the click is visible before the host answers. + const html = readFileSync(path.join(root, "src", "library", "detailHtml.ts"), "utf8"); + assert.match(html, /state: "pending"/); +}); + +/** `src/**\/*.ts`, relative path and text, for the source-level gates. */ +function sourceFiles(directory = path.join(root, "src")): Array<[string, string]> { + const out: Array<[string, string]> = []; + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const full = path.join(directory, entry.name); + if (entry.isDirectory()) { + out.push(...sourceFiles(full)); + } else if (entry.name.endsWith(".ts")) { + out.push([path.relative(root, full), readFileSync(full, "utf8")]); + } + } + return out; +} + test("shows editor title buttons only inside mcpp projects", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + // Both conditions matter: a project must be open, and the user must not have + // turned the buttons off with `mcpp.task.editorTitleButtons`. assert.deepEqual(manifest.contributes?.menus?.["editor/title"], [ - { command: "mcpp.run", group: "navigation@1", when: "mcpp.inProject" }, - { command: "mcpp.test", group: "navigation@2", when: "mcpp.inProject" }, + { command: "mcpp.run", group: "navigation@1", when: "mcpp.inProject && mcpp.editorTitleButtons" }, + { command: "mcpp.test", group: "navigation@2", when: "mcpp.inProject && mcpp.editorTitleButtons" }, ]); const commands = manifest.contributes?.commands ?? []; @@ -220,7 +455,7 @@ test("ships an injection grammar with module-specific scopes", () => { }); test("设置全局默认后先释放工具链锁再提供立即构建", () => { - const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const source = readFileSync(path.join(root, "src/cli/controller.ts"), "utf8"); const start = source.indexOf("private async selectDefaultToolchainFromInventory"); const end = source.indexOf("private async pickInstallSpec", start); assert.notEqual(start, -1); @@ -233,7 +468,7 @@ test("设置全局默认后先释放工具链锁再提供立即构建", () => { }); test("项目任务结束后先释放项目锁再刷新 C++ 模块语言服务", () => { - const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const source = readFileSync(path.join(root, "src/cli/controller.ts"), "utf8"); const start = source.indexOf("public async runProjectTask"); const end = source.indexOf("public async showToolchains", start); assert.notEqual(start, -1); @@ -246,7 +481,7 @@ test("项目任务结束后先释放项目锁再刷新 C++ 模块语言服务", }); test("安装流程把系统工具链和 target 兼容 spec 交给 mcpp 解析", () => { - const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const source = readFileSync(path.join(root, "src/cli/controller.ts"), "utf8"); const start = source.indexOf("public async installToolchain"); const end = source.indexOf("public async selectDefaultToolchain", start); assert.notEqual(start, -1); @@ -261,16 +496,16 @@ test("安装流程把系统工具链和 target 兼容 spec 交给 mcpp 解析", }); test("泛化 triple 工具链由 mcpp 最终校验", () => { - const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const source = readFileSync(path.join(root, "src/cli/controller.ts"), "utf8"); const start = source.indexOf("public async installToolchain"); const end = source.indexOf("public async selectDefaultToolchain", start); const method = source.slice(start, end); - assert.match(method, /可能携带 target 语义.*最终由 mcpp 校验/s); + assert.match(method, /may carry target semantics.*mcpp validates it in the end/s); }); test("新建工程先校验目标路径再确认创建,成功后只打开不构建", () => { - const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const source = readFileSync(path.join(root, "src/cli/controller.ts"), "utf8"); const start = source.indexOf("public async newProject"); const end = source.indexOf("private guarded", start); assert.notEqual(start, -1); @@ -292,7 +527,7 @@ test("新建工程先校验目标路径再确认创建,成功后只打开不 }); test("新建工程契约是创建并打开,不自动构建", () => { - const controller = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const controller = readFileSync(path.join(root, "src/cli/controller.ts"), "utf8"); const extension = readFileSync(path.join(root, "src/extension.ts"), "utf8"); assert.doesNotMatch(controller, /globalState|PENDING_NEW_PROJECT/); assert.doesNotMatch(extension, /PENDING_NEW_PROJECT/); @@ -300,7 +535,7 @@ test("新建工程契约是创建并打开,不自动构建", () => { test("声明 GitHub 仓库和扩展图标", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.icon, "images/logo.png"); + assert.equal(manifest.icon, "media/logo.png"); assert.equal(manifest.repository?.url, "https://github.com/mcpp-community/mcpp-vscode.git"); assert.equal(manifest.homepage, "https://github.com/mcpp-community/mcpp-vscode#readme"); assert.equal(manifest.bugs?.url, "https://github.com/mcpp-community/mcpp-vscode/issues"); @@ -309,24 +544,62 @@ test("声明 GitHub 仓库和扩展图标", () => { assert.deepEqual([...icon.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); }); -test("README 说明 mcpp 与 mcppls 的职责边界和升级限制", () => { +test("the activity bar gets the stencil, not the marketplace badge", () => { + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + const container = manifest.contributes?.viewsContainers?.activitybar?.[0]; + // VS Code masks a container icon with the theme's foreground colour + // (`mask: url(icon)` at 24 px), so what is painted is the icon's *alpha + // channel*. The official badge is an opaque rounded square with the wordmark + // on it: as a stencil that is a solid block, which is what shipped. The + // marketplace icon keeps the real logo; the activity bar gets its derived + // stencil, and `npm run check:icon` proves the derivation is current. + assert.equal(manifest.icon, "media/logo.png"); + assert.equal(container?.icon, "media/activity-bar.png"); + const stencil = readFileSync(path.join(root, "media", "activity-bar.png")); + assert.deepEqual([...stencil.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); + // Read from IHDR: 8-bit RGBA, and wider than tall, because the wordmark is. + assert.equal(stencil.readUInt32BE(16), 96); + assert.equal(stencil.readUInt32BE(20), 56); +}); + +test("the READMEs state the responsibility split, the boundary and the limits", () => { + // The English README is the Marketplace listing; the Chinese one is its mirror. const readme = readFileSync(path.join(root, "README.md"), "utf8"); - assert.match(readme, /sunrisepeak\.mcpp-language-server/); - assert.match(readme, /不启动第二个\s+LSP 客户端/); - assert.match(readme, /不表示本扩展读取用户的 `clangd\.\*` 设置/); - assert.match(readme, /mcpp\.path.*只控制/s); - assert.match(readme, /darwin-x64/); - assert.match(readme, /不再读取或写入它们/); - assert.doesNotMatch(readme, /mcpp\.clangd\.path.*匹配 LLVM/); - assert.doesNotMatch(readme, /当前完整的模块语义能力只支持 LLVM/); + const chinese = readFileSync(path.join(root, "README.zh-CN.md"), "utf8"); + + for (const text of [readme, chinese]) { + assert.match(text, /sunrisepeak\.mcpp-language-server/); + assert.match(text, /darwin-x64/); + assert.match(text, /mcpp\.path/); + } + assert.match(readme, /README\.zh-CN\.md/); + assert.match(chinese, /README\.md/); + + // The claims that must not creep back in: this extension is not a language + // client, does not read clangd settings, and does not claim LLVM-only support. + for (const text of [readme, chinese]) { + assert.doesNotMatch(text, /starts? (a|its own) (second )?LSP client/i); + assert.doesNotMatch(text, /reads your `clangd\./); + } + assert.doesNotMatch(readme, /only LLVM/i); }); test("嵌套工程提示不猜测它一定是 mcpp 工作区成员", () => { - const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + const source = readFileSync(path.join(root, "src/cli/controller.ts"), "utf8"); assert.doesNotMatch(source, /isWorkspaceMember/); assert.doesNotMatch(source, /当前是工作区成员/); }); +test("the rename offer is only marked answered after the user answered it", () => { + const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); + const body = /async function offerSettingRenames[\s\S]*?\n\}/.exec(source)?.[0] ?? ""; + assert.ok(body.length > 0, "offerSettingRenames must exist"); + assert.ok( + body.indexOf("showInformationMessage") < body.indexOf('workspaceState.update("mcpp.renamesOffered", true)'), + "the marker is written before the prompt again: a dismissed popup would strand the user's renamed settings forever (external review P2-9)", + ); +}); + test("tag release 工作流校验版本并发布 VSIX", () => { const workflow = readFileSync(path.join(root, ".github/workflows/release.yml"), "utf8"); assert.match(workflow, /push:\s*\n\s+tags:\s*\n\s+- "v\*"/); @@ -340,18 +613,68 @@ test("tag release 工作流校验版本并发布 VSIX", () => { assert.match(workflow, /gh release upload.*--clobber/s); }); -test("PR CI 分离单元打包和 Extension Host E2E", () => { +test("the release workflow guards the published artefact against local leakage", () => { + const workflow = readFileSync(path.join(root, ".github/workflows/release.yml"), "utf8"); + assert.match(workflow, /校验 VSIX 体积与内容/); + assert.match(workflow, /forbidden in \.dev-profile\/ \.agents\/ test\/ tools\/ src\/ node_modules\//); + assert.match(workflow, /unzip -t/); +}); + +test("tag release 同时发布两个市场:Open VSX 必发,Marketplace 未配 token 则跳过", () => { + const workflow = readFileSync(path.join(root, ".github/workflows/release.yml"), "utf8"); + // 两个市场收到的都是 GitHub Release 上的那只 VSIX,不重新打包。Open VSX 的 + // secret 名就是仓库实际配置的 OPENVSX_TOKEN(曾写成 OVSX_PAT,首版必然失败)。 + assert.match(workflow, /npx ovsx publish "mcpp-vscode-\$\{PACKAGE_VERSION\}\.vsix" --pat "\$OPENVSX_TOKEN"/); + assert.match(workflow, /npx vsce publish --packagePath "mcpp-vscode-\$\{PACKAGE_VERSION\}\.vsix"/); + // Open VSX 是必发目标:缺 token 显式失败。 + assert.match(workflow, /OPENVSX_TOKEN: \$\{\{ secrets\.OPENVSX_TOKEN \}\}/); + assert.match(workflow, /if \[ -z "\$\{OPENVSX_TOKEN:-\}" \]; then[\s\S]*?exit 1/); + // Marketplace 机会发布:缺 token 打一行说明就过,Release 不失败。 + assert.match(workflow, /VSCE_PAT: \$\{\{ secrets\.VSCE_PAT \}\}/); + assert.match(workflow, /if \[ -z "\$\{VSCE_PAT:-\}" \]; then[\s\S]*?exit 0/); + // 发布用的是仓库锁定的 ovsx:npx 不在发布中途去网络解析。 + const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; + assert.ok(manifest.devDependencies?.ovsx, "ovsx must be a devDependency the release resolves offline"); +}); + +test("CI runs the gates, the package checks, the drift check and every e2e variant", () => { const workflow = readFileSync(path.join(root, ".github/workflows/ci.yml"), "utf8"); assert.match(workflow, /pull_request:/); assert.match(workflow, /push:\s*\n\s+branches:\s*\n\s+- main/); assert.match(workflow, /permissions:\s*\n\s+contents: read/); assert.match(workflow, /concurrency:[\s\S]*cancel-in-progress: true/); - assert.match(workflow, /node-version: 22/); - assert.match(workflow, /unit-and-package:/); + assert.match(workflow, /node-version: \$\{\{ env.NODE_VERSION \}\}/); + assert.match(workflow, /NODE_VERSION: 22/); + // Cross-platform confidence: the unit gates, the Extension Host e2e and the + // isolated install all run the same three-OS matrix — Linux, macOS ARM64 and + // Windows, where the spawn and path handling actually differ. + const osMatrixes = workflow.match(/os: \[ubuntu-latest, macos-14, windows-latest\]/g) ?? []; + assert.equal( + osMatrixes.length, + 3, + "gates, extension-host-e2e and isolated-install must all run on Linux, macOS and Windows", + ); + assert.match(workflow, /gates:/); assert.match(workflow, /extension-host-e2e:/); + assert.match(workflow, /package:/); assert.match(workflow, /npm ci/); assert.match(workflow, /npm test/); assert.match(workflow, /npm run package/); assert.match(workflow, /unzip -t/); - assert.match(workflow, /xvfb-run -a npm run test:e2e/); + // A local dev profile once leaked into the VSIX (64 MB); the guard is now explicit. + assert.match(workflow, /The VSIX stays small and free of local state/); + assert.match(workflow, /forbidden in \.dev-profile\/ \.agents\/ test\/ tools\/ src\/ node_modules\//); + assert.match(workflow, /xvfb-run -a npm run test:e2e:one/); + // The snapshot drift gate only means something with an mcpp checkout present. + assert.match(workflow, /generated-drift:/); + assert.match(workflow, /repository: mcpp-community\/mcpp/); + assert.match(workflow, /MCPP_REPO: \$\{\{ github.workspace \}\}\/\.mcpp-source/); + // The end-to-end dependency resolution check. + assert.match(workflow, /isolated-install:/); + assert.match(workflow, /sunrisepeak.mcpp-language-server@/); + // The ubuntu runner ships no `code` on PATH, so the job downloads the CLI via + // the e2e tooling instead. A step that calls bare `code` again is the exact + // `code: command not found` failure this gate was added after. + assert.match(workflow, /tools\/ci-vscode-cli\.mjs/); + assert.doesNotMatch(workflow, /^\s+code --/m); }); diff --git a/test/buildscript/analysis.test.ts b/test/buildscript/analysis.test.ts new file mode 100644 index 0000000..ea3cb4b --- /dev/null +++ b/test/buildscript/analysis.test.ts @@ -0,0 +1,199 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { DIAGNOSTIC_CODES, analyseBuildScript } from "../../src/buildscript/analysis"; + +function codes(lines: readonly string[]): string[] { + return analyseBuildScript(lines, "warning").map((entry) => entry.code); +} + +test("a clean, realistic build script earns nothing", () => { + const lines = [ + "import std;", + "import mcpp;", + "", + "int main() {", + ' const std::string out = std::string(mcpp::out_dir()) + "/foo.pb.cc";', + ' mcpp::cxxflag("-DHAVE_BANNER=1");', + ' mcpp::link_lib("m");', + ' mcpp::link_search("vendor/lib");', + ' mcpp::link_flag("-Wl,--version-script=gen.map");', + ' mcpp::define("HAVE_FEATURE");', + ' mcpp::generated("src/gen.cpp");', + ' mcpp::include_dir("vendor/include");', + ' mcpp::runtime_search_dir("lib");', + ' mcpp::rerun_if_changed("config.h");', + ' mcpp::rerun_if_env_changed("USE_FAST");', + " mcpp::action a;", + ' a.id = "protoc:foo";', + " a.role = mcpp::roles::source;", + ' a.arg(mcpp::dep_bin("protobuf", "protoc"))', + ' .arg("--cpp_out=gen")', + ' .output("gen/foo.pb.cc")', + " .submit();", + " return 0;", + "}", + ]; + assert.deepEqual(analyseBuildScript(lines, "warning"), []); +}); + +test("rule 1 flags an unknown mcpp:: name and only that", () => { + assert.deepEqual(codes(["int main() {", ' mcpp::cxxflagk("-O2");', "}"]), [ + DIAGNOSTIC_CODES.unknownSymbol, + ]); + assert.deepEqual( + codes([ + ' mcpp::cxxflag("-O2");', + " mcpp::roles::prepare;", + ' mcpp::warning("x");', + ' mcpp::xpkg_source("a", "b");', + ' mcpp::toolchain("k", "v");', + ' mcpp::has_feature("f");', + ' mcpp::pack_format("dir");', + ' mcpp::windows_subsystem("app", "windows");', + ]), + [], + ); +}); + +test("rule 1 ignores mcpp:: inside strings and comments", () => { + assert.deepEqual( + codes([ + ' const char* s = "mcpp::bogus";', + " // mcpp::bogus();", + " /* mcpp::bogus(); */", + " const char* t = R\"(mcpp::bogus;)\";", + ]), + [], + ); +}); + +test("rule 1 reports 1-based line and column, at the requested severity", () => { + const diagnostics = analyseBuildScript(["mcpp::bogus();"], "info"); + assert.equal(diagnostics.length, 1); + assert.deepEqual( + { + code: diagnostics[0].code, + line: diagnostics[0].line, + startCharacter: diagnostics[0].startCharacter, + endCharacter: diagnostics[0].endCharacter, + severity: diagnostics[0].severity, + }, + { + code: DIAGNOSTIC_CODES.unknownSymbol, + line: 1, + startCharacter: 1, + endCharacter: 12, + severity: "info", + }, + ); +}); + +test("rule 2 flags a role spelled as a string literal", () => { + assert.deepEqual(codes(["mcpp::action a;", 'a.role = "source";', "a.submit();"]), [ + DIAGNOSTIC_CODES.roleLiteral, + ]); + assert.deepEqual(codes(["mcpp::action a;", "a.role = mcpp::roles::source;", "a.submit();"]), []); + // The frozen printf surface is allowed to spell the string (docs/30). + assert.deepEqual(codes(['printf("mcpp:action={\\"role\\":\\"source\\"}");']), []); +}); + +test("rule 3 flags a prepare action with no output_dir", () => { + assert.deepEqual( + codes([ + "mcpp::action a;", + "a.role = mcpp::roles::prepare;", + 'a.arg("install");', + "a.submit();", + ]), + [DIAGNOSTIC_CODES.prepareNeedsOutputDir], + ); + assert.deepEqual( + codes([ + "mcpp::action a;", + "a.role = mcpp::roles::prepare;", + "a.output_dir(prefix.c_str());", + "a.submit();", + ]), + [], + ); + // The raw payload carries the same obligation. + assert.deepEqual(codes(['printf("mcpp:action={\\"role\\":\\"prepare\\"}");']), [ + DIAGNOSTIC_CODES.prepareNeedsOutputDir, + ]); +}); + +test("rule 4 flags a run path spelled in link_flag", () => { + assert.deepEqual(codes(['mcpp::link_flag("-Wl,-rpath,$ORIGIN/../lib");']), [ + DIAGNOSTIC_CODES.rpathInLinkFlag, + ]); + assert.deepEqual( + codes([ + 'mcpp::link_flag("-Wl,--version-script=x.map");', + 'mcpp::runtime_search_dir("lib");', + ]), + [], + ); +}); + +test("rule 5 flags -I and -L spelled in raw flags", () => { + assert.deepEqual(codes(['mcpp::cxxflag("-Ivendor/include");']), [DIAGNOSTIC_CODES.rawPathFlag]); + assert.deepEqual(codes(['mcpp::link_flag("-Lvendor/lib");']), [DIAGNOSTIC_CODES.rawPathFlag]); + assert.deepEqual( + codes(['mcpp::include_dir("vendor/include");', 'mcpp::link_search("vendor/lib");']), + [], + ); +}); + +test("rule 6 flags shell syntax in an action command", () => { + assert.deepEqual( + codes([ + "mcpp::action a;", + "a.role = mcpp::roles::source;", + 'a.arg("CFLAGS=-O2").arg("make");', + "a.submit();", + ]), + [DIAGNOSTIC_CODES.shellSyntaxInAction], + ); + assert.deepEqual( + codes([ + "mcpp::action a;", + "a.role = mcpp::roles::source;", + 'a.arg("cd tools && make");', + "a.submit();", + ]), + [DIAGNOSTIC_CODES.shellSyntaxInAction], + ); + assert.deepEqual( + codes([ + "mcpp::action a;", + "a.role = mcpp::roles::source;", + 'a.env("GEN_MODE", "release").cwd("tools");', + 'a.arg("--out=build");', + "a.submit();", + ]), + [], + ); +}); + +test("rule 7 flags a missing or unknown action role", () => { + assert.deepEqual(codes(["mcpp::action a;", 'a.arg("gen");', "a.submit();"]), [ + DIAGNOSTIC_CODES.actionNeedsRole, + ]); + assert.deepEqual(codes(["mcpp::action a;", 'a.role = "banana";', "a.submit();"]), [ + DIAGNOSTIC_CODES.actionNeedsRole, + ]); + assert.deepEqual(codes(["mcpp::action a;", "a.role = mcpp::roles::object;", "a.submit();"]), []); +}); + +test("each rule id is the SPEC-007 code the plan names", () => { + assert.deepEqual(Object.values(DIAGNOSTIC_CODES), [ + "mcpp.buildscript.unknownSymbol", + "mcpp.buildscript.roleLiteral", + "mcpp.buildscript.prepareNeedsOutputDir", + "mcpp.buildscript.rpathInLinkFlag", + "mcpp.buildscript.rawPathFlag", + "mcpp.buildscript.shellSyntaxInAction", + "mcpp.buildscript.actionNeedsRole", + ]); +}); diff --git a/test/buildscript/api.test.ts b/test/buildscript/api.test.ts new file mode 100644 index 0000000..efab84d --- /dev/null +++ b/test/buildscript/api.test.ts @@ -0,0 +1,113 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +import apiJson from "../../data/buildscript-api.json"; +import { + ACTION_ROLES, + API, + apiProblems, + apiSource, + directive, + directives, +} from "../../src/buildscript/api"; + +// Compiled layout: /dist/test/buildscript/*.js. +const repositoryRoot = path.resolve(__dirname, "..", "..", ".."); + +/** The mcpp checkout to compare against, when one is reachable. */ +function findMcppRepo(): string | undefined { + const candidates = [ + process.env.MCPP_REPO, + path.resolve(process.cwd(), "..", "mcpp"), + path.resolve(repositoryRoot, "..", "mcpp"), + ].filter((candidate): candidate is string => typeof candidate === "string"); + return candidates.find((candidate) => + fs.existsSync(path.join(candidate, "modules", "buildmcpp", "src", "directives.cppm")), + ); +} + +const mcppRepo = findMcppRepo(); +const skipReason = + mcppRepo === undefined + ? "no mcpp checkout found (set MCPP_REPO to compare against the source)" + : false; + +test("the snapshot loads with a usable shape", () => { + assert.equal(typeof API.sourceVersion, "string"); + assert.ok(API.sourceVersion.length > 0); + assert.equal(typeof API.sourceCommit, "string"); + assert.ok(API.protocolVersion >= 1); + assert.ok(API.cacheEpoch >= 1); + assert.equal(API.directives.length, 31); + assert.equal(directives().length, apiJson.directives.length); +}); + +test("the snapshot's own invariants hold", () => { + assert.deepEqual(apiProblems(), []); +}); + +test("every role the engine knows is present", () => { + assert.equal(API.roles.length, ACTION_ROLES.length); + for (const role of ACTION_ROLES) { + assert.ok(API.roles.includes(role), `missing role ${role}`); + } +}); + +test("directive() resolves wire names and only wire names", () => { + const cxxflag = directive("cxxflag"); + assert.ok(cxxflag); + assert.equal(cxxflag.slot, "CxxFlags"); + assert.equal(cxxflag.scope, "PackagePrivate"); + assert.equal(cxxflag.transform, "Verbatim"); + assert.equal(cxxflag.mustExist, false); + assert.ok(cxxflag.since >= 1); + assert.ok(Array.isArray(cxxflag.rules)); + assert.equal(directive("cxx_flag"), undefined); + assert.equal(directive("no-such-directive"), undefined); +}); + +test("apiSource() reports the generated provenance", () => { + assert.deepEqual(apiSource(), { + version: API.sourceVersion, + commit: API.sourceCommit, + }); +}); + +test("provisions carry the kind and the wire name", () => { + assert.ok(API.provisions.length >= 3); + const tool = API.provisions.find((entry) => entry.wire === "tool"); + assert.ok(tool); + assert.equal(tool.kind, "tool"); + for (const entry of API.provisions) { + assert.equal(typeof entry.kind, "string"); + assert.ok(entry.wire.length > 0); + } +}); + +test( + "the table size and every wire name match the mcpp source", + { skip: skipReason }, + () => { + assert.ok(mcppRepo); + const source = fs.readFileSync( + path.join(mcppRepo, "modules", "buildmcpp", "src", "directives.cppm"), + "utf8", + ); + const declared = /inline constexpr\s+std::array\s+kTable\{\{/.exec(source); + assert.ok(declared, "the declared kTable size is missing"); + + const open = source.indexOf("kTable{{"); + const body = source.slice(open, source.indexOf("}};", open)); + const rows = body + .split(/\r?\n/) + .filter((line) => /Slot::\w+\s*,\s*Scope::\w+\s*,\s*Transform::\w+/.test(line)); + + assert.equal(Number(declared[1]), directives().length); + assert.equal(rows.length, directives().length); + for (const entry of directives()) { + assert.ok(body.includes(`"${entry.wire}"`), `source has no row for ${entry.wire}`); + } + }, +); diff --git a/test/buildscript/modules.test.ts b/test/buildscript/modules.test.ts new file mode 100644 index 0000000..0df8cff --- /dev/null +++ b/test/buildscript/modules.test.ts @@ -0,0 +1,118 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + isKnownModule, + knownModule, + knownModules, + scanImports, +} from "../../src/buildscript/modules"; + +test("scans plain, exported and dotted imports", () => { + const sites = scanImports([ + "import std;", + "export import mcpp;", + "import mcpp.plugins.tool; // trailing comment", + ]); + assert.deepEqual( + sites.map((site) => site.module), + ["std", "mcpp", "mcpp.plugins.tool"], + ); + assert.deepEqual( + sites.map((site) => site.line), + [0, 1, 2], + ); +}); + +test("reports the module name's own span, not the statement's", () => { + const [site] = scanImports(["import mcpp.core; "]); + assert.equal(site.module, "mcpp.core"); + assert.equal(site.startCharacter, 9); + assert.equal(site.endCharacter, 18); +}); + +test("finds two imports on one line", () => { + const sites = scanImports(["import a; import b.c;"]); + assert.deepEqual( + sites.map((site) => site.module), + ["a", "b.c"], + ); +}); + +test("ignores import inside strings, line comments and block comments", () => { + assert.deepEqual( + scanImports([ + "// import fake;", + 'const char* s = "import fake;";', + "/* import fake; */", + "/*", + "import fake;", + "*/", + 'const char* t = R"(import fake;)";', + "important(); // import fake;", + ]), + [], + ); +}); + +test("ignores header units, header imports and partitions", () => { + assert.deepEqual(scanImports(['import ;', 'import "vector.h";', "import :part;"]), []); +}); + +test("a comment after a real import does not hide it", () => { + const sites = scanImports(["import std; // import fake;"]); + assert.deepEqual( + sites.map((site) => site.module), + ["std"], + ); +}); + +test("recognises the standard library, the engine and mcpp plugins", () => { + for (const name of [ + "std", + "std.compat", + "mcpp", + "mcpp.core", + "mcpp.plugins.tool", + "mcpp.plugins.a.b", + "mcpp.deps.vcpkg", + "mcpp.rules.qt", + ]) { + assert.equal(isKnownModule(name), true, `${name} should be known`); + } +}); + +test("does not claim names that are not modules", () => { + for (const name of [ + "mcpp.toml", + "mcppx", + "stdlib", + "std.compat.x", + "mcpp.plugins", + "mcpp.plugins.", + "mcpp.core.x", + "", + ]) { + assert.equal(isKnownModule(name), false, `${name} should not be known`); + } +}); + +test("knownModule() describes a wildcard match with the queried name", () => { + const plugin = knownModule("mcpp.plugins.tool"); + assert.ok(plugin); + assert.equal(plugin.name, "mcpp.plugins.tool"); + assert.ok(plugin.description.length > 0); + assert.ok(plugin.docsUrl?.startsWith("https://github.com/mcpp-community/mcpp")); + assert.equal(knownModule("mcpp.toml"), undefined); + assert.equal(knownModule("nope"), undefined); +}); + +test("knownModules() lists the exact names a build program can import", () => { + const names = knownModules().map((entry) => entry.name); + for (const name of ["std", "std.compat", "mcpp", "mcpp.core", "mcpp.plugins.*"]) { + assert.ok(names.includes(name), `missing ${name}`); + } + for (const entry of knownModules()) { + assert.ok(entry.description.length > 0, `${entry.name} has no description`); + } +}); diff --git a/test/buildscript/settings.test.ts b/test/buildscript/settings.test.ts new file mode 100644 index 0000000..6d122a3 --- /dev/null +++ b/test/buildscript/settings.test.ts @@ -0,0 +1,136 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { analyseBuildScript } from "../../src/buildscript/analysis"; +import { isKnownModule } from "../../src/buildscript/modules"; +import { buildScriptContributions } from "../../src/buildscript/settings"; + +/** + * The four boolean keys `settings.ts` resolves, with the registry's defaults. + * A stub reader records what was asked for, so a wiring change that stops + * reading a key fails the first test instead of silently keeping a default. + */ +const DEFAULTS: Readonly> = { + "mcpp.buildScript.intelligence": true, + "mcpp.buildScript.diagnostics": true, + "mcpp.buildScript.snippets": true, + "mcpp.buildScript.imports.knownModules": true, +}; + +function reader(overrides: Record = {}): { + read: (key: string) => T; + keys: string[]; +} { + const keys: string[] = []; + const values: Record = { ...DEFAULTS, ...overrides }; + return { + keys, + read: (key: string): T => { + keys.push(key); + return values[key] as T; + }, + }; +} + +test("all four switches are resolved, each from its own registry key", () => { + const { read, keys } = reader(); + const contributions = buildScriptContributions(read, () => "warning"); + assert.deepEqual(contributions, { + intelligence: true, + diagnosticSeverity: "warning", + snippets: true, + knownModules: true, + }); + // The four literal keys matter: `test/config/wiring.test.ts` scans the sources + // for exactly this accessor shape to prove each declared setting is read. + assert.deepEqual(keys.sort(), [ + "mcpp.buildScript.diagnostics", + "mcpp.buildScript.imports.knownModules", + "mcpp.buildScript.intelligence", + "mcpp.buildScript.snippets", + ]); +}); + +test("mcpp.buildScript.snippets=false drops snippets and nothing else", () => { + const contributions = buildScriptContributions( + reader({ "mcpp.buildScript.snippets": false }).read, + () => "warning", + ); + assert.equal(contributions.snippets, false); + assert.equal(contributions.intelligence, true, "snippets must not turn the whole layer off"); + assert.equal(contributions.diagnosticSeverity, "warning", "diagnostics must stay"); + assert.equal(contributions.knownModules, true, "import completion must stay"); +}); + +test("mcpp.buildScript.imports.knownModules=false drops only the completion list", () => { + const contributions = buildScriptContributions( + reader({ "mcpp.buildScript.imports.knownModules": false }).read, + () => "warning", + ); + assert.equal(contributions.knownModules, false); + assert.equal(contributions.intelligence, true); + assert.equal(contributions.snippets, true); + assert.equal(contributions.diagnosticSeverity, "warning"); +}); + +test("mcpp.buildScript.diagnostics=false stops diagnostics but keeps completion and hover", () => { + const contributions = buildScriptContributions( + reader({ "mcpp.buildScript.diagnostics": false }).read, + () => "warning", + ); + assert.equal(contributions.diagnosticSeverity, undefined); + assert.equal(contributions.intelligence, true); + assert.equal(contributions.snippets, true); + assert.equal(contributions.knownModules, true); +}); + +test("mcpp.buildScript.intelligence=false leaves no build.mcpp intelligence at all", () => { + const contributions = buildScriptContributions( + reader({ "mcpp.buildScript.intelligence": false }).read, + () => "off", + ); + assert.equal(contributions.intelligence, false); + assert.equal(contributions.diagnosticSeverity, undefined); + assert.equal(contributions.snippets, false); + assert.equal(contributions.knownModules, false); +}); + +test("a severity of off publishes nothing, and a valid severity is passed through", () => { + const off = buildScriptContributions(reader().read, () => "off"); + assert.equal(off.diagnosticSeverity, undefined); + assert.equal(off.intelligence, true, "severity=off must not disable completion or hover"); + + const info = buildScriptContributions(reader().read, () => "info"); + assert.equal(info.diagnosticSeverity, "info"); +}); + +test("known modules stay recognised and are never reported missing with every switch off", () => { + const { read } = reader({ + "mcpp.buildScript.intelligence": false, + "mcpp.buildScript.diagnostics": false, + "mcpp.buildScript.snippets": false, + "mcpp.buildScript.imports.knownModules": false, + }); + const contributions = buildScriptContributions(read, () => "off"); + assert.equal(contributions.diagnosticSeverity, undefined, "no diagnostics with everything off"); + + // §3.2.1: mcpp keeps `build.mcpp` out of the compilation database, so this + // extension can never validate an import. The "never report std/std.compat/ + // mcpp.* as missing" rule is therefore absolute and lives in the analyser and + // the recognition table, not behind `mcpp.buildScript.imports.knownModules` + // (that key removes the *completion list* only). The two `mcpp.toml. + // indexCompletion*` switches act on a different language and cannot reach + // this path, so "every switch off" here is the five `mcpp.buildScript.*` keys. + const lines = [ + "import std;", + "import std.compat;", + "import mcpp;", + "import mcpp.core;", + "import mcpp.plugins.tool;", + "import mcpp.deps.something;", + ]; + assert.deepEqual(analyseBuildScript(lines, "warning"), []); + for (const name of ["std", "std.compat", "mcpp", "mcpp.core", "mcpp.plugins.tool"]) { + assert.equal(isKnownModule(name), true, `${name} stopped being recognised`); + } +}); diff --git a/test/buildscript/snippets.test.ts b/test/buildscript/snippets.test.ts new file mode 100644 index 0000000..d99bbcf --- /dev/null +++ b/test/buildscript/snippets.test.ts @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { ACTION_ROLES, API } from "../../src/buildscript/api"; +import { + actionSnippets, + buildScriptSnippets, + directiveCallSnippets, + snippetsForScope, +} from "../../src/buildscript/snippets"; + +test("every snapshot directive but action has exactly one call snippet", () => { + const expected = new Set( + API.directives.map((entry) => entry.wire.replace(/-/g, "_")).filter((name) => name !== "action"), + ); + const snippets = directiveCallSnippets(); + assert.equal(snippets.length, expected.size); + assert.deepEqual( + [...new Set(snippets.map((snippet) => snippet.filterText))].sort(), + [...expected].sort(), + ); + // A declaration is not a call: `mcpp::action` must not get an `action(...)` stub. + assert.ok(!snippets.some((snippet) => snippet.filterText === "action")); +}); + +test("a directive snippet is the typed spelling with one editable argument", () => { + const snippet = directiveCallSnippets().find((entry) => entry.filterText === "link_script"); + assert.ok(snippet !== undefined, "link_script disappeared from the snippet list"); + assert.equal(snippet.label, "link_script(...)"); + assert.equal(snippet.insertText, 'link_script("${1:value}")'); + assert.match(snippet.detail, /mcpp:link-script=/); +}); + +test("the action snippets carry a role, and the prepare one carries output_dir", () => { + const snippets = actionSnippets(); + assert.equal(snippets.length, 2); + for (const snippet of snippets) { + assert.equal(snippet.filterText, "action"); + assert.match(snippet.insertText, /\.submit\(\);/); + assert.match(snippet.insertText, /mcpp::roles::/); + } + const prepare = snippets.find((snippet) => snippet.label.includes("prepare")); + assert.ok(prepare !== undefined); + assert.match(prepare.insertText, /mcpp::roles::prepare/); + assert.match(prepare.insertText, /\.output_dir\("/); + assert.match(prepare.detail, /R3\.3/); + + const general = snippets.find((snippet) => snippet.label.includes("typed")); + assert.ok(general !== undefined); + // The role choice lists the engine's five roles. + for (const role of ACTION_ROLES) { + assert.ok(general.insertText.includes(role), `${role} is missing from the role choice`); + } +}); + +test("snippetsForScope narrows on the typed mcpp:: prefix", () => { + const all = buildScriptSnippets(); + assert.equal(snippetsForScope("").length, all.length); + const link = snippetsForScope("link_"); + assert.ok(link.length > 0); + assert.ok(link.every((snippet) => snippet.filterText.startsWith("link_"))); + assert.ok(!link.some((snippet) => snippet.filterText === "action")); + assert.equal(snippetsForScope("action").length, actionSnippets().length); + assert.deepEqual(snippetsForScope("zzz_not_a_directive"), []); +}); + +test("every snippet is insertable: it has a placeholder and a non-empty body", () => { + for (const snippet of buildScriptSnippets()) { + assert.ok(snippet.insertText.length > 0, `${snippet.label} has no insert text`); + assert.match(snippet.insertText, /\$\{1[:|]/, `${snippet.label} has no first placeholder`); + assert.ok(snippet.label.length > 0); + assert.ok(snippet.detail.length > 0); + } +}); diff --git a/test/cache/cacheCss.test.ts b/test/cache/cacheCss.test.ts new file mode 100644 index 0000000..1c7c97f --- /dev/null +++ b/test/cache/cacheCss.test.ts @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +/** + * The cache view's stylesheet, checked as text. + * + * Geometry cannot be measured outside a browser, but the properties §8.1 + * actually asks for can be: theme tokens only, tabular figures, the 26 / 12 / 11 + * scale, one 6 px primary bar, a legend that flows as one line, and a narrow-width + * answer. These assertions are what stops the next edit from quietly turning the + * sidebar back into three 14 px bars and a card per kind. + */ + +const css = readFileSync(path.join(process.cwd(), "media", "cache.css"), "utf8"); + +/** Every declaration that can carry a colour. */ +const COLOUR_DECLARATION = + /(?:^|[;{\s])(border-bottom|border-top|border-left|border-right|border-color|border|background-color|background|box-shadow|outline-color|outline|text-decoration-color|text-decoration|fill|stroke|color)\s*:\s*([^;}]+)/gi; + +/** Values that are not a colour at all, so they cannot be a hard-coded one. */ +const NOT_A_COLOUR = new Set([ + "transparent", + "currentColor", + "inherit", + "initial", + "unset", + "none", + "solid", + "dashed", + "dotted", + "double", + "underline", + "line-through", + "overline", + "wavy", + "0", + "1px", + "2px", + "3px", + "1", +]); + +test("every colour comes from a --vscode-* token", () => { + const offenders: string[] = []; + for (const match of css.matchAll(COLOUR_DECLARATION)) { + const property = match[1]; + const value = match[2].trim(); + // Strip the theme references: what is left must be geometry or a keyword. + const rest = value.replace(/var\([^)]*\)/g, " "); + for (const token of rest.split(/[\s,()]+/).filter((part) => part.length > 0)) { + if (NOT_A_COLOUR.has(token)) continue; + if (/^[\d.]+(px|em|rem|%|fr|deg)?$/.test(token)) continue; + offenders.push(`${property}: ${value} (offending token "${token}")`); + } + } + assert.deepEqual(offenders, [], `these declarations carry a literal colour:\n ${offenders.join("\n ")}`); +}); + +test("and no colour is hidden in a hex or function form either", () => { + assert.doesNotMatch(css, /#[0-9a-f]{3,8}\b/i, "a hex colour literal"); + assert.doesNotMatch(css, /\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color-mix|light-dark)\(/i, "a functional colour"); +}); + +test("the type scale is §8.1's 26 / 12 / 11 and the figures are tabular", () => { + assert.match(css, /font-variant-numeric:\s*tabular-nums/); + assert.match(css, /\.metric-value\s*\{[^}]*font-size:\s*26px/); + assert.match(css, /font-size:\s*12px/); + assert.match(css, /font-size:\s*11px/); +}); + +test("one 14 px composition bar and 6 px everywhere else", () => { + assert.match(css, /\.viz-bar\s*\{[^}]*height:\s*14px/); + assert.match(css, /\.viz-bar-thin\s*\{[^}]*height:\s*6px/); + // No third bar height sneaks in. + const heights = [...css.matchAll(/height:\s*(\d+)px/g)].map((match) => Number(match[1])); + assert.deepEqual([...new Set(heights)].sort((a, b) => a - b), [1, 6, 8, 14]); +}); + +test("the legend flows as one wrapped line separated by ·", () => { + assert.match(css, /\.legend-inline\s*\{[^}]*display:\s*block/); + assert.match(css, /\.legend-inline\s+\.legend-item\s*\{[^}]*display:\s*inline/); + assert.match(css, /content:\s*" · "/); + assert.doesNotMatch(css, /\.legend[^{]*\{[^}]*display:\s*grid/); +}); + +test("nothing in the stylesheet can force a 170 px sidebar to scroll sideways", () => { + const minimums = [...css.matchAll(/min-width:\s*(\d+)px/g)].map((match) => Number(match[1])); + for (const value of minimums) { + assert.ok(value <= 170, `min-width: ${value}px is wider than the narrowest sidebar`); + } + assert.match(css, /\.actions\s*\{[^}]*flex-wrap:\s*wrap/); + assert.match(css, /overflow-wrap:\s*anywhere/); + assert.match(css, /@media \(max-width: 260px\)/, "there is a rule for a narrow sidebar"); + // At 200 px the table stops being a table and each cell names its column. + assert.match(css, /td\[data-head\]::before\s*\{[^}]*content:\s*attr\(data-head\)/); +}); + +test("the house rules survive: hidden, the focus ring and the dashed disabled state", () => { + assert.match(css, /\[hidden\]\s*\{[^}]*display:\s*none\s*!important/); + assert.match(css, /:focus-visible/); + assert.match(css, /button\[disabled\]/); + assert.match(css, /button\[disabled\][^{]*\{[^}]*border-style:\s*dashed/); +}); + +test("the sections are separated by space, not by nested boxes", () => { + // §8.1: "去掉多余色块边框,靠留白分节" — no block, card or viz draws a border. + for (const selector of ["\\.block\\b", "\\.viz\\b", "\\.card\\b"]) { + const rule = new RegExp(selector + "\\s*\\{[^}]*\\}", "g"); + for (const match of css.matchAll(rule)) { + assert.doesNotMatch(match[0], /\bborder(-[a-z]+)?\s*:/, `${match[0]} draws a border`); + } + } +}); diff --git a/test/cache/cacheLegacy.test.ts b/test/cache/cacheLegacy.test.ts new file mode 100644 index 0000000..e082386 --- /dev/null +++ b/test/cache/cacheLegacy.test.ts @@ -0,0 +1,73 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { measureDirectory } from "../../src/cli/artifacts"; + +/** + * The bounded walk behind `mcpp.cache.showLegacy`: the pre-v1 cache lives + * outside the workspace, so it is measured by path rather than as `target/`. + * + * Each case creates one throwaway directory and removes it in the test's own + * cleanup hook. + */ + +function withScratch(t: { after(callback: () => void): void }, body: (root: string) => void): void { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-legacy-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + body(root); +} + +test("a missing legacy directory measures as absent with zeros, without throwing", (t) => { + withScratch(t, (root) => { + const estimate = measureDirectory(path.join(root, "does-not-exist")); + assert.equal(estimate.exists, false); + assert.equal(estimate.totalBytes, 0); + assert.equal(estimate.files, 0); + assert.deepEqual(estimate.byTopLevel, []); + }); +}); + +test("a non-empty legacy directory gets a byte figure", (t) => { + withScratch(t, (root) => { + mkdirSync(path.join(root, "pkg", "deep"), { recursive: true }); + writeFileSync(path.join(root, "pkg", "a.bin"), Buffer.alloc(700)); + writeFileSync(path.join(root, "pkg", "deep", "b.bin"), Buffer.alloc(300)); + const estimate = measureDirectory(root); + assert.equal(estimate.exists, true); + assert.equal(estimate.totalBytes, 1000); + assert.equal(estimate.files, 2); + }); +}); + +test("an empty legacy directory measures as zero bytes, not as absent", (t) => { + withScratch(t, (root) => { + const estimate = measureDirectory(root); + assert.equal(estimate.exists, true); + assert.equal(estimate.totalBytes, 0); + }); +}); + +test("the walk never follows a symlink back into its own ancestor", (t) => { + withScratch(t, (root) => { + mkdirSync(path.join(root, "pkg"), { recursive: true }); + writeFileSync(path.join(root, "pkg", "a.bin"), Buffer.alloc(64)); + symlinkSync(root, path.join(root, "pkg", "loop"), "dir"); + const estimate = measureDirectory(root, { maxEntries: 1000, maxDepth: 8 }); + // The link is counted at zero bytes, so the total stays the 64 it really holds. + assert.equal(estimate.totalBytes, 64); + }); +}); + +test("a walk that runs out of budget reports a floor instead of hanging", (t) => { + withScratch(t, (root) => { + for (let index = 0; index < 20; index += 1) { + writeFileSync(path.join(root, `f${index}.bin`), Buffer.alloc(10)); + } + const estimate = measureDirectory(root, { maxEntries: 5 }); + assert.equal(estimate.truncated, "entries"); + assert.ok(estimate.totalBytes <= 200); + }); +}); diff --git a/test/cache/cachePanelHtml.test.ts b/test/cache/cachePanelHtml.test.ts new file mode 100644 index 0000000..f87e138 --- /dev/null +++ b/test/cache/cachePanelHtml.test.ts @@ -0,0 +1,456 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +import { + CACHE_PANEL_UI, + ageRampStep, + decodeCachePanelMessage, + renderCachePanelHtml, + type CachePanelAssets, + type CachePanelModel, +} from "../../src/cache/cachePanelHtml"; + +const ASSETS: CachePanelAssets = { + cspSource: "vscode-webview://cache", + nonce: "nonce-cache123", + styleUri: "vscode-webview://cache/media/cache.css", +}; + +const UI: Record = { + [CACHE_PANEL_UI.htmlLang]: "en", + [CACHE_PANEL_UI.title]: "Cache statistics", + [CACHE_PANEL_UI.boundary]: "Sizes are estimates.", + [CACHE_PANEL_UI.projectTitle]: "Project cache", + [CACHE_PANEL_UI.projectFiles]: "{0} file(s) · {1} group(s)", + [CACHE_PANEL_UI.projectStale]: "Stale artifacts: about {0}", + [CACHE_PANEL_UI.sharedTitle]: "Global build cache", + [CACHE_PANEL_UI.sharedEntries]: "{0} entries", + [CACHE_PANEL_UI.sharedRoot]: "Root: {0}", + [CACHE_PANEL_UI.legacyTitle]: "Pre-v1 cache", + [CACHE_PANEL_UI.legacyPath]: "Path: {0}", + [CACHE_PANEL_UI.unknown]: "not recorded", + [CACHE_PANEL_UI.projectUnavailable]: "Project artifacts could not be measured.", + [CACHE_PANEL_UI.sharedUnavailable]: "The shared build cache could not be read.", + [CACHE_PANEL_UI.actions]: "Cache actions", + [CACHE_PANEL_UI.cleanStale]: "Clean stale artifacts", + [CACHE_PANEL_UI.cleanProject]: "Clean project artifacts", + [CACHE_PANEL_UI.prune]: "Drop entries unused for a while", + [CACHE_PANEL_UI.verify]: "Verify the cache", + [CACHE_PANEL_UI.cleanLegacy]: "Remove the pre-v1 cache", + [CACHE_PANEL_UI.collect]: "Collect to this budget", + [CACHE_PANEL_UI.detailsFor]: "Show cache entry details for {0}", + [CACHE_PANEL_UI.reasonShared]: "The shared build cache is not available.", + [CACHE_PANEL_UI.reasonProject]: "Project artifacts are not available.", + [CACHE_PANEL_UI.reasonLegacy]: "There is no pre-v1 cache to remove.", + [CACHE_PANEL_UI.composition]: "Composition by kind", + [CACHE_PANEL_UI.compositionEmpty]: "No cache entries were found.", + [CACHE_PANEL_UI.age]: "Last use", + [CACHE_PANEL_UI.ageEmpty]: "No entry has a recorded last use.", + [CACHE_PANEL_UI.ageUnder]: "under {0} day(s)", + [CACHE_PANEL_UI.ageRange]: "{0}–{1} day(s)", + [CACHE_PANEL_UI.ageOverflow]: "more than {0} day(s)", + [CACHE_PANEL_UI.ageUnknown]: "{0} entries have no recorded last use.", + [CACHE_PANEL_UI.top]: "Largest packages (top {0})", + [CACHE_PANEL_UI.topEmpty]: "No cache label was read.", + [CACHE_PANEL_UI.colLabel]: "Label", + [CACHE_PANEL_UI.colEntries]: "Entries", + [CACHE_PANEL_UI.colBytes]: "Size", + [CACHE_PANEL_UI.colOldest]: "Oldest use", + [CACHE_PANEL_UI.budgetLabel]: "Keep the shared build cache under", + [CACHE_PANEL_UI.budgetHint]: "Simulates mcpp cache gc --max-size.", + [CACHE_PANEL_UI.budgetUnit]: "GiB", + [CACHE_PANEL_UI.incompleteWarning]: "{0} cache entries are incomplete.", + [CACHE_PANEL_UI.sizeWarning]: "The shared build cache is {0}, at or above the {1} warning threshold.", +}; + +interface PageOptions { + project?: Partial; + shared?: Partial; + legacy?: CachePanelModel["legacy"]; + limits?: Partial; + estimate?: string; +} + +/** + * A known cache: 500 B in two kinds (300 B `pkg`, 200 B `std`) and, when asked + * for, a 500 B pre-v1 cache — so the composition percentages are exactly + * 30 / 20 / 50 and the SVG widths are 300 / 200 / 500 out of 1000. The project + * block is a 1000 B `target/` in 3 files over 1 group. + * + * The byte formatter keeps its unit after a space, which is what the metric + * split relies on: `1000 B` -> value `1000`, unit `B`. + */ +function makeModel(options: PageOptions = {}): CachePanelModel { + return { + ui: UI, + project: { available: true, totalBytes: 1000, files: 3, groups: 1, ...options.project }, + shared: { + available: true, + totalBytes: 500, + totalEntries: 4, + byKind: [ + { kind: "pkg", entries: 3, bytes: 300 }, + { kind: "std", entries: 1, bytes: 200 }, + ], + buckets: [ + { fromDays: 0, toDays: 1, entries: 1, bytes: 100 }, + { fromDays: 1, toDays: 7, entries: 1, bytes: 100 }, + { fromDays: 7, toDays: 30, entries: 1, bytes: 100 }, + { fromDays: 30, entries: 1, bytes: 200 }, + ], + top: [{ label: "zlib", entries: 2, bytes: 200, oldestAccessed: 1_758_636_000 }], + incomplete: 0, + ...options.shared, + }, + format: { bytes: (value) => `${value} B`, count: (value) => `c${value}` }, + limits: { topN: 5, warnAboveGiB: 0, ...options.limits }, + ...(options.legacy === undefined ? {} : { legacy: options.legacy }), + ...(options.estimate === undefined ? {} : { estimate: options.estimate }), + }; +} + +function page(options: PageOptions = {}): string { + return renderCachePanelHtml(makeModel(options), ASSETS); +} + +/** The single source line a one-line legend has to fit on. */ +function legendLine(html: string, id: string): string { + const line = html.split("\n").find((candidate) => candidate.includes(`data-legend="${id}"`)); + assert.ok(line !== undefined, `no legend line for ${id}`); + return line; +} + +test("the document carries the nonce and the strict CSP", () => { + const html = page(); + assert.ok(html.includes("default-src 'none'")); + assert.ok(html.includes("style-src vscode-webview://cache;")); + assert.ok(html.includes("script-src 'nonce-nonce-cache123'")); + assert.ok(html.includes('img-src vscode-webview://cache"')); + assert.ok(html.includes('', entries: 1, bytes: 10 }] }, + }); + assert.ok(!html.includes("', + description: "A bold description", + }), + ]); + assert.ok(!html.includes("', "import a.b;", ""], + usageLine: 2, + }, + ], + }), + ASSETS, + ); + assert.match(hostile, /class="tok-punctuation">#<\/span>/); + assert.match(hostile, /class="tok-keyword">include<\/span>/); + assert.match(hostile, /class="tok-comment">\/\/ <\/script><b><\/span>/); + assert.match(hostile, /class="tok-keyword">import<\/span>/); + assert.match(hostile, //); + assert.match(hostile, /a\.cpp · line 1/); + assert.match(hostile, /Example project: argparse/); + assert.equal(hostile.split("").length - 1, 1, "the hostile comment cannot close our script"); + + const none = renderDetailHtml(model({ snippets: [], exampleProject: undefined }), ASSETS); + assert.match(none, /This package has no test project in the index\./); +}); + +test("dependencies are listed as declared, and the resolved column is left out when unknown", () => { + const html = renderDetailHtml(model(), ASSETS); + assert.match(html, /compat\.vulkan<\/code> — 1\.4\.357\.3/); + assert.match(html, /What the descriptor declares\./); + const resolved = renderDetailHtml( + model({ dependencies: [{ id: "compat.zlib", version: "1.3", resolved: "1.2.13" }] }), + ASSETS, + ); + assert.match(resolved, /compat\.zlib<\/code> — 1\.3 · resolved 1\.2\.13/); + const none = renderDetailHtml(model({ dependencies: [] }), ASSETS); + assert.match(none, /This descriptor declares no dependencies\./); +}); + +test("the add button carries the exact command, and is disabled when there is no version", () => { + const html = renderDetailHtml(model(), ASSETS); + // One place chooses the version (the matrix) and one place shows what the + // choice produces; there is no second, redundant `Add to mcpp\.toml<\/button>/); + assert.match(html, //); + assert.match(none, /This index publishes no version for this platform\./); +}); + +test("a package the project already has says so, and the button offers the switch", () => { + // Same version as the manifest: nothing to do, and the button says so — with + // `data-installed`, which paints it the thinned green of a done thing. + const same = renderDetailHtml(model({ installed: { version: "3.2", dev: false } }), ASSETS); + assert.match( + same, + /${snippet.fileKey === undefined ? "" : ``}`, `
    ${lines}
    `, `

    ${escapeHtml(caption)}

    `, ].join("\n"); @@ -254,52 +303,16 @@ function renderSnippets(model: DetailModel, label: UiLabel): string { return blocks.join("\n"); } -/** - * The version matrix, as the page's **selector**: each version is a button that - * aims the command at the top of the page at itself. - * - * A `${escapeHtml(label(DETAIL_UI.addDev))}`, - ...(model.repo === undefined ? [] : [link(model.repo, label(DETAIL_UI.openRepo))]), - ...(model.indexUrl === undefined ? [] : [link(model.indexUrl, label(DETAIL_UI.indexLink))]), - ` `, - // The copy button sits beside the command, not inside it: `command.textContent` - // is what a click copies, and a button inside the paragraph would be copied too. - `
    `, - `

    ${escapeHtml(command)}

    `, - ` `, - `
    `, - disabled - ? `

    ${escapeHtml(label(DETAIL_UI.addNoVersion))}

    ` - : `

    ${escapeHtml(label(DETAIL_UI.addLatest))}

    `, - ...usage, - `

    ${escapeHtml(model.result?.message ?? "")}

    `, - ``, - ] - .filter((line) => line.length > 0) - .join("\n"); + const kind = model.dependencyKind ?? (model.projectState?.dev || model.installed?.dev ? "dev" : "normal"); + const groups = groupsFor(model); + const current = groups[kind]; + const version = selectedVersion(model); + const manual = kind === "build" || current.ambiguous || current.declared && current.sourceType !== "registry" || !current.declared && groups[kind === "dev" ? "normal" : "dev"].declared; + const already = current.declaredVersion !== undefined && current.declaredVersion === version && !manual; + const canOpen = model.manifestAvailable ?? !!model.projectRoot; + const disabled = !!model.busy || !!model.loading || (manual ? !canOpen : !!model.blockedReason || !version || already); + const location = dependencyLocation(model.id, kind); + const command = model.preview ? `mcpp search ${model.id}` : manual ? `[${location.table}] +${location.name} = ${JSON.stringify(version ?? "")}` : (kind === "dev" ? model.commandDevTemplate : model.commandTemplate).replace("{0}", model.id).replace("{1}", version ?? "?"); + const addLabel = model.busy ? label(DETAIL_UI.pending) : manual ? label(DETAIL_UI.openManifest) : already ? label(DETAIL_UI.alreadyAdded) : current.declared ? fill(label, DETAIL_UI.switchTo, [version ?? "?"]) : label(kind === "dev" ? DETAIL_UI.addDevAction : DETAIL_UI.add); + const link = (url: string, text: string) => ``; + const kinds: Array<[DependencyKind, string, string]> = [["normal", DETAIL_UI.kindNormal, "dependencies"], ["dev", DETAIL_UI.kindDev, "dev-dependencies"], ["build", DETAIL_UI.kindBuild, "build-dependencies"]]; + return `
    + ${model.loading ? `

    ${escapeHtml(label(DETAIL_UI.loading))}

    ` : ""} + ${model.preview ? `

    ${escapeHtml(label(DETAIL_UI.preview))}

    ${escapeHtml(label(DETAIL_UI.version))}: ${escapeHtml(model.previewVersion ?? "—")}

    ` : ""} + ${model.preview ? "" : `
    ${kind === "dev" ? "dev-dependencies" : kind === "build" ? "build-dependencies" : "dependencies"}${current.resolvedVersion && current.resolvedVersion !== current.declaredVersion ? `${escapeHtml(label(DETAIL_UI.locked))}: ${escapeHtml(current.resolvedVersion)}` : ""}
    `} +
    + ${model.preview ? "" : `
    `} + ${model.repo === undefined ? "" : link(model.repo, label(DETAIL_UI.openRepo))} + ${model.indexUrl === undefined ? "" : link(model.indexUrl, label(DETAIL_UI.indexLink))} +
    + ${model.versionNotice ? `

    ${escapeHtml(model.versionNotice)}

    ` : ""} + ${model.blockedReason ? `

    ${escapeHtml(model.blockedReason)}

    ` : ""} +

    ${escapeHtml(manual ? label(kind === "build" ? DETAIL_UI.buildHint : DETAIL_UI.moveHint) : "")}

    +

    ${escapeHtml(command)}

    +

    ${escapeHtml(model.result?.message ?? "")}

    +
    +
    `; +} + +export function detailClientState(model: DetailModel): Record { + return { + key: model.key ?? model.id, generation: model.generation ?? 0, projectRoot: model.projectRoot, + id: model.id, versions: model.currentVersions, version: selectedVersion(model), + groups: groupsFor(model), kind: model.dependencyKind ?? (model.projectState?.dev || model.installed?.dev ? "dev" : "normal"), + locations: Object.fromEntries((["normal", "dev", "build"] as const).map((kind) => [kind, dependencyLocation(model.id, kind)])), + loading: !!model.loading, canOpenManifest: model.manifestAvailable ?? !!model.projectRoot, + blocked: !!model.blockedReason, busy: !!model.busy, preview: !!model.preview, + command: model.commandTemplate, commandDev: model.commandDevTemplate, + labels: Object.fromEntries(Object.entries(DETAIL_UI).map(([name, key]) => [name, model.ui[key] ?? key])), + }; } -/** - * The client. It posts two things — `add` and `openUrl` — and applies - * the host's `{type:"result"}` in place, so running `mcpp add` never rebuilds the - * document and never resets the version the reader picked. - */ function clientScript(initialModel: string): string { return `(function () { "use strict"; var api = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : undefined; var state = ${initialModel}; - var devInput = document.getElementById("detail-dev"); - var command = document.getElementById("detail-command"); - var result = document.getElementById("detail-result"); - var addButton = document.getElementById("detail-add"); - var version = command ? (command.getAttribute("data-selected-version") || "") : ""; - var labels = (state && state.labels) || {}; - var installed = state && state.installed ? state.installed : ""; - - function post(message) { - if (api) { api.postMessage(message); } + var saved = api && api.getState ? api.getState() || {} : {}; + var pages = saved.pages || {}; + var busy = state.busy; + var version = state.version || ""; + var kind = state.kind || "normal"; + var copyId = 0; + var copyRequests = {}; + function copyLabel(button, text) { (button.querySelector("[data-action-label]") || button).textContent = text; } + function restoreCopyFeedback() { + Object.keys(copyRequests).forEach(function (token) { + var request = copyRequests[token]; + if (request.key !== pageKey() || request.until < Date.now()) { delete copyRequests[token]; return; } + var buttons = Array.from(document.querySelectorAll(request.command ? "[data-copy-command]" : "[data-copy]")); + var button = buttons.find(function (node) { return request.command ? document.getElementById("detail-command").textContent === request.text : node.getAttribute("data-copy") === request.text; }); + if (button) { button.setAttribute("data-copy-token", token); if (request.message) { copyLabel(button, request.message); } } + }); } - - function fillTemplate(template, values) { - var out = template; - for (var index = 0; index < values.length; index += 1) { - out = out.split("{" + index + "}").join(String(values[index])); - } - return out; + function pageKey() { return state.key + "|" + (state.projectRoot || ""); } + function post(message) { message.pageKey = state.key; message.generation = state.generation; if (api) { api.postMessage(message); } } + function persist() { + if (state.loading) { return; } + var expanded = Array.from(document.querySelectorAll("details[data-persist]")).filter(function (node) { return node.open; }).map(function (node) { return node.getAttribute("data-persist"); }); + pages[pageKey()] = { version: version, kind: kind, scroll: window.scrollY, expanded: expanded }; + var keys = Object.keys(pages); while (keys.length > 20) { delete pages[keys.shift()]; } + if (api && api.setState) { api.setState({ pages: pages }); } } - - function updateCommand() { - if (!command) { return; } - var dev = devInput ? devInput.checked === true : false; - var template = dev - ? (command.getAttribute("data-template-dev") || "") - : (command.getAttribute("data-template") || ""); - command.setAttribute("data-selected-version", version); - command.textContent = fillTemplate(template, [state.id, version || "?"]); + function closeKindMenu(focus) { + var menu = document.getElementById("detail-kind-menu"); + var toggle = document.getElementById("detail-kind-toggle"); + if (menu) { menu.hidden = true; } + if (toggle) { toggle.setAttribute("aria-expanded", "false"); if (focus) { toggle.focus(); } } } - - /** - * The button's label and enabled state follow the selection: adding a new - * package, switching the version of one that is already there, or nothing to do - * because this is the version the manifest already asks for. The inert state - * carries the data-installed attribute, which is what paints it the thinned - * green of a thing already done. - */ - function updateButton() { - if (!addButton) { return; } - if (!version) { - addButton.textContent = labels.add || ""; - addButton.disabled = true; - addButton.removeAttribute("data-installed"); - return; - } - if (installed && version === installed) { - addButton.textContent = labels.alreadyAdded || ""; - addButton.disabled = true; - addButton.setAttribute("data-installed", ""); - return; - } - addButton.textContent = installed - ? (labels.switchTo || "{0}").split("{0}").join(version) - : (labels.add || ""); - addButton.disabled = false; - addButton.removeAttribute("data-installed"); + function manualSelection() { + var group = state.groups[kind]; + return kind === "build" || group.ambiguous || group.declared && group.sourceType !== "registry" || !group.declared && state.groups[kind === "dev" ? "normal" : "dev"].declared; } - - /** One version button is the selection; the rest are alternatives. */ - function selectVersion(next) { - version = next; - var buttons = document.querySelectorAll("[data-version]"); - for (var index = 0; index < buttons.length; index += 1) { - var button = buttons[index]; - if (button.getAttribute("data-version") === next) { - button.setAttribute("data-selected", ""); - } else { - button.removeAttribute("data-selected"); - } + function update() { + var select = document.getElementById("detail-version"); + var button = document.getElementById("detail-add"); + if (state.preview) { return; } + var group = state.groups[kind]; + var manual = manualSelection(); + var same = !!group.declaredVersion && group.declaredVersion === version && !manual; + if (select) { select.value = version; select.disabled = busy || state.loading || !state.versions.length; } + var toggle = document.getElementById("detail-kind-toggle"); if (toggle) { toggle.disabled = busy || state.loading; } + var kindLabel = document.getElementById("detail-kind-label"); + if (kindLabel) { kindLabel.textContent = kind === "dev" ? "dev-dependencies" : kind === "build" ? "build-dependencies" : "dependencies"; } + document.querySelectorAll("[data-kind]").forEach(function (item) { item.setAttribute("aria-checked", String(item.getAttribute("data-kind") === kind)); }); + if (button) { + button.disabled = busy || state.loading || (manual ? !state.canOpenManifest : state.blocked || !version || same); + button.textContent = busy ? state.labels.pending : manual ? state.labels.openManifest : same ? state.labels.alreadyAdded : group.declared ? state.labels.switchTo.split("{0}").join(version) : kind === "dev" ? state.labels.addDevAction : state.labels.add; + if (same) { button.setAttribute("data-installed", ""); } else { button.removeAttribute("data-installed"); } } - updateCommand(); - updateButton(); + var note = document.getElementById("detail-selection-note"); if (note) { note.textContent = manual ? kind === "build" ? state.labels.buildHint : state.labels.moveHint : ""; note.hidden = !manual; } + var command = document.getElementById("detail-command"); + var location = state.locations[kind]; + if (command) { command.textContent = manual ? "[" + location.table + "]\\n" + location.name + " = " + JSON.stringify(version || "") : (kind === "dev" ? state.commandDev : state.command).split("{0}").join(state.id).split("{1}").join(version || "?"); } } - - function showResult(payload) { - if (!result) { return; } - result.hidden = false; - result.setAttribute( - "data-state", - payload.state === "ok" ? "ok" : payload.state === "pending" ? "pending" : "error", - ); - result.textContent = payload.message || ""; + function restore() { + var page = pages[pageKey()] || {}; + if (state.versions.indexOf(page.version) >= 0) { version = page.version; } + kind = ["normal", "dev", "build"].indexOf(page.kind) >= 0 ? page.kind : state.kind || "normal"; + document.querySelectorAll("details[data-persist]").forEach(function (node) { node.open = (page.expanded || []).indexOf(node.getAttribute("data-persist")) >= 0; }); + update(); window.scrollTo(0, page.scroll || 0); } - - if (devInput) { devInput.addEventListener("change", updateCommand); } - + document.addEventListener("change", function (event) { + if (event.target.id === "detail-version") { version = event.target.value; } + if (event.target.id === "detail-version") { update(); persist(); } + }); + document.addEventListener("toggle", persist, true); + window.addEventListener("scroll", persist); document.addEventListener("click", function (event) { - var target = event.target; - if (!target || typeof target.closest !== "function") { return; } - var link = target.closest("[data-open-url]"); - if (link) { - event.preventDefault(); - var url = link.getAttribute("data-open-url") || ""; - // Say something the instant the click lands: the host's answer replaces - // this line, and if it never arrives the reader can see that too. - showResult({ state: "pending", message: (labels.opening || "{0}").split("{0}").join(url) }); - post({ type: "openUrl", url: url }); + var target = event.target.closest && event.target.closest("button"); + if (!target || target.disabled) { return; } + if (target.id === "detail-kind-toggle") { + var menu = document.getElementById("detail-kind-menu"); menu.hidden = !menu.hidden; + target.setAttribute("aria-expanded", String(!menu.hidden)); + if (!menu.hidden) { menu.querySelector('[aria-checked="true"]').focus(); } return; } - var copy = target.closest("[data-copy-command], [data-copy]"); - if (copy) { - // The command's own text is what gets copied — the button beside it, not - // inside it, is why the command's textContent is exactly the command. - var text = copy.hasAttribute("data-copy-command") - ? (command ? String(command.textContent || "") : "") - : String(copy.getAttribute("data-copy") || ""); - if (!text) { return; } - showResult({ state: "pending", message: labels.copying || "" }); - post({ type: "copy", text: text }); - return; + if (target.hasAttribute("data-kind")) { + kind = target.getAttribute("data-kind"); + var declared = state.groups[kind].declaredVersion; + if (state.versions.indexOf(declared) >= 0) { version = declared; } + closeKindMenu(true); update(); persist(); return; } - var pick = target.closest("[data-version]"); - if (pick) { - selectVersion(pick.getAttribute("data-version") || ""); - return; + closeKindMenu(false); + var action = target.getAttribute("data-action"); + if (action) { post({ type: action }); return; } + if (target.hasAttribute("data-open-dependency")) { persist(); post({ type: "openDependency", key: target.getAttribute("data-open-dependency") }); return; } + if (target.hasAttribute("data-open-file")) { post({ type: "openFile", key: target.getAttribute("data-open-file") }); return; } + if (target.hasAttribute("data-open-url")) { post({ type: "openUrl", url: target.getAttribute("data-open-url") }); return; } + if (target.hasAttribute("data-copy") || target.hasAttribute("data-copy-command")) { + var token = "copy-" + (++copyId); target.setAttribute("data-copy-token", token); + copyRequests[token] = { key: pageKey(), command: target.hasAttribute("data-copy-command"), text: target.hasAttribute("data-copy") ? target.getAttribute("data-copy") : document.getElementById("detail-command").textContent, until: Date.now() + 20000 }; + post({ type: "copy", text: target.hasAttribute("data-copy") ? target.getAttribute("data-copy") : document.getElementById("detail-command").textContent, token: token }); return; } - var add = target.closest("#detail-add"); - if (add && !add.disabled) { + if (target.id === "detail-add" && !busy) { + if (manualSelection()) { post({ type: "openManifest" }); return; } if (!version) { return; } - post({ type: "add", version: version, dev: devInput ? devInput.checked === true : false }); + busy = true; update(); + var result = document.getElementById("detail-result"); result.hidden = false; result.dataset.state = "pending"; result.textContent = state.labels.pending; + persist(); post({ type: "add", version: version, dev: kind === "dev" }); } }); - - /** Move the "added" marker onto the version the project now asks for. */ - function markInstalled(next) { - installed = next; - var marks = document.querySelectorAll(".detail-installed"); - for (var index = 0; index < marks.length; index += 1) { marks[index].parentNode.removeChild(marks[index]); } - var buttons = document.querySelectorAll("[data-version]"); - for (var index2 = 0; index2 < buttons.length; index2 += 1) { - var button = buttons[index2]; - if (button.getAttribute("data-version") !== next) { - button.removeAttribute("data-installed"); - continue; - } - button.setAttribute("data-installed", ""); - var mark = document.createElement("span"); - mark.className = "detail-installed"; - mark.textContent = labels.installed || ""; - button.parentNode.insertBefore(mark, button.nextSibling); - } - updateButton(); - } - + document.addEventListener("click", function (event) { if (!event.target.closest || !event.target.closest(".detail-add-group")) { closeKindMenu(false); } }); + document.addEventListener("keydown", function (event) { + var menu = document.getElementById("detail-kind-menu"); + if (!menu || menu.hidden) { return; } + if (event.key === "Escape") { event.preventDefault(); closeKindMenu(true); return; } + if (event.key === "Tab") { closeKindMenu(false); return; } + var options = Array.from(menu.querySelectorAll("[data-kind]")); + if (["ArrowDown", "ArrowUp", "Home", "End"].indexOf(event.key) < 0) { return; } + event.preventDefault(); var index = options.indexOf(document.activeElement); + index = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : (index + (event.key === "ArrowDown" ? 1 : options.length - 1)) % options.length; + options[index].focus(); + }); window.addEventListener("message", function (event) { var data = event.data; - if (data && data.type === "result" && data.result) { showResult(data.result); } - if (data && data.added && data.added.version) { markInstalled(data.added.version); } + if (!data) { return; } + if (data.type === "detailUpdate") { + persist(); + var focused = document.activeElement && document.activeElement.id; + var doc = new DOMParser().parseFromString(data.html, "text/html"); + ["detail-header", "detail-main", "detail-footer"].forEach(function (id) { var old = document.getElementById(id); var next = doc.getElementById(id); if (old && next) { old.replaceWith(next); } }); + state = data.state; busy = state.busy; version = state.version || ""; kind = state.kind || "normal"; + restore(); restoreCopyFeedback(); + var nextFocus = focused && document.getElementById(focused); if (nextFocus) { nextFocus.focus({ preventScroll: true }); } + return; + } + if (data.key !== state.key || data.generation !== state.generation) { return; } + if (data.type === "copyResult") { + var request = copyRequests[data.token]; + if (request) { + request.message = data.message; request.until = Date.now() + 1800; restoreCopyFeedback(); + setTimeout(function () { var button = document.querySelector('[data-copy-token="' + data.token + '"]'); if (button) { copyLabel(button, state.labels.copy); } delete copyRequests[data.token]; }, 1800); + } + } + if (data.type === "linkResult") { var status = document.getElementById("detail-link-result"); status.textContent = data.message; status.hidden = !data.message; } }); - - updateCommand(); - updateButton(); + restore(); + post({ type: "mounted" }); })();`; } -/** The little state the client needs; the page is already rendered from the rest. */ -function clientState(model: DetailModel): Record { - return { - id: model.id, - ...(model.latest === undefined ? {} : { latest: model.latest }), - // The three labels the button can wear, and the version the project already - // has: the client re-decides the label every time the selection changes, so - // the host cannot be the only one that knows what the button means. The - // `installed` word belongs here too — `markInstalled()` writes it next to - // the version button after a successful add, and an empty string there is - // a marker nobody can read (§22). - labels: { - add: model.ui[DETAIL_UI.add] ?? "", - switchTo: model.ui[DETAIL_UI.switchTo] ?? "", - alreadyAdded: model.ui[DETAIL_UI.alreadyAdded] ?? "", - opening: model.ui[DETAIL_UI.opening] ?? "", - installed: model.ui[DETAIL_UI.installed] ?? "", - copying: model.ui[DETAIL_UI.copying] ?? "", - }, - ...(model.installed === undefined ? {} : { installed: model.installed.version }), - }; -} - /** A model embedded in the page's own script; `<` is escaped so it cannot close it. */ function embedJson(value: unknown): string { return JSON.stringify(value) @@ -637,35 +605,31 @@ export function renderDetailHtml(model: DetailModel, assets: DetailAssets): stri ${escapeHtml(title)} -
    -

    ${escapeHtml(model.id)}

    +
    +

    ${escapeHtml(model.id)}

    ${renderMeta(model, label)}
    ${renderProject(model, label)}
    ${model.description === undefined ? "" : `

    ${escapeHtml(model.description)}

    `} - ${renderMeta(model, label)}
    -
    +
    ${renderActions(model, label)} -
    -

    ${escapeHtml(versionsHeading)}

    - ${renderVersions(model, label)} -
    -
    -

    ${escapeHtml(label(DETAIL_UI.dependencies))}

    - ${renderDependencies(model, label)} -
    -
    -

    ${escapeHtml(label(DETAIL_UI.code))}

    - ${renderSnippets(model, label)} - ${model.exampleProject === undefined ? "" : `

    ${escapeHtml(fill(label, DETAIL_UI.codeProject, [model.exampleProject]))}

    `} -
    -
    -

    ${escapeHtml(label(DETAIL_UI.overview))}

    - ${renderExtras(model, label)} - ${model.parseNotice === undefined ? "" : `

    ${escapeHtml(model.parseNotice)}

    `} -
    + + ${model.preview ? "" : `
    +
    +

    ${escapeHtml(label(DETAIL_UI.usage))}

    + ${model.usage.length ? `

    ${escapeHtml(label(DETAIL_UI.usageSource))}

    ` + model.usage.map((line) => `
    ${escapeHtml(line)}
    `).join("") : model.snippets.length ? "" : `

    ${escapeHtml(label(DETAIL_UI.unknownUsage))}

    `} + ${model.snippets.length ? renderSnippets(model, label) : ""} + ${model.exampleProject === undefined ? "" : `

    ${escapeHtml(fill(label, DETAIL_UI.codeProject, [model.exampleProject]))}

    `} + ${model.exampleProvenance === undefined ? "" : `

    ${escapeHtml(model.exampleProvenance)}

    `} +
    + +
    `}
    -
    ${escapeHtml(model.dataSource)}
    +
    ${escapeHtml(label(DETAIL_UI.source))}

    ${escapeHtml(model.dataSource)}

    ${model.projectRoot ? `` : ""}

    ${escapeHtml(badgeLabels(model.badges, label).join(" · "))}

    ${model.parseNotice === undefined ? "" : `

    ${escapeHtml(model.parseNotice)}

    `}
    @@ -691,6 +655,15 @@ export function decodeDetailMessage(raw: unknown): DetailMessage | undefined { return undefined; } switch (raw.type) { + case "mounted": + case "selectProject": + case "showOutput": + case "retry": + case "openManifest": + return { type: raw.type }; + case "openDependency": + case "openFile": + return nonEmptyString(raw.key) ? { type: raw.type, key: raw.key } : undefined; case "add": return nonEmptyString(raw.version) && typeof raw.dev === "boolean" ? { type: "add", version: raw.version, dev: raw.dev } @@ -705,7 +678,7 @@ export function decodeDetailMessage(raw: unknown): DetailMessage | undefined { // hostile document cannot park a novel in the clipboard — and everything // else about the text is the user's own click. return nonEmptyString(raw.text) && raw.text.length <= 16_384 - ? { type: "copy", text: raw.text } + ? { type: "copy", text: raw.text, ...(typeof raw.token === "string" && /^copy-\d+$/.test(raw.token) ? { token: raw.token } : {}) } : undefined; default: return undefined; diff --git a/src/library/detailPanel.ts b/src/library/detailPanel.ts index b784b26..2fb0adb 100644 --- a/src/library/detailPanel.ts +++ b/src/library/detailPanel.ts @@ -18,7 +18,7 @@ * file. */ -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import * as path from "node:path"; import * as vscode from "vscode"; @@ -27,28 +27,28 @@ import { read } from "../config/access"; import { languagePreference, t } from "../i18n/t"; import { localeFromEditorLanguage } from "../i18n/translate"; import { WebviewDocument } from "../webview/document"; +import { projectDependencyState, readProjectDeclarations, operationMatches } from "./projectState"; +import type { LibraryRow } from "./libraryHtml"; import { addDependency } from "./addDependency"; import { badgesOf, - codeSnippets, descriptorDependencies, - installedFor, lockPackageVersions, mergeSurfaces, parseXpkgJson, platformKey, surfaceLabel, - syntheticUsageLines, usageLinesFor, type LibraryEntry, type Surface, } from "./indexModel"; import { cachedDescriptorText, loadSnapshot, readDescriptorText, readExampleFiles } from "./indexLocator"; +import { bundledExample, ExampleDocuments, EXAMPLE_SCHEME } from "./examples"; import { DETAIL_UI, decodeDetailMessage, renderDetailHtml, - type DetailInstalled, + detailClientState, type DetailModel, type DetailVersionGroup, type DetailResult, @@ -63,7 +63,9 @@ const STYLESHEET = "library.css"; const PARSE_TIMEOUT_MS = 20_000; export interface DetailPanelDeps { - mcppExecutable: () => string; + mcppExecutable: (projectRoot?: string) => string; + chooseProject?: () => Promise; + onPackageOpened?: (key: string) => void; projectRoot: () => string | undefined; output: vscode.OutputChannel; isTrusted: () => boolean; @@ -74,19 +76,30 @@ export interface DetailPanelDeps { * library view's `refresh()`. */ onDependenciesChanged?: () => void; + onProjectFilesRead?: (root: string, manifest: string, lock: string) => void; } /** Build the opener once; `extension.ts` passes `openLibraryDetail` to the view. */ export function createLibraryDetailOpener( context: vscode.ExtensionContext, deps: DetailPanelDeps, -): (id: string) => Promise { +): LibraryDetailOpener { + const sourceChanged = new vscode.EventEmitter(); + const exampleDocuments = new ExampleDocuments((uri) => sourceChanged.fire(vscode.Uri.parse(uri))); + context.subscriptions.push(sourceChanged, vscode.workspace.registerTextDocumentContentProvider(EXAMPLE_SCHEME, { + onDidChange: sourceChanged.event, + provideTextDocumentContent: (uri) => exampleDocuments.read(uri.toString()) ?? "", + })); const session: DetailSession = { context, + exampleDocuments, deps, panel: undefined, id: undefined, busy: false, + generation: 0, + projectRoot: deps.projectRoot(), + painted: false, webviewDocument: new WebviewDocument(STYLESHEET), }; context.subscriptions.push({ @@ -95,17 +108,41 @@ export function createLibraryDetailOpener( session.panel = undefined; }, }); - return (id: string) => open(session, id); + const opener = (id: string, preview?: LibraryRow) => open(session, id, preview); + opener.refresh = async () => { + if (session.id === undefined || session.panel === undefined) { return; } + const root = deps.projectRoot(); + if (root !== session.projectRoot) { + session.projectRoot = root; session.generation++; session.result = undefined; + if (session.model) { paintModel(session, { ...session.model, generation: session.generation, projectRoot: root, projectState: undefined, installed: undefined, loading: true, blockedReason: t("Reading package details…"), result: undefined }); } + } + await render(session, session.id); + }; + return opener; +} + +export interface LibraryDetailOpener { + (id: string, preview?: LibraryRow): Promise; + refresh(): Promise; } /** One panel per window, remembered so a second `open` reveals rather than stacks. */ export interface DetailSession { + exampleDocuments: ExampleDocuments; context: vscode.ExtensionContext; deps: DetailPanelDeps; panel: vscode.WebviewPanel | undefined; id: string | undefined; /** `mcpp add` in flight: a second click must not start a second command. */ busy: boolean; + generation: number; + renderRequest?: number; + projectRoot?: string; + preview?: LibraryRow; + model?: DetailModel; + painted: boolean; + lastHtml?: string; + result?: DetailResult; /** * The document and its nonce, kept per session so re-opening the *same* * package renders byte for byte the same document and does not reload the @@ -117,7 +154,7 @@ export interface DetailSession { /** The parse result per descriptor, so re-opening a package costs no process. */ const parsed = new Map(); -export async function open(session: DetailSession, id: string): Promise { +export async function open(session: DetailSession, id: string, preview?: LibraryRow): Promise { if (session.panel === undefined) { const panel = vscode.window.createWebviewPanel( PANEL_VIEW_TYPE, @@ -133,6 +170,9 @@ export async function open(session: DetailSession, id: string): Promise { panel.onDidDispose(() => { session.panel = undefined; session.id = undefined; + session.generation++; + session.painted = false; + session.lastHtml = undefined; // The next open creates a brand-new webview: what was on screen says // nothing about it, so the document comparison starts from nothing. session.webviewDocument.invalidate(); @@ -142,161 +182,198 @@ export async function open(session: DetailSession, id: string): Promise { }); } const panel = session.panel; + const changed = session.id !== id; session.id = id; - panel.title = id; + session.preview = preview; + session.deps.onPackageOpened?.(id); + if (changed) { session.generation++; session.result = undefined; } + const nextProject = session.deps.projectRoot(); + if (nextProject !== session.projectRoot) { session.generation++; session.result = undefined; } + session.projectRoot = nextProject; + panel.title = preview?.id ?? path.basename(id, ".lua"); + if (changed || !session.painted) { + const loading = emptyModel(preview?.id ?? path.basename(id, ".lua"), labels(), t("Reading package details…")); + loading.key = id; loading.generation = session.generation; loading.projectRoot = session.projectRoot; + loading.loading = true; loading.blockedReason = t("Reading package details…"); + paintModel(session, loading); + } panel.reveal(vscode.ViewColumn.Active, false); await render(session, id); } async function handle(session: DetailSession, raw: unknown): Promise { const message = decodeDetailMessage(raw); - if (message === undefined || session.panel === undefined) { + if (message === undefined || session.panel === undefined || session.id === undefined) { return; } + const envelope = raw as { pageKey?: unknown; generation?: unknown }; + if (message.type === "mounted") { + if (session.model) { paintModel(session, session.model, true); } return; } + if (envelope.pageKey !== session.id || envelope.generation !== session.generation) { return; } + const operation = { key: session.id, projectRoot: session.projectRoot, generation: session.generation }; + const current = () => operationMatches(operation, { key: session.id ?? "", projectRoot: session.projectRoot, generation: session.generation }); switch (message.type) { - case "openUrl": { - // `decodeDetailMessage` already restricted this to https. - await openExternal(session, message.url); + case "selectProject": { + const root = await session.deps.chooseProject?.(); + if (root !== undefined && current()) { + session.projectRoot = root; session.generation++; session.result = undefined; + if (session.model) { paintModel(session, { ...session.model, generation: session.generation, projectRoot: root, projectState: undefined, installed: undefined, loading: true, blockedReason: t("Reading package details…"), result: undefined }); } + await render(session, session.id); + } + return; + } + case "showOutput": session.deps.output.show(true); return; + case "openManifest": + if (session.projectRoot) { await vscode.window.showTextDocument(vscode.Uri.file(path.join(session.projectRoot, "mcpp.toml"))); } + return; + case "retry": + if (session.result?.state === "error") { session.result = undefined; } + await render(session, session.id); return; + case "openDependency": + if (session.model?.dependencies.some((dependency) => dependency.key === message.key)) { await open(session, message.key); } + return; + case "openFile": { + if (!session.model?.snippets.some((snippet) => snippet.fileKey === message.key)) { return; } + if (message.key.startsWith(`${EXAMPLE_SCHEME}:`)) { await vscode.window.showTextDocument(vscode.Uri.parse(message.key)); } return; } case "copy": { - // The command preview and the usage lines: both are text a reader pastes - // somewhere else, and a click that answers with nothing looks like a dead - // button (the lesson `openExternal` learned in §20.2). - try { - await vscode.env.clipboard.writeText(message.text); - postResult(session, { state: "ok", message: t("Copied to the clipboard.") }); - } catch (error) { - session.deps.output.appendLine( - `mcpp library: copying to the clipboard failed: ${error instanceof Error ? error.message : String(error)}`, - ); - postResult(session, { state: "error", message: t("Could not write the clipboard.") }); - } + let result: string; + try { await vscode.env.clipboard.writeText(message.text); result = t("Copied"); } + catch { result = t("Copy failed"); } + if (current()) { void session.panel?.webview.postMessage({ type: "copyResult", key: operation.key, generation: operation.generation, token: message.token, message: result }); } + return; + } + case "openUrl": { + // Only links in this package's model can ask the host to open a browser. + if (message.url !== session.model?.repo && message.url !== session.model?.indexUrl) { return; } + let opened = false; + try { opened = await vscode.env.openExternal(vscode.Uri.parse(message.url)); } catch { /* report below */ } + let copied = false; + if (!opened) { try { await vscode.env.clipboard.writeText(message.url); copied = true; } catch { /* accurate feedback below */ } } + if (current()) { void session.panel?.webview.postMessage({ type: "linkResult", key: operation.key, generation: operation.generation, + message: opened ? t("Opened in your browser.") : copied ? t("Could not open the browser. Link copied to clipboard.") : t("Could not open the browser or copy the link: {0}", message.url) }); } return; } case "add": { - if (session.busy || session.id === undefined) { - return; - } + const model = session.model; + if (session.busy || !model || model.blockedReason || model.preview || !model.currentVersions.includes(message.version)) { return; } + const state = model.dependencyGroups?.[message.dev ? "dev" : "normal"] ?? model.projectState; + const other = model.dependencyGroups?.[message.dev ? "normal" : "dev"]; + if (state?.ambiguous || state?.declared && state.sourceType !== "registry" || !state?.declared && other?.declared) { return; } session.busy = true; - let result: DetailResult; + session.result = { state: "pending", message: t("Adding dependency…") }; + paintModel(session, { ...model, busy: true, result: session.result }); try { - const outcome = await addDependency( - { - mcppExecutable: session.deps.mcppExecutable, - projectRoot: session.deps.projectRoot, - output: session.deps.output, - isTrusted: session.deps.isTrusted, - }, - { id: session.id, version: message.version, dev: message.dev }, - ); - result = { state: outcome.ok ? "ok" : "error", message: outcome.message }; - if (outcome.ok) { - session.deps.onDependenciesChanged?.(); + const executable = session.deps.mcppExecutable(operation.projectRoot); + const snapshot = await loadSnapshot({ projectRoot: operation.projectRoot }); + const declarations = readProjectDeclarations(operation.projectRoot ? readTextTolerantly(path.join(operation.projectRoot, "mcpp.toml")) : ""); + const catalog = snapshot.entries.map((entry) => entry.id); + const fresh = projectDependencyState(model.id, declarations, [], catalog, message.dev ? "dev" : "normal"); + const otherGroup = projectDependencyState(model.id, declarations, [], catalog, message.dev ? "normal" : "dev"); + if (fresh.ambiguous || fresh.declared && fresh.sourceType !== "registry" || !fresh.declared && otherGroup.declared) { + if (current()) { session.result = { state: "error", message: t("The dependency declaration changed. Review mcpp.toml before adding it.") }; } + return; + } + const outcome = await addDependency({ + mcppExecutable: () => executable, + projectRoot: () => operation.projectRoot, + isTrusted: session.deps.isTrusted, + output: session.deps.output, + quiet: true, + }, { id: model.id, version: message.version, dev: message.dev }); + if (outcome.ok) { session.deps.onDependenciesChanged?.(); } + if (current() && session.panel) { + session.result = { state: outcome.ok ? "ok" : "error", message: outcome.message }; + } else { + void (outcome.ok ? vscode.window.showInformationMessage(outcome.message) : vscode.window.showErrorMessage(outcome.message)); } + } catch (error) { + session.deps.output.appendLine(String(error)); + if (current()) { session.result = { state: "error", message: t("Could not add the dependency. See output for details.") }; } } finally { session.busy = false; + if (session.panel && session.id) { await render(session, session.id); } } - if (session.panel === undefined) { - return; - } - postResult(session, result, result.state === "ok" ? message.version : undefined); return; } - default: - return; } } -/** - * Open a link, and say what happened. - * - * This used to be `void vscode.env.openExternal(…)`: the boolean it resolves to - * was dropped, so a host with no browser (or no way to reach one) answered a - * click with nothing at all — indistinguishable from a dead button. Now the page - * is told either way, a failure is copied to the clipboard so the click is still - * worth something, and the output channel keeps a record. - */ -async function openExternal(session: DetailSession, url: string): Promise { - let opened = false; - try { - opened = await vscode.env.openExternal(vscode.Uri.parse(url)); - } catch (error) { - session.deps.output?.appendLine( - `mcpp library: opening ${url} failed: ${error instanceof Error ? error.message : String(error)}`, - ); - } - if (opened) { - postResult(session, { state: "ok", message: t("Opened {0} in your browser.", url) }); - return; - } - session.deps.output?.appendLine(`mcpp library: VS Code could not open ${url}; copying it instead.`); - try { - await vscode.env.clipboard.writeText(url); - } catch { - // A clipboard that refuses is not worth a second error. +function paintModel(session: DetailSession, model: DetailModel, replay = false): void { + const panel = session.panel; + if (!panel) { return; } + session.model = model; + const html = renderDetailHtml(model, session.webviewDocument.assets(vscode.Uri.joinPath(session.context.extensionUri, MEDIA_DIRECTORY), panel.webview)); + if (session.lastHtml === html && !replay) { return; } + session.lastHtml = html; + if (!session.painted && !replay) { + session.webviewDocument.paint(panel.webview, html); session.painted = true; + } else { + void panel.webview.postMessage({ type: "detailUpdate", html, state: detailClientState(model) }); } - postResult(session, { - state: "error", - message: t("VS Code could not open {0}. The link is on your clipboard.", url), - }); -} - -/** - * The page's status line, in the same shape the add flow uses. - * - * `added` tells the page that the project now depends on that version, so it can - * move its "added" marker and re-label the button without rebuilding the - * document — which would throw away the reader's scroll position and their - * version selection, the one thing running `mcpp add` must not do. - */ -function postResult(session: DetailSession, result: DetailResult, added?: string): void { - void session.panel?.webview.postMessage({ - type: "result", - result, - ...(added === undefined ? {} : { added: { version: added } }), - }); } async function render(session: DetailSession, id: string): Promise { const panel = session.panel; - if (panel === undefined) { - return; - } - const model = await buildModel(session, id); - if (session.panel !== panel || session.id !== id) { - return; + const generation = session.generation; + const request = session.renderRequest = (session.renderRequest ?? 0) + 1; + const root = session.projectRoot; + if (!panel) { return; } + let model: DetailModel; + try { model = await buildModel(session, id, request); } + catch (error) { + session.deps.output.appendLine(String(error)); + model = emptyModel(path.basename(id, ".lua"), labels(), t("Could not read package details. See output for details.")); + model.blockedReason = model.parseNotice; + model.result = { state: "error", message: model.parseNotice ?? "" }; } - const html = renderDetailHtml( - model, - session.webviewDocument.assets( - vscode.Uri.joinPath(session.context.extensionUri, MEDIA_DIRECTORY), - panel.webview, - ), - ); - session.webviewDocument.paint(panel.webview, html); + if (session.panel !== panel || session.id !== id || generation !== session.generation || request !== session.renderRequest || root !== session.projectRoot) { return; } + paintModel(session, { ...model, key: id, generation, busy: session.busy, result: session.result ?? model.result }); } -async function buildModel(session: DetailSession, id: string): Promise { - const projectRoot = session.deps.projectRoot(); +async function buildModel(session: DetailSession, id: string, request: number): Promise { + const projectRoot = session.projectRoot; + const generation = session.generation; const snapshot = await loadSnapshot(projectRoot === undefined ? {} : { projectRoot }); - const entry = snapshot.entries.find((candidate) => candidate.id === id); + const entry = snapshot.entries.find((candidate) => candidate.file === id || candidate.id === id); // The root the descriptor lives under, matched by path: two roots may share a // directory name, and the site link belongs to the one that holds the file. - const root = entry === undefined ? undefined : snapshot.roots.find((candidate) => entry.file.startsWith(candidate.path)); + const root = entry === undefined ? undefined : snapshot.roots.find((candidate) => { const relative = path.relative(candidate.path, entry.file); return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); }); const ui = labels(); if (entry === undefined) { - return emptyModel(id, ui, t("This package is not in the local index any more.")); + const preview = session.preview; + const model = emptyModel(preview?.id ?? path.basename(id, ".lua"), ui, preview ? t("This is a search preview. Set an index path containing this package to read its complete details.") : t("This package is not in the local index any more.")); + model.preview = preview !== undefined; + model.previewVersion = preview?.version; + model.description = preview?.description; + model.registry = preview?.source ?? ""; + model.projectRoot = projectRoot; + model.blockedReason = preview ? t("Search previews cannot be added. Open a complete local descriptor first.") : t("The package descriptor is missing. Reread the local index."); + return model; } - const text = cachedDescriptorText(id) ?? (await readDescriptorText(entry.file)) ?? ""; + const text = cachedDescriptorText(entry.file) ?? (await readDescriptorText(entry.file)) ?? ""; const platform = platformKey(process.platform); // Keyed by the index revision as well as the path, so a refreshed index is not // answered from a parse of the previous one. - const cacheKey = `${snapshot.revision}\u0000${entry.file}`; + const cacheKey = `${snapshot.revision}\u0000${entry.file}\u0000${session.deps.isTrusted()}\u0000${session.deps.mcppExecutable(projectRoot)}\u0000${projectRoot ?? ""}`; let info = parsed.get(cacheKey); if (!parsed.has(cacheKey)) { - info = await readXpkg(session, entry); - parsed.set(cacheKey, info); + if (session.id === id && session.generation === generation && session.renderRequest === request && session.model?.loading) { + const lightweight: DetailModel = { + ...emptyModel(entry.id, ui, t("Reading package details…")), + key: id, generation, projectRoot, platform: platform ?? process.platform, + description: entry.description, registry: entry.registry, surface: entry.surface, surfaces: entry.surfaces, + licenses: entry.licenses, repo: entry.repo, badges: badgesOf(entry), + currentVersions: entry.versions[platform ?? ""] ?? [], latest: entry.version, + versions: versionGroups(entry.versions, platform).map((group) => ({ ...group, current: group.platform === platform })), + loading: true, blockedReason: t("Reading package details…"), dataSource: dataSource(entry, snapshot.roots.length), + }; + paintModel(session, lightweight); + } + info = await readXpkg(session, entry, projectRoot); + if (info !== undefined) { parsed.set(cacheKey, info); } + while (parsed.size > 100) { parsed.delete(parsed.keys().next().value!); } } const versions = info === undefined ? entry.versions : info.versions; @@ -308,44 +385,69 @@ async function buildModel(session: DetailSession, id: string): Promise ({ + file: file.path, startLine: 1, usageLine: 0, lines: file.text.split(/\r?\n/).slice(0, 80), + fileKey: session.exampleDocuments.capture(file), + })); + // Only verified lines from real example source are copyable. const realUsage = usageLinesFor(exampleFiles, entry.id); - const usage = realUsage.length > 0 ? realUsage : syntheticUsageLines(mergeSurfaces(info, text), entry.id); + const usage = realUsage; // The lock answers what a build actually resolved for each declared edge — // the same matching rule `installedFor` uses for this package itself. - const lock = lockPackageVersions(readInstalledLock(session)); + const lockText = projectRoot ? readTextTolerantly(path.join(projectRoot, "mcpp.lock")) : ""; + const manifestText = projectRoot ? readTextTolerantly(path.join(projectRoot, "mcpp.toml")) : ""; + if (projectRoot) { session.deps.onProjectFilesRead?.(projectRoot, manifestText, lockText); } + const lock = lockPackageVersions(lockText); + const declarations = readProjectDeclarations(manifestText); + const catalog = snapshot.entries.map((entry) => entry.id); + const dependencyGroups = { + normal: projectDependencyState(entry.id, declarations, lock, catalog, "normal"), + dev: projectDependencyState(entry.id, declarations, [], catalog, "dev"), + build: projectDependencyState(entry.id, declarations, [], catalog, "build"), + }; + const dependencyKind = dependencyGroups.normal.declared ? "normal" : dependencyGroups.dev.declared ? "dev" : dependencyGroups.build.declared ? "build" : "normal"; + const state = dependencyGroups[dependencyKind]; const dependencies = descriptorDependencies(text).map((dependency) => { - const short = dependency.id.slice(dependency.id.lastIndexOf(".") + 1); - const resolved = lock.find((entry) => entry.id === dependency.id || entry.id === short)?.version; - return resolved === undefined ? { ...dependency } : { ...dependency, resolved }; + const resolved = lock.find((entry) => entry.id === dependency.id)?.version; + const local = snapshot.entries.find((entry) => entry.id === dependency.id); + return { ...dependency, ...(resolved ? { resolved } : {}), ...(local ? { key: local.file } : {}) }; }); const model: DetailModel = { ui, + projectRoot, + manifestAvailable: !!projectRoot && existsSync(path.join(projectRoot, "mcpp.toml")), + platform: platform ?? process.platform, + projectState: state, + dependencyGroups, + dependencyKind, + blockedReason: !session.deps.isTrusted() ? t("Trust this workspace to run mcpp and add dependencies.") + : !projectRoot || !existsSync(path.join(projectRoot, "mcpp.toml")) ? t("Select an mcpp project before adding this library.") + : platform === undefined ? t("The current environment platform is not recognized by this index.") + : latest === undefined ? info === undefined ? t("Version availability could not be confirmed. Retry reading this descriptor.") : t("No version is published for the current environment platform.") + : undefined, + versionNotice: info === undefined ? t("Versions are from descriptor text; mcpp will confirm availability when adding.") : undefined, id: entry.id, name: entry.name, ...(entry.description === undefined ? {} : { description: entry.description }), licenses: entry.licenses, ...(entry.repo === undefined ? {} : { repo: entry.repo }), registry: entry.registry, - ...(entry.surface === undefined ? {} : { surface: entry.surface }), - surfaces: entry.surfaces, + ...(mergeSurfaces(info, text)[0] === undefined ? {} : { surface: mergeSurfaces(info, text)[0] }), + surfaces: mergeSurfaces(info, text), badges: badgesOf(entry), versions: groups, currentVersions: sortVersions(currentVersions), ...(latest === undefined ? {} : { latest }), - // §22: the page finally learns what the project already has. Without this - // the button said "Add" to a package mcpp.toml listed all along. - ...readInstalled(session, entry.id), ...(info?.standard === undefined ? {} : { standard: info.standard }), dependencies, includeDirs: info?.includeDirs ?? [], targets: (info?.targets ?? []).map((target) => target.name ?? "").filter((name) => name.length > 0), snippets, - ...(entry.example === undefined ? {} : { exampleProject: entry.example.project }), + ...(bundled ? { exampleProject: bundled.project, exampleProvenance: t("Bundled official index tests · commit {0}", bundled.commit.slice(0, 12)) } : entry.example ? { exampleProject: entry.example.project } : {}), usage, // One discreet link, and only when this root really is the index that // publishes those package pages (see `IndexRoot.site`). @@ -367,13 +469,13 @@ async function buildModel(session: DetailSession, id: string): Promise { +async function readXpkg(session: DetailSession, entry: LibraryEntry, projectRoot?: string): Promise { // `mcpp.path` is resource-scoped, so an untrusted workspace must not name // the program: the refusal degrades to the descriptor's own text. const result = await vscode.window.withProgress( { location: vscode.ProgressLocation.Window, title: t("Reading {0}…", entry.id) }, () => - runMcpp(session.deps.isTrusted(), session.deps.mcppExecutable(), ["xpkg", "parse", entry.file, "--json"], session.deps.projectRoot(), { + runMcpp(session.deps.isTrusted(), session.deps.mcppExecutable(projectRoot), ["xpkg", "parse", entry.file, "--json"], projectRoot, { timeoutMs: PARSE_TIMEOUT_MS, maxBufferMiB: read("mcpp.runtime.maxOutputMiB"), }), @@ -424,42 +526,41 @@ function emptyModel(id: string, ui: Record, notice: string): Det }; } -/** - * The workspace's own answer about one package (§20.1, wired in §22): - * `mcpp.toml` first, `mcpp.lock` second, both read tolerantly — a missing or - * unreadable file is simply "not installed", never a broken page. The spread - * sets `installed` only when there is one. - */ -function readInstalled(session: DetailSession, id: string): { installed?: DetailInstalled } { - const root = session.deps.projectRoot(); - if (root === undefined) { - return {}; - } - const toml = readTextTolerantly(path.join(root, "mcpp.toml")); - const lock = readTextTolerantly(path.join(root, "mcpp.lock")); - const installed = installedFor(id, toml, lock); - return installed === undefined ? {} : { installed }; -} - -/** An unreadable or missing file reads as empty text; the view degrades, not breaks. */ function readTextTolerantly(file: string): string { - try { - return readFileSync(file, "utf8"); - } catch { - return ""; - } -} - -/** The workspace's `mcpp.lock` text, empty when there is none to read. */ -function readInstalledLock(session: DetailSession): string { - const root = session.deps.projectRoot(); - return root === undefined ? "" : readTextTolerantly(path.join(root, "mcpp.lock")); + try { return readFileSync(file, "utf8"); } catch { return ""; } } /** Every visible string, resolved once per model. */ function labels(): Record { return { [DETAIL_UI.htmlLang]: htmlLanguage(), + [DETAIL_UI.platforms]: t("Platforms and versions"), + [DETAIL_UI.kind]: t("Dependency type"), + [DETAIL_UI.kindNormal]: t("Project dependency"), + [DETAIL_UI.kindDev]: t("Development dependency"), + [DETAIL_UI.kindBuild]: t("Build dependency"), + [DETAIL_UI.addDevAction]: t("Add development dependency"), + [DETAIL_UI.buildHint]: t("Build dependencies are used by the build script. Copy this entry into mcpp.toml; mcpp add does not support this group yet."), + [DETAIL_UI.moveHint]: t("Changing this declaration requires a manifest edit. Copy the entry and update its dependency group in mcpp.toml."), + [DETAIL_UI.chooseProject]: t("Choose project"), + [DETAIL_UI.platform]: t("Environment platform"), + [DETAIL_UI.version]: t("Version"), + [DETAIL_UI.otherPlatforms]: t("Other platforms (read only)"), + [DETAIL_UI.declared]: t("Declared"), + [DETAIL_UI.locked]: t("Resolved in lockfile"), + [DETAIL_UI.sourceDependency]: t("Source dependency"), + [DETAIL_UI.unknownUsage]: t("No verified import or header path is available locally. See the repository or index site for usage."), + [DETAIL_UI.usageSource]: t("From the index's example source"), + [DETAIL_UI.source]: t("Source and diagnostics"), + [DETAIL_UI.loading]: t("Reading package details…"), + [DETAIL_UI.pending]: t("Adding dependency…"), + [DETAIL_UI.groupChange]: t("Changing dependency groups is not supported here. Edit mcpp.toml to move the declaration."), + [DETAIL_UI.output]: t("Show output"), + [DETAIL_UI.retry]: t("Retry reading details"), + [DETAIL_UI.openManifest]: t("Open mcpp.toml"), + [DETAIL_UI.openFile]: t("Source"), + [DETAIL_UI.readOnlySource]: t("Open read-only source"), + [DETAIL_UI.preview]: t("Search preview: only the fields returned by mcpp search are available."), [DETAIL_UI.title]: t("Library package {0}"), [DETAIL_UI.overview]: t("Build shape"), [DETAIL_UI.license]: t("License"), @@ -477,23 +578,23 @@ function labels(): Record { [DETAIL_UI.dependencies]: t("Dependencies"), [DETAIL_UI.dependenciesNone]: t("This descriptor declares no dependencies."), [DETAIL_UI.dependenciesHint]: t( - "What the descriptor declares. The resolved version lives in a project's mcpp.lock, not here.", + "Descriptor-level dependencies; the selected version may differ. Resolved values refer to this project's lockfile.", ), [DETAIL_UI.resolved]: t("resolved {0}"), [DETAIL_UI.code]: t("Example code"), [DETAIL_UI.codeNone]: t("This package has no test project in the index, so there is no example to show."), [DETAIL_UI.codeSource]: t("{0} · line {1}"), [DETAIL_UI.codeProject]: t("Example project: {0} — built and run by the index's CI, not written for this page."), - [DETAIL_UI.add]: t("Add to mcpp.toml"), + [DETAIL_UI.add]: t("Add dependency"), [DETAIL_UI.addDev]: t("dev dependency"), [DETAIL_UI.addLatest]: t("The version is required: mcpp accepts an exact version only."), [DETAIL_UI.switchTo]: t("Switch to {0}"), - [DETAIL_UI.alreadyAdded]: t("Already added"), + [DETAIL_UI.alreadyAdded]: t("Already declared"), [DETAIL_UI.installed]: t("added"), [DETAIL_UI.opening]: t("Opening {0}…"), [DETAIL_UI.addNoVersion]: t("This index publishes no version for this platform, so there is nothing to add."), [DETAIL_UI.command]: t("Command"), - [DETAIL_UI.usage]: t("Bring it into your code"), + [DETAIL_UI.usage]: t("Usage"), [DETAIL_UI.copy]: t("Copy"), [DETAIL_UI.copied]: t("Copied to the clipboard."), [DETAIL_UI.copying]: t("Copying…"), diff --git a/src/library/examples.ts b/src/library/examples.ts new file mode 100644 index 0000000..71a3c00 --- /dev/null +++ b/src/library/examples.ts @@ -0,0 +1,22 @@ +import snapshot from "../../data/library-examples.json"; +import { ReadOnlyDocuments } from "../webview/readOnlyDocuments"; +import { exampleCatalog, type CodeFile } from "./indexModel"; + +const catalog = exampleCatalog(snapshot.projects.map((project) => ({ + project: project.project, text: project.text, sources: project.files.map((file) => file.path), +}))); +const files = new Map(snapshot.projects.flatMap((project) => project.files.map((file) => [file.path, file.text] as const))); + +/** Official-index fallback only. Local checkouts use their own current tests. */ +export function bundledExample(id: string): { project: string; files: CodeFile[]; commit: string } | undefined { + const ref = catalog.get(id); + return ref ? { project: ref.project, commit: snapshot.commit, files: ref.paths.map((path) => ({ path, text: files.get(path)! })) } : undefined; +} + +export function bundledExampleText(path: string): string | undefined { return files.get(path.replace(/^\//, "")); } +export const EXAMPLE_SCHEME = "mcpp-example"; + +/** Immutable virtual documents keep local and bundled index sources equally read-only. */ +export class ExampleDocuments extends ReadOnlyDocuments { + constructor(captured?: (uri: string) => void) { super(EXAMPLE_SCHEME, captured); } +} diff --git a/src/library/indexLocator.ts b/src/library/indexLocator.ts index a29770a..afa6291 100644 --- a/src/library/indexLocator.ts +++ b/src/library/indexLocator.ts @@ -1,33 +1,7 @@ -/** - * Where the index lives, and how it is read **cheaply**. - * - * The catalog is a directory of Lua descriptors: - * `/.mcpp/registry/data//pkgs//.lua`. Three - * registries are installed on the reference machine (`mcpplibs` with 239 - * descriptors, `xim-pkgindex`, `xim-pkgindex-local`), and `mcpp index status` - * prints a table of them — which is deliberately **not** parsed: `--format json` - * is an unknown option there, and a second human-output parser is exactly what - * this project forbids. Globbing the data directory under `` gives the - * same answer without parsing anything. - * - * `` is `$MCPP_HOME`, then `~/.mcpp` — every one of them that holds a data - * directory — and when none does, whatever `mcpp self env --format json` - * reports, which is a machine shape and therefore allowed to be read. - * - * Two budgets shape this file: - * - * - **No process per package.** The list is read from the descriptor text - * (`src/library/indexModel.ts`); `mcpp xpkg parse --json` runs on demand, from - * the detail panel, never here. - * - **One read per index revision.** A snapshot is cached and invalidated by a - * cheap revision key (the `pkgs` directory's mtime, the `.mcpp-index-updated` - * marker and the descriptor count), so re-rendering the view after a filter - * change costs a few stats, not 239 file reads. - * - * A missing index is **not** an error: `locateIndexRoots()` returns `[]` and the - * caller renders the message this module hands it. +/** Local package discovery. Official indexes are separate from the xlings tool registry. + * Descriptor contents are cached independently from the project's dependency state. */ - +import { createHash } from "node:crypto"; import { promises as fs } from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -35,10 +9,10 @@ import * as vscode from "vscode"; import { runMcpp } from "../cli/process"; import { read } from "../config/access"; +import { projectDependencyState, readProjectDeclarations } from "./projectState"; import { descriptorEntry, descriptorId, - declaredDependencies, exampleCatalog, openkalFacetFor, parseDescriptorLua, @@ -68,6 +42,8 @@ export const INDEX_SITE = "https://mcpplibs.github.io/mcpp-index/packages"; export interface IndexRoot { /** The directory name, e.g. `mcpplibs`. */ registry: string; + kind: "official" | "custom"; + updatedAt?: number; /** The index root: it contains `pkgs`, and may contain `tests/` and `.xpkgindex/`. */ path: string; /** `/pkgs`. */ @@ -124,164 +100,83 @@ async function isFile(target: string): Promise { } /** One index root, or `undefined` when the directory is not one. */ -async function rootAt(directory: string): Promise { +async function rootAt(directory: string, official = false): Promise { const pkgs = path.join(directory, "pkgs"); if (!(await isDirectory(pkgs))) { return undefined; } + directory = await fs.realpath(directory); const registry = path.basename(directory); + const updated = await statSafe(path.join(directory, ".mcpp-index-updated")); const generator = await isFile(path.join(directory, ".xpkgindex", "plugins", "mcpp.py")); return { registry, + kind: official ? "official" : "custom", + ...(updated === undefined ? {} : { updatedAt: updated.mtimeMs }), path: directory, - pkgs, + pkgs: path.join(directory, "pkgs"), hasExamples: await isDirectory(path.join(directory, "tests", "examples")), hasOpenkal: await isFile(path.join(directory, ".xpkgindex", "openkal-compat.json")), - ...(registry === "mcpplibs" || generator ? { site: INDEX_SITE } : {}), + ...(official || generator ? { site: INDEX_SITE } : {}), }; } -/** - * The index roots to read. - * - * `mcpp.library.indexPath` wins when the user set it — it may name one index - * root (a directory with `pkgs/`), a `pkgs` directory itself, or a whole - * `/.mcpp/registry/data` directory of registries. With no setting, the - * default glob is the data directory under each known mcpp home. - * - * **Finding ``.** Two are tried, in order: `$MCPP_HOME` (the variable mcpp - * itself honours) and `~/.mcpp`. Both are listed when both hold something, and - * the view's footer names the registries it read, so a stale second home shows - * up as an extra source rather than as a wrong answer. When *neither* has a - * `registry/data` with anything in it, mcpp is asked directly — `mcpp self env - * --format json` reports `mcppHome`, which is the only authoritative answer for - * an install in a place nothing can guess. That probe costs one process **per - * session** and only runs when the cheap globs came up empty, so the ordinary - * machine never pays for it; it is also skipped in an untrusted workspace, - * because `mcpp.path` is a resource-scoped setting and a workspace must not - * choose a binary to run. - * - * `[]` means "no index found"; the caller turns that into the message rather - * than into an error, because a machine without the index is a normal machine. - * - * (`locateIndexRoots` is kept as an alias: the name reads better at the call - * site, the exported name is the one `extension.ts` uses.) - */ -export async function readIndexRoots(): Promise { +/** Explicit paths never silently fall back. Only the official library source is auto-discovered. */ +export async function readIndexRoots(projectRoot?: string): Promise { const configured = read("mcpp.library.indexPath"); - const candidates: string[] = []; - if (typeof configured === "string" && configured.trim().length > 0) { - const configured_path = path.resolve(expandHome(configured.trim())); - // A `pkgs` directory was named directly. - if (path.basename(configured_path) === "pkgs" && (await isDirectory(configured_path))) { - candidates.push(path.dirname(configured_path)); - } else { - const own = await rootAt(configured_path); - if (own !== undefined) { - candidates.push(own.path); - } else { - // A data directory: every registry inside it. - for (const entry of await readdirSafe(configured_path)) { - candidates.push(path.join(configured_path, entry)); - } - } - } - } else { - for (const data of await defaultDataDirectories()) { - for (const entry of await readdirSafe(data)) { - candidates.push(path.join(data, entry)); - } - } - } + return discoverIndexRoots(configured, configured.trim() ? path.join(os.homedir(), ".mcpp") : await activeHome(projectRoot)); +} - const roots: IndexRoot[] = []; - for (const candidate of candidates) { - const root = await rootAt(candidate); - if (root !== undefined && !roots.some((seen) => seen.path === root.path)) { - roots.push(root); +/** Filesystem seam used by the extension and fixture-based tests. */ +export async function discoverIndexRoots(configured: string, home: string): Promise { + if (configured.trim()) { + const target = path.resolve(expandHome(configured.trim())); + const own = await rootAt(path.basename(target) === "pkgs" ? path.dirname(target) : target); + const roots = own === undefined + ? (await Promise.all((await readdirSafe(target)).sort().map((name) => rootAt(path.join(target, name))))) + .filter((root): root is IndexRoot => root !== undefined) + : [own]; + if (roots.length === 0) { + throw new Error(`No package index (pkgs/) at ${target}`); } + return roots.filter((root, index) => roots.findIndex((other) => other.path === root.path) === index); + } + for (const directory of [path.join(home, "indices", "mcpplibs"), path.join(home, "registry", "data", "mcpplibs")]) { + const root = await rootAt(directory, true); + if (root !== undefined) { return [root]; } } - return roots.sort((a, b) => (a.registry < b.registry ? -1 : a.registry > b.registry ? 1 : 0)); + return []; } -/** Set once per session, so `mcpp self env` runs at most once. */ -let probedHome: string | undefined; -let probeAttempted = false; +let homeProbe: { key: string; promise: Promise } | undefined; -/** The homes mcpp may have used, most specific first, without duplicates. */ -function candidateHomes(): string[] { - const homes: string[] = []; - const fromEnvironment = process.env.MCPP_HOME?.trim(); - if (fromEnvironment !== undefined && fromEnvironment.length > 0) { - homes.push(fromEnvironment); - } - homes.push(path.join(os.homedir(), ".mcpp")); - if (probedHome !== undefined) { - homes.push(probedHome); - } - return [...new Set(homes)]; +/** CLI also supports self-contained installs: ask it once rather than merge unrelated homes. */ +async function activeHome(projectRoot?: string): Promise { + const explicit = process.env.MCPP_HOME?.trim(); + if (explicit) { return path.resolve(expandHome(explicit)); } + return (await mcppHomeFromCli(projectRoot)) ?? path.join(os.homedir(), ".mcpp"); } -/** - * The `/registry/data` directories worth listing. - * - * The cheap answer first: a data directory that already holds something is the - * answer, and no process is started. Only when all of them are missing or empty - * is `mcpp self env` asked where its home is — and its answer is picked up by - * re-reading the candidate list, because the probe is what adds the home to it. - */ -async function defaultDataDirectories(): Promise { - const candidates = (): string[] => candidateHomes().map((home) => path.join(home, "registry", "data")); - const present: string[] = []; - for (const data of candidates()) { - if ((await readdirSafe(data)).length > 0) { - present.push(data); - } - } - if (present.length > 0) { - return present; - } - await mcppHomeFromCli(); - return [...new Set(candidates())]; +export function invalidateIndexCache(): void { + cache = undefined; + homeProbe = undefined; } -/** - * `mcpp self env --format json`, for the one field that says where the index is. - * - * It runs at most once per session and never in an untrusted workspace. A - * missing or failing mcpp is not an error here: the caller falls back to the - * directories it already has, and the view says "no index" the way it does when - * there is none. - */ -async function mcppHomeFromCli(): Promise { - if (probeAttempted) { - return probedHome; - } - probeAttempted = true; - const executable = read("mcpp.path").trim(); - // `mcpp.path` is resource-scoped, so the probe goes through the trust-gated - // seam; a refusal is the same "no answer" as a failing mcpp. - const result = await runMcpp( - vscode.workspace.isTrusted, - executable.length === 0 ? "mcpp" : executable, - ["self", "env", "--format", "json"], - undefined, - { timeoutMs: SELF_ENV_TIMEOUT_MS, maxBufferMiB: 1 }, - ); - if (result === undefined || result.exitCode !== 0) { - return undefined; - } - const env = parseSelfEnv(result.stdout); - if (env === undefined) { - return undefined; - } - if (env.mcppHome !== undefined) { - probedHome = env.mcppHome; - } else if (env.registry !== undefined) { - // `registry` is `/registry`; the home is what the other paths hang off. - probedHome = path.dirname(env.registry); - } - return probedHome; +/** Share concurrent probes, with separate answers for different configured executables/projects. */ +async function mcppHomeFromCli(projectRoot?: string): Promise { + const resource = projectRoot === undefined ? undefined : vscode.Uri.file(projectRoot); + const executable = read("mcpp.path", resource).trim() || "mcpp"; + const key = `${executable}\u0000${projectRoot ?? ""}\u0000${vscode.workspace.isTrusted}`; + if (homeProbe?.key === key) { return homeProbe.promise; } + const promise = (async () => { + const result = await runMcpp(vscode.workspace.isTrusted, executable, ["self", "env", "--format", "json"], projectRoot, + { timeoutMs: SELF_ENV_TIMEOUT_MS, maxBufferMiB: 1 }); + if (result === undefined || result.exitCode !== 0) { return undefined; } + const env = parseSelfEnv(result.stdout); + return env?.mcppHome ?? (env?.registry === undefined ? undefined : path.dirname(env.registry)); + })(); + homeProbe = { key, promise }; + return promise; } export { readIndexRoots as locateIndexRoots }; @@ -324,23 +219,15 @@ async function descriptorFiles(pkgs: string): Promise { return out.sort(); } -/** - * A cheap key that changes when the index does. - * - * The `pkgs` directory's own mtime catches an added or removed letter - * directory; `.mcpp-index-updated` is written whenever mcpp refreshes the index, - * which catches a changed descriptor body. The descriptor count is included so a - * tree that stops being updated still costs one full read when its size changes. - */ +/** Include source identity and every descriptor stat, including checkout edits without a marker. */ async function revisionOf(roots: readonly IndexRoot[]): Promise { const parts: string[] = []; for (const root of roots) { - const pkgs = await statSafe(root.pkgs); - const marker = await statSafe(path.join(root.path, ".mcpp-index-updated")); - const letters = await readdirSafe(root.pkgs); - parts.push(`${root.registry}:${pkgs?.mtimeMs ?? 0}:${marker?.mtimeMs ?? 0}:${letters.length}`); + const files = await descriptorFiles(root.pkgs); + const stats = await Promise.all(files.map(async (file) => `${file}:${JSON.stringify(await statSafe(file))}`)); + parts.push(`${root.path}:${root.kind}:${root.updatedAt ?? ""}:${stats.join(";")}`); } - return parts.join("|"); + return createHash("sha256").update(parts.join("|")).digest("hex"); } async function statSafe(target: string): Promise<{ mtimeMs: number; size: number } | undefined> { @@ -416,17 +303,14 @@ async function readOpenkal(roots: readonly IndexRoot[]): Promise> { - if (projectRoot === undefined) { - return new Set(); - } - try { - const text = await fs.readFile(path.join(projectRoot, "mcpp.toml"), "utf8"); - return new Set(declaredDependencies(text)); - } catch { - return new Set(); - } +/** Project overlay is always fresh and never changes the descriptor cache. */ +async function withProject(snapshot: LibrarySnapshot, projectRoot?: string): Promise { + const text = projectRoot === undefined ? "" : await fs.readFile(path.join(projectRoot, "mcpp.toml"), "utf8").catch(() => ""); + const declarations = readProjectDeclarations(text); + const ids = snapshot.entries.map((entry) => entry.id); + return { ...snapshot, entries: snapshot.entries.map((entry) => ({ + ...entry, added: (["normal", "dev", "build"] as const).some((kind) => projectDependencyState(entry.id, declarations, [], ids, kind).declared), + })) }; } interface Cache { @@ -441,15 +325,14 @@ let cache: Cache | undefined; /** * The list, read from disk only when the revision changed. * - * `projectRoot` is the workspace folder whose `mcpp.toml` decides the "already - * declared" state; it is part of the cache key, because switching workspaces - * changes the answer without changing the index. + * Only descriptor contents are cached. Project declarations are reapplied on + * every read, so disk writes and project switches cannot reuse old badges. */ export async function loadSnapshot(options: { projectRoot?: string; platform?: string } = {}): Promise { - const roots = await readIndexRoots(); - const revision = `${await revisionOf(roots)}|${options.projectRoot ?? ""}`; + const roots = await readIndexRoots(options.projectRoot); + const revision = `${await revisionOf(roots)}|${options.platform ?? platformKey(process.platform)}`; if (cache !== undefined && cache.revision === revision) { - return cache.snapshot; + return withProject(cache.snapshot, options.projectRoot); } if (roots.length === 0) { const empty: LibrarySnapshot = { entries: [], roots, revision }; @@ -458,21 +341,18 @@ export async function loadSnapshot(options: { projectRoot?: string; platform?: s } const platform = options.platform ?? platformKey(process.platform); - const added = await readAdded(options.projectRoot); const entries: LibraryEntry[] = []; const texts = new Map(); /** Example refs, keyed by package id, from every root that has examples. */ const exampleRefs = new Map(); for (const root of roots) { for (const [id, ref] of exampleCatalog(await readExamples(root))) { - if (!exampleRefs.has(id)) { - exampleRefs.set(id, ref); - } + exampleRefs.set(`${root.path}\u0000${id}`, ref); } } - const openkal = await readOpenkal(roots); for (const root of roots) { + const openkal = await readOpenkal([root]); for (const file of await descriptorFiles(root.pkgs)) { let text: string; try { @@ -485,7 +365,7 @@ export async function loadSnapshot(options: { projectRoot?: string; platform?: s // `descriptorEntry` decides the identity; the example and openkal lookups // below must use that same id, so it is computed the same way here. const id = descriptorId(fields, fileName).id; - const example = exampleRefs.get(id); + const example = exampleRefs.get(`${root.path}\u0000${id}`); const facet = openkalFacetFor(openkal, id); const entry = descriptorEntry({ fileName, @@ -493,19 +373,19 @@ export async function loadSnapshot(options: { projectRoot?: string; platform?: s file, text, platform, - added: added.has(id), + added: false, ...(example === undefined ? {} : { example }), ...(facet === undefined ? {} : { openkal: facet }), }); entries.push(entry); - texts.set(entry.id, text); + texts.set(entry.file, text); } } entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); const snapshot: LibrarySnapshot = { entries, roots, revision }; cache = { revision, snapshot, texts }; - return snapshot; + return withProject(snapshot, options.projectRoot); } /** The descriptor text of one package, from the cache when possible. */ @@ -518,8 +398,8 @@ export async function readDescriptorText(file: string): Promise { const catalog = new Map(); + const priorities = new Map(); const sorted = [...inputs].sort((a, b) => (a.project < b.project ? -1 : a.project > b.project ? 1 : 0)); for (const input of sorted) { if (input.sources.length === 0) { @@ -949,8 +950,12 @@ export function exampleCatalog(inputs: readonly ExampleInput[]): Map; rows: LibraryRow[]; + selectedKey?: string; query: string; networkSearch: boolean; /** @@ -71,6 +74,9 @@ export interface LibraryModel { networkSearchSetting: string; /** The data source, in plain words, for the footer. */ dataSource: string; + sourceDetails?: string; + status?: string; + projectNotice?: string; /** `{0}` visible of `{1}` known, already localized; the header count uses it. */ countTemplate: string; /** Rows before filtering; the count line shows it against what is visible. */ @@ -112,6 +118,12 @@ export const LIBRARY_UI = { count: "library.count", open: "library.open", refresh: "library.refresh", + sources: "library.sources", + other: "library.other", + settings: "library.settings", + update: "library.update", + output: "library.output", + clear: "library.clear", } as const; /** One badge key's `ui` key, so the renderer has a single lookup table. */ @@ -124,23 +136,13 @@ const BADGE_KEY_UI: Readonly> = { "openkal-platform": LIBRARY_UI.badgeOpenkalPlatform, }; -/** - * What the document says to the host. - * - * There deliberately **no `ready` message**. The document is the data — every - * row, badge and count is already in it — so a "I have loaded" announcement can - * only invite the host to render the same thing again. It did exactly that, and - * because assigning `webview.html` reloads the document while every render mints - * a fresh CSP nonce, the page never matched the previous one: the view reloaded - * itself forever (flicker, unclickable rows, a pegged CPU). The host now also - * refuses to re-assign an identical document — both rules live in - * `src/webview/document.ts` (`WebviewDocument`) and `src/webview/render.ts` - * (`documentNeedsRender`) — so this cannot come back by accident. +/** A mounted handshake replays data in place after the initial document loads. + * The host never assigns webview.html in response, so it cannot create a reload loop. */ export type LibraryMessage = - | { type: "refresh" } + | { type: "refresh" | "settings" | "update" | "output" | "mounted" } | { type: "search"; query: string } - | { type: "networkSearch"; enabled: boolean } + | { type: "networkSearch"; enabled: boolean; token?: string } | { type: "open"; id: string }; export type UiLabel = (key: string) => string; @@ -187,7 +189,7 @@ export function badgeLabels(badges: readonly BadgeKey[], label: UiLabel): string } function renderRow(row: LibraryRow, label: UiLabel): string { - const badges = badgeLabels(row.badges, label); + const badges: string[] = []; const openTitle = fill(label, LIBRARY_UI.open, [row.id]); const surface = row.surface === undefined ? undefined : surfaceLabel(row.surface, label); const version = @@ -197,14 +199,14 @@ function renderRow(row: LibraryRow, label: UiLabel): string { ...badges.map((badge) => `${escapeHtml(badge)}`), row.added ? `${escapeHtml(label(LIBRARY_UI.added))}` : "", row.unreadable ? `${escapeHtml(label(LIBRARY_UI.unreadable))}` : "", - row.crossRegistry === true ? `${escapeHtml(label(LIBRARY_UI.crossRegistry))}` : "", + row.crossRegistry === true || row.source ? `${escapeHtml(row.source ?? label(LIBRARY_UI.crossRegistry))}` : "", ] .filter((part) => part.length > 0) .join(""); const description = row.description === undefined ? "" : row.description; return [ `
  • `, - ` `, ` `, + ` ${escapeHtml(fill(label, LIBRARY_UI.count, [model.rows.filter((row) => !row.crossRegistry && rowMatchesClient(model, row)).length, model.rows.filter((row) => !row.crossRegistry).length]))}`, ``, ].join("\n"); } function renderList(model: LibraryModel, label: UiLabel): string { - if (model.rows.length === 0) { - return `

    ${escapeHtml(model.notice ?? label(LIBRARY_UI.noResults))}

    `; - } - return [ - `
      `, - model.rows.map((row) => renderRow(row, label)).join("\n"), - `
    `, - ``, - ].join("\n"); + const local = model.rows.filter((row) => !row.crossRegistry); + const extra = model.rows.filter((row) => row.crossRegistry); + return `
    + ${model.projectNotice ? `

    ${escapeHtml(model.projectNotice)}

    ` : ""} +
      ${local.map((row) => renderRow(row, label)).join("\n")}
    +
    +

    ${escapeHtml(label(LIBRARY_UI.other))} (${extra.length})

    +
      ${extra.map((row) => renderRow(row, label)).join("\n")}
    +
    +

    ${escapeHtml(model.notice ?? label(LIBRARY_UI.noResults))}

    +

    ${escapeHtml(model.status ?? "")}

    +
    `; } /** @@ -272,72 +279,98 @@ function clientScript(initialModel: string): string { "use strict"; var api = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : undefined; var state = ${initialModel}; + var saved = api && api.getState ? api.getState() || {} : {}; var searchInput = document.getElementById("library-search"); var networkInput = document.getElementById("library-network"); - var list = document.getElementById("library-list"); - var empty = document.getElementById("library-empty"); - var countElement = document.getElementById("library-count"); - - function post(message) { - if (api) { api.postMessage(message); } + var networkToken; + var networkSequence = 0; + var selected = state.selectedKey || saved.selected || ""; + if (searchInput && typeof saved.query === "string") { searchInput.value = saved.query; } + function post(message) { if (api) { api.postMessage(message); } } + function persist() { + if (api && api.setState) { api.setState({ query: searchInput.value, selected: selected, scroll: window.scrollY }); } } - - // Every whitespace-separated word has to appear in the row's haystack, which - // the host builds from the id, the name, the description and the usage labels. - function rowMatches(row, query) { - if (query.length === 0) { return true; } - var haystack = row.getAttribute("data-haystack") || ""; - var parts = query.split(/\\s+/); - for (var index = 0; index < parts.length; index += 1) { - if (parts[index].length > 0 && haystack.indexOf(parts[index]) < 0) { return false; } - } - return true; - } - function apply() { - var query = searchInput ? searchInput.value.trim().toLowerCase() : ""; - var any = false; - var visible = 0; + document.querySelector('[data-action="clear"]').hidden = searchInput.value.length === 0; + var query = searchInput.value.trim().toLowerCase(); + var parts = query.split(/\\s+/); + var rows = Array.from(document.querySelectorAll("[data-row]")); + var local = 0; + var extra = 0; + rows.forEach(function (row) { + var haystack = row.getAttribute("data-haystack") || ""; + var shown = parts.every(function (part) { return !part || haystack.indexOf(part) >= 0; }); + row.parentNode.hidden = !shown; + if (shown) { if (row.getAttribute("data-cross") === "true") { extra++; } else { local++; } } + if (row.getAttribute("data-row") === selected) { row.setAttribute("aria-current", "true"); } + else { row.removeAttribute("aria-current"); } + }); + var list = document.getElementById("library-list"); if (list) { - var rows = list.querySelectorAll("[data-row]"); - for (var index = 0; index < rows.length; index += 1) { - var shown = rowMatches(rows[index], query); - rows[index].parentNode.hidden = !shown; - if (shown) { any = true; visible += 1; } - } - list.hidden = !any; - } - if (empty) { empty.hidden = any || !list; } - if (countElement) { - var template = state && state.countTemplate ? state.countTemplate : ""; - var total = state && state.total ? state.total : 0; - countElement.textContent = template.split("{0}").join(String(visible)).split("{1}").join(String(total)); + Array.from(list.children).sort(function (a, b) { + function rank(item) { + var id = item.firstElementChild.getAttribute("data-id").toLowerCase(); + var name = id.slice(id.lastIndexOf(".") + 1); + return !query ? 2 : id === query || name === query ? 0 : id.indexOf(query) === 0 || name.indexOf(query) === 0 ? 1 : 2; + } + return rank(a) - rank(b) || a.firstElementChild.getAttribute("data-id").localeCompare(b.firstElementChild.getAttribute("data-id")); + }).forEach(function (item) { list.appendChild(item); }); } + var empty = document.getElementById("library-empty"); + if (empty) { empty.hidden = local + extra > 0; } + var group = document.getElementById("library-extra"); + if (group) { group.hidden = extra === 0; } + document.getElementById("library-count").textContent = state.countTemplate.split("{0}").join(String(local)).split("{1}").join(String(state.localTotal)); + persist(); } - - if (searchInput) { - searchInput.addEventListener("input", function () { - apply(); - post({ type: "search", query: searchInput.value }); - }); - } - if (networkInput) { - networkInput.addEventListener("change", function () { - post({ type: "networkSearch", enabled: networkInput.checked === true }); - }); - } + searchInput.addEventListener("input", function () { apply(); post({ type: "search", query: searchInput.value }); }); + networkInput.addEventListener("change", function () { networkToken = Date.now() + "-" + (++networkSequence); post({ type: "networkSearch", enabled: networkInput.checked, token: networkToken }); }); document.addEventListener("click", function (event) { - var target = event.target; - if (!target || typeof target.closest !== "function") { return; } - var refresh = target.closest("[data-action]"); - if (refresh && refresh.getAttribute("data-action") === "refresh") { - post({ type: "refresh" }); - return; + var target = event.target.closest && event.target.closest("[data-action], [data-row]"); + if (!target) { return; } + var action = target.getAttribute("data-action"); + if (action === "clear") { searchInput.value = ""; searchInput.focus(); apply(); post({ type: "search", query: "" }); return; } + if (action) { post({ type: action }); return; } + selected = target.getAttribute("data-row"); apply(); post({ type: "open", id: selected }); + }); + document.addEventListener("keydown", function (event) { + if (event.key === "Escape" && event.target === searchInput) { + searchInput.value = ""; apply(); post({ type: "search", query: "" }); } - var row = target.closest("[data-row]"); - if (row) { post({ type: "open", id: row.getAttribute("data-row") }); } + if (event.key !== "ArrowDown" && event.key !== "ArrowUp") { return; } + var rows = Array.from(document.querySelectorAll("[data-row]")).filter(function (row) { return !row.parentNode.hidden; }); + if (!rows.length || (event.target !== searchInput && !event.target.hasAttribute("data-row"))) { return; } + event.preventDefault(); + var index = rows.indexOf(document.activeElement); + var next = event.key === "ArrowDown" ? Math.min(rows.length - 1, index + 1) : Math.max(0, index - 1); + rows[next].focus(); + }); + window.addEventListener("scroll", persist); + window.addEventListener("message", function (event) { + var payload = event.data; + if (payload && payload.type === "networkSearchSaved") { if (payload.token === networkToken) { networkToken = undefined; networkInput.checked = payload.enabled; } return; } + if (!payload || payload.type !== "libraryUpdate") { return; } + var scroll = window.scrollY; + var focused = document.activeElement && document.activeElement.getAttribute("data-row"); + var details = document.querySelector("footer details"); + var expanded = details && details.open; + var doc = new DOMParser().parseFromString(payload.html, "text/html"); + ["library-results", "library-footer"].forEach(function (id) { + var old = document.getElementById(id); var next = doc.getElementById(id); + if (old && next) { old.replaceWith(next); } + }); + state = payload.state; + if (state.selectedKey) { selected = state.selectedKey; } + if (!networkToken) { networkInput.checked = payload.networkSearch; } + apply(); + var nextDetails = document.querySelector("footer details"); if (nextDetails) { nextDetails.open = expanded; } + if (focused) { Array.from(document.querySelectorAll("[data-row]")).some(function (row) { if (row.getAttribute("data-row") === focused) { row.focus(); return true; } return false; }); } + window.scrollTo(0, scroll); }); apply(); + window.scrollTo(0, saved.scroll || 0); + post({ type: "mounted" }); + post({ type: "search", query: searchInput.value }); })();`; } @@ -348,10 +381,12 @@ function clientScript(initialModel: string): string { * embedding the model again would double the page for nothing. `<` is escaped in * `embedJson`, so even this cannot close the script element. */ -function clientState(model: LibraryModel): Record { +export function libraryClientState(model: LibraryModel): Record { return { countTemplate: model.countTemplate, + selectedKey: model.selectedKey, total: model.total, + localTotal: model.rows.filter((row) => !row.crossRegistry).length, query: model.query, }; } @@ -380,20 +415,24 @@ export function renderLibraryHtml(model: LibraryModel, assets: LibraryAssets): s ${escapeHtml(label(LIBRARY_UI.title))} -
    +

    ${escapeHtml(label(LIBRARY_UI.title))}

    -

    ${escapeHtml(count)}

    ${renderToolbar(model, label)}
    ${renderList(model, label)}
    -
  • + + + diff --git a/mide-plugins/vscode/test/parity/snapshots/0.9.0/detail-installed.html b/mide-plugins/vscode/test/parity/snapshots/0.9.0/detail-installed.html new file mode 100644 index 0000000..82e858b --- /dev/null +++ b/mide-plugins/vscode/test/parity/snapshots/0.9.0/detail-installed.html @@ -0,0 +1,216 @@ + + + + + + + +detail.title + + +
    +

    nlohmann.json

    #includeC++23MIT
    linux
    +

    JSON for Modern C++

    +
    +
    +
    + + +
    dependencies
    +
    +
    + + +
    + + + +

    mcpp add nlohmann.json@3.12.0

    + + +
    + +
    +
    +

    detail.usage

    +

    detail.usageSource

    import nlohmann.json;
    +

    detail.code

    +
    import nlohmann.json;
    +int main() {}
    +
    +

    detail.code.source

    +

    detail.code.project

    + +
    + +
    +
    +
    detail.source

    mcpp xpkg parse

    library.badge.examples

    + + + diff --git a/mide-plugins/vscode/test/parity/snapshots/0.9.0/detail-loading.html b/mide-plugins/vscode/test/parity/snapshots/0.9.0/detail-loading.html new file mode 100644 index 0000000..78a5bec --- /dev/null +++ b/mide-plugins/vscode/test/parity/snapshots/0.9.0/detail-loading.html @@ -0,0 +1,216 @@ + + + + + + + +detail.title + + +
    +

    nlohmann.json

    #includeC++23MIT
    linux
    +

    JSON for Modern C++

    +
    +
    +
    +

    detail.loading

    + +
    dependencies
    +
    +
    + + +
    + + + +

    mcpp add nlohmann.json@3.12.0

    + + +
    + +
    +
    +

    detail.usage

    +

    detail.usageSource

    import nlohmann.json;
    +

    detail.code

    +
    import nlohmann.json;
    +int main() {}
    +
    +

    detail.code.source

    +

    detail.code.project

    + +
    + +
    +
    +
    detail.source

    mcpp xpkg parse

    library.badge.examples

    + + + diff --git a/mide-plugins/vscode/test/parity/snapshots/0.9.0/graph.html b/mide-plugins/vscode/test/parity/snapshots/0.9.0/graph.html new file mode 100644 index 0000000..5833c06 --- /dev/null +++ b/mide-plugins/vscode/test/parity/snapshots/0.9.0/graph.html @@ -0,0 +1,36 @@ + + + +

    Dependency graph

    +
    +
    + + + +
    View options
    + + + +
    +
    +
    Select a node to explore its relationships.
    +
    +
    +
    +
    + + +
    Legend and source
    Projectpath / partitionindex / externalUnavailable

    Packages show declarations; modules show mcppls imports. External modules stop after one hop.

    + + \ No newline at end of file diff --git a/mide-plugins/vscode/test/parity/snapshots/0.9.0/library-empty.html b/mide-plugins/vscode/test/parity/snapshots/0.9.0/library-empty.html new file mode 100644 index 0000000..21ba308 --- /dev/null +++ b/mide-plugins/vscode/test/parity/snapshots/0.9.0/library-empty.html @@ -0,0 +1,145 @@ + + + + + + + +library.title + + +
    +

    library.title

    +
    +
    + + + + + library.count +
    +
    +
    + +
      + +

      No library index was found.

      +

      +
      +
      +
      +
      the local mcpp-index

      /home/user/.mcpp/registry/data/mcpp-index

      + +
      + + + diff --git a/mide-plugins/vscode/test/parity/snapshots/0.9.0/library-list.html b/mide-plugins/vscode/test/parity/snapshots/0.9.0/library-list.html new file mode 100644 index 0000000..cf9dafe --- /dev/null +++ b/mide-plugins/vscode/test/parity/snapshots/0.9.0/library-list.html @@ -0,0 +1,173 @@ + + + + + + + +library.title + + +
      +

      library.title

      +
      +
      + + + + + library.count +
      +
      +
      + +
      • + +
      • +
      • + +
      • +
      • + +
      + + +

      +
      +
      +
      +
      the local mcpp-index

      /home/user/.mcpp/registry/data/mcpp-index

      + +
      + + + diff --git a/mide-plugins/vscode/test/parity/snapshots/0.9.0/settings.html b/mide-plugins/vscode/test/parity/snapshots/0.9.0/settings.html new file mode 100644 index 0000000..beb5972 --- /dev/null +++ b/mide-plugins/vscode/test/parity/snapshots/0.9.0/settings.html @@ -0,0 +1,321 @@ + + + + + + + +panel.title + + +
      +

      panel.title

      +

      panel.boundary

      +

      panel.resourceLabel

      +
      +
      + + + + +
      + +
      +
      +

      + +

      +
      +
      +
      + Set the mcpp executable path + + + + panel.source.default +
      +

      Path to the mcpp CLI.

      + + +
      +
      + + +
      +
      +
      +
      + Reveal the task terminal + panel.modified + + + panel.source.workspace +
      +

      When to show the terminal.

      + +

      panel.applies.nextBuild

      +
      +
      + + +
      +
      + +
      +
      + Build arguments + + + + panel.source.default +
      +

      Extra arguments.

      + +

      panel.applies.nextBuild

      +

      panel.arrayHint

      +
      + + +
      +
      + +
      +
      +
      + + + + From d71f8e2b04101ee42731a9d52bfd3170aabcf7db Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 01:44:06 +0900 Subject: [PATCH 072/119] mcpp-vscode: CI and release as 0.9.0's, in the repository; the 1.0 adapter retired CI: the gates on Linux, macOS and Windows; the manifest generated from spec/ with no drift and the snapshots against mcpp's source; the parity gates; the package with the bundle and every system's okrun; the Extension Host suite on the three systems over the bundled backend, its fake mcpp answering the contract from the recorded fixtures; the isolated install; the graph page in Chromium. A release publishes the VSIX CI built to Open VSX (OPENVSX_TOKEN). `gen vscode` writes the declarations in place; check-i18n leaves the extension's own strings to its l10n gate. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 278 +- .github/workflows/release.yml | 21 + mide-plugins/vscode-1.0/.vscodeignore | 2 - mide-plugins/vscode-1.0/CHANGELOG.md | 18 - mide-plugins/vscode-1.0/README.md | 59 - .../vscode-1.0/media/activity-bar.png | Bin 2426 -> 0 bytes mide-plugins/vscode-1.0/media/logo.png | Bin 31749 -> 0 bytes mide-plugins/vscode-1.0/package-lock.json | 4852 ----------------- mide-plugins/vscode-1.0/package.json | 301 - mide-plugins/vscode-1.0/src/backend/client.ts | 164 - mide-plugins/vscode-1.0/src/backend/locate.ts | 93 - mide-plugins/vscode-1.0/src/backend/mcppls.ts | 15 - mide-plugins/vscode-1.0/src/commands.ts | 211 - mide-plugins/vscode-1.0/src/debug.ts | 66 - mide-plugins/vscode-1.0/src/diagnostics.ts | 88 - mide-plugins/vscode-1.0/src/extension.ts | 84 - mide-plugins/vscode-1.0/src/jobs.ts | 105 - mide-plugins/vscode-1.0/src/mapping.ts | 56 - mide-plugins/vscode-1.0/src/panels.ts | 165 - mide-plugins/vscode-1.0/src/projectTree.ts | 117 - mide-plugins/vscode-1.0/src/projects.ts | 69 - mide-plugins/vscode-1.0/src/protocol.ts | 150 - mide-plugins/vscode-1.0/src/run.ts | 27 - mide-plugins/vscode-1.0/src/session.ts | 183 - mide-plugins/vscode-1.0/src/status.ts | 49 - mide-plugins/vscode-1.0/src/tasks.ts | 115 - mide-plugins/vscode-1.0/src/testing.ts | 94 - mide-plugins/vscode-1.0/src/toml.ts | 81 - .../syntaxes/mcpp-modules.tmLanguage.json | 72 - .../syntaxes/mcpp-toml.tmLanguage.json | 116 - mide-plugins/vscode-1.0/test/e2e/runTest.ts | 49 - .../vscode-1.0/test/e2e/suite/index.ts | 85 - .../vscode-1.0/test/unit/client.test.ts | 38 - mide-plugins/vscode-1.0/test/unit/harness.ts | 39 - .../vscode-1.0/test/unit/locate.test.ts | 55 - .../vscode-1.0/test/unit/mapping.test.ts | 27 - mide-plugins/vscode-1.0/tsconfig.json | 17 - mide-plugins/vscode/package.json | 2 +- mide-plugins/vscode/src/extension.ts | 8 +- mide-plugins/vscode/test/artifacts.test.ts | 79 +- .../vscode/test/e2e/fixtures/fake-mcpp.js | 34 +- mide-plugins/vscode/test/e2e/runTest.ts | 15 +- .../vscode/test/e2e/suite/extension.e2e.ts | 32 + src/bin/dev.cpp | 97 +- 44 files changed, 416 insertions(+), 7812 deletions(-) delete mode 100644 mide-plugins/vscode-1.0/.vscodeignore delete mode 100644 mide-plugins/vscode-1.0/CHANGELOG.md delete mode 100644 mide-plugins/vscode-1.0/README.md delete mode 100644 mide-plugins/vscode-1.0/media/activity-bar.png delete mode 100644 mide-plugins/vscode-1.0/media/logo.png delete mode 100644 mide-plugins/vscode-1.0/package-lock.json delete mode 100644 mide-plugins/vscode-1.0/package.json delete mode 100644 mide-plugins/vscode-1.0/src/backend/client.ts delete mode 100644 mide-plugins/vscode-1.0/src/backend/locate.ts delete mode 100644 mide-plugins/vscode-1.0/src/backend/mcppls.ts delete mode 100644 mide-plugins/vscode-1.0/src/commands.ts delete mode 100644 mide-plugins/vscode-1.0/src/debug.ts delete mode 100644 mide-plugins/vscode-1.0/src/diagnostics.ts delete mode 100644 mide-plugins/vscode-1.0/src/extension.ts delete mode 100644 mide-plugins/vscode-1.0/src/jobs.ts delete mode 100644 mide-plugins/vscode-1.0/src/mapping.ts delete mode 100644 mide-plugins/vscode-1.0/src/panels.ts delete mode 100644 mide-plugins/vscode-1.0/src/projectTree.ts delete mode 100644 mide-plugins/vscode-1.0/src/projects.ts delete mode 100644 mide-plugins/vscode-1.0/src/protocol.ts delete mode 100644 mide-plugins/vscode-1.0/src/run.ts delete mode 100644 mide-plugins/vscode-1.0/src/session.ts delete mode 100644 mide-plugins/vscode-1.0/src/status.ts delete mode 100644 mide-plugins/vscode-1.0/src/tasks.ts delete mode 100644 mide-plugins/vscode-1.0/src/testing.ts delete mode 100644 mide-plugins/vscode-1.0/src/toml.ts delete mode 100644 mide-plugins/vscode-1.0/syntaxes/mcpp-modules.tmLanguage.json delete mode 100644 mide-plugins/vscode-1.0/syntaxes/mcpp-toml.tmLanguage.json delete mode 100644 mide-plugins/vscode-1.0/test/e2e/runTest.ts delete mode 100644 mide-plugins/vscode-1.0/test/e2e/suite/index.ts delete mode 100644 mide-plugins/vscode-1.0/test/unit/client.test.ts delete mode 100644 mide-plugins/vscode-1.0/test/unit/harness.ts delete mode 100644 mide-plugins/vscode-1.0/test/unit/locate.test.ts delete mode 100644 mide-plugins/vscode-1.0/test/unit/mapping.test.ts delete mode 100644 mide-plugins/vscode-1.0/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e72d05c..db1ef50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ concurrency: env: XLINGS_NON_INTERACTIVE: '1' + NODE_VERSION: 22 jobs: # The backend on each system the editors run on: build every program (the backend, the mock mcpp, @@ -83,9 +84,71 @@ jobs: name: backend path: backend - # The VS Code extension (mide-plugins/vscode): unit tests (the wire, the mappings) and the shared panels in a DOM, both - # against the backend built here; the extension in a real VS Code (the oldest it supports) over the - # mock mcpp, with that backend and with the bundle it carries; then the package, and its file list. + # The VS Code extension (mide-plugins/vscode), mcpp-vscode 0.9.0 over the backend. Its gates + # (the registry, the translations, the unit suite) on the three desktop systems; the manifest + # generated from spec/ with no drift, and the snapshots generated from mcpp's source; the parity + # gates against 0.9.0; the package with the bundle and every system's okrun; the Extension Host + # suite against every mcppls variant on the three systems, over the bundled backend; an isolated + # install that resolves the language service; the graph page in Chromium. + vscode-gates: + name: VS Code gates (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + defaults: + run: + working-directory: mide-plugins/vscode + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: mide-plugins/vscode/package-lock.json + - run: npm ci + - name: Registry, translation and unit gates + run: npm test + + vscode-drift: + name: VS Code manifest and snapshots + runs-on: ubuntu-24.04 + timeout-minutes: 45 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v5 + - uses: actions/checkout@v5 + with: + repository: mcpp-community/mcpp + path: .mcpp-source + fetch-depth: 1 + - uses: ./.github/actions/setup-mcpp + - uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: mide-plugins/vscode/package-lock.json + - name: The manifest, its strings and the settings registry are spec/'s + run: | + mcpp run mcpp-ide-dev -- gen vscode + git diff --exit-code -- mide-plugins/vscode spec + - run: npm ci + working-directory: mide-plugins/vscode + - name: The committed snapshots match the current mcpp + working-directory: mide-plugins/vscode + env: + MCPP_REPO: ${{ github.workspace }}/.mcpp-source + run: npm run check:generated + - name: The settings documentation matches the registry + working-directory: mide-plugins/vscode + run: | + npm run gen:docs + git diff --exit-code docs/settings.md + vscode: name: VS Code extension needs: backend @@ -99,7 +162,7 @@ jobs: - uses: ./.github/actions/setup-mcpp - uses: actions/setup-node@v5 with: - node-version: 22 + node-version: ${{ env.NODE_VERSION }} cache: npm cache-dependency-path: mide-plugins/vscode/package-lock.json - uses: actions/download-artifact@v5 @@ -114,31 +177,208 @@ jobs: echo "MCPP_IDE_OKB=$GITHUB_WORKSPACE/backend/mcpp-ide.okb" echo "MCPP_IDE_OKRUN=$GITHUB_WORKSPACE/backend/okrun" } >> "$GITHUB_ENV" - - name: Install + - run: npm ci working-directory: mide-plugins/vscode - run: npm ci - - name: Unit and panel tests + - name: Unit tests, with the backend built here working-directory: mide-plugins/vscode run: npm test - - name: The panels type-check + - name: Parity with 0.9.0 (the manifest and strings, every page's document) working-directory: mide-plugins/vscode - run: npm run check:webui - - name: The extension in VS Code ${{ env.VSCODE_VERSION }} + run: npm run test:parity + - name: Package, with the bundle and every system's okrun working-directory: mide-plugins/vscode - run: xvfb-run -a npm run test:e2e - - name: Package + run: npm run package + - name: Verify the VSIX archive + working-directory: mide-plugins/vscode + run: unzip -t mcpp-vscode-*.vsix + - name: The manifest keeps its promises working-directory: mide-plugins/vscode run: | - npm run package - (cd dist/extension && npx vsce ls --no-dependencies) | tee files.txt - grep -qx "backend/mcpp-ide.okb" files.txt - for p in linux-x86_64/okrun linux-aarch64/okrun macos-aarch64/okrun windows-x86_64/okrun.exe; do grep -qx "backend/okrun/$p" files.txt; done - [ "$(grep -c '\.okb$' files.txt)" = 1 ] - ls -l dist/*.vsix + node -e ' + const p = require("./package.json"); + const fail = (message) => { console.error(message); process.exit(1); }; + if (p.extensionDependencies?.[0] !== "sunrisepeak.mcpp-language-server") fail("the dependency moved"); + if (p.extensionDependencies.some((id) => id.includes("clangd"))) fail("clangd is back in the dependency list"); + if (p.dependencies?.["vscode-languageclient"] || p.devDependencies?.["vscode-languageclient"]) fail("a second language client crept in"); + if ((p.activationEvents ?? []).some((event) => event.startsWith("onCommand:"))) fail("onCommand activations are derived since 1.74"); + if (!p.contributes?.viewsContainers?.activitybar?.length) fail("the view container disappeared"); + console.log("manifest ok"); + ' + - name: The VSIX carries the runtime artefacts and the backend + working-directory: mide-plugins/vscode + run: | + set -euo pipefail + VSIX=$(ls mcpp-vscode-*.vsix) + for entry in \ + extension/l10n/bundle.l10n.zh-cn.json \ + extension/package.nls.json \ + extension/package.nls.zh-cn.json \ + extension/dist/data/config-registry.json \ + extension/media/settings.css \ + extension/backend/mcpp-ide.okb \ + extension/backend/okrun/linux-x86_64/okrun \ + extension/backend/okrun/linux-aarch64/okrun \ + extension/backend/okrun/macos-aarch64/okrun \ + extension/backend/okrun/windows-x86_64/okrun.exe; do + unzip -l "$VSIX" | grep -q "$entry" || { echo "missing from the VSIX: $entry"; exit 1; } + done + echo "VSIX contents ok" + - name: The VSIX stays small and free of local state + working-directory: mide-plugins/vscode + run: | + set -euo pipefail + VSIX=$(ls mcpp-vscode-*.vsix) + SIZE=$(stat -c %s "$VSIX") + COUNT=$(unzip -l "$VSIX" | tail -1 | awk '{print $2}') + echo "VSIX: $COUNT files, $((SIZE / 1024)) KiB" + if [ "$COUNT" -gt 220 ]; then echo "the VSIX has $COUNT files; something local leaked in"; exit 1; fi + # Offline Mermaid, the pinned official tests, the backend bundle (two images) and four okruns. + if [ "$SIZE" -gt 12582912 ]; then echo "the VSIX is over 12 MiB ($SIZE bytes)"; exit 1; fi + for required in dist/vendor/mermaid.js dist/vendor/MERMAID-LICENSE dist/data/library-examples.json; do + unzip -p "$VSIX" "extension/$required" > /dev/null + done + for forbidden in .dev-profile/ .agents/ .agnets/ test/ tools/ src/ node_modules/; do + if unzip -l "$VSIX" | grep -q "extension/$forbidden"; then + echo "leaked into the VSIX: $forbidden" + exit 1 + fi + done + - name: No second language client in the build output + working-directory: mide-plugins/vscode + run: | + test ! -e dist/src/languageClient.js + ! grep -rqsE "require\(['\"]vscode-languageclient['\"]\)|from ['\"]vscode-languageclient['\"]" dist/src package.json - uses: actions/upload-artifact@v5 with: name: mcpp-vscode-vsix - path: mide-plugins/vscode/dist/*.vsix + path: mide-plugins/vscode/mcpp-vscode-*.vsix + if-no-files-found: error + + # One Extension Host run per mcppls variant, on the three desktop systems, over the backend the + # package carries (staged into the extension): the spawn and path handling the suite exercises is + # exactly what differs between them. + vscode-e2e: + name: VS Code Extension Host (${{ matrix.os }}) + needs: backend + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + defaults: + run: + shell: bash + working-directory: mide-plugins/vscode + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: mide-plugins/vscode/package-lock.json + - uses: actions/download-artifact@v5 + with: + name: backend + path: backend + - run: npm ci + - run: npm run compile + - name: The backend the package carries + run: MCPP_IDE_OKB="$GITHUB_WORKSPACE/backend/mcpp-ide.okb" MCPP_IDE_OKRUN="$GITHUB_WORKSPACE/backend/okrun" npm run stage:backend + - name: Cache the pinned VS Code download + uses: actions/cache@v5 + with: + path: mide-plugins/vscode/.vscode-test + key: vscode-1.91.0-${{ runner.os }}-${{ runner.arch }} + - name: Extension Host smoke tests against every mcppls variant + run: | + if [ "$(uname)" = "Linux" ]; then + xvfb-run -a npm run test:e2e:one + else + npm run test:e2e:one + fi + + # The only check that proves the dependency resolves end to end: install the packed VSIX into a + # private profile and let VS Code pull the platform build of the language service. + vscode-isolated-install: + name: VS Code isolated install (${{ matrix.os }}) + needs: vscode + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + defaults: + run: + shell: bash + working-directory: mide-plugins/vscode + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: mide-plugins/vscode/package-lock.json + - run: npm ci + - uses: actions/download-artifact@v5 + with: + name: mcpp-vscode-vsix + path: mide-plugins/vscode + - name: Cache the pinned VS Code download + uses: actions/cache@v5 + with: + path: mide-plugins/vscode/.vscode-test + key: vscode-1.91.0-${{ runner.os }}-${{ runner.arch }} + - name: Download the VS Code CLI + run: | + set -euo pipefail + CODE=$(node tools/ci-vscode-cli.mjs | tr '\\' '/') + "$CODE" --version + echo "CODE=$CODE" >> "$GITHUB_ENV" + - name: Install into a private profile + run: | + set -euo pipefail + "$CODE" --extensions-dir "$PWD/.ci-extensions" --user-data-dir "$PWD/.ci-user" \ + --install-extension mcpp-vscode-*.vsix --force + "$CODE" --extensions-dir "$PWD/.ci-extensions" --user-data-dir "$PWD/.ci-user" \ + --install-extension sunrisepeak.mcpp-language-server + - name: The dependency resolved, with a version + run: | + set -euo pipefail + LIST=$("$CODE" --extensions-dir "$PWD/.ci-extensions" --user-data-dir "$PWD/.ci-user" \ + --list-extensions --show-versions | tr -d '\r') + echo "$LIST" + echo "$LIST" | grep -q "^mcpp-community.mcpp-vscode@" || { echo "the extension is missing"; exit 1; } + echo "$LIST" | grep -q "^sunrisepeak.mcpp-language-server@" || { echo "the dependency did not resolve"; exit 1; } + VERSION=$(printf '%s\n' "$LIST" | sed -n 's/^sunrisepeak\.mcpp-language-server@//p') + echo "resolved mcppls $VERSION" + echo "resolved mcppls \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" + + vscode-graph-browser: + name: VS Code graph page in Chromium + runs-on: ubuntu-latest + timeout-minutes: 30 + defaults: + run: + working-directory: mide-plugins/vscode + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: mide-plugins/vscode/package-lock.json + - run: npm ci + - run: npx playwright install --with-deps chromium + - name: Render and interact with the shipped graph in Chromium + run: npm run test:graphs-ui + - uses: actions/upload-artifact@v5 + if: always() + with: + name: graph-browser-evidence + path: mide-plugins/vscode/.dev-profile/graph-ui-artifacts + if-no-files-found: warn # The CLion plugin (mide-plugins/clion; the protocol's editor `jetbrains`, the IntelliJ platform): # its tests (the wire and a session over the mock mcpp), the archive carrying the bundle and every diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a436781..6604718 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,3 +39,24 @@ jobs: cp "artifacts/backend/mcpp-ide-${version}-openkal.tar.gz" dist/ (cd dist && sha256sum * > SHA256SUMS && cat SHA256SUMS) gh release create "v${version}" dist/* --target "$GITHUB_SHA" --title "mcpp-ide ${version}" --notes-file CHANGELOG.md + # Open VSX receives the exact VSIX attached to the GitHub Release; the Visual Studio + # Marketplace and the JetBrains Marketplace receive the same assets through their portals. + - uses: actions/setup-node@v5 + with: + node-version: 22 + - name: Publish to Open VSX + env: + OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }} + run: | + set -euo pipefail + if [ -z "${OPENVSX_TOKEN:-}" ]; then + echo "OPENVSX_TOKEN is not set: Open VSX is a required destination of a release." + echo "Create a token with the publish scope at open-vsx.org (Settings, Access Tokens) and add it" + echo "as the OPENVSX_TOKEN secret of this repository (Settings, Secrets and variables, Actions)." + exit 1 + fi + (cd mide-plugins/vscode && npm ci --ignore-scripts) + vsix="$(ls dist/mcpp-vscode-*.vsix)" + (cd mide-plugins/vscode && npx ovsx publish "$GITHUB_WORKSPACE/$vsix" --pat "$OPENVSX_TOKEN") + - name: The Marketplaces + run: echo "Upload the GitHub Release VSIX manually to Visual Studio Marketplace, and the CLion zip to JetBrains Marketplace." diff --git a/mide-plugins/vscode-1.0/.vscodeignore b/mide-plugins/vscode-1.0/.vscodeignore deleted file mode 100644 index 40fbe32..0000000 --- a/mide-plugins/vscode-1.0/.vscodeignore +++ /dev/null @@ -1,2 +0,0 @@ -**/*.map -out/test/** diff --git a/mide-plugins/vscode-1.0/CHANGELOG.md b/mide-plugins/vscode-1.0/CHANGELOG.md deleted file mode 100644 index 0ad0958..0000000 --- a/mide-plugins/vscode-1.0/CHANGELOG.md +++ /dev/null @@ -1,18 +0,0 @@ -# Changelog - -## 1.0.0 - -- The extension is the VS Code adapter of mcpp-ide: its decisions are the mcpp-ide backend's - (`mcpp-ide serve`, shipped inside the extension), which reads mcpp's control contract. The same - features exist in CLion through mcpp-clion. -- Builds and tests run as tasks in a pseudoterminal that shows mcpp's own narration; closing the - terminal cancels the build. Problems come from mcpp's located diagnostics, not from a pattern. -- The Testing view lists every test of every project (`mcpp test --list`) and runs one or all. -- Debugging through a launch configuration of type `mcpp`, for CodeLLDB, the C/C++ extension, - lldb-dap or Native Debug; an ok image is debugged through `okrun`. -- Targets and platforms: the targets mcpp knows, their toolchains and the platform that configures - each; `mcpp.build.target` chooses the target build, run, test, debug and pack use. -- mcpp.toml's problems come from mcpp; the extension's own TOML checks are removed and their - settings deprecated. -- mcppls is installed with the extension as an extension pack rather than required; the project - view reports whether the installed mcppls reads mcpp's result record (0.0.18 and later). diff --git a/mide-plugins/vscode-1.0/README.md b/mide-plugins/vscode-1.0/README.md deleted file mode 100644 index 18c8212..0000000 --- a/mide-plugins/vscode-1.0/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# mcpp for Visual Studio Code - -**English** | [简体中文](https://github.com/mcpp-community/mcpp-ide/blob/main/README.zh-CN.md) - -Build, run, test, debug and pack [mcpp](https://github.com/mcpp-community/mcpp) C++23 module -projects, browse their libraries, dependency and module graphs, targets and platforms, and manage -mcpp's shared build cache. - -This extension is the VS Code adapter of [mcpp-ide](https://github.com/mcpp-community/mcpp-ide). -Its decisions are made by the mcpp-ide backend (`mcpp-ide serve`), which reads only mcpp's -machine contract; JetBrains CLion has the same features through mcpp-clion, and Neovim through -mcpp-nvim. - -## Language features come from mcppls - -Completion, navigation, highlighting and diagnostics of C++ files come from the -[C++ Modules language server](https://marketplace.visualstudio.com/items?itemName=sunrisepeak.mcpp-language-server) -(mcppls), which this extension installs with it (an extension pack: it can be uninstalled -separately). This extension registers no language service. mcppls 0.0.18 and later refresh after -every build, whoever runs it; the project view says when the installed mcppls is older or missing. - -## Features - -| Feature | Where | -|---|---| -| Build and test | Tasks of type `mcpp` in a terminal that shows mcpp's own output, in colour; closing it cancels the build | -| Run | The program in a terminal of its own, started the way `mcpp run` starts it | -| Debug | A launch configuration of type `mcpp`; the debugger is CodeLLDB, the C/C++ extension, lldb-dap or Native Debug. An ok image (`--target -openkal`) is debugged through `okrun` | -| Tests | The Testing view: every test of every project, run one or all | -| Problems | mcpp's diagnostics, located where mcpp reports them; mcpp.toml's problems from mcpp itself | -| Targets and platforms | The targets mcpp knows, their toolchains and the platform that configures each; the build target is chosen in the status bar | -| Libraries | The packages of mcpp's indexes; `mcpp add` from a package's page | -| Graphs | The package dependency graph and the module graph of a project | -| Cache | The shared build cache by kind, package and age; cleanups with a preview and a confirmation at their level of danger | -| Pack | The formats a project can be packed into, and the result | -| mcpp.toml | Completion, hover and outline from mcpp's own key registry | - -## Requirements - -- mcpp 2026.10.26.1 or later (`xlings install mcpp`). -- The mcpp-ide backend: shipped inside the extension for every system (one ok-image bundle run by - the okrun the extension carries for Linux x64/arm64, macOS arm64 and Windows x64, or by okrun on - PATH elsewhere); otherwise `xlings install mcpp-ide`, or set `mcpp.ide.backendPath` (a program or - an `.okb`). - -## Upgrading from 0.9 - -The extension ID is unchanged (`mcpp-community.mcpp-vscode`), and so are its command IDs and -setting keys. Settings that described the removed TOML checks are kept and marked deprecated; the -problems of mcpp.toml now come from mcpp. - -## Untrusted workspaces - -In an untrusted workspace mcpp runs with the effects listed in `mcpp.trust.denyEffects` denied -(`mcpp --deny`): a command that needs one of them stops before it and says so. - -## License - -Apache-2.0 diff --git a/mide-plugins/vscode-1.0/media/activity-bar.png b/mide-plugins/vscode-1.0/media/activity-bar.png deleted file mode 100644 index 4cec8e65d255552d28058723d9e2a1be177f4d06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2426 zcmV-=35E8FP)1i$6z08_QlKb70TBch(5e+dkVOnG&=wWJg1A6c+#n`It0=CZh>B4ZK|_E*T)_p! z4F&fQRMg^zqNr7iC~GMcwS{hPi97Xs_00#g(1K{w+20*6uAaelh0KFc3gA;9MDec(oD7 zaf2cau<`8x6ash?K&fTjWdJGw>;+KHfA6q9Q^Mz|!GEI~z^x&kt3d(i3*Zm@f7Aom z0AN6X4O9;`8o-|f@^=6q0yq~ydYm3s8Oun^I93Dr3*pB5Jn@YNfU5!2Y8G102kl@u zPZt26Y35OYOyuc^Wu0AkQp*wBP{!7$F#+(X$6~9j2fohm$espZyD-mAo{q3C=OpVi zu5!4NScaP8P6p?s1CUNX>NVCIvWGlwv^1Ur&@#m458}zHL;S`j8cZbF0JOE(!#bbE zmNBsU%{AJ`J$w(KW5D(G1Mmgiq1RB~`?(d*-mqBD3IIR)+@t>QBeJBV0&s-Ik-znM zI7QzsHJIy4pTJl~N@%*aYycnoY~&UzFh4*VdMGVgT_^CwRr|Q2hvE>MVgle;QZ>JE zmq8qu!k8fq1S0`~eV*%Q06mfkz^NJvsvED80($xCSw0(f zfQ-vzT`Gy%>M_?(016DO_Y}r3Y3%2kBFO?^JQEndp*VeSk9ky(e%EJ?YRUnAYwBvT zo>v6dquj=8%-W-dL0o~k*<)QQZe%43fIAG?MIGs=G*7+(kTpJY*sN@}uqFk_ivjWy zXY%%uk8x!u3xG!gP`9N5caAa;)*1j%|k7o zHCt`T3|XS0;}Z>lavcw9EKXU&g_I0*etoM$NmRadra|y^#+IqVTIOqnHnp}VLg+`* z^G*`n(6$)jOK1QN7I?ej%O^9zSs+-k9k5hUNPt!87sX6^Vr%_i?>puI{HUqDyW+?7Mj z5-95}aVQ;T(uGAdU}SpK1X?igM;iiwBLxN2&GV!5*)^KYDHYtzMoI~Nh2qRAGAH{x z+OtIH9u=|rWdMg*r+FR=@ka$T(3{8x&uRz&F4r7+Wyy<(yz{fY48=Oz_s^z|6&Hs~7Hq4F@fS#PYG9Z{L{=Lq1+#PQK+6Wae1<1t$eOqc!5K&{d!(x;bEfW|= z50~_?TTnZXjuC(pcv7nZ0^pP`;ts%BLf}*tr0yR1!ruk}6uU~7b2Py1bQQ#NHGv+? z#A6MPeffPs0cge3TTihvZUD3(wbW&s3p})S)s66t0RT_AlC;}2no>EFE~i0M2(GGo zi28PGUTv7qRFMo!CI|!Ac$Vb~$ac`pqU^qj>v)OfSXDRa&r{hvEC4w?k~+S9p2vOa z3m@~5tyKbdql-0MBQUK|E;_tf=%P3=z~<+W9>2wGeGS`T>Ur&DR=thi_bkam8vB+O z^S<|}h1EEUNM(+Dm1`Uu27m~a$ZCOacQ&X{c?IKA@_FYyBQ;EEKS_Z7nQlTaQDwk3 zmg^1Ul+zF{qq|ui;69tI1O}k3pnTTTSEm^;gL?#SZwq*BH090;4O1#|&AUWkR11B| zpzfBKtpvD=fxUvyr{?;AiP!Tiug)bINTaL#oA=tSjkONau$=C7Iqy9`C;$;28=bhe zFF-LubsaqJ3gZ8;b780^+36@y(M7I!Qy^r*>eyeP^SOe73}Tj>OV>7*Zm7y3=tVeR z)Fp-FQh*%5dF)o57aU4#l=nu00+8boL_Y|a?yJRIL>*}vRm9F7MeSQP-?_oXmRd7G zs}tH9Cxq-RJ&_7)7#eCPhp(H|i~TliiAMmP79!oZ+rmv$%3+#bp>qD+`MKeO9Budh z6ohCc>`zxz6;RU7a-4u-hc*@%fHX?5QnOTLfIgkDS67Yfp^V{X*Qv2%OtnXlFVtk} zekY)mzI{Z%Q*Mii8!4BB0wCX~9b>J@@Cct*0E0V@8s+Xp3xilw}nr^`I zbp5C^c{0<(0MLb;)~h--H9>E(%oOU*y1vXle4E89-*}%)B+GC!!z!xLSmfx*K85lU zW`v0VV63JRb$~8K_xUq<^u5Mj_6Ut?46jq^{^;dT>q#fm6STl%Y4Pr{P_VKHH+H*0 zVpPsfxyh{LwRXf7n{sCGjhLe;gA_6bO(RR2$nVPu`_6#wkJosd0F@7%oj?m|DRfR0 zd$g;!peDaFK*9ePQsYbbR^%l5|M`q9UlG(=fslL`wgf6ku^T{TFeYMUmB}s zgQ@m^$Pjo s!~z!Q(^3GWt`q<%08&>9fTni+3)&=8YG~wN3;+NC07*qoM6N<$f?xY-6951J diff --git a/mide-plugins/vscode-1.0/media/logo.png b/mide-plugins/vscode-1.0/media/logo.png deleted file mode 100644 index a9c8f84cfb9425a7e0a38a60b24f8f5b50669be5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31749 zcmeEt1yfwx67CEP?mjrdA-KD{Yw+M9xVsGyAZXCw5L^NY!JPquySs(p?hbFxJ@>x< z@Ty*|nwqLTd-m$ptGieC*WZrPP?N_%B|!xM02qo2GFkutF!rAhG9v8CpkS&J>fodn`qWqXK)=P$T!pi78Q zYv)6>6+F@Hk-zKQsx2!~r2&@ye}~m3P>=BEFZN#))&Ct31ayDA|NHYc5Dje5JlHYd z2mN=zMYJPT{&&<|KoDXpSFB8}M9uykz@9bux&ICWUnrd1JEt1&|h z6~ICi@S}u&B8bfAR9zJM>sb52a@yqRSC9yf;6(Sb+ccNBqCQLQjlj zh&fG6=#TG#wbu~XkkAw{W0~SW05^QlOgVJGvin!``)u2+;_-Q`iJ1RR){zw(A{&!{rQEUJ<`5rop8T#KhkFWpgq+%Uj z8c>H(r-BBC_Z@f%{&$fFw=j1*=bXu<+sMFO&TL2nNA{-Olkx z&L8{#PyYYa{{OVWX{b`T0G;@aDa4Im44YQo1DJ~b;1m_)D9~+5Fzc$Kc}*FzmVbbgJgQKPE5W6ysO)jkYzTF^ znEkc;0p_`9L0CSKY7)~92~otoN!(BnxW4%XCIn#CZ3C?g_l+tnzAVLsYrwl|YO zy)BcmNGxXQ=QJB$!1q`iw*Vmf#wCEl`{BJBFbY3G1@4?SLE&}mDI>V?gXo3;R+Ffq z&Q8h5QphlC=cd~}))oKjp)FGp+W5uyMrx0We(}rGCq?R1B`zyK(t|f=P&_~2^o*&@ z^cn$FQhBm$Ql0|WH#qv)L?m`$drP6W5P%o#a61vm;Q zK2}cZVPh}RNEm^dCQ(+9f%8HT`zLpXi9!G7-P`_|sKqUPtiSs}?@^7W!_1AFd0Y9U zpK`%%z^1k!=$5X8BD^9ZpGVJbUmhY5F2aM&S|WcrZ+jg~PFp|CM403!lhnD*z;f#8 z8*mn%j`*4`2uHZKsr_g|P#7(3@ng~J^Dh@_Q>SyXVWAV(mA%mYGS-)laL%Lb}%={MPvJbJ5>R3j56e$9a+skC4C zx*U4Kj0Q$%#)2kLak$e~2jEr*eU&4^kfho}cyhprl*2j)VJibBLD*4;>Wru>$nd2< z;g*dc(nOG*VF~%ViQ1aeMv8$|_*iPN?%Dn-za3$lYVrm-pC+Gu6S1n#)IL*51=l{) zj>+^dgkVo~NzYN~tFO5S6k2#zcekGakwx*i=gSE89jn`L!bQd*eoP+2g4gzb0Z}|o z*^>QhHaNCH^3)kWc=S%#NRCdcA&IBt&{M=t?k&;~q+P)6C}2(vUXIr)SVj&vAMA2N zUtLSE5Qw4a1Cd=*m0KI3S9)F&`qqea1AQub4R$;_==w2^OJ_aHf<)Y zzU`l^lf4m##h65PgcL2{*l8n|SVFssu&tI?OyE3(fedM>8(MA;?Wl#8Btwn*dBSyB zNq1H_C3bB7Xv7sP6}C8)96E*9HT2Ao*$Y>mS2svY)W4&}@(Iw&J44R`yr%mmotS%c zG4>FYB?}=iRJ>hEr;hO~aDP=srVK_qKeS0as=FlieXorM_7zfV$(Q2?yrq9@R+(9e z)1UK9y%-F~uPjGoqz+FS50GaEmi}P=sKdM{8K+JY{b&Qc9Rqxboq50vfm~-zB2UgrAH;j-ZFWTe%v{2h5G& zG@STBZ7G8z_6rAR%qDyJ%Uv9|Ze2?fTQ8hk$H|XNc(i`tVF`SmenFz#8?&xUDh>`h z_Xm>dSCO;tVbGYz5Eqq0s??7lT|Q8N7sA<{1q9H&ggHNbojfD1jLLi|ylT+5x0SQ? zFdcv(;9iK^zLDVW)x3#0)3eHf`|=BIgH#l$17{i*UxC$Z(9hk+%8esJf3q>N<(P4# zkGdsg#!n>g?u216#A2f?>;jvRpt1g&>(bmI^^*K-1pLt%<6`BwH&tSpMgkY@JM4o|1r47aof?z z9P3%F^}%j}`_507XrSnpO_%vEAf$EL0!eScc!4a>Q#q%}45zf!yJXe(ptCF*Xhh*o zylFHIr!~CZXsaWyVaXvZzVh5;0v zAJEYq=5-YQfOc{6Zev1r!~fwGyT?jJyg>*)1QRgG?uN z2ckm|ndb_=R+Ip4<5;2PvO11akRIx$imt)%AS?lObJKh1Rr#)B3->Zi1C=MTQ_F=9 z=;aVI7q3OSyT{xZkYx?k3*|)UT^gH;f)=>$jHTOFB)sLNn`_o>o5>v0-NrkP^!p8fu=xg6_vNSToSX$(w&Q`+o87n@M{%(O~=kZlQ zLnGX>8vL@FEoZhF*8&X&F#3(<1}QImhh}ZD>fAC9F`MbP3nMGEX3oX#HDB_4n$9_= z^0%X3Uqrlv4WR?GHnFq=MD-uroIVF6u{HckO<9$Ced0SyK3)nP8e?K0G5Sav2DQqC8#*QdEPA?K=QOj)Gcl+2$%eIeaDaa zXixY2#Ht8ma(WrZc5@t))+zUUv}dXd4ouCSd@;?J{rfAu$;$O;Wea@;?0LDS3rKBV z3a#PkUtJ4aeG;#87{0=pnjlp_H-Y;U!r;HyI0J+YA)^c-jbGr2oDNd%zA;6hxmO~s z$;lD8Z;*>B+lofShWigNZu>AdZMK5-s;~NsNKBs&Y-PreW%oAhhghvyiC}fXK(A2% zYiaAe+Wg>24V|f-rhX*V*W_YH^H7Zr_`o?5k9@>@mIa8k01vN17FX06D7Y0GJLrp%Q9O2-Q zy$n%XK|LeD@CBCvbYM!;gnbL~uFDsd@OuehTK?}p6RR6jb>hdYtuMp1xk;4fW@Nf1 z#VOa`)Nmhgzm3KM$MS#!$(xI=bK)5ARRl%f&-US`umha8eC`<7LfBQ0yGSC0q0Q0M zp5{6=+KPBdpWg&M*J*^y7f}c485mB)<-UHLEus zPqRXHZ{TcxyuD3IjtQ+i2`sbL=- zh^^0s=gsSk#eY5?5?d!EeBmLcW%$dnz8MovHu>bgk2hv2(gL`f(fU?0z*Ihhx&VUq zL5OaFuTSw_s^{xuUw^$_*#yN%gq?{Yl&sOg`xY2zPM4*u^VNVS`e#-?o7zj(c@jw> znMywS#az0z!v82>VTYaE-=?iCvUcXroZ(n z%8B%0c;f*{LC@7SOgR{ZyQkM$|mRk7s*^4`|%ZLHo+P5Iu$HrAstb_sXXSY8* z|A`4?r*u*Kt2tobux&eR#@lzNPo`ZZ}jt7LQJ~N%%rt0%5T~vLp<{y@sf$~m>OX5dlu){~rGbbyA>Az$7_ZsB)LT^ro~Eer zce-iVlc;etkhqs6qWWWvvBl=|+SI{?UHLnmF?MFj)PF6xjE~1cx1&70vMKlMt`#3&9zBDUai)YJ2e%9E!jcIfp{B_ zXW4F%&rppf4_{mJG#8M&hC$Y$F;?6Bym;Bqte;>R)2$V*k-1w--9V0~5?qII*UHbV zGc~(~S0J&zw|G{{%ks2ED6q9JlB$FKIXnTqQ88bD*pX;Q&rS_Ol}LqVMOIEL8Kx;` zyr(Qf@ijV_S2|pAUR#`ujK?ajB%6Mh6iYb;N9c;gLQfrqpe2s$q=^|8$D1`v<28O2 zvHzGWa1x9orMbLz`2KwU&MZ&bXGURY-~pi!y`)6p`MuKCUw+|2ft9;;Q{93-rL5i! zZGpn5OhK%|4_%g1D0IANNZ`wt!3kB`<)2hR?F3O-Hi=EX@99enc+}*U>Rm>3@I}{G z@3Y^O6T`uZio1UHIi2OQMXh9})?%&CCSnnL`;MPEcxP+i6%2+FDCS|wARGU|(WH>2 z1c6|Q(U7e}p>-+#wAO2j=AvPanM1ktm29=XpsdjsWm^@|+UvVWqmb9r`JuPHe1D@k zd{J+q{BDG>o#t8v-Z1ZyOS1 z;6RRoy(&bi6+IwOLaW+8QZ>~1FM5VnX;bo^XVb9!*8xvDr|i@joxQU|Eo>XZQNuJ? zY5UiWn+1%-l2wLqsx^^$>+R$~WZjWVE*bn<0vCxF%UWM~s?nL%Ve!^YFg8?sJpj)P zC%bMp)hy#?aU=Foqj9g$g+De)q0phPPD>2)ffaz&*3LX<+Rv)=I6^}* zgdtJNn-}wG^%7*Ft$IR{xQ%Qdhswh=dohX%$Q63gf-gRtXML0OGJ(~zXK^{~yrgr2 zLxh^eS_Nr1VlNHom%VJ{?l)E1OSQF-#F)aa1(<3!R-AoAnkMuNh>|Ux^(mj!XO_S%l71`m6W`>l9%p7AUZ%(B0<-wp~}0 zxSImT@nnCT;?$W&e!ZhBGdU}>Gpr75_^m$6WkrOh*deymB8|EXLd&Q%+trRz(SZ{M z-pQ#&IvdZhce@dMz~ndevufB z_3w)3y!Xert(X!Elwl{6I$C+*DXfxqE=XGAfaNA}ormzu*>z%~?IUxO28A`gHs_{2 zU3Js*Jl%IrEpN;2@|JEH0zBTFnNk8vF!U&x#K^w(l*tWXmgk*e{ z>ZEdu77d5WceOG#ygw~txB7rK@kr^qsa!ag5?W+4RD_D!fOqoaIQxsxl+^CxZ7EyZ z6+Fi@YmQ%*kHWr90q$}w~|=(Qnll;cv|k_T`7Zgon|hEa=i zK4evjjx+!E^H8?r(GW^?AfMZTDD4s(6~WfWHg5Xta51X^Fitde8LnAI_gmefH&+b# zS+LNA=0Opl8;LuNTtwH*|L}@UYHsC@BR2yqj9@fd;WnXDV8Z_k+7dx5=O}wVi{pow z*g(b<_OB~58Z2q$pm&!@Ks*1mw=ElJyhn>Y|roICkqrIlz+qN-E z?kSnYh0}F!{N*oQQAKC@x`9h{NW)@gwYBIPSFt`yf(W#HP=LJd&@RuS26&@YZx=bG z6qj667=EWx!iZ)6IT`9L7{}UIxtl=!_t9v`mwq)QlGN#Ljh08WYG%1r)SOKzr7k9ys>PAXsJrvxc$FM;hm>MI^yp{I$7#eN_ zy=?%jFQ$y@vYpruWc@}tfmzkZ?;oork*=cCBmP7$aAi4k#=>Iun}8!$T1)dB;|~vm zQA}koY+=fz2qbfXx>MS8wN_FRh0pD$G#-EXNfI&Ci1lExbH(GY?4O7MWEl2(mYUn( z|Lw8}6GZG_DhzshM%-Aio((nAY)kJ`bQ&vjL47$q^!)Z~&a~q`X2ei1PYP-#RB;4( z^E*^~BRT2>srYU5fqO66#B`=C{-nFVwVSY?7nTLlmy+`S9PNd%>oqH#IhWhhHv(I^ z#_NU?hl0ne9a}cYl&e%Z+SFS@cO!6eeDeXb_^e)d9LQuVw8Oj!d=vL>6kE%u%=;AD zT%aTilGehAjC+mS|8W84fmkDm9YJ}xm_l91U?LQ-kq&!N<<1S2fFn7a_Nmsxmy$tQ zadKOfxVl33^-JVqe@#2^Z1rF&28R^?Wb-M)r!SphzpLEgNWvyyHtaqLYXV0oC}B@! z*)$c`_&S%H);{x`QMRUfIKK)~Wip-+yU2x8Nd~dj%m@{)-jM(cRS$`_LFjXg3b&fv z>YmMlM&D{dvNb$dxJK4=H3GG0leLeZMASdUF|Q0Ef)n{ZeR)FM#LJQ;{bZr_-zf(v zd&elXGPUt~2IU-2F;*3ByJaaB$d{2TmE}#bMiKi`N|s%r$_YU9lA;ciKI9&gdDWy}oI_TR%~BrUR{SaXU35hK)EffZJ(g6drw17HK`B#b9f=H#8e!yZ z`2M@ChrYm^9JrO6bYRVqBVXlwez@-#mKr-D2VM;LBF2Mt_Zt@ht)^ONk#SjxqS)r< zo}_b0^;v(+|SO^K-T>@skMmh1q6T(uf>oL|Sg{{u^%o8r-|)Gb7Ick+`i* zTd+ahbON=v2z3^Tu5>^Ua)@GZZY3vLF>cb$pvIk%FVQMl~~)Wq%wt`}1ECkZ0{1zXHoi8s`NhWD7t za_cSDd0*+_yd5nE>!cT=RhGO<3iXFKREGaMdx>#JwGs*vgWxCWil3J|BAV0)Pi_on zjm{rS@qxeVIFqPMQ0WYUy;-Y$+V<5Mk9@Wf(be4j<;P@3E3dh{HF=cjt=J%0#3-QV z{9ZP)DT5eh9?a8O79ac&CVS||uuGO=jt7CDDuiE`NE5yr1i#tHSbF<~j=BG_b6K%k zV}4nu1eOb-McU_-DH6oXTX}XkR@pM&ns<`S3H~1dGf)@9zp~Jcy4y}iEtF*~BeNmq znUNOCj!E}uB}+-WFLzaFQy9I(AuwQk>B7KP+SlMcl@=7qX6^06`U|J~<`2@`ZDBNj zV!>hoa&?ZAl}ECI-5UZg=ukl8KPl2OksdJEgCrAg0fd1GAcW_+$wgRmLgXU@t_j%vxEXZc-uN?vgIbVcMfcn~U8XgJ01kk= z;ft4b+41|Q(l+ie{Crhrju(A#rt13#gWONGsX)Qt{jnbgTM~ABI-9Z9%<~Oqp4~!i zAX)qsl@v-ah+H_B31{^mBX~Bs8iA~xuERg|hP5yyAr@+EUGd6(VGQCal8fn;!AOQm zo_~WUg6!S^I#;YQIgklK$Vr?pYeE}q8A{3B!QM%!XdF!;2=pxB!9z-*(n`cwd*dRA z6>ii7#$sDR74vtJPG7N6XPwwwfn8$*H@D_P`RE8_8>4_i8zRpi#rE5Sprk*SmQpy~ zpPjHX5n^B1gz{b%F|HzS%cIF&9V{%Z7^V}Il_b@cJWAYQZMm?|U4kGtCQYnAMyKgp zDX4xS2wR??&$pBz#due37`ZKpr*D8^pihn(^Y?+CgqqJfaEe2V?}Z}bkHsa6 ztRwI11z$fCay+!k744OLy`4Wne7Op8PoW~mF`HIAHXACtDgRx%5-NWhps#^) zSmW=1cQO%UiTL_Hb&1Y;2yD>i0TA#~L|Px{mBFOfzQ*yTve=<6hIIL&mp}!6>y`1& zGDEbYQ!B#MrV$1!zZ_bB>e}qp0wAI-=jLyPd$oGKM5eM2d3Z>+Tui8b%37C$6>qu_ z`%Wvib^X0j&F)+ONOc^u5;#hGIX2ToCPWa2P*GsBD6SV3YLH!f6~SL?T(;n|Sx=}X8l(lXSjM>S zes~;MjEL?=cF(|$q^%VaHQe>5Qd7A&A=*=^irQm?V3X%hHAQbCVj(PQ254P1I*(-k zQEvD^jSkm0N4bbxn3u(tcIBa~yRyKhgwTw%TXt2(w3WQ)bU*h%!P$AYCMV)+I&yJ=IL*9c=H| z9;I|w+lzR2lX+~}6++Cr)9HnR(O}?}PMH4{w&=_5JN(E}JDcxz0j~$xXE*0hdQc1T z(iW6&SmU}0?lq~X9LVUg^mj!@t#_`rm$W&}`yTjct1NYLMh(NKceBZAUcr;&ao2Cu zr)fd-Kk*Mz3&t(0m6-#S?ZLu9b}?&w-A&W*MLago7AN}d$s;qR-vSO^eK35?^X33$ zf8^_1RQeMKotLBDLN{ZeOU+bc^ zG90*%_R-~h@<~50H!L-ToFq#Uo4ZU_8a++6Lx0nAf;7=(qApQ^!}YRl&rLp)Ux_uWg(z&vn8Jmg1_qA8^$ldWJle;~dzpyQn|ZvK zf11LnJADeT znjfsbk2iTWXJ~)CXWOaa{rhm1*+Zz&l2-FrS}w;lDp=P~jAAE_Z&yTI>TqyFxKVBb z-SHst+hJ6_T@f~6C{t~i$0bA70iiW4xZ!Po$c#}X|FLSEXMzH2Mtpl4P5iv;=JYHJ zj@>~9zg{&&(cB0iA`OU$?N5{&U4US4vQlaMxrH|~-6i4XmNS|Qty4Brw*~WU0<4>_ z$^9q^i&}DHjPLzffnmm4px9OQD7HQ-9#ZowuSr4Q(U3aWF&gAwUt*~O&O0%HeDKCc zTl1eXQO>^pM$cDlHa`xMo5{MXyQSRfU#^Rvgf*o7X&Sd|^lb$z8%tznM4NLqbAEOZ zyko^4swITyC|V#V3x$4y;U+gscXFel%DK#Lz8wd_soiJfO^E zLMHtjj|jLgY|JFN%aIeC^K`tt$fI)Lb`y_iI8E~}(5}&Coq62*oOFtN7$)LI3N1A= zEJ2Iv2?>4xaM;vwK{G4yI5Gu&D9r2*>ZKP~4~W@Vz2p-yKarb={8ZwoK%eTYTuo3O zuHz)oYCYq$J-p(Jho`}uCKS6Nl!^?Z<0itFtC@>InyN2?&hVBQJrvc=*L<}Kz@%Sd zn!biLFK|R_@y-v!F8uVNdqHRy<+>W1LxSALNxiV<&8qd(xoy4Z|igfT$EaGBF0gSFW>!AB0mqY|=@%!HDA8*jO0b5Pe)m@MrB{b73jwfE(7 zxA`<}6Z+Ip%#FgXXB4ak=cfs3*L@K~9)**;jTzhI5z`H}%5O-}>RcXX2!!`W><^cyudEs3%LFVMJUJ z+;{^Xml#IOc-23T0uG2x#cxhI!%W zsZtasif^RD$tXrFMm*h6qkQI-}06lId6kZ1=&oxdxIMdy!umc*`R!J)b>M( zIis}Q&_c%Va{B}B>WIm#UkhrI)Lu%W9LnXhs>Db)O(7RikGsOUr!C?)hH&7RGZrtr z0fhv8MZ$Ra{IWTA`*U5iP~!DhoLJkarC135wC3wGue+&0!}J{MGc62z3oC6vVY?tA<{C5L z5FcBTeY~Nx&-b5T+$VM`7^pwM?- ztrnAVrB7yUodSG?>o`X|d+qpYqNo3A^c9WGRAS-d;n!fquKnAAh#u9!6wcKyuhFyf zMp*f~-Bmd4I25w_g+^i{LU`euIBy(@t-^0k8OA+9g0X?H`BXrU9|Fy1t6klEV3es~ z)WWQ`f~;`egySe2;wjK+Kr*kSP=QatIrb$hs`Ox_?^^v7upXo*oXT}c6u24;VsR>( z$=lfYR*sRCWANlKW>wvOyZ!9VHGkpXmqKRG7}F2WEq^%OiL#|F-kPTWWK;3EC&tvc zYJ&TSb&zdc6IR`}rL9U1Id3s*%g zqEQGJ4oi3~QFb6z`!jF{PtF(T381G7Bi@|_Y}Z;i_WV{s?25@#6~#P|;ZW_nkB#Ti z=3^QTpJ!Ip?7sXK(HNBi?h55a!)138ES@>J!pK5H*0}6D@Z@kjLZuOKi(`4%b#!8f zaeS5D)PI+6`zu42fc-2OGK_jZAXE(~GEdZ%+&>4T-t zTyqxz?}d@c+R9iPKnimz@6_kMz8`yET{REVcbTosOQ1_*Z3k6srSU{t%9AXACRQbo zp3Zh00b%^e0Hpr>^;4W9FlQp9y7*mFA$kVBL17Lu4AP!ij3V7lh>kppupa2V;ph|L zXaa_1(N2qwBQ-n(j?x-@5@)pg99m?)BT(nFmd(YWeG+^$rN$YGB$|HWBcyfqrz;++?%^1dL z?8LnO-U-RPr&J?q9TnAei58e@QNC`0#jL?UUAAn^=T-|p`0cP7$V+i0m<{qL)0FwZ zM8eHYeYwJ_%x}WIk-RN2oS$Ltxh334m89uQjNCJnpcvL4SQXrj^5RaE5p`i??+%vwV^03^ zvN+(r6FgF}c>W`t1<7-o{%VLyIyo>si>)!$EB^kciGgt;)5MNVo3BbFfuMhluzING zq%oL_#B*3I4n|tYXpwD+?}>79lan1koqv!mNH}OapMJXtG=R*h)eRQ#UWh4S+1WLk zN@a87kbSI_xDd}zCFaxIkrKHsJW04DXpei#kZ{yQ?@onT*M$MzBCQX++7yIVPJU?h zXky~BQ7aH1`s6k}QokRX#bATiQ75t8})Qn<^q=b4oo8ULm++d66}Z?Q^GxmBI!d}lSoa`$G#iiD!+3Mi`^R7CgK{Z^2 zM_#y4g|OA&CspvwrGJR&$y9!GK-{WUo=Lu)w~DrI(j*p(7dLOepOkaoKb&*w!>BSAfWBN*{-ADBH>sQWF-0yAW za=$P2exVWYtSS=lk9JDzjJ~9VB{A}k4S63~!(@YY7dAk#-}GAD%Y@JWauvgII-c?z zBFc7@1n#0PBNb7JzKcW^WbZ~EG@Bc^n%edWVEYrsCHNg|u6fK}Xufs{FZe2niVMe& zwn2|{=M+)@L29-gh9E$54&mDlSSqn24CP2lVr{?b6iJt~EH}O=*+1mW6_O-B_6n$a7MZhJO ziN*SACAD0UjCp$zKh5Iig`$wR6e}*_x+<}Dh=au4_xaS!@cJTSCaKWrxi=h+o!yJhq^?&WF@oc}6t5ueo{T z+1CKO4{J!L0~vDH>DAahC~TE^!n%NteYp2t@h_y4@l zFlQpxsx_X$#Hn5q@RjVBj;4o!Hm5Xm{ilKzE1 z%H>POm0p|j5);^t;ePI0dn_(}&bfNfh1<3fBJ>ZI%+ZtC!ABxY5Is7x%BKJljsw3* z^7&F`gji?cU+sP_K*YSML85vmp|b%p$P8)8@qp}c^dBg=wFuQh;n>TK#bcbTT%t=M z3>2cqcOjeT^=Z{2@&P+`C|mreGW0uBcW1zF5m{v5R+ckxeZ2th7JhR0_R9YF&zC~4 zE=#ktnASPtkNp`jzvH-3!H=eJOj3xou3~Ql9wiZC6S@6n|NJ;Ywq!Zb3k1Sgv(aaB zdB%tulkq0mS{`->&Q<*pX?_+|G{kvav-anOF$7=G#$Lmwe4hL0&hL`DIt zV7U=~+Luym+O_ah&cpvB>3v)+?6>PKs5U;2%VDJ-193egKm+5Y6?{0M(dvXzorHG6M)o2;#e#|O!eJJDKW!(+`<~Ock&xMZ=_DZcO$pe-28Wqz0 zEcuY&0JMc*il!;e1?c;6cT3hVgj4Z0z4CDss=qyMNhkyg#{9dTLF7#oLu*JyIy}5^$S)SC0$FNts(7@YmJ@R}2JdzOh z_2QIGm5}rRAsZxY_pNOD52Q>snEJ-7bsMYGHZ0ixfbmuo)mCi=FK_YYBgxHKu3jBl zOG46L2Ir7eY6An?r$%etM~iq^k#52F^~OkS+X{$Ajei<78t?Fwz`OjV7ex?PR2cBW zY6jri9Q8}|{)-OdX6O=`H5qfQu2$c?opLSRyo4c=RvPRSiSf-1tn2j&>!?^1&UcKjZFs+~H1gvj(u8`T~~=>W1M`}Tb47<~4j;^Mnp z9(&fGcuaGuiBw$OX1_0ob1H1d-t8L4=CIZ)=V6n7Jn+e?S%!2WZ}F0vs~FHEPta2Q zO)%1x1v_E2Ni!6fujF%V!CTHb1ZT)zlJ1DAAsyeEqkeVG|7xj|rRQ(X=> z8j2XQICb}T4~`za322InchvcIw_2}6BsJ=&egEPxUifn!hX>0P@P486z^@tqd5}&v z?$kONV%vj6m>KmyF2JZP1eQ=+h_oAda=Ia0KBVT!9w#x-ky}%6sG+6l?0r*JbQMUt zriQ(VcQ(U!?We70r4&sPE2#h)T*5^tboUdn-HKjg^Nn1YhYZ>l_uIE`Wy zhZCz_phl3GWtCp}0iA7DqRzbI&$Q_C3~x&xb{V?&jh7V$gTkn@Wm0xfEPHXCc02Ceg?jtly& z5yL#uwZBGtl_1?VL_=_GxDeBd=?p}JHg!?QS}0Xs6Vre48)831)G{A02WOV>k53>e ze6h$>`bL^3!Hn?2pze|4P3I{xEmoW&NkAak=@FRNlm!1R`{U`jLM0<_Rs46`{IZ}q zrr}xMk_45X$)9YE8G8H+ul8oKz_j(2uoG8+r6QAWXOAi6clif7FqxL6y=_B|S6TCu6JUOV@Ey1FCuQZnk_sU5Cp zbS-9aCE|rv&PP+4iT2y~(eb`X(g7`VDH?2-r?wn_4H3{S737-7yl91fmUi{+eb8x# zB<4R+k>8bm{r;X8;j;>3y3Y4nbQf!Xe2iCoF8c zxVGmHE}}&e=6`i;usH-Md+e{~I8`ZkEXbC==e(N3NV7dAHt~(09*|bB6`aS`9*1Zs z|Nf@S`v#vRMa`e|f(f9=Q{yTvO>=hMVFJRZEtmehorS$gjJ_7Z2_wxRyfz^O)-(;( zda!)Do7KL!T16mC8ZyUET$8b;8GXu+mw_UB~*V!X&QY5MiCb%$kMg$lLvdEJgF&kqGG!ik&{#0oL+31<)RCcF=il z|5_A<7g1h<)LI-Z>7F#+Tyyq%OA4V$^Pr$1+GISisNPVm_VMUwZb2UpN$hRy51GfZ zU*aI5(VrRlKkZ#*R~^B!#a#mgcZc8*+}+*X-7UC7u;A`4!QC~uI|p}%0|eK1a^G6- z2fWYs+gY_vm4VW&7vbpu!ajQ{Xy z6}0^5)@|x9WaZpnE!qE>BNzyJSC;b7JKetAcfiiH~QUaKxX6M0YN4hfQUBN&Pho@&^P=e>O5al&44ip$SJLapGIYBrFog z$V+;nJXNtb4)7hU{x@}0-o&K&u3z`^Vd_+_ec>YS<06|nr`pF9rBFZk2oK<`U7|Gf z&Qr%FSttnx7F#z3H!|5KlO{!sb)HmlbM7xJnuEV&=x;5fuU;DtRC9^F8p~sNP{PV9 zfLZ&TW*M?{7*?-aOY86i)ehuNCOlWr;ilFU&FP1ua{6yqAfiw(NtP(A3=p=)pwGzL z^jS>3+{dlJp_mGGqYYkpKd1uRA>OX+(9DyOHeJL4r)KC9#v)FI-)`A@mD@b=iX!c= z>2BVchn@brA%0*Zr|b5s)@U9Z!qYXx(hvzrW}O^1{<@zuZ*M(!Yf<8_%tB5OE(dl} z@Yv1F>sG#8?EDBw=KtL0P{rMEr;w-h6rG`P`(%buBEW&zW(O1RhU_RM0jn=~>ZqqF zT)VyxDJ<%t(X(~H$rerN;##(GzLW$DnC4gQ_fjWaScJSOpkmCN3qXXyy& zmvc(}R`UpYA+BMqVj|Rbjy3DKmbp^Q>d zkMR>@Y9mIfL-WGKbEXz@>5Rsf?zWVTqmnKnl>Rw`wUI_}jfZ*H$kEWY6j|E-V@@0) z_~4b*xQpsrACS8U+H0BBg+)caJWZnchT8i27B{KpB(lsRM4 zKWgHj#(*RA!Pq_WMDP6ZM-1D*9RkKVrwNw=FI(UXjMqZmCSlpV1Lj62+Kp< zQ|2C{7ZWV7XagswOW!Mx-#eKDYansAAwW^nilP_F;RnG@5I1D-^%GaS4p%7oM#iu4 z(5dIWJNX{T12Zg(v(Rrn4oHKN9LL1B{ii`09UWI%FD=(bkechyNk-~C*b|4*fj%=+g0CMxTjHisM>`Ecx_E~q6z}y0r=!} z-@NI`%Ctw=2qccD$t1bdHh56zs%ar}MV^ z4-fI#6ay2~h(J|%7{1(ngLoMPtv}OOl5NlSAQC0h9p-OTW)SDQQIg&f^Y2dSJl-b% z6!K3fkL0(5qd{N4nnJ!XgZ5mYW&e+RwH!|4xgT6v@^VFH#MD#NPYh*lP6>apHP?c) zh_taz?f!!2Y1fFzkRwfv-gxUfEYR%dZNcPjr)51pi%k_$N8Y@U{hGM^2@K%S71Qqh znY}?q$S|gqkt@)k4ON!iB=s$ zl9wOddB)k{%v0i!LupZFE!eg0AYs1d0hck@a zn%oF?g}*U-`73fAWS89^3M=OPz{f(F;X;4C=^O-@dz3K$ChCh{AB3)3%`xVqq=ZXZ zm%}S-)4tg9{Q9&K+m5XypDR0K>iqug)^+eO^p)Vt6Ruj#`Q-*LSziXePNz{v*|WhiVpp zFuDI|+g-!d-D0;qk`@*JMK=K+Q;&Q__RRI6f_nG%1#G_JS~3&)q}5<~jIqLAPy|#M zUSMN}mhI6%HL9~ub%i^(gfNuF4v47p8ob8%-dVJ|Fr)V_3)}Q}n0C127k;JcWlRH& z_=FdxDusV{b&`o#)vl9M0)YzDf=KLr*>R*SVW>)dzxLb}457MAW1R1X ze*ZU&oSCZ7T7VkmmU2OL+rh6^P7?NoA7;Tl(V*Q|Zq3O3XWXuz3^|zB+0k9Q=0}y{ z?kTl%E9M0}{kg&^WQY(lqcv&?PKK|$fsWP~Or@aFzAkG4x-NCgorIhl?ULEuT!xcys-Y_& z<*WoeP_N#PmyMRI%f4b%V=oZ0)LyrU(=aj*I|_FXmFnN7dmE< zm`+BXZ*nd&%H{TTK^z#_UTzP}%}PH-o%;L3cb>}1>3fOAL&&vLoxEEmFj+k%4(%-F zj@`{05{f&KVrme1t4wvK;J$rW!k){jt9iT834!d0rIAu~rHA<#op=?B{aC*I@_LlZ5hQ)$(cUnQwgqZN3 z*OiMC*YO-RA}z{CGTLy?eN(~s#wsKhKRMZl2F9(=L=ig2S9?q72wHbLrK8rC4&OJv z0c>p6ei*uNU9JOdh_Z`AhUU9RA2-_D8-DQFve(%aO5hQv7kT4SSt_>y4CJCElx&73|AMO~}7Y;j<@;eeJJHt#R0rM_Oo@;{E(7xoQ z$3BrSzIltRBAig)M9gX_XA&AusvYxqIfWgCyz;~4H9C+oMoyraL)U2Ke_>lMV5)@6 zF(s8cxA&8|8ttio69ePxyXmhsxk4V~KN&IBWb)=DGby`Cw`+Kd7xXu6iM4}RtOZ74 zIn<^nR0Ua)MSzB7#ql_o%x9+%FYDqq;D27w=h^$O=%pR+l^>0vumnC*#WDyO_DzDW1>C1vh1IL2G;7*{CvWp)2{fq|xlYNX3zt-OjgWECwB_I7n7 z0zIEbTpBxTEUR$JRy{n>8o9@xg^|04+~p0n>ccpqVbv;`$1XXO0#P1+kT5@RK%$vh z$m4B;FA`yh?NISF^|$cv#p+#hvHfTgX5jinuqqH;LbE$aamO$I-FaBtSJq-hnT;c5 zad5meg0xH99U&@4&$~*stzmDs=4G648w$c?U&}f>zaf7%`sdL|&ag0V`^P_&{IW{a zCE5B+xo_VN5`?ad_Qm@j6{B4)`w|A2viWv=o~bv-EO(fY9HD17NOK=|1TVssnb=g7 zvI_!oZc>0f0FGo~XVW%T+ZT;^QVX(ewKUA$M#gD@-$Rl(x>Cl>EieOOsRfXCs|t2gGUcLO!JF;E#CmLh}=h=J$y2|Al9#HArKx7E~SN-+^? zS!cQzc$~WXF@k`RU;B)7RIhS+!1n)0gra=v&}kd;(&$17^tk&E2|VY9jE2bgzU zmb+<$=>z8!L;FdmXXlVbjzMZU!Wy7H$cNz*@dgX@%D8UIyJzOI>R(tmbADNvrkOW8 zE~-$fiO!v4r=}$>vB|h95FtM2i{X(1S2Xl!jroa94h!*TtFe;Q`K1ztHZd$!@%+s= zb6gCs^7Ysf;VzqY7mRA@F;#(PBLvESnFYlV?875O0{3<&>%Kt$i?Y&6Ab|`;Nz!7Z zwXEl=)1GcQy@I1SNQ=QFO;N2m=6Aq=Nk-{oY{qYnLW6nT!)RuN-qGCQdXVguc!lYm z?x59Xkknr)KdrWO@;Le3GQc)E9X2yp{0JsuP_UVMm9Er#863(!)gZMU1T;CK1V}kS zGqe5&UxP6GM;mTq+oS`l1c@C>ECqYzj-9wJlo7(OmjgN8o3U(KS`2+9z)NgmbumwY z-T9zvgP-PhEje>t_rn3!4!$E;Ex{rjU0H=&Br6_e2Z&Mk93Ca#Ga^s!{ef1yV3_>_7=&9Sp%dmUM))Vz%qN=LO?W42fFl@kC zE9ae?cQS(q57`I9MQ;-5O!|8iI02cvM}PiAr-(q8$00{R!TXrt)v2vqn~!?4ml_r0`9oqb_N+! zfWd6Gsfm{DZ1`#4EGM1|sD3l4=9Tq=hk2sGN()ULuKRT2OtPYQiT+Y(X|WSE}2Q$ovhLD4kd56=XfU-HZQ} z)4XP2TR4<@*(bPJz_Z=eqItXXvfm}-bf!OVGI5n_vdHT2s#2E39V!mN{csXp+C2HC zIOSN5r$=sN&q;4{FYaF|8Kc-lK_8SQc7Xq^sXq@7<9pC*-N@FrqSO%S<5Y85>^v zdOzjyeZ+F%W)>;+8LN=6SCQDsfv(hS1xp|HV`&$TT?P=ARnUUb<|`jvn2v{1#fDXa z+=2Ld7!8xRLv1m`-il5_vTz$~qamIaYiGph`kxnQKLD>$Q=$EW=fSCNIAHu)Ea@^6 z9;XNIDYG=+{~lXhvqW%TRQPRJI~1jh;<6c~VH^U9Bdbt@3_h1f(!I(6rf}lm>csjy@6eI@si^;`=^f zF4n$4fS7t`kIB+8QXGBL{E9^&6)*)I19LD5i_sWDWzQL~tGo(p+ENv{gC^JxvIC-* zD~n4UR^b}X*<--xr$1_Z%ym8V8q6ejqDbti*fK*)2?rmJ{Q*8p$9ZOGG9$A!W!tVa z!{EW4=gbg-rLtaV0Em=mqrZRQ;6-*B>)fnlm~#7W$62kL022e&sdZ&%I_4w|debJw zYA?y(bt5-|I-{`H?!EmBC#Syv#7#m8PcR#K$SX8`Tu{)_|L3R8Gvep@13jDH5j>E<`%$Lo30ut?WYR%dhKQLvnBb-oWrActqF^2mL@6H<3Jam<5gvF>-3o z38=2Bl~bxf6S%mwU4ucxD|aYA*6g5sd*cDzqPZChMrmEhF7`!Kv-|8Hl+UHJ&U+0|oNTnTRI zM0s@?2=xSB@e0Hu0ZczF+6GOR&#GUkKMrHvQ-z^HHY}Z!hsmYt$9igU!4xw z$~3s5%fz1$$~p0K5{h%=l8o9>Tf!AnN+t0eG5*%m(hP}m`Ip_mBGcEI(8E^6+x~gR zRX||EX=1x&cthIlY2u)FNrP<;NZRB5vB_~;$BU1-L>k~p8#A<+OiSJjVi#Tb@8?N- z<<=?>r?`EMVNcI=fsQySRkAZJPLTWg$M)$DRlEX)M@jp-Q=VzA0_Waf|Ap$~##>%G zleJ|*Wg4yQSpC!OV_7$7E z#KBpc;<1ZyKA(kSS@{+pn3YSGLgZ07q`k{GZ(@KlMd3_(ky&ar8XChQBEY*(htyBZk9C>o&FSj&1xd5 z4QvSTL$!GlpqxW-NgJ7tP3JRhQDlEICHUOLBBeWv7%i&G$BMI+VbnPLCtiBFo29 zwYG)lGBJ}S1~TznQqx^RRv5W^W!sQ^^j>$O$a72dPH%{@2LuY&iJuBf@l*l9<=>-7 zFlcXaDO(H^;OL}JwW2I)mE!2&`$3$%d3|s5Yvm&J;ymh!p@v4SqN(f#2J2X+i7xV%@e-9 zW{;7^$ian+ZqRdMbyH<$C#&u}uuf?YxTHICTBCA1j+*blNM^fV{B1@b6bjP#dH9Xs zJ3V@8>BZsQ^{pCfIyvh)-q}xTj1Sq|+h+-T2ACw*JCPCS43xu~28rE}BbJuQVCl~e z#!zvs-VYe;l&zDpWn6z^Js-wv|~Ea zDWhb|h_*!Mm60rEMyl@il^W0ZVWg=!Ek1J~B?kY_*7ecF)t? zi8}<<$k^)CN&8;sKQRjK8Nk`7&mUX($##BLjCB?W%(ltxl=gzizUTTz=ho_|>~lO* zWs)fO30Gvy`v$Zp-Zoj2Ic_bIGE`RkH&RMEl||AxT)-@ zm6jZXWg)H@0T5uI?Slv^g8l&IIzd@N#FD=wf)2|45mWDAYvPocr*kOMUQ+f&?$n?h zZHcuwUAi~7XT+-K0TA3045qU%-6dVvFC3SyRR$03D zsNmB`6$BXWR|8rg;Kl%FyZk=7cFSHOh^djxz*XXFEip{)TFGM|OOmuxuNV$UKd7ny zMFQ}R|0F_8PM51TC)HR`f?z-rtHW4T&AgNA*QXJvf4Wp7zy1xY9!v&2ZGFsh&Y z{gs{y6ASIM5{u?v;DUFK{Vi(5F;{X=2N5JWX+y2DZf<>fp!erQa}f*-KkMH&0))h! zI8qiOL(Us>z;PD2jTAEZgtI%12#W}YO*pAL033tD-5bTc6VAgS=WOsO$*J#5sE~@^ zpo0xA^XV6H#2aSc=fNq-&7z{rLM2|+^cdUT-3Q1i?qllzQmU(a$Sx??{gt8`3QIf7 zr+Ka4A|+H`u7X{JoCZTu@!#OU(h`(K%%oyRb!i{Sc#!cJ^L1ns3hQ>|RBKtxbmi+S zPjX}F_2j$;6*!2f&h&6ogz2tXu*S%^7F0?X66`Pz`@T~9yJb zxsMbBQV+}DlI#9X`~2mO^|IKM-;~|aSX4sbpSa7ah0l}74G|7&w(3XZ1V{!*j>=z_ z$11s`7et#Ob-S>q{YCANeM;6ZE3KCJ!Ga%Q?q%`ZQ&pDA_PI4tPtkNiWav`O|H-iJ zT7L4#4aq!=x>Vs%TDl?zC~53yb=Pb_~srT0Ul3U`hq~ zBttsw80RmWR?I_P0E;G4kpYZwFuH`(vUh<5Q%9FA5hAHyn~J)gWW(Rg^1k{kI)K0S znzJ^ECyu)~@o$RmHba_Y{g2GMgSHm_5I%+C3#ax1At__b52d8w5V!g}AlxdA5{p@c zmmcuj)*eA0i61w3$YoOrn&jCP!B_48*MkQMWjI^wuL`Y9w0)4x{*=WOT25y^LH(>? zz>xRj&7oT3dNiW!*PmjzYNiW|YTRr5FTErU(!&EkL72ohooFZSo0&pM^bSgV zyC^lnw;X^|^t%}2B5Dj;8{phAa(+JPq4hKN^9p^tA^e~QmrUn^Bn)v3gXXkaE z2a{EPn)aDJ4FmCWU@m2MKXJu*O^v7TrB+;C-$eCjq+12 zYZIQp?!}iDO+z$BLxznQBB=){Be-i%emw}x<`+Ds0N=nYk^-T5Xm*Ij3^n|qK$3K@ za(`;0^hho#VWO`leH1~VIc5nSr_82jzwQpy?WVzEY6>6ab|C-CXx>>UCt1UKh`kT$ zRz?cd)g^6CcOOdIZYe>2BS3t+d4GvFmRZk{N7<#hj@BP%aBr_}Yhuox$?y6dumQ0+ z=?B$=&bCuSn0bWFI{TqMIWcNQMwHN$EEks~ovN?i%1GjW4xyslazH0qb4|{YGl-l{ zpbYUd(v~DVZbY}ka!olF9!x594c5e-(W3STr{NVTff8{HF`mzQOKCA3l62scMl~xv&~UNzJDxR&Hs^nFIYDq&_3kB(ach-h%aFc_gyN zIV?Mi>P8dN>IUnc3jKxYiYX;pnj8Tzk9c|b>D&lUAVeFiC1i2(?zLsXuvHQ&x?QzJ zXunlbRJ1r-sp=}X_-;P>g78p#U-4lfWdB{{K3VM5FR9~K%8{8ed+F^^3irW0v15*s z^Hg>X`ox@r8k^1>7#R#i86BxJL^<@+xDWpPfSI=po>K@+IyK4x844gd+z+X}5y3K5 zITyQ+cHK2vNS-jg$r!TS;vJJ#TfCccF|F*(xSaK9sR;$>L@PZIbaj z3uN5IQs^coB(@}RNo$#8(#a}NyrI*wp<8-V81e*F5N@i)K*7w+2A+?J!DpO{x{U; zDR{-0$w%22A0P(gN<>f*_N7^ChEFBCr13e2b}Zo66c3Cgs~jM=u@$7NM?f$9XH07F zatTxB9Zva-JnX0AA9mVs+v4524vF4rh0QBQlNYL_Oqt`&d$W8ewQLb<1)b6=$VycC;TW=_!a=>?YS)M~K(7x|aoE~w6{fT!mn(3)07IN8pWX1EXrz*5mS zQXc$>08j7Ln{LBcxJITO)%!^VB7M)(`AT?+ytHe5Ly$cyk+9(CirwGIYk~lYAvMSA zT6|Opdt@E<=*g=mIjY{xLBr%|;NxOYbz=O|-MkXmB; zTuDP4S2ytW{cM^>txAV_>*wzqr+o=D78y@N#oXn(9(-)_xw2^Et6rXeR$YMGPH#^v z{Q@yKeB-q8oz^K`WOTga4$HZv0Fns=A!O6(JnhmzKh7SK=0RlUN*1rS2p(DiLUMr{ zAejd6M`uDIDm*3*Z|InN2{pVh5^lWS;w>p;|m-U>a9;)6)cHY zap{h=hg7q;%6P60P)x*NHBHz7g{>kE;Zc0@^ErRPLk4tXzK1Vo>cUi*teSLoL$CTQ zZ#cUOd`XzQT!J%AU*?obxgz8dhR#@JIcOqtnvSlj;S7y)5IXK(QA2sO;C3V`@^}Ag z%>!-C&DUWB1nSH?TE$xnKzz?S6w=}Jve&=ExH&mbq0o_>wM_`KVA&XAY zlCunp_+27-Q;*1+KSiMueE~h&8P(VHs9#l3?>V80dl<5S+>ykLJO;crB4M8794Ai; zqAC~Z8-d}I(pSYOwy)GRRtQnu_v<=pwd*?OK7sB?%RT9dFOx1t%QS=srcFBK`gW?B zug4FS8Zg|$4#}PVW9%utUvt-tznD9nD4sBLt@JJHd8l#31eF;VBY}CyOJkoQy?#{j z2?^6BvWLg@LYJ0s`3wwrrlkKRL*hw22#H}7=sCIDmF)@k;u>hM3=pE{VGW@UA#I&8S(%`4}7t8-%`a2;L+DV^ruBD(5sVxxaV%r+o$$ z*tgs&H3t=z?^hIAeEI?|;DA*2jz=EW%LJGON9QTiR4mES!p60!JP=VMb` z+(V=kgocmDT3Ie#MEE}2n0RW`(*EvO6Z0!M6Hp=4Y9V0lI{$ocW$1YmyJpP1hh_>e zs4i-S76oiIu6G+x`*A+4RP$8P>gv88EpaFkg&`o&&4>uz*(Co_mWS}xcu??Zk7wF6 zutSM+Va4oxx>O|s6LguByNS&eP)k<~LzXPH`+J?-8>bM#_v_;Z_Z&Z~erlbnYonE} z&&5rWg#Nt-Pi6c{z~{oLd=>vCSj~}B-VsrvXT@MEOvi;TOVggaS)jTH&Pz`eOG30k zee+z{di}yw(ieMiuC@C@^8To%^SHCJNCai3?(yYgnjfSMG>(568rFs2^_x!Z`5Lju zne5L9Py1+iPABaj&g2PnnO}0i=8scbk*{7f@71|qD!=@Wsx6R&s{h4NM#nBc?rmPK z0^Fn3)w<}%kQfFS|#!kfw-FqN0_}~>ymROleVTn#C7~kou8C8ybA&TPp<(RuiM7O5{3oc zXOO1yuH++GX@%f0=hbc0h-M77CY518`nQ~Wd5lVy`!Y+uS6%nAaZfu0!s7$|yEtHr z(uI>t9PGGclRW3SQkr`WeV!lmrt@-?dwT(u(i!k_Dqn3_Z`~4WQNZV|ZpkoU8Sn3i zsQMPdFoVY6w+N$y-0+4U%|0?5Ax%kihAy+H4NxNWh|nGCkuB^2&scYMuDI0sgeqtE ze#hz-#9R7yL#7PEV_AMr@l5jkcU{L1$+t=gqC?yiCg8ebhbUXS#1HIOox(`3~!I#oNZ}-Rv(tveM@1G z=QyMNfq|W=4plTc!}Iv@3AMPcOLp_DJK5u&2KZHP;>vofs3zYaC}>;iQ4`l|_3*O3 z^s38tk*k9H1$rc+ncs6hn$RJusb^Nxhf8;Q?X0;9M#GDQE_{`K-JKEvd59#n3-|<> zj8^(|zI0KU-^@rdjz}8!E~jYUO1u9=TocSm#;#wk#exbk>{ud0L+Es1eZ6zHFzlMj za;cE?or^HBVCpu17$t2qd>_2XSfO7Hq_<7$kY2Yxls_q{ZFb|}+7ls*BmY6XplvA> zIbO^eq+)m#D9_$< zV7kR-`bo;Rls(T_$&Nx>hsWo&F^J{m`a?5Jk3kdqX7Q)yY*!2`oglZ&c5rrM!K= zCY_le5$niwIa=->>GY}njqUA0lb{6As8ULSfqEEac9T@++q{89^rs;;_3QDbt=8DA zzPSIPtVL0uB~d>O8dchvFa#a>-w#^QVq$rn6lkmt89K5kH~SfdK^F;>MD@9T&0(`Z z_E&VjX8%DOG25$WAc2PV$QoQ$KRyvp;FBxX^G8gn19G;$j0~q93U_-Qs)<5i*7D)9 zbE?NT0&rIjcv{{AY7a36yp#iiigMEmOwtL^%5lJ8 zSaHz;MBEqete8(4zhHuu*^O&fAfeD=h|-}PjC0^eCzMu(@h!VTiuUkiu66avde?PF zd3ovfUq@cMKgVxW7g^6!srS7z!Gh-&76zxVB!niEYlF6xsWx>y}* z6NVt>62R2Jz_7WbJXvpb(YauYG|=d7TF;64TwW-))nUWj<1d!Mltv*7zsdKGp)417 zI)X3Ikzrj?y?mArI}3Kd^?}?6FZeQG{i{lEzE6~tB*O7!&VV|=MJT|X*UwAlQ1rl$ z#!zwj9MMhB4PEUFcu>?AA!a(B`t)WPaQKLpl^(}|i+!JN$KfqP z3tow-;g%o1MTVU`D{bMoOh;RwMrVDV*X5k5g{X*e24V0dI9R!Qa-DENzQ@UxYYNC& z`W%Nj1X!58#z)ysRn1~0eJvgmmx34EIA>2Z^&}wToTf1ZoJ1Hk_QD!TKB1aUp~`*< z>IHMB#)N8-MbbK7O#Y9=peWC$mb(0tqfmRCzLnsq*b5ydSAtxvFGyPh`%PGvTDFMtBf4eh!ABR$+=*#%N+J8ZDhB1Do?6Mi!RsF`Hf7K)3o+sV zwrEO7qG5=XL^>@hJ>e#JUyu&CWB;s3ub!YA!2mc+j| z^B6BZ8nQR?6K2m;pap`~l@ba>7~=5ua@NH7zA3N5G>T13(*=6?W+=05#&oY z0yvydz8KWnd~l1xc+5w8hF`T9SeyF41&U@RzeCFQDt5jw$%7sP?Dpvgwz?`$2Z0QO z1wG-s2*Li zL#ThoOUPzhP@4QqECifmRQO>xzWp=G0pHY3Q1aW24(FTf&7CmFg>n>d=uS#Jd%_$9 zmOB3z)a+BX2^a3dlAP2J4(bOO#3AMEoQoQ#tO{O{IW2T=1-<$f zY#8sqEoS!Ns+>TblPG+6&-}XXpJ^JVpkLk54*W=6F}DH~8{8lVz|l`=JkZaLcg(M6 z|MM5>pqj2|98~g0R?N53121l3yem8iG^r#Z%`NdNqk}@>_IP$;o~m##sBy{seW6Di zj80_l+8!A2>sx;I4HnSOMg*>o|0x^;5QhsF$V((;Y3$Sb=jgd?{$(z5$)UK~+^5zo-?5pk`(g$*(r^55+KfC&5ZZMwJm9xdjL4K5V> z6U|BdpAXe=V!-f@RoJRBqk}CN=U9z}iAPc}a5zlS{JI}QVGfqv&>@tSCPWZ55Fj%~ z3=p?;{s3vmXa^?)g#J;Wtgd6Ey&Ics%)fweGKQ!`EHUe9$s!pF>`D#}a{4ogY5LDv zHhF%2i-(Z@mx5724{|8eW~Xwe-H}F!aCWo35pYn@^o7Ao;oK!EiKRUL1n)`+cK+}? zmeugO*L}xFkIc{?28FFTMZ6DkyU<(rs9qEfNjC)!cB3it>#$NxK}^`cx!s4d;r9>F zfayPR58ojEHz*XKr;xN9o5a7E|4AKa^oQ*L;a_|vk^~x2uuS2=gL*`g3l@w%6HgBs z3N)!;u;7G0?4}g|8vg)2?D#}$K+X%1z=M8&S%^va*LV-~u)aZRhxp$cyrE79E9h(V T315P~>kTF?t{_$;Vif#8pYitM diff --git a/mide-plugins/vscode-1.0/package-lock.json b/mide-plugins/vscode-1.0/package-lock.json deleted file mode 100644 index 19d5e66..0000000 --- a/mide-plugins/vscode-1.0/package-lock.json +++ /dev/null @@ -1,4852 +0,0 @@ -{ - "name": "mcpp-vscode", - "version": "1.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mcpp-vscode", - "version": "1.0.1", - "license": "Apache-2.0", - "devDependencies": { - "@types/jsdom": "^27.0.0", - "@types/node": "^24.0.0", - "@types/vscode": "~1.91.0", - "@vscode/test-electron": "^3.1.0", - "@vscode/vsce": "^3.9.2", - "jsdom": "^27.0.0", - "typescript": "^5.9.0" - }, - "engines": { - "vscode": "^1.91.0" - } - }, - "node_modules/@acemir/cssom": { - "version": "0.9.32", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz", - "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@asamuzakjp/css-color": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", - "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^3.0.0", - "@csstools/css-color-parser": "^4.0.1", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.5" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@azu/format-text": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", - "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@azu/style-format": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", - "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "@azu/format-text": "^1.0.1" - } - }, - "node_modules/@azure/abort-controller": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", - "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/core-auth": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", - "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-util": "^1.13.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/core-client": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.1.tgz", - "integrity": "sha512-2QygG2F76ZpMP2eMztiJvAiFMu71M9rDeU7vO/QKg5Css7MgM4frUOslFjhVjRhbGaCNPtz/S8M6y46/fFKVuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.10.0", - "@azure/core-rest-pipeline": "^1.22.0", - "@azure/core-tracing": "^1.3.0", - "@azure/core-util": "^1.13.0", - "@azure/logger": "^1.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/core-process": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", - "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/core-rest-pipeline": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", - "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.10.0", - "@azure/core-tracing": "^1.3.0", - "@azure/core-util": "^1.13.0", - "@azure/logger": "^1.3.0", - "@typespec/ts-http-runtime": "^0.3.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/core-tracing": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", - "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/core-util": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", - "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@typespec/ts-http-runtime": "^0.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/identity": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.14.0.tgz", - "integrity": "sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.2", - "@azure/core-process": "^1.0.0", - "@azure/core-rest-pipeline": "^1.17.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^5.5.0", - "@azure/msal-node": "^6.0.0", - "open": "^11.0.0", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/logger": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", - "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typespec/ts-http-runtime": "^0.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@azure/msal-browser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.26.0.tgz", - "integrity": "sha512-GYKRquzDZmZggCdStWy2bNJKnPui1upssSuznJgP+/h6Mb0ldkUkaGvaY6uSGGmZOm0CGiBQ3b5BiGy43BpXSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/msal-common": "16.14.3" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-common": { - "version": "16.14.3", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.3.tgz", - "integrity": "sha512-tz3GvMrR9V/plb2lDdW8qaO8CPpfZevdExBKjltzzbAtyhK0eB2UIvji1aU+gkyHj3fxQ2ie+LNHyMXdmo8wOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz", - "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/msal-common": "16.14.1", - "jsonwebtoken": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", - "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", - "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", - "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.2", - "@csstools/css-tokenizer": "^4.0.2" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", - "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.1.2", - "@csstools/css-calc": "^3.4.3" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.2", - "@csstools/css-tokenizer": "^4.0.2" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", - "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.2" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", - "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", - "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", - "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@secretlint/config-creator": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", - "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@secretlint/types": "^10.2.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@secretlint/config-loader": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", - "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@secretlint/profiler": "^10.2.2", - "@secretlint/resolver": "^10.2.2", - "@secretlint/types": "^10.2.2", - "ajv": "^8.17.1", - "debug": "^4.4.1", - "rc-config-loader": "^4.1.3" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@secretlint/core": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", - "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@secretlint/profiler": "^10.2.2", - "@secretlint/types": "^10.2.2", - "debug": "^4.4.1", - "structured-source": "^4.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@secretlint/formatter": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", - "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@secretlint/resolver": "^10.2.2", - "@secretlint/types": "^10.2.2", - "@textlint/linter-formatter": "^15.2.0", - "@textlint/module-interop": "^15.2.0", - "@textlint/types": "^15.2.0", - "chalk": "^5.4.1", - "debug": "^4.4.1", - "pluralize": "^8.0.0", - "strip-ansi": "^7.1.0", - "table": "^6.9.0", - "terminal-link": "^4.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@secretlint/formatter/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@secretlint/node": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", - "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@secretlint/config-loader": "^10.2.2", - "@secretlint/core": "^10.2.2", - "@secretlint/formatter": "^10.2.2", - "@secretlint/profiler": "^10.2.2", - "@secretlint/source-creator": "^10.2.2", - "@secretlint/types": "^10.2.2", - "debug": "^4.4.1", - "p-map": "^7.0.3" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@secretlint/profiler": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", - "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", - "dev": true, - "license": "MIT" - }, - "node_modules/@secretlint/resolver": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", - "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@secretlint/secretlint-formatter-sarif": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", - "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-sarif-builder": "^3.2.0" - } - }, - "node_modules/@secretlint/secretlint-rule-no-dotenv": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", - "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@secretlint/types": "^10.2.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@secretlint/secretlint-rule-preset-recommend": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", - "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@secretlint/source-creator": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", - "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@secretlint/types": "^10.2.2", - "istextorbinary": "^9.5.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@secretlint/types": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", - "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@textlint/ast-node-types": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.8.0.tgz", - "integrity": "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@textlint/linter-formatter": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.8.0.tgz", - "integrity": "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azu/format-text": "^1.0.2", - "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "15.8.0", - "@textlint/resolver": "15.8.0", - "@textlint/types": "15.8.0", - "debug": "^4.4.3", - "js-yaml": "^4.3.0", - "lodash": "^4.18.1", - "pluralize": "^2.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "table": "^6.9.0", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=20.18.0" - } - }, - "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@textlint/linter-formatter/node_modules/pluralize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", - "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@textlint/module-interop": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.8.0.tgz", - "integrity": "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@textlint/resolver": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.8.0.tgz", - "integrity": "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@textlint/types": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.8.0.tgz", - "integrity": "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@textlint/ast-node-types": "15.8.0" - } - }, - "node_modules/@types/jsdom": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", - "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "node_modules/@types/node": { - "version": "24.19.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", - "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sarif": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", - "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/vscode": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.91.0.tgz", - "integrity": "sha512-PgPr+bUODjG3y+ozWUCyzttqR9EHny9sPAfJagddQjDwdtf66y2sDKJMnFZRuzBA2YtBGASqJGPil8VDUPvO6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typespec/ts-http-runtime": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", - "integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==", - "dev": true, - "license": "MIT", - "dependencies": { - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@vscode/test-electron": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", - "integrity": "sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", - "jszip": "^3.10.1", - "ora": "^8.1.0", - "semver": "^7.6.2" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@vscode/vsce": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", - "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/identity": "^4.1.0", - "@secretlint/node": "^10.1.2", - "@secretlint/secretlint-formatter-sarif": "^10.1.2", - "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", - "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", - "@vscode/vsce-sign": "^2.0.0", - "azure-devops-node-api": "^12.5.0", - "chalk": "^4.1.2", - "cheerio": "^1.0.0-rc.9", - "cockatiel": "^3.1.2", - "commander": "^12.1.0", - "form-data": "^4.0.0", - "glob": "^13.0.6", - "hosted-git-info": "^4.0.2", - "jsonc-parser": "^3.2.0", - "leven": "^3.1.0", - "markdown-it": "^14.1.0", - "mime": "^1.3.4", - "minimatch": "^10.2.2", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "secretlint": "^10.1.2", - "semver": "^7.5.2", - "tmp": "^0.2.3", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.5.0", - "yauzl": "^3.2.1", - "yazl": "^2.2.2" - }, - "bin": { - "vsce": "vsce" - }, - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "keytar": "^7.7.0" - } - }, - "node_modules/@vscode/vsce-sign": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.1.0.tgz", - "integrity": "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g==", - "dev": true, - "hasInstallScript": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optionalDependencies": { - "@vscode/vsce-sign-alpine-arm64": "2.0.6", - "@vscode/vsce-sign-alpine-x64": "2.0.6", - "@vscode/vsce-sign-darwin-arm64": "2.0.6", - "@vscode/vsce-sign-darwin-x64": "2.0.6", - "@vscode/vsce-sign-linux-arm": "2.0.6", - "@vscode/vsce-sign-linux-arm64": "2.0.6", - "@vscode/vsce-sign-linux-x64": "2.0.6", - "@vscode/vsce-sign-win32-arm64": "2.0.6", - "@vscode/vsce-sign-win32-x64": "2.0.6" - } - }, - "node_modules/@vscode/vsce-sign-alpine-arm64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", - "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "alpine" - ] - }, - "node_modules/@vscode/vsce-sign-alpine-x64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", - "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "alpine" - ] - }, - "node_modules/@vscode/vsce-sign-darwin-arm64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", - "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@vscode/vsce-sign-darwin-x64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", - "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@vscode/vsce-sign-linux-arm": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", - "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-linux-arm64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", - "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-linux-x64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", - "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-win32-arm64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", - "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vscode/vsce-sign-win32-x64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", - "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", - "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/azure-devops-node-api": { - "version": "12.5.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", - "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", - "dev": true, - "license": "MIT", - "dependencies": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/bidi-js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", - "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/binaryextensions": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", - "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", - "dev": true, - "license": "Artistic-2.0", - "dependencies": { - "editions": "^6.21.0" - }, - "engines": { - "node": ">=4" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/boundary": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", - "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/bundle-name": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", - "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cheerio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", - "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.1.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.19.0", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=20.18.1" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cockatiel": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", - "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssstyle": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", - "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^4.1.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/data-urls": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", - "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^15.1.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/data-urls/node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/default-browser": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", - "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/editions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", - "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", - "dev": true, - "license": "Artistic-2.0", - "dependencies": { - "version-range": "^4.15.0" - }, - "engines": { - "ecmascript": ">= es5", - "node": ">=4" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-uri": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", - "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", - "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/fs-extra": { - "version": "11.4.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", - "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", - "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globby": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.3", - "path-type": "^6.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ignore": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", - "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/index-to-position": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-in-ssh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/istextorbinary": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", - "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", - "dev": true, - "license": "Artistic-2.0", - "dependencies": { - "binaryextensions": "^6.11.0", - "editions": "^6.21.0", - "textextensions": "^6.11.0" - }, - "engines": { - "node": ">=4" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", - "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@acemir/cssom": "^0.9.28", - "@asamuzakjp/dom-selector": "^6.7.6", - "@exodus/bytes": "^1.6.0", - "cssstyle": "^5.3.4", - "data-urls": "^6.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.1.0", - "ws": "^8.18.3", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/entities": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", - "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/jsdom/node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "dev": true, - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jszip": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", - "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", - "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keytar": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", - "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/markdown-it": { - "version": "14.3.3", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", - "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/mdurl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", - "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-abi": { - "version": "3.101.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.101.0.tgz", - "integrity": "sha512-bkb/RkuWCE64eWu1PCh3Fu2pGc9mKRs+kOE+FU2bGHRYlQa86/D321DSATV54AsCQMKz3/xSt9sRgraZTpqdhQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-sarif-builder": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", - "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sarif": "^2.1.7", - "fs-extra": "^11.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", - "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.5.1", - "define-lazy-prop": "^3.0.0", - "is-in-ssh": "^1.0.0", - "is-inside-container": "^1.0.0", - "powershell-utils": "^0.2.1", - "wsl-utils": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.1.1.tgz", - "integrity": "sha512-6Lh3jQPLthYujSrlw6ccWXBGZTkoTCz27Hd92g79LH/kjAbgoEu0t442cCCNbegPSJsA0lwDX8z0bcKtkpA32g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)" - }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-semver": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.1.0" - } - }, - "node_modules/parse-semver/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/path-type": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/powershell-utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", - "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.0.tgz", - "integrity": "sha512-5LK5BZ2XDHMvcUJaf0CQ5HL3aVgKbpklvByC7qLSxYkfE8COEEbJg5xCHWCTqzqi0Gs4Jn5Mv5v3R38W6edscA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc-config-loader": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", - "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "js-yaml": "^4.1.1", - "json5": "^2.2.3", - "require-from-string": "^2.0.2" - } - }, - "node_modules/read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "mute-stream": "~0.0.4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", - "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/secretlint": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", - "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@secretlint/config-creator": "^10.2.2", - "@secretlint/formatter": "^10.2.2", - "@secretlint/node": "^10.2.2", - "@secretlint/profiler": "^10.2.2", - "debug": "^4.4.1", - "globby": "^14.1.0", - "read-pkg": "^9.0.1" - }, - "bin": { - "secretlint": "bin/secretlint.js" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.24", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", - "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/structured-source": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", - "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boundary": "^2.0.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", - "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=14.18" - }, - "funding": { - "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/table": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", - "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/table/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar-fs": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", - "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/terminal-link": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", - "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "supports-hyperlinks": "^3.2.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/textextensions": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", - "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", - "dev": true, - "license": "Artistic-2.0", - "dependencies": { - "editions": "^6.21.0" - }, - "engines": { - "node": ">=4" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/tldts": { - "version": "7.4.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.19.tgz", - "integrity": "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.19" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.19.tgz", - "integrity": "sha512-goNXV6Ho9TIOfpJZanTd6CKG5DyYgYAL+k+22T0SLh8l6+KHIfBUPFuePuSZv05UttIaaLmI9tb7TmGIdK5AkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", - "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-rest-client": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", - "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "qs": "^6.9.1", - "tunnel": "0.0.6", - "underscore": "^1.12.1" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/underscore": { - "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici": { - "version": "7.30.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", - "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/version-range": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", - "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", - "dev": true, - "license": "Artistic-2.0", - "engines": { - "node": ">=4" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/ws": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", - "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wsl-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", - "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0", - "powershell-utils": "^0.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yauzl": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", - "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3" - } - } - } -} diff --git a/mide-plugins/vscode-1.0/package.json b/mide-plugins/vscode-1.0/package.json deleted file mode 100644 index 58297ee..0000000 --- a/mide-plugins/vscode-1.0/package.json +++ /dev/null @@ -1,301 +0,0 @@ -{ - "name": "mcpp-vscode", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.1", - "publisher": "mcpp-community", - "license": "Apache-2.0", - "icon": "media/logo.png", - "repository": { - "type": "git", - "url": "https://github.com/mcpp-community/mcpp-ide.git" - }, - "homepage": "https://github.com/mcpp-community/mcpp-ide#readme", - "bugs": { - "url": "https://github.com/mcpp-community/mcpp-ide/issues" - }, - "engines": { - "vscode": "^1.91.0" - }, - "extensionPack": [ - "sunrisepeak.mcpp-language-server" - ], - "categories": [ - "Programming Languages", - "Debuggers", - "Testing", - "Other" - ], - "keywords": [ - "mcpp", - "C++", - "C++23", - "modules", - "build" - ], - "activationEvents": [ - "workspaceContains:**/mcpp.toml", - "onLanguage:mcpp-toml", - "onDebugResolve:mcpp", - "onDebugDynamicConfigurations:mcpp" - ], - "main": "./out/src/extension.js", - "capabilities": { - "untrustedWorkspaces": { - "supported": "limited", - "description": "%untrustedWorkspaces.description%", - "restrictedConfigurations": [ - "mcpp.path", - "mcpp.ide.backendPath" - ] - } - }, - "contributes": { - "languages": [ - { - "id": "cpp", - "filenames": [ - "build.mcpp" - ] - }, - { - "id": "mcpp-toml", - "aliases": [ - "mcpp TOML", - "mcpp.toml" - ], - "filenames": [ - "mcpp.toml" - ] - } - ], - "grammars": [ - { - "language": "mcpp-toml", - "scopeName": "source.toml.mcpp", - "path": "./syntaxes/mcpp-toml.tmLanguage.json" - }, - { - "scopeName": "source.cpp.mcpp-modules", - "injectTo": [ - "source.cpp" - ], - "path": "./syntaxes/mcpp-modules.tmLanguage.json" - } - ], - "viewsContainers": { - "activitybar": [ - { - "id": "mcpp", - "title": "%viewsContainer.mcpp.title%", - "icon": "media/activity-bar.png" - } - ] - }, - "views": { - "mcpp": [ - { - "id": "mcpp.project", - "name": "%view.mcpp.project.name%", - "contextualTitle": "%view.mcpp.project.name%", - "when": "!mcpp.sidebarHidden && mcpp.views.project" - }, - { - "id": "mcpp.library", - "name": "%view.mcpp.library.name%", - "contextualTitle": "%view.mcpp.library.name%", - "description": "%view.mcpp.library.description%", - "type": "webview", - "when": "!mcpp.sidebarHidden && mcpp.views.library", - "initialSize": 40 - }, - { - "id": "mcpp.cache", - "name": "%view.mcpp.cache.name%", - "contextualTitle": "%view.mcpp.cache.name%", - "description": "%view.mcpp.cache.description%", - "type": "webview", - "visibility": "collapsed", - "when": "!mcpp.sidebarHidden && mcpp.views.cache" - } - ] - }, - "taskDefinitions": [ - { - "type": "mcpp", - "required": [ - "command" - ], - "properties": { - "command": { - "type": "string", - "enum": [ - "build", - "test", - "run", - "clean" - ], - "description": "%taskDefinition.mcpp.command%" - }, - "target": { - "type": "string", - "description": "%taskDefinition.mcpp.target%" - }, - "profile": { - "type": "string", - "enum": [ - "dev", - "release" - ], - "description": "%taskDefinition.mcpp.profile%" - }, - "args": { - "type": "array", - "items": { - "type": "string" - }, - "description": "%taskDefinition.mcpp.args%" - }, - "root": { - "type": "string", - "description": "%taskDefinition.mcpp.root%" - } - } - } - ], - "debuggers": [ - { - "type": "mcpp", - "label": "%debugger.mcpp.label%", - "languages": [ - "cpp", - "c" - ], - "configurationAttributes": { - "launch": { - "properties": { - "target": { - "type": "string", - "description": "%debug.mcpp.target%" - }, - "profile": { - "type": "string", - "enum": [ - "dev", - "release" - ], - "description": "%debug.mcpp.profile%" - }, - "engine": { - "type": "string", - "enum": [ - "auto", - "cppdbg", - "lldb", - "lldb-dap", - "gdb" - ], - "description": "%debug.mcpp.engine%" - }, - "args": { - "type": "array", - "items": { - "type": "string" - }, - "description": "%debug.mcpp.args%" - } - } - } - }, - "initialConfigurations": [ - { - "type": "mcpp", - "request": "launch", - "name": "mcpp: debug" - } - ], - "configurationSnippets": [ - { - "label": "mcpp: debug", - "body": { - "type": "mcpp", - "request": "launch", - "name": "mcpp: debug" - } - } - ] - } - ], - "colors": [ - { - "id": "mcpp.cacheOkForeground", - "description": "%colors.mcpp.cacheOkForeground%", - "defaults": { - "dark": "#89d185", - "light": "#388a34", - "highContrast": "#00ff00" - } - }, - { - "id": "mcpp.cacheStaleForeground", - "description": "%colors.mcpp.cacheStaleForeground%", - "defaults": { - "dark": "#e2c08d", - "light": "#bf8803", - "highContrast": "#ffff00" - } - } - ], - "configurationDefaults": { - "files.associations": { - "*.ccm": "cpp", - "*.cppm": "cpp", - "*.ixx": "cpp", - "*.mpp": "cpp" - } - }, - "menus": { - "view/item/context": [ - { - "command": "mcpp.test", - "when": "view == mcpp.project && viewItem == mcppTest", - "group": "inline@1" - }, - { - "command": "mcpp.run", - "when": "view == mcpp.project && viewItem == mcppExecutable", - "group": "inline@1" - } - ], - "view/title": [ - { - "command": "mcpp.build", - "when": "view == mcpp.project", - "group": "navigation@1" - }, - { - "command": "mcpp.selectTarget", - "when": "view == mcpp.project", - "group": "navigation@3" - } - ] - } - }, - "scripts": { - "compile": "tsc -p tsconfig.json", - "gen": "cd ../.. && mcpp run mcpp-ide-dev -- gen vscode --out mide-plugins/vscode/dist/extension --stage", - "test": "npm run compile && node --test \"out/test/unit/**/*.test.js\" && node --test \"../../webui/tests/*.test.js\"", - "test:e2e": "npm run compile && npm run gen && node out/test/e2e/runTest.js && MCPP_E2E_BUNDLED=1 node out/test/e2e/runTest.js", - "package": "npm run compile && npm run gen && cd dist/extension && vsce package --no-dependencies -o ../mcpp-vscode-$npm_package_version.vsix", - "check:webui": "tsc -p ../../webui/tsconfig.json" - }, - "devDependencies": { - "@types/node": "^24.0.0", - "@types/vscode": "~1.91.0", - "@vscode/test-electron": "^3.1.0", - "@vscode/vsce": "^3.9.2", - "jsdom": "^27.0.0", - "@types/jsdom": "^27.0.0", - "typescript": "^5.9.0" - } -} diff --git a/mide-plugins/vscode-1.0/src/backend/client.ts b/mide-plugins/vscode-1.0/src/backend/client.ts deleted file mode 100644 index daec36f..0000000 --- a/mide-plugins/vscode-1.0/src/backend/client.ts +++ /dev/null @@ -1,164 +0,0 @@ -// The backend's wire, as the adapter speaks it: `mcpp-ide serve` started as a child process, -// JSON-RPC 2.0 framed as the LSP base protocol on its standard streams. No editor API here, so the -// unit tests and the panel tests drive a real backend through the same code. -import { spawn, type ChildProcess } from "node:child_process"; -import { EventEmitter } from "node:events"; - -export interface RpcError { - code: number; - message: string; - data?: unknown; -} - -export class BackendError extends Error { - constructor(public readonly method: string, public readonly error: RpcError) { - super(error.message); - } -} - -type Pending = { resolve: (v: unknown) => void; reject: (e: unknown) => void; method: string }; - -/** Splits a byte stream into the bodies of Content-Length frames. */ -export class FrameReader { - private buffer = Buffer.alloc(0); - - feed(chunk: Buffer): string[] { - this.buffer = Buffer.concat([this.buffer, chunk]); - const out: string[] = []; - for (;;) { - const headerEnd = this.buffer.indexOf("\r\n\r\n"); - if (headerEnd < 0) break; - const headers = this.buffer.subarray(0, headerEnd).toString("ascii"); - const match = /content-length:\s*(\d+)/i.exec(headers); - if (!match) { - this.buffer = this.buffer.subarray(headerEnd + 4); - continue; - } - const length = Number(match[1]); - if (this.buffer.length < headerEnd + 4 + length) break; - out.push(this.buffer.subarray(headerEnd + 4, headerEnd + 4 + length).toString("utf8")); - this.buffer = this.buffer.subarray(headerEnd + 4 + length); - } - return out; - } -} - -export function encode(message: unknown): Buffer { - const body = Buffer.from(JSON.stringify(message), "utf8"); - return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii"), body]); -} - -export interface BackendOptions { - program: string; - args?: string[]; - cwd?: string; - env?: NodeJS.ProcessEnv; - /** Lines the backend writes to its standard error (its log). */ - log?: (line: string) => void; -} - -/** - * One backend process. `request` sends a method and resolves with its result; `notifications` - * emits each notification by method name (and "*" with the method first). `exited` resolves when - * the process ends. - */ -export class Backend { - readonly notifications = new EventEmitter(); - readonly exited: Promise; - private readonly child: ChildProcess; - private readonly pending = new Map(); - private readonly reader = new FrameReader(); - private nextId = 1; - private alive = true; - - constructor(options: BackendOptions) { - this.notifications.setMaxListeners(0); - this.child = spawn(options.program, options.args ?? ["serve"], { - cwd: options.cwd, - env: options.env ?? process.env, - stdio: ["pipe", "pipe", "pipe"], - windowsHide: true, - }); - this.child.stdout!.on("data", (chunk: Buffer) => { - for (const body of this.reader.feed(chunk)) this.dispatch(body); - }); - let errText = ""; - this.child.stderr!.on("data", (chunk: Buffer) => { - errText += chunk.toString("utf8"); - let nl: number; - while ((nl = errText.indexOf("\n")) >= 0) { - options.log?.(errText.slice(0, nl)); - errText = errText.slice(nl + 1); - } - }); - this.exited = new Promise((resolve) => { - const done = (code: number | null) => { - if (!this.alive) return; - this.alive = false; - for (const p of this.pending.values()) p.reject(new BackendError(p.method, { code: -32099, message: "the mcpp-ide backend ended" })); - this.pending.clear(); - resolve(code); - }; - this.child.on("exit", (code) => done(code)); - this.child.on("error", (e) => { - options.log?.(`mcpp-ide could not start: ${e.message}`); - done(null); - }); - }); - } - - get running(): boolean { - return this.alive; - } - - request(method: string, params: unknown = {}): Promise { - if (!this.alive) return Promise.reject(new BackendError(method, { code: -32099, message: "the mcpp-ide backend is not running" })); - const id = this.nextId++; - return new Promise((resolve, reject) => { - this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject, method }); - this.child.stdin!.write(encode({ jsonrpc: "2.0", id, method, params })); - }); - } - - notify(method: string, params: unknown = {}): void { - if (this.alive) this.child.stdin!.write(encode({ jsonrpc: "2.0", method, params })); - } - - cancel(id: number): void { - this.notify("$/cancelRequest", { id }); - } - - async stop(): Promise { - if (!this.alive) return; - try { - await Promise.race([this.request("shutdown"), new Promise((r) => setTimeout(r, 2000))]); - } catch { - // ended already - } - this.notify("exit"); - this.child.stdin!.end(); - const ended = await Promise.race([this.exited.then(() => true), new Promise((r) => setTimeout(() => r(false), 3000))]); - if (!ended) this.child.kill(); - } - - private dispatch(body: string): void { - let message: { id?: number; method?: string; params?: unknown; result?: unknown; error?: RpcError }; - try { - message = JSON.parse(body); - } catch { - return; - } - if (message.id !== undefined && message.id !== null && (message.result !== undefined || message.error !== undefined)) { - const p = this.pending.get(message.id); - if (!p) return; - this.pending.delete(message.id); - if (message.error) p.reject(new BackendError(p.method, message.error)); - else p.resolve(message.result); - return; - } - if (message.method) { - this.notifications.emit(message.method, message.params); - this.notifications.emit("*", message.method, message.params); - } - } -} diff --git a/mide-plugins/vscode-1.0/src/backend/locate.ts b/mide-plugins/vscode-1.0/src/backend/locate.ts deleted file mode 100644 index 9638e12..0000000 --- a/mide-plugins/vscode-1.0/src/backend/locate.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Where the backend is, as the command that starts it (mcpp design 2026-10-28 part 2 §15): -// -// 1. mcpp.ide.backendPath: a program, or an ok-image bundle (`*.okb`) run by okrun; -// 2. the bundle the extension carries, `backend/mcpp-ide.okb`, run by the okrun it carries for -// this system (`backend/okrun/-/okrun[.exe]`), else by okrun on PATH -- one package -// for every system and architecture; -// 3. the mcpp-ide xlings installed (its command runs okrun on its bundle), then PATH. -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; - -export interface Located { - program: string; - /** Arguments before the backend's own (`serve`): the bundle, when okrun runs one. */ - args: string[]; - source: "setting" | "bundle" | "xlings" | "PATH"; -} - -/** mcpp's names for a system and an architecture: "linux-x86_64", "macos-aarch64", "windows-x86_64". */ -export function okrunDir(platform: NodeJS.Platform = process.platform, arch: string = process.arch): string { - const system = platform === "win32" ? "windows" : platform === "darwin" ? "macos" : platform; - const machine = arch === "x64" ? "x86_64" : arch === "arm64" ? "aarch64" : arch; - return `${system}-${machine}`; -} - -function executable(name: string, platform: NodeJS.Platform): string { - return platform === "win32" ? `${name}.exe` : name; -} - -function isFile(p: string): boolean { - try { - return fs.statSync(p).isFile(); - } catch { - return false; - } -} - -// A package's archive need not keep the mode bits. -function runnable(p: string, platform: NodeJS.Platform): string { - if (platform !== "win32") { - try { fs.accessSync(p, fs.constants.X_OK); } catch { try { fs.chmodSync(p, 0o755); } catch { /* reported when it is started */ } } - } - return p; -} - -export function onPath(name: string, env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string | undefined { - const dirs = (env.PATH ?? env.Path ?? "").split(platform === "win32" ? ";" : ":").filter(Boolean); - for (const d of dirs) { - const candidate = path.join(d, executable(name, platform)); - if (isFile(candidate)) return candidate; - } - return undefined; -} - -export function locateBackend(options: { - configured?: string; - extensionRoot: string; - env?: NodeJS.ProcessEnv; - platform?: NodeJS.Platform; - arch?: string; - home?: string; -}): Located | undefined { - const platform = options.platform ?? process.platform; - const arch = options.arch ?? process.arch; - const env = options.env ?? process.env; - const bundledOkrun = path.join(options.extensionRoot, "backend", "okrun", okrunDir(platform, arch), executable("okrun", platform)); - const okrun = (): string | undefined => (isFile(bundledOkrun) ? runnable(bundledOkrun, platform) : onPath("okrun", env, platform)); - - const configured = options.configured?.trim(); - if (configured) { - if (configured.endsWith(".okb")) { - const runner = okrun(); - return runner ? { program: runner, args: [configured], source: "setting" } : undefined; - } - return { program: configured, args: [], source: "setting" }; - } - const bundle = path.join(options.extensionRoot, "backend", "mcpp-ide.okb"); - if (isFile(bundle)) { - const runner = okrun(); - if (runner) return { program: runner, args: [bundle], source: "bundle" }; - } - const exe = executable("mcpp-ide", platform); - const home = options.home ?? os.homedir(); - for (const candidate of [ - env.XLINGS_HOME ? path.join(env.XLINGS_HOME, "subos", "current", "bin", exe) : undefined, - path.join(home, ".xlings", "subos", "current", "bin", exe), - path.join(home, ".xlings", "subos", "default", "bin", exe), - ]) { - if (candidate && isFile(candidate)) return { program: candidate, args: [], source: "xlings" }; - } - const found = onPath("mcpp-ide", env, platform); - return found ? { program: found, args: [], source: "PATH" } : undefined; -} diff --git a/mide-plugins/vscode-1.0/src/backend/mcppls.ts b/mide-plugins/vscode-1.0/src/backend/mcppls.ts deleted file mode 100644 index 2e052b0..0000000 --- a/mide-plugins/vscode-1.0/src/backend/mcppls.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Where the mcppls VS Code extension keeps its server: its payload directory (MCPPLS_PAYLOAD, else -// `/payload`), whose payload.json's `server` the backend starts -- the runner and the okb, or a -// program -- and MCPPLS_SERVER. The backend reads its version (languageService/status). -import * as fs from "node:fs"; -import * as path from "node:path"; - -export function mcpplsCandidates(extensionPath: string | undefined, env: NodeJS.ProcessEnv = process.env): string[] { - const out: string[] = []; - if (env.MCPPLS_SERVER) out.push(env.MCPPLS_SERVER); - if (!extensionPath) return out; - const payload = env.MCPPLS_PAYLOAD ? path.resolve(env.MCPPLS_PAYLOAD) : path.join(extensionPath, "payload"); - // An installation without a payload answers nothing here; PATH and xlings are tried after. - if (fs.existsSync(path.join(payload, "payload.json"))) out.push(payload); - return out; -} diff --git a/mide-plugins/vscode-1.0/src/commands.ts b/mide-plugins/vscode-1.0/src/commands.ts deleted file mode 100644 index 066d165..0000000 --- a/mide-plugins/vscode-1.0/src/commands.ts +++ /dev/null @@ -1,211 +0,0 @@ -// The commands: every id the extension declares (generated from spec/commands.json) is registered -// at activation and does what its row's `action` says. The adapter maps an action to VS Code's UI; -// which argv runs, what a cleanup costs and what a menu offers are the backend's answers. -import * as vscode from "vscode"; - -import type { Debugging } from "./debug"; -import type { Jobs } from "./jobs"; -import type { Panels } from "./panels"; -import type { Projects } from "./projects"; -import type { CleanPlan, CommandEntry, LanguageServiceStatus, TargetRow } from "./protocol"; -import { runProgram } from "./run"; -import type { Session } from "./session"; -import type { Tasks } from "./tasks"; - -export interface CommandContext { - session: Session; - projects: Projects; - jobs: Jobs; - tasks: Tasks; - panels: Panels; - debugging: Debugging; -} - -/** plan -> the editor's confirmation at the plan's level -> apply. */ -export async function cleanup(c: CommandContext, action: string): Promise { - const root = c.projects.project?.root; - const t = (k: string, ...a: unknown[]) => c.session.t(k, ...a); - const plan = await c.session.request("cache/plan", { action, root }); - if (plan.refusedUntrusted) { void vscode.window.showWarningMessage(t("this action needs a trusted workspace")); return; } - const confirmation: Record = {}; - if (plan.level > 0) { - let preview = ""; - if (typeof plan.preview === "object" && plan.preview.source === "mcpp") preview = plan.preview.text ?? ""; - else if (typeof plan.preview === "object") preview = t("About {0} would be freed ({1} entries).", plan.preview.bytesText, plan.preview.count); - const yes = t("Continue"); - const choice = await vscode.window.showWarningMessage(plan.title, { modal: true, detail: [plan.detail, preview].filter(Boolean).join("\n\n") }, yes); - if (choice !== yes) return; - confirmation.confirmed = true; - confirmation.previewShown = !!plan.preview; - if (plan.acknowledge) { - const ack = t("I understand this affects every mcpp project on this machine"); - if ((await vscode.window.showWarningMessage(plan.title, { modal: true, detail: ack }, ack)) !== ack) return; - confirmation.acknowledged = true; - } - } - await c.jobs.start({ action, root, confirmation }, { - output: (text) => c.session.output.append(text), - finished: (p) => { - if (p.status === "ok") void vscode.window.showInformationMessage(t("mcpp: {0}", plan.title.replace(/\?$/, ""))); - else void vscode.window.showErrorMessage(t("mcpp {0}: {1}", plan.argv.join(" "), p.status)); - }, - }, "cache/apply"); -} - -async function selectTarget(c: CommandContext): Promise { - const t = (k: string, ...a: unknown[]) => c.session.t(k, ...a); - const r = await c.session.request<{ host: string; rows: TargetRow[] }>("targets/list", { root: c.projects.project?.root }); - const pick = await vscode.window.showQuickPick( - r.rows.map((row) => ({ - label: `${row.selected ? "$(check) " : ""}${row.target}`, - description: [row.host ? t("this machine") : "", row.platformText].filter(Boolean).join(" · "), - detail: row.toolchains.map((tc) => `${tc.spec || "—"} (${tc.status})`).join(", ") || undefined, - row, - })), - { title: t("Select the build target"), placeHolder: t("build, run, test, debug and pack use it") }); - if (!pick) return; - await vscode.workspace.getConfiguration().update("mcpp.build.target", pick.row.host ? "" : pick.row.target, vscode.ConfigurationTarget.Workspace); - if (!pick.row.installed && pick.row.pin) { - const install = t("Install"); - if ((await vscode.window.showInformationMessage(t("No toolchain is installed for {0}.", pick.row.target), install)) === install) - await c.jobs.start({ spec: pick.row.pin, target: pick.row.target }, { output: (x) => c.session.output.append(x) }, "toolchains/install"); - } -} - -async function installToolchain(c: CommandContext): Promise { - const spec = await vscode.window.showInputBox({ title: c.session.t("Install an mcpp toolchain"), placeHolder: "gcc@16.1.0, llvm@23.1.3" }); - if (!spec) return; - c.session.output.show(true); - await c.jobs.start({ spec }, { output: (x) => c.session.output.append(x) }, "toolchains/install"); -} - -async function selectDefaultToolchain(c: CommandContext): Promise { - const r = await c.session.request<{ rows: { target: string; spec: string; status: string; default: boolean }[]; host: string }>("toolchains/list", {}); - const rows = r.rows.filter((x) => x.target === r.host && x.spec); - const pick = await vscode.window.showQuickPick(rows.map((x) => ({ label: `${x.default ? "$(check) " : ""}${x.spec}`, description: x.status, spec: x.spec })), - { title: c.session.t("Choose the mcpp global default toolchain") }); - if (!pick) return; - await c.session.request("toolchains/setDefault", { spec: pick.spec }); -} - -async function newProject(c: CommandContext): Promise { - const t = (k: string, ...a: unknown[]) => c.session.t(k, ...a); - const folder = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, openLabel: t("Create the project here") }); - if (!folder?.[0]) return; - const name = await vscode.window.showInputBox({ title: t("New mcpp project (1/2)"), prompt: t("The project's name, which is also its directory") }); - if (!name) return; - const handle = await c.jobs.start({ parent: folder[0].fsPath, name }, { output: (x) => c.session.output.append(x) }, "project/new"); - const done = await handle.done; - if (done.status !== "ok") { void vscode.window.showErrorMessage(t("mcpp new {0}: {1}", name, done.status)); return; } - const open = t("Open"); - if ((await vscode.window.showInformationMessage(t("Created {0}.", name), open)) === open) - await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.joinPath(folder[0], name), { forceNewWindow: false }); -} - -async function languageService(c: CommandContext): Promise { - const s = await c.session.request("languageService/status", { candidates: c.session.mcpplsCandidates() }); - const show = s.state === "ok" ? vscode.window.showInformationMessage : vscode.window.showWarningMessage; - const choice = await show(s.message, ...s.actions.map((a) => a.title)); - const action = s.actions.find((a) => a.title === choice); - if (action) await act(action.id); -} - -async function act(id: string): Promise { - if (id === "install") await vscode.commands.executeCommand("workbench.extensions.installExtension", "sunrisepeak.mcpp-language-server"); - else await vscode.commands.executeCommand("workbench.extensions.search", "@id:sunrisepeak.mcpp-language-server"); -} - -async function selfCheck(c: CommandContext): Promise { - const r = await c.session.request<{ backend: { version: string }; checks: { status: string; message: string }[] }>("selfCheck", {}); - const out = c.session.output; - out.appendLine(""); - out.appendLine(`===== mcpp: ${c.session.t("Environment Self-check")} =====`); - out.appendLine(`mcpp-vscode ${c.session.context.extension.packageJSON.version} · VS Code ${vscode.version} · ${process.platform}-${process.arch}`); - out.appendLine(`mcpp-ide ${r.backend.version}`); - for (const check of r.checks) out.appendLine(`${check.status.padEnd(8)} ${check.message}`); - out.appendLine("========================================"); - out.show(true); -} - -async function quickMenu(c: CommandContext): Promise { - const r = await c.session.request<{ items: { command: string; title: string; group: string; enabled: boolean; reason?: string; detail?: string }[] }>( - "menu/quick", { root: c.projects.project?.root, editor: "vscode" }); - const pick = await vscode.window.showQuickPick(r.items.map((i) => ({ - label: i.title, description: i.enabled ? i.group : i.reason, detail: i.detail, command: i.command, enabled: i.enabled, - })), { title: c.session.t("Open the mcpp project and toolchain quick menu") }); - if (pick?.enabled) await vscode.commands.executeCommand(pick.command); -} - -async function forward(c: CommandContext, commands: string[], args: unknown[]): Promise { - const known = await vscode.commands.getCommands(true); - const command = commands.find((x) => known.includes(x)); - if (!command) { - await languageService(c); - return; - } - await vscode.commands.executeCommand(command, ...args); -} - -export async function dispatch(c: CommandContext, entry: CommandEntry, args: unknown[]): Promise { - const a = entry.action; - const root = async () => (await c.projects.require())?.root; - const options = (args[0] && typeof args[0] === "object" ? args[0] : {}) as { filter?: string; bin?: string; root?: string; args?: { filter?: string } }; - switch (a.kind) { - case "job": { - const r = options.root ?? (await root()); - if (!r) return; - const filter = options.filter ?? options.args?.filter; - const exec = await c.tasks.execute(a.job === "test" ? "test" : "build", r, filter ? { args: [filter] } : {}); - if (a.then === "languageService") { - const sub = vscode.tasks.onDidEndTaskProcess((e) => { if (e.execution === exec) { sub.dispose(); void languageService(c); } }); - } - return; - } - case "run": { const r = options.root ?? (await root()); if (r) await runProgram(c.session, r, { bin: options.bin }); return; } - case "debug": await c.debugging.start(); return; - case "cache": await cleanup(c, a.plan!); return; - case "panel": c.panels.show(a.panel === "library" ? "libraries" : a.panel!, { root: c.projects.project?.root, graph: a.graph, id: typeof args[0] === "string" ? args[0] : undefined }); return; - case "cancel": { - const ids = c.jobs.runningIds; - for (const id of ids) await c.session.request("job/cancel", { jobId: id }); - return; - } - case "selectTarget": await selectTarget(c); return; - case "installToolchain": await installToolchain(c); return; - case "selectDefaultToolchain": await selectDefaultToolchain(c); return; - case "newProject": await newProject(c); return; - case "quickMenu": await quickMenu(c); return; - case "selfCheck": await selfCheck(c); return; - case "languageService": await languageService(c); return; - case "forward": await forward(c, a.commands ?? [], args); return; - case "editor": await vscode.commands.executeCommand(a.vscode!, ...(a.args ?? [])); return; - case "rpc": { - const handle = await c.jobs.start({ root: c.projects.project?.root }, { output: (x) => c.session.output.append(x) }, a.method!); - const done = await handle.done; - if (done.status !== "ok") void vscode.window.showErrorMessage(c.session.t("mcpp: {0}", done.status)); - return; - } - } -} - -/** Registers every declared command; the backend's table says what each one does. */ -export function registerCommands(context: vscode.ExtensionContext, c: CommandContext): void { - const declared = (context.extension.packageJSON?.contributes?.commands ?? []) as { command: string }[]; - for (const { command } of declared) { - context.subscriptions.push(vscode.commands.registerCommand(command, async (...args: unknown[]) => { - try { - if (!c.session.commands.length) await c.session.start(); - const entry = c.session.commands.find((x) => x.id === command); - if (!entry) throw new Error(c.session.t("unknown command {0}", command)); - await dispatch(c, entry, args); - } catch (e) { - void vscode.window.showErrorMessage(c.session.t("mcpp: {0}", e instanceof Error ? e.message : String(e))); - } - })); - } - // Commands the views use and the palette does not list. - context.subscriptions.push( - vscode.commands.registerCommand("mcpp.languageServer.act", (id: string) => act(id)), - vscode.commands.registerCommand("mcpp.openProjectPanel", () => c.panels.show("project", { root: c.projects.project?.root })), - ); -} diff --git a/mide-plugins/vscode-1.0/src/debug.ts b/mide-plugins/vscode-1.0/src/debug.ts deleted file mode 100644 index f67f355..0000000 --- a/mide-plugins/vscode-1.0/src/debug.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Debugging: a launch configuration of type "mcpp" names what to debug (target, profile, arguments); -// the backend builds the program and answers the configuration of the debugger at hand (CodeLLDB, -// the C/C++ extension, lldb-dap or Native Debug), which this provider returns in its place. A -// program that runs through a runner (an ok image under okrun) is debugged as the runner. -import * as vscode from "vscode"; - -import { availableDebuggers } from "./mapping"; -import type { Projects } from "./projects"; -import type { Session } from "./session"; - -export class Debugging implements vscode.DebugConfigurationProvider, vscode.Disposable { - private readonly disposables: vscode.Disposable[] = []; - - constructor(private readonly session: Session, private readonly projects: Projects) { - this.disposables.push( - vscode.debug.registerDebugConfigurationProvider("mcpp", this), - vscode.debug.registerDebugConfigurationProvider("mcpp", { provideDebugConfigurations: () => [{ type: "mcpp", request: "launch", name: "mcpp: debug" }] }, - vscode.DebugConfigurationProviderTriggerKind.Dynamic), - ); - } - - async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration): Promise { - if (!config.type) Object.assign(config, { type: "mcpp", request: "launch", name: "mcpp: debug" }); - const project = (config.root && { root: config.root as string }) || (await this.projects.require()); - if (!project) return undefined; - const available = availableDebuggers((id) => vscode.extensions.getExtension(id) !== undefined); - if (available.length === 0) { - const install = this.session.t("Install CodeLLDB"); - const choice = await vscode.window.showErrorMessage( - this.session.t("No debugger extension is installed: install CodeLLDB, the C/C++ extension or lldb-dap."), install); - if (choice === install) await vscode.commands.executeCommand("workbench.extensions.search", "@id:vadimcn.vscode-lldb"); - return undefined; - } - try { - const answer = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Window, title: this.session.t("mcpp: building for the debugger…") }, - () => this.session.request<{ engine: string; configuration: vscode.DebugConfiguration & { mcppNote?: string } }>("debug/configuration", { - root: project.root, - ...(config.target !== undefined ? { target: config.target } : {}), - ...(config.profile ? { profile: config.profile } : {}), - ...(config.engine ? { engine: config.engine } : {}), - args: Array.isArray(config.args) ? config.args : [], - available, - platform: process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux", - })); - const { mcppNote, ...resolved } = answer.configuration; - if (mcppNote) this.session.output.appendLine(`mcpp: ${mcppNote}`); - return Object.assign({}, resolved, { name: config.name || resolved.name }); - } catch (e) { - const detail = (e as { error?: { data?: { narration?: string } } }).error?.data?.narration; - if (detail) this.session.output.appendLine(detail); - void vscode.window.showErrorMessage(this.session.t("mcpp: {0}", e instanceof Error ? e.message : String(e))); - return undefined; - } - } - - /** `mcpp.debug`: the same, without a launch.json. */ - async start(): Promise { - const folder = this.projects.project ? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.projects.project.root)) : undefined; - await vscode.debug.startDebugging(folder, { type: "mcpp", request: "launch", name: "mcpp: debug" }); - } - - dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } -} diff --git a/mide-plugins/vscode-1.0/src/diagnostics.ts b/mide-plugins/vscode-1.0/src/diagnostics.ts deleted file mode 100644 index d2ecd2d..0000000 --- a/mide-plugins/vscode-1.0/src/diagnostics.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Problems from mcpp, never from a regular expression over a terminal: each `job/diagnostic` of a -// build goes into the Problems panel located where mcpp says, a job's start clears its project's, -// and the manifest's own problems (`manifest/diagnostics`, `manifest/check`) sit on mcpp.toml. -import * as path from "node:path"; -import * as vscode from "vscode"; - -import { severityOf, zeroBased } from "./mapping"; -import type { Diagnostic } from "./protocol"; -import type { Session } from "./session"; - -const SEVERITY: Record = { - error: vscode.DiagnosticSeverity.Error, - warning: vscode.DiagnosticSeverity.Warning, - information: vscode.DiagnosticSeverity.Information, - hint: vscode.DiagnosticSeverity.Hint, -}; - -export function toVscode(d: Diagnostic): vscode.Diagnostic { - const start = zeroBased(d.range?.start); - const end = zeroBased(d.range?.end); - // A point range is widened to the rest of the line's word by VS Code itself when it is empty. - const range = new vscode.Range(start.line, start.character, end.line, Math.max(end.character, start.character + 1)); - const out = new vscode.Diagnostic(range, d.message, SEVERITY[severityOf(d)]); - out.source = d.source || "mcpp"; - if (d.code) out.code = d.code; - return out; -} - -export class Problems implements vscode.Disposable { - private readonly build = vscode.languages.createDiagnosticCollection("mcpp"); - private readonly manifest = vscode.languages.createDiagnosticCollection("mcpp.toml"); - private readonly byRoot = new Map>(); - private readonly disposables: vscode.Disposable[] = [this.build, this.manifest]; - - constructor(session: Session) { - this.disposables.push( - session.on("job/started", (p: { root: string; kind: string }) => { if (p.kind === "build" || p.kind === "test") this.clear(p.root); }), - session.on("job/diagnostic", (p: Diagnostic & { root: string }) => this.add(p.root, p)), - session.on("job/finished", (p: { root?: string; kind?: string; diagnostics?: Diagnostic[] }) => { - // The envelope's diagnostics that the stream did not already carry (a manifest error ends a - // build before any event). - if (!p.root) return; - for (const d of p.diagnostics ?? []) if (d.path && !this.has(p.root, d)) this.add(p.root, d); - }), - session.on("manifest/diagnostics", (p: { root: string; diagnostics: Diagnostic[] }) => this.setManifest(p.root, p.diagnostics)), - vscode.workspace.onDidSaveTextDocument(async (doc) => { - if (path.basename(doc.fileName) !== "mcpp.toml") return; - try { - const r = await session.request<{ root: string; diagnostics: Diagnostic[] }>("manifest/check", { root: path.dirname(doc.fileName) }); - this.setManifest(r.root, r.diagnostics); - } catch { - // mcpp unavailable: the self-check says why. - } - }), - ); - } - - private has(root: string, d: Diagnostic): boolean { - const list = this.byRoot.get(root)?.get(d.path!) ?? []; - return list.some((x) => x.message === d.message); - } - - private add(root: string, d: Diagnostic): void { - if (!d.path) return; - if (!this.byRoot.has(root)) this.byRoot.set(root, new Map()); - const files = this.byRoot.get(root)!; - const file = path.isAbsolute(d.path) ? d.path : path.join(root, d.path); - const list = files.get(file) ?? []; - list.push(toVscode(d)); - files.set(file, list); - this.build.set(vscode.Uri.file(file), list); - } - - private clear(root: string): void { - for (const file of this.byRoot.get(root)?.keys() ?? []) this.build.delete(vscode.Uri.file(file)); - this.byRoot.delete(root); - } - - private setManifest(root: string, diagnostics: Diagnostic[]): void { - const file = vscode.Uri.file(path.join(root, "mcpp.toml")); - const enabled = vscode.workspace.getConfiguration().get("mcpp.toml.diagnostics.enabled") ?? true; - this.manifest.set(file, enabled ? diagnostics.map(toVscode) : []); - } - - dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } -} diff --git a/mide-plugins/vscode-1.0/src/extension.ts b/mide-plugins/vscode-1.0/src/extension.ts deleted file mode 100644 index be06921..0000000 --- a/mide-plugins/vscode-1.0/src/extension.ts +++ /dev/null @@ -1,84 +0,0 @@ -// mcpp for VS Code: the adapter of mcpp-ide. It starts the backend (`mcpp-ide serve`) and maps its -// protocol onto VS Code: tasks in pseudoterminals, the Testing view, debug configurations, the -// Problems panel, the project view, the shared panels, mcpp.toml editing help. Language features of -// C++ files are mcppls's (the extension pack installs it); this extension registers none. -import * as vscode from "vscode"; - -import { dispatch, registerCommands, type CommandContext } from "./commands"; -import { Debugging } from "./debug"; -import { Problems } from "./diagnostics"; -import { Jobs } from "./jobs"; -import { Panels } from "./panels"; -import { ProjectTree } from "./projectTree"; -import { Projects } from "./projects"; -import { Session } from "./session"; -import { StatusBar } from "./status"; -import { Tasks } from "./tasks"; -import { Testing } from "./testing"; -import { registerToml } from "./toml"; - -export interface McppApi { - session: Session; - projects: Projects; -} - -export async function activate(context: vscode.ExtensionContext): Promise { - const output = vscode.window.createOutputChannel("mcpp"); - const session = new Session(context, output); - const projects = new Projects(session); - const jobs = new Jobs(session); - const tasks = new Tasks(session, jobs, projects); - const debugging = new Debugging(session, projects); - // The panels run commands, and the commands open panels: the context is completed once both exist. - const c = { session, projects, jobs, tasks, debugging } as CommandContext; - c.panels = new Panels(context, session, async (command, params) => { - const entry = session.commands.find((x) => x.id === command); - if (!entry) throw new Error(session.t("unknown command {0}", command)); - await dispatch(c, entry, [params?.args ?? params]); - return true; - }); - const panels = c.panels; - context.subscriptions.push(output, session, projects, jobs, tasks, debugging, panels, - new Problems(session), new Testing(session, jobs, projects), new ProjectTree(session, projects, jobs), new StatusBar(session, projects, jobs)); - registerCommands(context, c); - registerToml(context, session); - - const views = () => { - const get = (k: string) => vscode.workspace.getConfiguration().get(k) ?? true; - void vscode.commands.executeCommand("setContext", "mcpp.sidebarHidden", !get("mcpp.views.enabled")); - void vscode.commands.executeCommand("setContext", "mcpp.views.project", get("mcpp.views.project.show")); - void vscode.commands.executeCommand("setContext", "mcpp.views.library", get("mcpp.views.library.show")); - void vscode.commands.executeCommand("setContext", "mcpp.views.cache", get("mcpp.views.cache.show")); - }; - views(); - context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("mcpp.views")) views(); }), - jobs.onDidChangeRunning.event((n) => void vscode.commands.executeCommand("setContext", "mcpp.jobRunning", n > 0)), - // A save builds the file's project when asked to (mcpp.build.onSave); a save while a build - // waits replaces it, and the running one is superseded. - vscode.workspace.onDidSaveTextDocument(async (doc) => { - if (!(vscode.workspace.getConfiguration().get("mcpp.build.onSave") ?? false)) return; - if (!/\.(cppm|cpp|cc|cxx|ixx|mpp|h|hpp|toml|mcpp)$/.test(doc.fileName)) return; - const project = await projects.resolve(); - if (project && doc.fileName.startsWith(project.root)) await tasks.execute("build", project.root); - }), - ); - - if (await session.start()) { - if (vscode.workspace.getConfiguration().get("mcpp.diagnostics.selfCheckOnStartup")) - void vscode.commands.executeCommand("mcpp.selfCheck"); - // The language service is not what it should be: said once, with what to do; activation does not - // wait for an answer. - const ls = session.info?.languageService; - if (ls && ls.state !== "ok") - void vscode.window.showWarningMessage(`mcpp: ${ls.message}`, ...ls.actions.map((a) => a.title)).then((choice) => { - const action = ls.actions.find((a) => a.title === choice); - if (action) void vscode.commands.executeCommand("mcpp.languageServer.act", action.id); - }); - } - return { session, projects }; -} - -export function deactivate(): void { - // Everything registered in activate() is disposed by VS Code; the session stops the backend. -} diff --git a/mide-plugins/vscode-1.0/src/jobs.ts b/mide-plugins/vscode-1.0/src/jobs.ts deleted file mode 100644 index f90923d..0000000 --- a/mide-plugins/vscode-1.0/src/jobs.ts +++ /dev/null @@ -1,105 +0,0 @@ -// A job of the backend, followed from the moment it is asked for: its notifications can arrive -// before the reply that names its id (the job's thread and the reply race), so everything with an -// id not yet claimed is kept briefly and replayed to the handle that claims it. -import * as vscode from "vscode"; - -import type { JobFinished } from "./protocol"; -import type { Session } from "./session"; - -const JOB_EVENTS = ["job/started", "job/output", "job/plan", "job/progress", "job/diagnostic", "job/artifact", "job/record", "job/finished", "test/result", "test/summary"]; - -export interface JobEvents { - output?(text: string): void; - started?(p: { argv: string[] }): void; - plan?(p: any): void; - progress?(p: { finished: number; total: number }): void; - testResult?(p: any): void; - testSummary?(p: any): void; - finished?(p: JobFinished): void; -} - -export class JobHandle { - readonly done: Promise; - private resolveDone!: (p: JobFinished) => void; - constructor(readonly id: string, private readonly session: Session) { - this.done = new Promise((r) => { this.resolveDone = r; }); - } - cancel(): Promise { - return this.session.request("job/cancel", { jobId: this.id }); - } - /** @internal */ - finish(p: JobFinished): void { - this.resolveDone(p); - } -} - -export class Jobs implements vscode.Disposable { - private readonly unclaimed = new Map(); - private readonly claimed = new Map(); - private readonly running = new Set(); - private readonly disposables: vscode.Disposable[] = []; - readonly onDidChangeRunning = new vscode.EventEmitter(); - - constructor(private readonly session: Session) { - for (const method of JOB_EVENTS) - this.disposables.push(session.on(method, (params: any) => this.route(method, params))); - this.disposables.push(this.onDidChangeRunning); - } - - get runningCount(): number { - return this.running.size; - } - - get runningIds(): string[] { - return [...this.running]; - } - - private route(method: string, params: any): void { - const id: string | undefined = params?.jobId; - if (!id) return; - if (method === "job/started") { this.running.add(id); this.onDidChangeRunning.fire(this.running.size); } - if (method === "job/finished") { this.running.delete(id); this.onDidChangeRunning.fire(this.running.size); } - const owner = this.claimed.get(id); - if (!owner) { - const list = this.unclaimed.get(id) ?? []; - list.push({ method, params }); - this.unclaimed.set(id, list); - // A job nobody claims (started by a panel) is kept only until it ends. - if (method === "job/finished") setTimeout(() => this.unclaimed.delete(id), 30_000); - return; - } - this.deliver(owner, method, params); - } - - private deliver(owner: { events: JobEvents; handle: JobHandle }, method: string, params: any): void { - const e = owner.events; - switch (method) { - case "job/output": e.output?.(params.text); break; - case "job/started": e.started?.(params); break; - case "job/plan": e.plan?.(params); break; - case "job/progress": e.progress?.(params); break; - case "test/result": e.testResult?.(params); break; - case "test/summary": e.testSummary?.(params); break; - case "job/finished": - e.finished?.(params); - owner.handle.finish(params); - this.claimed.delete(params.jobId); - break; - } - } - - /** Asks for a job (`job/start`, or another method that answers a job id) and follows it. */ - async start(params: Record, events: JobEvents, method = "job/start"): Promise { - const { jobId } = await this.session.request<{ jobId: string }>(method, params); - const handle = new JobHandle(jobId, this.session); - const owner = { events, handle }; - this.claimed.set(jobId, owner); - for (const m of this.unclaimed.get(jobId) ?? []) this.deliver(owner, m.method, m.params); - this.unclaimed.delete(jobId); - return handle; - } - - dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } -} diff --git a/mide-plugins/vscode-1.0/src/mapping.ts b/mide-plugins/vscode-1.0/src/mapping.ts deleted file mode 100644 index 3ed2b08..0000000 --- a/mide-plugins/vscode-1.0/src/mapping.ts +++ /dev/null @@ -1,56 +0,0 @@ -// The protocol's values in VS Code's terms, as pure functions (the unit tests run them without an -// editor): positions, severities, test identities, the debuggers present, the "when" of a command. -import type { Diagnostic, Position } from "./protocol"; - -/** mcpp's positions are 1-based lines and columns; VS Code's are 0-based. */ -export function zeroBased(p: Position | undefined): { line: number; character: number } { - if (!p) return { line: 0, character: 0 }; - return { line: Math.max(0, p.line - 1), character: Math.max(0, p.column - 1) }; -} - -export type Severity = "error" | "warning" | "information" | "hint"; - -export function severityOf(d: Pick): Severity { - switch (d.severity) { - case "error": return "error"; - case "warning": return "warning"; - case "note": case "help": return "hint"; - default: return "information"; - } -} - -/** One identity per test: its project, its workspace member and its name. */ -export function testId(root: string, member: string, test: string): string { - return `${root}::${member}::${test}`; -} - -export function parseTestId(id: string): { root: string; member: string; test: string } | undefined { - const parts = id.split("::"); - if (parts.length < 3) return undefined; - const test = parts.pop()!; - const member = parts.pop()!; - return { root: parts.join("::"), member, test }; -} - -/** The debuggers the backend may configure, from the extensions present. */ -export function availableDebuggers(installed: (id: string) => boolean): string[] { - const out: string[] = []; - if (installed("vadimcn.vscode-lldb")) out.push("lldb"); - if (installed("ms-vscode.cpptools")) out.push("cppdbg"); - if (installed("llvm-vs-code-extensions.lldb-dap")) out.push("lldb-dap"); - if (installed("webfreak.debug")) out.push("gdb"); - return out; -} - -/** The mcppls extension's id on the Marketplace. */ -export const MCPPLS_EXTENSION = "sunrisepeak.mcpp-language-server"; - -/** Substitutes `{0}`, `{1}` in a runtime string. */ -export function format(template: string, args: unknown[]): string { - return args.reduce((text, a, i) => text.split(`{${i}}`).join(String(a)), template); -} - -/** The panel title "mcpp: ", the way every panel of this extension is named. */ -export function panelTitle(title: string): string { - return `mcpp: ${title}`; -} diff --git a/mide-plugins/vscode-1.0/src/panels.ts b/mide-plugins/vscode-1.0/src/panels.ts deleted file mode 100644 index 1c8917e..0000000 --- a/mide-plugins/vscode-1.0/src/panels.ts +++ /dev/null @@ -1,165 +0,0 @@ -// The shared panels (webui/) in VS Code: editor-area panels and the sidebar's webview views. The -// panel speaks the backend's protocol through this host (rpc in, notifications out) and asks it for -// what only the editor does: open a file, confirm, write a setting, run a command. -import { randomBytes } from "node:crypto"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as vscode from "vscode"; - -import { MCPPLS_EXTENSION, panelTitle } from "./mapping"; -import type { Session } from "./session"; - -export interface PanelParams { - root?: string; - graph?: string; - id?: string; - focus?: string; -} - -type HostAction = (params: any) => Promise<unknown> | unknown; - -export class Panels implements vscode.Disposable { - private readonly open = new Map<string, vscode.WebviewPanel>(); - private readonly webviews = new Set<vscode.Webview>(); - private readonly disposables: vscode.Disposable[] = []; - private readonly webuiRoot: vscode.Uri; - - constructor(private readonly context: vscode.ExtensionContext, private readonly session: Session, - private readonly runCommand: (command: string, params: any) => Promise<unknown>) { - this.webuiRoot = vscode.Uri.joinPath(context.extensionUri, "webui"); - // Every notification of the backend reaches every panel; each panel takes what it shows. - this.disposables.push(session.on("*", ({ method, params }: { method: string; params: unknown }) => { - for (const w of this.webviews) void w.postMessage({ type: "notify", method, params }); - })); - for (const [view, panel] of [["mcpp.library", "libraries"], ["mcpp.cache", "cache"]] as const) - this.disposables.push(vscode.window.registerWebviewViewProvider(view, { - resolveWebviewView: (v) => { const forget = this.attach(v.webview, panel, () => this.paramsForView()); v.onDidDispose(forget); }, - })); - } - - private paramsForView(): PanelParams { - const editor = vscode.window.activeTextEditor; - const projects = this.session.info?.projects ?? []; - const root = editor ? projects.find((p) => editor.document.uri.fsPath.startsWith(p.root))?.root : undefined; - return { root: root ?? projects[0]?.root }; - } - - /** Opens (or reveals) a panel in the editor area. */ - show(panel: string, params: PanelParams = {}): void { - const key = `${panel}:${params.root ?? ""}:${params.graph ?? ""}:${params.id ?? ""}`; - const existing = this.open.get(key); - if (existing) { existing.reveal(); return; } - const titles: Record<string, string> = { - project: this.session.t("Project"), libraries: this.session.t("Libraries"), library: params.id ?? this.session.t("Libraries"), - cache: this.session.t("Cache"), settings: this.session.t("Settings"), pack: this.session.t("Pack"), - targets: this.session.t("Targets and Platforms"), - graph: params.graph === "modules" ? this.session.t("Module graph") : this.session.t("Dependency graph"), - }; - const p = vscode.window.createWebviewPanel(`mcpp.${panel}`, panelTitle(titles[panel] ?? panel), vscode.ViewColumn.Active, - { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [this.webuiRoot] }); - p.iconPath = vscode.Uri.joinPath(this.context.extensionUri, "media", "logo.png"); - this.open.set(key, p); - const forget = this.attach(p.webview, panel === "library" ? "libraries" : panel, () => params); - p.onDidDispose(() => { this.open.delete(key); forget(); }); - } - - /** Loads the panel into a webview; the returned function forgets it when the webview ends. */ - private attach(webview: vscode.Webview, panel: string, params: () => PanelParams): () => void { - webview.options = { enableScripts: true, localResourceRoots: [this.webuiRoot] }; - const nonce = randomBytes(16).toString("base64"); - const base = `${webview.asWebviewUri(this.webuiRoot).toString()}/`; - const csp = `default-src 'none'; img-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}' ${webview.cspSource};`; - const html = fs.readFileSync(path.join(this.webuiRoot.fsPath, "index.html"), "utf8") - .split("{{base}}").join(base).split("{{csp}}").join(csp).split("{{nonce}}").join(nonce) - .split("{{lang}}").join(vscode.env.language); - this.webviews.add(webview); - const sub = webview.onDidReceiveMessage((m) => this.receive(webview, panel, params, m)); - webview.html = html; - return () => { this.webviews.delete(webview); sub.dispose(); }; - } - - private async receive(webview: vscode.Webview, panel: string, params: () => PanelParams, m: any): Promise<void> { - if (m?.type === "ready") { - await webview.postMessage({ type: "init", panel, params: params(), bundle: this.session.strings, editor: "vscode" }); - return; - } - if (m?.type === "rpc") { - try { - const result = await this.session.request(m.method, m.params); - await webview.postMessage({ type: "rpcResult", id: m.id, result }); - } catch (e) { - const error = (e as { error?: unknown }).error ?? { code: -32603, message: e instanceof Error ? e.message : String(e) }; - await webview.postMessage({ type: "rpcError", id: m.id, error }); - } - return; - } - if (m?.type === "host") { - const action = this.actions()[m.action]; - try { - if (!action) throw new Error(`unknown host action ${m.action}`); - const result = await action(m.params ?? {}); - await webview.postMessage({ type: "hostResult", id: m.id, result: result ?? null }); - } catch (e) { - await webview.postMessage({ type: "hostError", id: m.id, error: { message: e instanceof Error ? e.message : String(e) } }); - } - } - } - - private actions(): Record<string, HostAction> { - const t = (k: string, ...a: unknown[]) => this.session.t(k, ...a); - const target = (key: string) => { - const i = vscode.workspace.getConfiguration().inspect(key); - if (i?.workspaceFolderValue !== undefined) return vscode.ConfigurationTarget.WorkspaceFolder; - if (i?.workspaceValue !== undefined) return vscode.ConfigurationTarget.Workspace; - return vscode.ConfigurationTarget.Global; - }; - return { - settings: () => this.session.settings(), - setSetting: async (p) => { await vscode.workspace.getConfiguration().update(p.key, p.value, target(p.key)); return true; }, - resetSettings: async (p) => { - for (const key of p.keys as string[]) await vscode.workspace.getConfiguration().update(key, undefined, target(key)); - return true; - }, - migrateSettings: async (p) => { - for (const r of p.renames as { from: string; to: string }[]) { - const i = vscode.workspace.getConfiguration().inspect(r.from); - if (i?.globalValue !== undefined) await vscode.workspace.getConfiguration().update(r.to, i.globalValue, vscode.ConfigurationTarget.Global); - if (i?.workspaceValue !== undefined) await vscode.workspace.getConfiguration().update(r.to, i.workspaceValue, vscode.ConfigurationTarget.Workspace); - } - return true; - }, - openFile: async (p) => { - const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(p.path)); - const line = Math.max(0, (p.line ?? 1) - 1); - await vscode.window.showTextDocument(doc, { selection: new vscode.Range(line, 0, line, 0) }); - return true; - }, - openExternal: (p) => vscode.env.openExternal(vscode.Uri.parse(p.url)), - reveal: (p) => vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(p.path)), - copy: async (p) => { await vscode.env.clipboard.writeText(p.text); void vscode.window.showInformationMessage(t("Copied.")); return true; }, - notify: (p) => (p.level === "error" ? vscode.window.showErrorMessage(p.message) : p.level === "warning" ? vscode.window.showWarningMessage(p.message) : vscode.window.showInformationMessage(p.message)), - openPanel: (p) => { const { panel, ...rest } = p; this.show(panel, rest); return true; }, - runCommand: (p) => this.runCommand(p.command, p), - confirm: async (p) => { - const yes = t("Continue"); - const detail = [p.detail, p.preview].filter(Boolean).join("\n\n"); - const choice = await vscode.window.showWarningMessage(p.title, { modal: true, detail }, yes); - if (choice !== yes) return { confirmed: false }; - if (!p.acknowledge) return { confirmed: true }; - const ack = t("I understand this affects every mcpp project on this machine"); - const second = await vscode.window.showWarningMessage(p.title, { modal: true, detail: ack }, ack); - return { confirmed: second === ack, acknowledged: second === ack }; - }, - languageService: async (p) => { - if (p.action === "install") await vscode.commands.executeCommand("workbench.extensions.installExtension", MCPPLS_EXTENSION); - else await vscode.commands.executeCommand("workbench.extensions.search", `@id:${MCPPLS_EXTENSION}`); - return true; - }, - }; - } - - dispose(): void { - for (const p of this.open.values()) p.dispose(); - this.disposables.forEach((d) => d.dispose()); - } -} diff --git a/mide-plugins/vscode-1.0/src/projectTree.ts b/mide-plugins/vscode-1.0/src/projectTree.ts deleted file mode 100644 index becb336..0000000 --- a/mide-plugins/vscode-1.0/src/projectTree.ts +++ /dev/null @@ -1,117 +0,0 @@ -// The project view (mcpp.project): the current project's executables, tests and libraries, the -// target it builds for and the platform that configures it, its last build, and the language -// service that answers its sources. Everything shown is the backend's `project/summary` and -// `languageService/status`. -import * as vscode from "vscode"; - -import type { Jobs } from "./jobs"; -import type { Projects } from "./projects"; -import type { LanguageServiceStatus, ProjectSummary } from "./protocol"; -import type { Session } from "./session"; - -class Node extends vscode.TreeItem { - children?: Node[]; - constructor(label: string, opts: { description?: string; icon?: string; context?: string; command?: vscode.Command; tooltip?: string; children?: Node[]; expanded?: boolean } = {}) { - super(label, opts.children ? (opts.expanded === false ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded) : vscode.TreeItemCollapsibleState.None); - this.description = opts.description; - if (opts.icon) this.iconPath = new vscode.ThemeIcon(opts.icon); - this.contextValue = opts.context; - this.command = opts.command; - this.tooltip = opts.tooltip; - this.children = opts.children; - } -} - -export class ProjectTree implements vscode.TreeDataProvider<Node>, vscode.Disposable { - private readonly changed = new vscode.EventEmitter<void>(); - readonly onDidChangeTreeData = this.changed.event; - private readonly disposables: vscode.Disposable[] = []; - private summary: ProjectSummary | undefined; - private languageService: LanguageServiceStatus | undefined; - private error: string | undefined; - - constructor(private readonly session: Session, private readonly projects: Projects, private readonly jobs: Jobs) { - const refresh = () => void this.load(); - this.disposables.push( - this.changed, - vscode.window.registerTreeDataProvider("mcpp.project", this), - projects.onDidChange.event(refresh), - session.on("result/changed", refresh), - session.on("manifest/changed", refresh), - session.on("job/plan", refresh), - jobs.onDidChangeRunning.event(() => this.changed.fire()), - vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("mcpp.build.target")) refresh(); }), - vscode.extensions.onDidChange(refresh), - ); - } - - async load(): Promise<void> { - const project = this.projects.project; - this.error = undefined; - if (!project) { this.summary = undefined; this.changed.fire(); return; } - try { - [this.summary, this.languageService] = await Promise.all([ - this.session.request<ProjectSummary>("project/summary", { root: project.root }), - this.session.request<LanguageServiceStatus>("languageService/status", { candidates: this.session.mcpplsCandidates() }), - ]); - } catch (e) { - this.error = e instanceof Error ? e.message : String(e); - } - this.changed.fire(); - } - - getTreeItem(n: Node): vscode.TreeItem { - return n; - } - - getChildren(n?: Node): Node[] { - if (n) return n.children ?? []; - const t = (k: string, ...a: unknown[]) => this.session.t(k, ...a); - const project = this.projects.project; - if (!project) return [new Node(t("No mcpp project in this workspace"), { icon: "info" })]; - if (this.error) return [new Node(t("mcpp could not answer"), { icon: "error", tooltip: this.error, command: { command: "mcpp.selfCheck", title: "" } })]; - const s = this.summary; - if (!s) return [new Node(t("Loading…"), { icon: "loading~spin" })]; - const cmd = (command: string, ...args: unknown[]): vscode.Command => ({ command, title: "", arguments: args }); - const out: Node[] = []; - out.push(new Node(project.name, { - description: s.toolchain, icon: "project", tooltip: project.root, - command: cmd("mcpp.openProjectPanel"), - })); - out.push(new Node(t("Target"), { - description: `${s.target || t("this machine")}${s.platformText ? ` · ${s.platformText}` : ""}`, icon: "target", - command: cmd("mcpp.selectTarget"), tooltip: s.hostPlatformText ? `${t("Host platform")}: ${s.hostPlatformText}` : undefined, - })); - if (s.lastBuild) { - const ok = s.lastBuild.status === "ok"; - out.push(new Node(t("Last build"), { description: `${s.lastBuild.status}${s.lastBuild.client ? ` · ${s.lastBuild.client}` : ""}`, icon: ok ? "pass" : "error" })); - } - if (this.jobs.runningCount > 0) - out.push(new Node(t("{0} job(s) running", this.jobs.runningCount), { icon: "sync~spin", command: cmd("mcpp.cancelJob"), tooltip: t("Cancel the Running Job") })); - out.push(new Node(t("Executables"), { icon: "symbol-event", children: s.targets.map((x) => - new Node(x.name, { icon: "play", context: "mcppExecutable", command: cmd("mcpp.run", { bin: x.name }) })) })); - out.push(new Node(t("Tests"), { icon: "beaker", children: s.tests.map((x) => - new Node(x.name, { icon: "beaker", context: "mcppTest", command: cmd("mcpp.test", { filter: x.name }) })) })); - out.push(new Node(t("Libraries"), { icon: "library", expanded: false, children: s.libraries.map((x) => new Node(x.name, { description: x.kind, icon: "package" })) })); - out.push(new Node(t("Dependencies"), { icon: "type-hierarchy", context: "mcppProjectDependencies", command: cmd("mcpp.project.showDependencyGraph") })); - const ls = this.languageService; - if (ls) { - const state = ls.state === "ok" ? "pass" : ls.state === "missing" ? "error" : "warning"; - out.push(new Node(t("C++ Modules language service"), { - description: ls.state === "ok" ? `mcppls ${ls.version}` : t("needs attention"), icon: state, tooltip: ls.message, - children: [ - new Node(ls.message, { icon: "info", tooltip: ls.message }), - ...ls.actions.map((a) => new Node(a.title, { icon: "cloud-download", command: cmd("mcpp.languageServer.act", a.id) })), - new Node(t("Restart the language server"), { icon: "debug-restart", command: cmd("mcpp.languageServer.restart") }), - new Node(t("Open the log"), { icon: "output", command: cmd("mcpp.languageServer.showLogs") }), - ], - expanded: ls.state !== "ok", - })); - } - return out; - } - - dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } -} diff --git a/mide-plugins/vscode-1.0/src/projects.ts b/mide-plugins/vscode-1.0/src/projects.ts deleted file mode 100644 index 136efe9..0000000 --- a/mide-plugins/vscode-1.0/src/projects.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Which project the user is in: the one the active editor's file belongs to (the backend answers, -// `projects/forFile`), else the first project of the window. The answer sets `mcpp.inProject` and -// `mcpp.editorTitleButtons`, which the generated menus and keybindings read. -import * as vscode from "vscode"; - -import type { Project } from "./protocol"; -import type { Session } from "./session"; - -export class Projects implements vscode.Disposable { - private current: Project | undefined; - private readonly disposables: vscode.Disposable[] = []; - readonly onDidChange = new vscode.EventEmitter<Project | undefined>(); - - constructor(private readonly session: Session) { - this.disposables.push( - this.onDidChange, - vscode.window.onDidChangeActiveTextEditor(() => void this.refresh()), - vscode.workspace.onDidChangeWorkspaceFolders(() => void this.refresh()), - vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("mcpp.task.editorTitleButtons")) void this.refresh(); }), - session.onDidStart.event(() => void this.refresh()), - ); - } - - get project(): Project | undefined { - return this.current; - } - - get all(): Project[] { - return this.session.info?.projects ?? []; - } - - /** The current project, asking the backend if the answer is not known yet. */ - async resolve(): Promise<Project | undefined> { - if (!this.current) await this.refresh(); - return this.current; - } - - async refresh(): Promise<void> { - let next: Project | undefined; - const editor = vscode.window.activeTextEditor; - try { - if (editor && editor.document.uri.scheme === "file") { - const folder = vscode.workspace.getWorkspaceFolder(editor.document.uri); - const boundary = vscode.workspace.getConfiguration().get<string>("mcpp.project.discoveryBoundary") === "filesystem" ? "" : folder?.uri.fsPath ?? ""; - next = (await this.session.request<Project | null>("projects/forFile", { path: editor.document.uri.fsPath, boundary })) ?? undefined; - } - } catch { - next = undefined; - } - if (!next) next = this.current && this.all.some((p) => p.root === this.current!.root) ? this.current : this.all[0]; - const changed = next?.root !== this.current?.root; - this.current = next; - await vscode.commands.executeCommand("setContext", "mcpp.inProject", !!next); - await vscode.commands.executeCommand("setContext", "mcpp.editorTitleButtons", - vscode.workspace.getConfiguration().get<boolean>("mcpp.task.editorTitleButtons") ?? true); - if (changed) this.onDidChange.fire(next); - } - - /** The current project, or a message that there is none. */ - async require(): Promise<Project | undefined> { - const p = await this.resolve(); - if (!p) void vscode.window.showWarningMessage(this.session.t("No mcpp.toml was found in this workspace. Run this command inside an mcpp project.")); - return p; - } - - dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } -} diff --git a/mide-plugins/vscode-1.0/src/protocol.ts b/mide-plugins/vscode-1.0/src/protocol.ts deleted file mode 100644 index 167bfaf..0000000 --- a/mide-plugins/vscode-1.0/src/protocol.ts +++ /dev/null @@ -1,150 +0,0 @@ -// The shapes of protocol/protocol.schema.json this adapter reads. The schema is the authority; these -// declarations only name what the code below touches. - -export interface Project { - root: string; - workspaceRoot: string; - name: string; - isWorkspaceRoot: boolean; -} - -export interface Check { - id: string; - status: "ok" | "info" | "warning" | "error"; - message: string; -} - -export interface LanguageServiceStatus { - state: "ok" | "outdated" | "missing" | "unreadable"; - program: string; - source: string; - version: string; - required: string; - message: string; - actions: { id: "install" | "update"; title: string }[]; -} - -export interface InitializeResult { - server: { name: string; version: string }; - protocol: number; - mcpp: { path: string; version: string; contract: boolean } | null; - languageService: LanguageServiceStatus | null; - checks: Check[]; - projects: Project[]; - capabilities: string[]; -} - -export interface CommandAction { - kind: string; - job?: string; - plan?: string; - panel?: string; - graph?: string; - method?: string; - commands?: string[]; - vscode?: string; - args?: unknown[]; - then?: string; - refresh?: boolean; -} - -export interface CommandEntry { - id: string; - title: string; - group: string; - groupTitle: string; - when?: string; - quickMenu?: boolean; - deprecated?: boolean; - action: CommandAction; -} - -export interface Position { - line: number; - column: number; -} - -export interface Diagnostic { - severity: string; - message: string; - code?: string; - path?: string; - range?: { start: Position; end: Position }; - source?: string; -} - -export interface Platform { - name?: string; - version?: string | null; - source?: string; - fields?: Record<string, { value?: unknown; source?: string }>; -} - -export interface ProjectSummary { - root: string; - targets: { name: string; kind: string; configuration: string }[]; - tests: { name: string; kind: string; configuration: string }[]; - libraries: { name: string; kind: string; configuration: string }[]; - toolchain: string; - target?: string | null; - lastBuild?: { status: string; finishedAt?: number | null; client?: string; command?: string }; - artifacts?: { kind: string; name: string; path: string }[]; - platform?: Platform | null; - platformText?: string; - hostPlatformText?: string; - error?: string; -} - -export interface Launch { - argv: string[]; - cwd: string; - env: Record<string, string>; - program?: string; - via?: string | null; -} - -export interface TargetRow { - target: string; - note: string; - pin: string; - host: boolean; - toolchains: { spec: string; status: string; default: boolean }[]; - platform: Platform | null; - platformText: string; - selected: boolean; - installed: boolean; -} - -export interface CleanPlan { - action: string; - argv: string[]; - level: number; - preview: boolean | { source: string; text?: string; bytesText?: string; count?: number }; - acknowledge: boolean; - title: string; - detail: string; - refusedUntrusted: boolean; -} - -export interface JobFinished { - jobId: string; - status: "ok" | "failed" | "cancelled" | "denied" | "superseded"; - exitCode: number | null; - durationMs?: number; - root?: string; - kind?: string; - envelope?: { kind?: string; data?: unknown }; - diagnostics?: Diagnostic[]; - artifacts?: { kind: string; name: string; path: string }[]; -} - -export interface TestResult { - jobId: string; - root: string; - member: string; - test: string; - status: "passed" | "failed" | "timedOut" | "skipped" | "errored"; - durationMs: number; - output: string; - reason: string; -} diff --git a/mide-plugins/vscode-1.0/src/run.ts b/mide-plugins/vscode-1.0/src/run.ts deleted file mode 100644 index a77bb01..0000000 --- a/mide-plugins/vscode-1.0/src/run.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Running: the backend builds the program and answers how it starts (mcpp run --print-launch: argv, -// the environment mcpp adds, the working directory, the runner); the program then runs in a terminal -// of its own, where it reads input and its colours are its own. -import * as vscode from "vscode"; - -import type { Launch } from "./protocol"; -import type { Session } from "./session"; - -export async function runProgram(session: Session, root: string, options: { target?: string; bin?: string } = {}): Promise<void> { - const args = vscode.workspace.getConfiguration().get<string[]>("mcpp.task.runArgs") ?? []; - let launch: Launch; - try { - launch = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Window, title: session.t("mcpp: building…") }, - () => session.request<Launch>("run/launch", { root, args, ...(options.target !== undefined ? { target: options.target } : {}), ...(options.bin ? { bin: options.bin } : {}) })); - } catch (e) { - const data = (e as { error?: { data?: { narration?: string } } }).error?.data; - if (data?.narration) { session.output.appendLine(data.narration); session.output.show(true); } - void vscode.window.showErrorMessage(session.t("mcpp: {0}", e instanceof Error ? e.message : String(e))); - return; - } - const [program, ...rest] = launch.argv; - const name = `mcpp run: ${(launch.program ?? program).split(/[\\/]/).pop()}`; - vscode.window.terminals.find((t) => t.name === name)?.dispose(); - const terminal = vscode.window.createTerminal({ name, shellPath: program, shellArgs: rest, cwd: launch.cwd, env: launch.env }); - terminal.show(!(vscode.workspace.getConfiguration().get<boolean>("mcpp.task.focusTerminal") ?? true)); -} diff --git a/mide-plugins/vscode-1.0/src/session.ts b/mide-plugins/vscode-1.0/src/session.ts deleted file mode 100644 index 7517119..0000000 --- a/mide-plugins/vscode-1.0/src/session.ts +++ /dev/null @@ -1,183 +0,0 @@ -// The session with the backend: started at activation, given the editor's settings, trust, -// language and folders, kept current as they change, and restarted (once, then on request) when it -// ends. Everything else in the adapter reaches the backend through `session.request`. -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as vscode from "vscode"; - -import { Backend, BackendError } from "./backend/client"; -import { locateBackend, type Located } from "./backend/locate"; -import { mcpplsCandidates } from "./backend/mcppls"; -import { format, MCPPLS_EXTENSION } from "./mapping"; -import type { CommandEntry, InitializeResult } from "./protocol"; - -export class Session implements vscode.Disposable { - private backend: Backend | undefined; - private located: Located | undefined; - private initResult: InitializeResult | undefined; - private bundle: Record<string, string> = {}; - private commandTable: CommandEntry[] = []; - private restarts = 0; - private starting: Promise<boolean> | undefined; - private readonly disposables: vscode.Disposable[] = []; - private readonly listeners = new Map<string, Set<(params: any) => void>>(); - readonly onDidStart = new vscode.EventEmitter<InitializeResult>(); - - constructor(readonly context: vscode.ExtensionContext, readonly output: vscode.OutputChannel) { - // The strings the extension carries (spec/i18n/runtime.zh-cn.json): what is said before the - // backend runs, or when it cannot. - if (vscode.env.language.toLowerCase().startsWith("zh")) { - try { - this.bundle = JSON.parse(fs.readFileSync(path.join(context.extensionPath, "i18n", "runtime.zh-cn.json"), "utf8")); - } catch { - this.bundle = {}; - } - } - this.disposables.push( - this.onDidStart, - vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration("mcpp.ide.backendPath")) { void this.restart(); return; } - if (e.affectsConfiguration("mcpp")) void this.pushSettings(); - }), - vscode.workspace.onDidGrantWorkspaceTrust(() => void this.pushSettings()), - vscode.workspace.onDidChangeWorkspaceFolders(() => void this.restart()), - vscode.extensions.onDidChange(() => void this.request("languageService/status", { candidates: this.mcpplsCandidates() }).catch(() => undefined)), - ); - } - - /** The backend's command and where it was found. */ - get started(): Located | undefined { - return this.located; - } - - get info(): InitializeResult | undefined { - return this.initResult; - } - - get commands(): CommandEntry[] { - return this.commandTable; - } - - /** A runtime string in the session's language. */ - t(key: string, ...args: unknown[]): string { - return format(this.bundle[key] ?? key, args); - } - - /** The bundle for webviews, which translate on their own. */ - get strings(): Record<string, string> { - return this.bundle; - } - - /** Every mcpp.* value, as VS Code resolves it for the window. */ - settings(): Record<string, unknown> { - const out: Record<string, unknown> = {}; - const props = (this.context.extension.packageJSON?.contributes?.configuration?.properties ?? {}) as Record<string, unknown>; - const config = vscode.workspace.getConfiguration(); - for (const key of Object.keys(props)) out[key] = config.get(key); - return out; - } - - mcpplsCandidates(): string[] { - return mcpplsCandidates(vscode.extensions.getExtension(MCPPLS_EXTENSION)?.extensionPath); - } - - on(method: string, fn: (params: any) => void): vscode.Disposable { - if (!this.listeners.has(method)) this.listeners.set(method, new Set()); - this.listeners.get(method)!.add(fn); - return new vscode.Disposable(() => this.listeners.get(method)?.delete(fn)); - } - - async start(): Promise<boolean> { - if (this.starting) return this.starting; - this.starting = this.doStart().finally(() => { this.starting = undefined; }); - return this.starting; - } - - private async doStart(): Promise<boolean> { - const configured = vscode.workspace.getConfiguration().get<string>("mcpp.ide.backendPath") ?? ""; - this.located = locateBackend({ configured: vscode.workspace.isTrusted ? configured : "", extensionRoot: this.context.extensionPath }); - if (!this.located) { - const message = this.t("The mcpp-ide backend was not found. Install it with `xlings install mcpp-ide`, or set mcpp.ide.backendPath."); - this.output.appendLine(`mcpp: ${message}`); - const open = this.t("Open Settings"); - void vscode.window.showErrorMessage(`mcpp: ${message}`, open).then((c) => { - if (c === open) void vscode.commands.executeCommand("workbench.action.openSettings", "mcpp.ide.backendPath"); - }); - return false; - } - this.output.appendLine(`mcpp-ide: starting ${[this.located.program, ...this.located.args].join(" ")} (${this.located.source})`); - const backend = new Backend({ - program: this.located.program, - args: [...this.located.args, "serve"], - log: (line) => this.output.appendLine(`[backend] ${line}`), - }); - this.backend = backend; - backend.notifications.on("*", (method: string, params: unknown) => { - for (const fn of this.listeners.get(method) ?? []) { - try { fn(params); } catch (e) { this.output.appendLine(`mcpp: ${method}: ${String(e)}`); } - } - for (const fn of this.listeners.get("*") ?? []) fn({ method, params }); - }); - void backend.exited.then((code) => this.onExit(backend, code)); - try { - const folders = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath); - this.initResult = await backend.request<InitializeResult>("initialize", { - client: { name: "VS Code", version: vscode.version }, - editor: "vscode", - locale: vscode.env.language, - trusted: vscode.workspace.isTrusted, - settings: this.settings(), - workspaceFolders: folders, - languageService: { candidates: this.mcpplsCandidates() }, - }); - this.bundle = await backend.request<Record<string, string>>("i18n/bundle", {}); - this.commandTable = (await backend.request<{ commands: CommandEntry[] }>("commands/list", { editor: "vscode" })).commands; - for (const c of this.initResult.checks) if (c.status === "error") this.output.appendLine(`mcpp: ${c.message}`); - this.onDidStart.fire(this.initResult); - return true; - } catch (e) { - this.output.appendLine(`mcpp-ide: initialize failed: ${String(e)}`); - return false; - } - } - - private onExit(backend: Backend, code: number | null): void { - if (backend !== this.backend) return; - this.backend = undefined; - this.output.appendLine(`mcpp-ide: the backend ended (${code ?? "signal"})`); - if (this.restarts < 1) { - ++this.restarts; - void this.start(); - return; - } - const again = this.t("Restart"); - void vscode.window.showErrorMessage(`mcpp: ${this.t("The mcpp-ide backend ended.")}`, again).then((c) => { - if (c === again) { this.restarts = 0; void this.start(); } - }); - } - - async restart(): Promise<void> { - const old = this.backend; - this.backend = undefined; - if (old) await old.stop(); - this.restarts = 0; - await this.start(); - } - - async request<T = unknown>(method: string, params: unknown = {}): Promise<T> { - if (!this.backend?.running) { - if (!(await this.start())) throw new BackendError(method, { code: -32099, message: this.t("the mcpp-ide backend is not running") }); - } - return this.backend!.request<T>(method, params); - } - - async pushSettings(): Promise<void> { - if (!this.backend?.running) return; - await this.backend.request("settings/update", { settings: this.settings(), trusted: vscode.workspace.isTrusted, locale: vscode.env.language }); - } - - dispose(): void { - this.disposables.forEach((d) => d.dispose()); - void this.backend?.stop(); - } -} diff --git a/mide-plugins/vscode-1.0/src/status.ts b/mide-plugins/vscode-1.0/src/status.ts deleted file mode 100644 index 458c8be..0000000 --- a/mide-plugins/vscode-1.0/src/status.ts +++ /dev/null @@ -1,49 +0,0 @@ -// The status bar: the target the project builds for (and its platform), and whether a job runs. -// A click on the first selects another target; on the second, cancels the running jobs. -import * as vscode from "vscode"; - -import type { Jobs } from "./jobs"; -import type { Projects } from "./projects"; -import type { Session } from "./session"; - -export class StatusBar implements vscode.Disposable { - private readonly target = vscode.window.createStatusBarItem("mcpp.target", vscode.StatusBarAlignment.Left, 50); - private readonly job = vscode.window.createStatusBarItem("mcpp.job", vscode.StatusBarAlignment.Left, 49); - private readonly disposables: vscode.Disposable[] = [this.target, this.job]; - private platformText = ""; - - constructor(private readonly session: Session, private readonly projects: Projects, private readonly jobs: Jobs) { - this.target.name = "mcpp target"; - this.target.command = "mcpp.selectTarget"; - this.job.name = "mcpp job"; - this.job.command = "mcpp.cancelJob"; - this.disposables.push( - projects.onDidChange.event(() => this.update()), - jobs.onDidChangeRunning.event(() => this.update()), - session.on("job/plan", (p: { root: string; platformText?: string }) => { - if (p.root === projects.project?.root) { this.platformText = p.platformText ?? ""; this.update(); } - }), - vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("mcpp.build.target") || e.affectsConfiguration("mcpp.ui.statusBar.show")) this.update(); }), - ); - this.update(); - } - - private update(): void { - const t = (k: string, ...a: unknown[]) => this.session.t(k, ...a); - const show = vscode.workspace.getConfiguration().get<boolean>("mcpp.ui.statusBar.show") ?? true; - if (!show || !this.projects.project) { this.target.hide(); this.job.hide(); return; } - const target = vscode.workspace.getConfiguration().get<string>("mcpp.build.target") || t("this machine"); - this.target.text = `$(target) ${target}`; - this.target.tooltip = [t("The target build, run, test, debug and pack use"), this.platformText].filter(Boolean).join("\n"); - this.target.show(); - if (this.jobs.runningCount > 0) { - this.job.text = `$(sync~spin) mcpp`; - this.job.tooltip = t("{0} job(s) running", this.jobs.runningCount); - this.job.show(); - } else this.job.hide(); - } - - dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } -} diff --git a/mide-plugins/vscode-1.0/src/tasks.ts b/mide-plugins/vscode-1.0/src/tasks.ts deleted file mode 100644 index 952bfc8..0000000 --- a/mide-plugins/vscode-1.0/src/tasks.ts +++ /dev/null @@ -1,115 +0,0 @@ -// mcpp's builds and tests as VS Code tasks (type "mcpp"): each runs as a backend job in a -// pseudoterminal that shows mcpp's own narration, in colour, and ends with the job's exit code. -// Closing the terminal cancels the job; mcpp then ends the steps it started and records the build as -// cancelled. A task's problems come from mcpp's diagnostics (diagnostics.ts), not from a matcher. -import * as vscode from "vscode"; - -import type { Jobs, JobHandle } from "./jobs"; -import type { Projects } from "./projects"; -import type { Session } from "./session"; - -export interface McppTaskDefinition extends vscode.TaskDefinition { - type: "mcpp"; - command: "build" | "test" | "run" | "clean"; - target?: string; - profile?: "dev" | "release"; - args?: string[]; - root?: string; -} - -/** A terminal that renders one backend job. */ -export class JobTerminal implements vscode.Pseudoterminal { - private readonly writeEmitter = new vscode.EventEmitter<string>(); - private readonly closeEmitter = new vscode.EventEmitter<number>(); - readonly onDidWrite = this.writeEmitter.event; - readonly onDidClose = this.closeEmitter.event; - private handle: JobHandle | undefined; - private ended = false; - - constructor(private readonly start: (term: JobTerminal) => Promise<JobHandle | undefined>, private readonly session: Session) {} - - print(text: string): void { - this.writeEmitter.fire(text.replace(/\r?\n/g, "\r\n")); - } - - end(code: number): void { - if (this.ended) return; - this.ended = true; - this.closeEmitter.fire(code); - } - - open(): void { - if (vscode.workspace.getConfiguration().get<boolean>("mcpp.task.clearTerminal")) this.writeEmitter.fire("\x1bc"); - this.start(this).then((h) => { this.handle = h; if (!h) this.end(1); }, (e) => { - this.print(`\x1b[31m${this.session.t("mcpp: {0}", e instanceof Error ? e.message : String(e))}\x1b[0m\n`); - this.end(1); - }); - } - - close(): void { - // The terminal closed while the job runs: ask mcpp to end it. - if (!this.ended && this.handle) void this.handle.cancel(); - } -} - -export class Tasks implements vscode.TaskProvider, vscode.Disposable { - private readonly registration: vscode.Disposable; - - constructor(private readonly session: Session, private readonly jobs: Jobs, private readonly projects: Projects) { - this.registration = vscode.tasks.registerTaskProvider("mcpp", this); - } - - provideTasks(): vscode.Task[] { - const root = this.projects.project?.root; - if (!root) return []; - return (["build", "test"] as const).map((command) => this.task({ type: "mcpp", command }, root)); - } - - resolveTask(task: vscode.Task): vscode.Task | undefined { - const def = task.definition as McppTaskDefinition; - return this.task(def, def.root || this.projects.project?.root || "", task.scope); - } - - task(def: McppTaskDefinition, root: string, scope?: vscode.WorkspaceFolder | vscode.TaskScope): vscode.Task { - const folder = scope ?? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(root)) ?? vscode.TaskScope.Workspace; - const execution = new vscode.CustomExecution(async () => new JobTerminal((term) => this.run(def, root, term), this.session)); - const task = new vscode.Task(def, folder, def.command, "mcpp", execution, []); - task.group = def.command === "build" ? vscode.TaskGroup.Build : def.command === "test" ? vscode.TaskGroup.Test : undefined; - const reveal = vscode.workspace.getConfiguration().get<string>("mcpp.task.revealTerminal"); - task.presentationOptions = { - reveal: reveal === "never" ? vscode.TaskRevealKind.Never : reveal === "silent" ? vscode.TaskRevealKind.Silent : vscode.TaskRevealKind.Always, - focus: vscode.workspace.getConfiguration().get<boolean>("mcpp.task.focusTerminal") ?? false, - clear: false, - }; - return task; - } - - private async run(def: McppTaskDefinition, root: string, term: JobTerminal): Promise<JobHandle | undefined> { - if (!root) { term.print(`${this.session.t("No mcpp project in this workspace")}\n`); return undefined; } - const params: Record<string, unknown> = { kind: def.command === "test" ? "test" : "build", root, color: true, supersede: true }; - if (def.target !== undefined) params.target = def.target; - if (def.profile) params.profile = def.profile; - if (def.args?.length) params.args = def.args; - const handle = await this.jobs.start(params, { - output: (text) => term.print(text), - testResult: (r) => { - const mark = r.status === "passed" ? "\x1b[32m✓\x1b[0m" : r.status === "skipped" ? "-" : "\x1b[31m✗\x1b[0m"; - term.print(`${mark} ${r.test} (${r.durationMs} ms)\n`); - }, - finished: (p) => { - term.print(`\n${p.status === "ok" ? "\x1b[32m" : "\x1b[31m"}${this.session.t("mcpp {0}: {1}", def.command, p.status)}\x1b[0m\n`); - term.end(p.exitCode ?? (p.status === "ok" ? 0 : 1)); - }, - }); - return handle; - } - - /** Runs a build or test task now, as the commands do. */ - async execute(command: "build" | "test", root: string, extra: Partial<McppTaskDefinition> = {}): Promise<vscode.TaskExecution> { - return vscode.tasks.executeTask(this.task({ type: "mcpp", command, ...extra }, root)); - } - - dispose(): void { - this.registration.dispose(); - } -} diff --git a/mide-plugins/vscode-1.0/src/testing.ts b/mide-plugins/vscode-1.0/src/testing.ts deleted file mode 100644 index e853f65..0000000 --- a/mide-plugins/vscode-1.0/src/testing.ts +++ /dev/null @@ -1,94 +0,0 @@ -// mcpp's tests in VS Code's Testing view. The tests of each project come from `test/list` (mcpp -// test --list: no build), each item located at the file that holds its `main`; a run is one test -// job, filtered to the test asked for, and each `test/result` marks its item. -import * as vscode from "vscode"; - -import type { Jobs } from "./jobs"; -import { parseTestId, testId } from "./mapping"; -import type { Projects } from "./projects"; -import type { Session } from "./session"; -import type { TestResult } from "./protocol"; - -export class Testing implements vscode.Disposable { - private readonly controller = vscode.tests.createTestController("mcpp", "mcpp"); - private readonly disposables: vscode.Disposable[] = [this.controller]; - - constructor(private readonly session: Session, private readonly jobs: Jobs, private readonly projects: Projects) { - this.controller.resolveHandler = async (item) => { if (!item) await this.discover(); }; - this.controller.refreshHandler = async () => { await this.discover(); }; - this.controller.createRunProfile(session.t("Run"), vscode.TestRunProfileKind.Run, (request, token) => this.run(request, token), true); - this.disposables.push( - session.on("manifest/changed", () => void this.discover()), - session.on("result/changed", () => void this.discover()), - session.onDidStart.event(() => void this.discover()), - ); - } - - async discover(): Promise<void> { - for (const project of this.projects.all) { - let tests: { test: string; member: string; main: string }[] = []; - try { - tests = (await this.session.request<{ tests: { test: string; member: string; main: string }[] }>("test/list", { root: project.root })).tests; - } catch { - continue; - } - let group = this.controller.items.get(project.root); - if (!group) { - group = this.controller.createTestItem(project.root, project.name, vscode.Uri.file(project.root)); - this.controller.items.add(group); - } - const items = tests.map((t) => { - const item = this.controller.createTestItem(testId(project.root, t.member, t.test), t.member ? `${t.member} · ${t.test}` : t.test, - t.main ? vscode.Uri.file(t.main) : undefined); - if (t.main) item.range = new vscode.Range(0, 0, 0, 0); - return item; - }); - group.children.replace(items); - } - } - - private async run(request: vscode.TestRunRequest, token: vscode.CancellationToken): Promise<void> { - const run = this.controller.createTestRun(request); - // One job per project; a single test asked for becomes the job's filter. - const byRoot = new Map<string, vscode.TestItem[]>(); - const collect = (item: vscode.TestItem) => { - const id = parseTestId(item.id); - if (!id) { item.children.forEach(collect); return; } - if (request.exclude?.includes(item)) return; - if (!byRoot.has(id.root)) byRoot.set(id.root, []); - byRoot.get(id.root)!.push(item); - }; - if (request.include) request.include.forEach(collect); - else this.controller.items.forEach(collect); - for (const [root, items] of byRoot) { - if (token.isCancellationRequested) break; - items.forEach((i) => run.enqueued(i)); - const index = new Map(items.map((i) => [parseTestId(i.id)!.test, i])); - const filter = items.length === 1 && request.include ? parseTestId(items[0].id)!.test : undefined; - const handle = await this.jobs.start({ kind: "test", root, filter, color: true }, { - output: (text) => run.appendOutput(text.replace(/\r?\n/g, "\r\n")), - testResult: (r: TestResult) => { - const item = index.get(r.test); - if (!item) return; - const ms = r.durationMs; - if (r.status === "passed") run.passed(item, ms); - else if (r.status === "skipped") run.skipped(item); - else { - const message = new vscode.TestMessage(r.output || r.reason || r.status); - if (r.status === "errored") run.errored(item, message, ms); else run.failed(item, message, ms); - } - }, - }); - const cancel = token.onCancellationRequested(() => void handle.cancel()); - const done = await handle.done; - cancel.dispose(); - if (done.status !== "ok" && done.status !== "failed") - for (const i of items) run.skipped(i); - } - run.end(); - } - - dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } -} diff --git a/mide-plugins/vscode-1.0/src/toml.ts b/mide-plugins/vscode-1.0/src/toml.ts deleted file mode 100644 index ab6f369..0000000 --- a/mide-plugins/vscode-1.0/src/toml.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Editing help for mcpp.toml and build.mcpp, answered by the backend from mcpp's own key registry -// and build-program directives (toml/complete, toml/hover, toml/symbols, buildscript/complete). -// Text analysis only: no mcpp process runs, so it works in an untrusted workspace too. Whether the -// manifest is valid is mcpp's to say (diagnostics.ts). -import * as path from "node:path"; -import * as vscode from "vscode"; - -import type { Session } from "./session"; - -interface Item { - label: string; - kind?: string; - detail?: string; - documentation?: string; - insertText?: string; - snippet?: boolean; - range?: { start: { line: number; character: number }; end: { line: number; character: number } }; -} - -const KINDS: Record<string, vscode.CompletionItemKind> = { - section: vscode.CompletionItemKind.Module, - table: vscode.CompletionItemKind.Module, - key: vscode.CompletionItemKind.Property, - value: vscode.CompletionItemKind.Value, - enum: vscode.CompletionItemKind.EnumMember, - function: vscode.CompletionItemKind.Function, - template: vscode.CompletionItemKind.Snippet, - snippet: vscode.CompletionItemKind.Snippet, -}; - -function toItem(i: Item): vscode.CompletionItem { - const item = new vscode.CompletionItem(i.label, KINDS[i.kind ?? "key"] ?? vscode.CompletionItemKind.Text); - item.detail = i.detail; - if (i.documentation) item.documentation = new vscode.MarkdownString(i.documentation); - if (i.insertText) item.insertText = i.snippet ? new vscode.SnippetString(i.insertText) : i.insertText; - if (i.range) item.range = new vscode.Range(i.range.start.line, i.range.start.character, i.range.end.line, i.range.end.character); - return item; -} - -export function registerToml(context: vscode.ExtensionContext, session: Session): void { - const enabled = (key: string) => vscode.workspace.getConfiguration().get<boolean>(key) ?? true; - const params = (doc: vscode.TextDocument, pos: vscode.Position) => ({ text: doc.getText(), line: pos.line, character: pos.character }); - context.subscriptions.push( - vscode.languages.registerCompletionItemProvider({ language: "mcpp-toml" }, { - async provideCompletionItems(doc, pos) { - if (!enabled("mcpp.toml.completion")) return []; - const r = await session.request<{ items: Item[] }>("toml/complete", params(doc, pos)).catch(() => ({ items: [] as Item[] })); - return r.items.map(toItem); - }, - }, "[", ".", "=", "\"", " "), - vscode.languages.registerHoverProvider({ language: "mcpp-toml" }, { - async provideHover(doc, pos) { - if (!enabled("mcpp.toml.hover")) return undefined; - const r = await session.request<{ contents: string } | null>("toml/hover", params(doc, pos)).catch(() => null); - return r ? new vscode.Hover(new vscode.MarkdownString(r.contents)) : undefined; - }, - }), - vscode.languages.registerDocumentSymbolProvider({ language: "mcpp-toml" }, { - async provideDocumentSymbols(doc) { - if (!enabled("mcpp.toml.navigation")) return []; - type Sym = { name: string; kind: string; line: number; children?: Sym[] }; - const r = await session.request<Sym[]>("toml/symbols", { text: doc.getText() }).catch(() => [] as Sym[]); - const convert = (s: Sym): vscode.DocumentSymbol => { - const line = doc.lineAt(Math.min(s.line, doc.lineCount - 1)).range; - const sym = new vscode.DocumentSymbol(s.name, "", s.kind === "table" ? vscode.SymbolKind.Namespace : vscode.SymbolKind.Property, line, line); - sym.children = (s.children ?? []).map(convert); - return sym; - }; - return r.map(convert); - }, - }), - // build.mcpp is C++ (mcppls answers it); only `mcpp::` directives are completed here. - vscode.languages.registerCompletionItemProvider({ language: "cpp", pattern: "**/build.mcpp" }, { - async provideCompletionItems(doc, pos) { - if (!enabled("mcpp.buildScript.intelligence") || path.basename(doc.fileName) !== "build.mcpp") return []; - const r = await session.request<{ items: Item[] }>("buildscript/complete", params(doc, pos)).catch(() => ({ items: [] as Item[] })); - return r.items.map(toItem); - }, - }, ":"), - ); -} diff --git a/mide-plugins/vscode-1.0/syntaxes/mcpp-modules.tmLanguage.json b/mide-plugins/vscode-1.0/syntaxes/mcpp-modules.tmLanguage.json deleted file mode 100644 index 39bd45d..0000000 --- a/mide-plugins/vscode-1.0/syntaxes/mcpp-modules.tmLanguage.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "mcpp C++ Modules", - "scopeName": "source.cpp.mcpp-modules", - "injectionSelector": "L:source.cpp", - "patterns": [ - { - "include": "#module-declaration" - }, - { - "include": "#import-declaration" - } - ], - "repository": { - "module-declaration": { - "match": "^\\s*(?:(export)\\s+)?(module)\\b\\s*(?:([A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*(:)\\s*([A-Za-z_][A-Za-z0-9_]*))?|(:)\\s*([A-Za-z_][A-Za-z0-9_]*))?(?:\\s*;)?", - "captures": { - "1": { - "name": "keyword.control.export.cpp" - }, - "2": { - "name": "keyword.control.module.cpp" - }, - "3": { - "name": "entity.name.namespace.module.cpp" - }, - "4": { - "name": "punctuation.separator.module-partition.cpp" - }, - "5": { - "name": "entity.name.namespace.module.partition.cpp" - }, - "6": { - "name": "punctuation.separator.module-partition.cpp" - }, - "7": { - "name": "entity.name.namespace.module.partition.cpp" - } - } - }, - "import-declaration": { - "match": "^\\s*(?:(export)\\s+)?(import)\\b\\s*(?:(:)([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*(?::[A-Za-z_][A-Za-z0-9_]*)?)|(<[^>]+>)|(\\\"[^\\\"]+\\\"))(?:\\s*;)?", - "captures": { - "1": { - "name": "keyword.control.export.cpp" - }, - "2": { - "name": "keyword.control.import.cpp" - }, - "3": { - "name": "punctuation.separator.module-partition.cpp" - }, - "4": { - "name": "entity.name.namespace.module.partition.cpp" - }, - "5": { - "name": "entity.name.namespace.module.cpp" - }, - "6": { - "name": "string.quoted.other.header.cpp" - }, - "7": { - "name": "string.quoted.double.header.cpp" - } - } - }, - "module-name": { - "match": "\\b[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*(?::[A-Za-z_][A-Za-z0-9_]*)?\\b", - "name": "entity.name.namespace.module.cpp" - } - } -} diff --git a/mide-plugins/vscode-1.0/syntaxes/mcpp-toml.tmLanguage.json b/mide-plugins/vscode-1.0/syntaxes/mcpp-toml.tmLanguage.json deleted file mode 100644 index e0df854..0000000 --- a/mide-plugins/vscode-1.0/syntaxes/mcpp-toml.tmLanguage.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "mcpp TOML", - "scopeName": "source.toml.mcpp", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#table" - }, - { - "include": "#key" - }, - { - "include": "#string" - }, - { - "include": "#datetime" - }, - { - "include": "#number" - }, - { - "include": "#boolean" - }, - { - "include": "#punctuation" - } - ], - "repository": { - "comment": { - "match": "#.*$", - "name": "comment.line.number-sign.toml" - }, - "table": { - "match": "^\\s*(\\[\\[?)([^\\]\\r\\n]+?)(\\]\\]?)\\s*(?=#|$)", - "captures": { - "1": { - "name": "punctuation.definition.table.begin.toml" - }, - "2": { - "name": "entity.name.section.toml" - }, - "3": { - "name": "punctuation.definition.table.end.toml" - } - } - }, - "key": { - "match": "^\\s*((?:[A-Za-z0-9_-]+|\"[^\"]+\"|'[^']+')(?:\\s*\\.\\s*(?:[A-Za-z0-9_-]+|\"[^\"]+\"|'[^']+'))*)\\s*(=)", - "captures": { - "1": { - "name": "variable.other.key.toml" - }, - "2": { - "name": "keyword.operator.assignment.toml" - } - } - }, - "string": { - "patterns": [ - { - "begin": "\"\"\"", - "end": "\"\"\"", - "name": "string.quoted.triple.basic.toml", - "patterns": [ - { - "include": "#escape" - } - ] - }, - { - "begin": "'''", - "end": "'''", - "name": "string.quoted.triple.literal.toml" - }, - { - "begin": "\"", - "end": "\"", - "name": "string.quoted.double.toml", - "patterns": [ - { - "include": "#escape" - } - ] - }, - { - "begin": "'", - "end": "'", - "name": "string.quoted.single.toml" - } - ] - }, - "escape": { - "match": "\\\\(?:[btnfr\"\\\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})", - "name": "constant.character.escape.toml" - }, - "datetime": { - "match": "(?<![A-Za-z0-9_-])(?:\\d{4}-\\d{2}-\\d{2}(?:[Tt ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})?)?|\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)(?![A-Za-z0-9_-])", - "name": "constant.other.datetime.toml" - }, - "number": { - "match": "(?<![A-Za-z0-9_-])[-+]?(?:0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(?:\\d(?:_?\\d)*)(?:\\.\\d(?:_?\\d)*)?(?:[eE][-+]?\\d(?:_?\\d)*)?|inf|nan)(?![A-Za-z0-9_-])", - "name": "constant.numeric.toml" - }, - "boolean": { - "match": "(?<![A-Za-z0-9_-])(?:true|false)(?![A-Za-z0-9_-])", - "name": "constant.language.boolean.toml" - }, - "punctuation": { - "match": "[\\[\\]{},.]", - "name": "punctuation.separator.toml" - } - } -} diff --git a/mide-plugins/vscode-1.0/test/e2e/runTest.ts b/mide-plugins/vscode-1.0/test/e2e/runTest.ts deleted file mode 100644 index bef9bba..0000000 --- a/mide-plugins/vscode-1.0/test/e2e/runTest.ts +++ /dev/null @@ -1,49 +0,0 @@ -// The extension in a real VS Code: the staged extension (dist/extension, what `vsce package` packs), -// a workspace holding one project, the backend this repository built, and the mock mcpp answering -// it from the golden fixtures. MCPP_E2E_CODE runs an installed VS Code instead of downloading one. -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; - -import { runTests } from "@vscode/test-electron"; - -import { built, extensionDir } from "../unit/harness"; - -async function main(): Promise<void> { - const backend = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); - if (!backend || !mock) throw new Error("build the backend first (mcpp build)"); - const temp = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-e2e-")); - const workspace = path.join(temp, "app"); - fs.mkdirSync(path.join(workspace, "src"), { recursive: true }); - fs.mkdirSync(path.join(workspace, "tests"), { recursive: true }); - fs.writeFileSync(path.join(workspace, "mcpp.toml"), '[package]\nname = "app"\nversion = "0.1.0"\n'); - fs.writeFileSync(path.join(workspace, "src", "main.cpp"), "int main() { return 0; }\n"); - fs.writeFileSync(path.join(workspace, "tests", "test_smoke.cpp"), "int main() { return 0; }\n"); - const user = path.join(temp, "user-data", "User"); - fs.mkdirSync(user, { recursive: true }); - // MCPP_E2E_BUNDLED: the backend the staged extension carries (backend/mcpp-ide.okb, run by the - // okrun it carries for this system), as a user gets it. - fs.writeFileSync(path.join(user, "settings.json"), JSON.stringify({ - ...(process.env.MCPP_E2E_BUNDLED ? {} : { "mcpp.ide.backendPath": backend }), - "mcpp.path": mock, - "security.workspace.trust.enabled": false, - })); - const executable = process.env.MCPP_E2E_CODE; - // MCPP_E2E_EXTENSION: another extension directory (an unpacked .vsix) in place of the staged one. - const extensionDevelopmentPath = process.env.MCPP_E2E_EXTENSION ?? path.join(extensionDir, "dist", "extension"); - await runTests({ - ...(executable ? { vscodeExecutablePath: executable } : { version: "1.91.0" }), - extensionDevelopmentPath, - extensionTestsPath: path.join(extensionDir, "out", "test", "e2e", "suite", "index.js"), - launchArgs: [workspace, "--user-data-dir", path.join(temp, "user-data"), "--extensions-dir", path.join(temp, "extensions"), - "--disable-gpu", "--disable-workspace-trust", "--skip-welcome", "--skip-release-notes", - // No display (a container, an agent's shell): Chromium's headless platform. - ...(process.env.MCPP_E2E_HEADLESS ? ["--ozone-platform=headless"] : [])], - extensionTestsEnv: { MCPPIDE_E2E_WORKSPACE: workspace, MCPPIDE_E2E_SOURCE: process.env.MCPP_E2E_BUNDLED ? "bundle" : "setting" }, - }); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/mide-plugins/vscode-1.0/test/e2e/suite/index.ts b/mide-plugins/vscode-1.0/test/e2e/suite/index.ts deleted file mode 100644 index 7723c1d..0000000 --- a/mide-plugins/vscode-1.0/test/e2e/suite/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -// The extension host's tests: each case below, in order, failing at the first assertion. -import * as assert from "node:assert/strict"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as vscode from "vscode"; - -import type { McppApi } from "../../../src/extension"; - -const workspace = process.env.MCPPIDE_E2E_WORKSPACE!; -const until = async (what: string, ok: () => boolean | Promise<boolean>, ms = 30_000) => { - const end = Date.now() + ms; - while (Date.now() < end) { - if (await ok()) return; - await new Promise((r) => setTimeout(r, 100)); - } - throw new Error(`timed out: ${what}`); -}; - -const cases: [string, (api: McppApi) => Promise<void>][] = [ - ["every declared command is registered", async () => { - const declared = vscode.extensions.getExtension("mcpp-community.mcpp-vscode")!.packageJSON.contributes.commands as { command: string }[]; - const known = new Set(await vscode.commands.getCommands(true)); - for (const c of declared) assert.ok(known.has(c.command), c.command); - assert.ok(declared.length > 40); - }], - ["the backend found the project and mcpp", async (api) => { - // As the run asked: the backend the settings name, or the bundle the extension carries. - assert.equal(api.session.started?.source, process.env.MCPPIDE_E2E_SOURCE); - const info = api.session.info!; - assert.equal(info.projects.length, 1); - assert.equal(info.projects[0].name, "app"); - assert.equal(info.mcpp?.contract, true); - await until("the current project", async () => (await api.projects.resolve())?.name === "app"); - }], - ["a build runs as a task and ends ok", async (api) => { - const finished = new Promise<{ status: string }>((resolve) => { const d = api.session.on("job/finished", (p) => { d.dispose(); resolve(p); }); }); - await vscode.commands.executeCommand("mcpp.build"); - assert.equal((await finished).status, "ok"); - await until("the task to end", () => vscode.tasks.taskExecutions.length === 0); - }], - ["a failed build puts mcpp's diagnostic on the source", async (api) => { - fs.writeFileSync(path.join(workspace, ".mock-mcpp-fail"), "build"); - const finished = new Promise<{ status: string }>((resolve) => { const d = api.session.on("job/finished", (p) => { d.dispose(); resolve(p); }); }); - await vscode.commands.executeCommand("mcpp.build"); - assert.equal((await finished).status, "failed"); - fs.rmSync(path.join(workspace, ".mock-mcpp-fail")); - await until("the task to end", () => vscode.tasks.taskExecutions.length === 0); - await until("a diagnostic", () => vscode.languages.getDiagnostics().some(([uri, ds]) => uri.fsPath.endsWith("main.cpp") && ds.some((d) => d.source === "mcpp"))); - }], - ["the targets panel opens", async () => { - await vscode.commands.executeCommand("mcpp.showTargets"); - await until("a panel tab", () => vscode.window.tabGroups.all.some((g) => g.tabs.some((t) => t.label.startsWith("mcpp: ")))); - }], - ["mcpp.toml is completed from mcpp's key registry", async () => { - const doc = await vscode.workspace.openTextDocument(path.join(workspace, "mcpp.toml")); - await vscode.window.showTextDocument(doc); - assert.equal(doc.languageId, "mcpp-toml"); - const list = await vscode.commands.executeCommand<vscode.CompletionList>("vscode.executeCompletionItemProvider", doc.uri, new vscode.Position(3, 0)); - const labels = list.items.map((i) => (typeof i.label === "string" ? i.label : i.label.label)); - assert.ok(labels.includes("description"), `a key of [package]: ${labels.slice(0, 10).join(", ")}`); - }], - ["run starts the program in a terminal", async () => { - await vscode.commands.executeCommand("mcpp.run"); - await until("the run terminal", () => vscode.window.terminals.some((t) => t.name.startsWith("mcpp run:"))); - }], -]; - -export async function run(): Promise<void> { - const ext = vscode.extensions.getExtension<McppApi>("mcpp-community.mcpp-vscode"); - assert.ok(ext, "the extension is installed"); - const api = await ext.activate(); - // The notifications, for the log of a failing run. - api.session.on("*", ({ method, params }: { method: string; params: { jobId?: string; status?: string } }) => { - if (method.startsWith("job/started") || method.startsWith("job/finished")) console.log(` ${method} ${params.jobId ?? ""} ${params.status ?? ""}`); - }); - for (const [name, fn] of cases) { - try { - await fn(api); - console.log(`ok ${name}`); - } catch (e) { - console.log(`FAIL ${name}`); - throw e; - } - } -} diff --git a/mide-plugins/vscode-1.0/test/unit/client.test.ts b/mide-plugins/vscode-1.0/test/unit/client.test.ts deleted file mode 100644 index 4f5d8da..0000000 --- a/mide-plugins/vscode-1.0/test/unit/client.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -// The wire against the real backend: frames split anywhere reassemble; `initialize` over the mock -// mcpp finds the project; a build's notifications arrive and end in job/finished. -import * as assert from "node:assert/strict"; -import { test } from "node:test"; - -import { Backend, encode, FrameReader } from "../../src/backend/client"; -import { built, project } from "./harness"; - -test("frames reassemble whatever the chunking", () => { - const all = Buffer.concat([Buffer.from("Junk: 1\r\n\r\n"), encode({ a: 1 }), encode({ b: "é" })]); - const r = new FrameReader(); - const bodies: string[] = []; - for (let i = 0; i < all.length; i++) bodies.push(...r.feed(all.subarray(i, i + 1))); - assert.deepEqual(bodies.map((b) => JSON.parse(b)), [{ a: 1 }, { b: "é" }]); -}); - -test("a session over the mock mcpp", async (t) => { - const backend = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); - if (!backend || !mock) { t.skip("build the backend first (mcpp build)"); return; } - const root = project(); - const b = new Backend({ program: backend }); - const init = await b.request<{ projects: { root: string }[]; mcpp: { contract: boolean } }>("initialize", { - editor: "vscode", settings: { "mcpp.path": mock }, workspaceFolders: [root], - }); - assert.equal(init.projects.length, 1); - assert.equal(init.mcpp.contract, true); - const seen: string[] = []; - b.notifications.on("*", (method: string) => seen.push(method)); - const finished = new Promise((resolve) => b.notifications.on("job/finished", resolve)); - const { jobId } = await b.request<{ jobId: string }>("job/start", { kind: "build", root }); - assert.match(jobId, /^job-/); - const done = (await finished) as { status: string }; - assert.equal(done.status, "ok"); - for (const m of ["job/started", "job/plan", "job/progress", "job/artifact"]) assert.ok(seen.includes(m), m); - await assert.rejects(b.request("no/such/method"), /unknown method/); - await b.stop(); - assert.equal(b.running, false); -}); diff --git a/mide-plugins/vscode-1.0/test/unit/harness.ts b/mide-plugins/vscode-1.0/test/unit/harness.ts deleted file mode 100644 index d716e09..0000000 --- a/mide-plugins/vscode-1.0/test/unit/harness.ts +++ /dev/null @@ -1,39 +0,0 @@ -// What the unit tests share: the extension's directory (mide-plugins/vscode), the repository, the -// programs its build produced (the backend and the mock mcpp, newest first), and a project directory -// the mock answers for. -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; - -// Compiled to out/test/unit. -export const extensionDir = path.resolve(__dirname, "..", "..", ".."); -export const repo = path.resolve(extensionDir, "..", ".."); - -export function built(name: string): string | undefined { - const exe = process.platform === "win32" ? `${name}.exe` : name; - if (name === "mcpp-ide" && process.env.MCPP_IDE_BACKEND) return process.env.MCPP_IDE_BACKEND; - let best: string | undefined; - let bestTime = 0; - const walk = (dir: string, depth: number) => { - if (depth > 4) return; - let entries: fs.Dirent[] = []; - try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } - for (const e of entries) { - const p = path.join(dir, e.name); - if (e.isDirectory()) walk(p, depth + 1); - else if (e.name === exe && path.basename(dir) === "bin") { - const t = fs.statSync(p).mtimeMs; - if (t > bestTime) { best = p; bestTime = t; } - } - } - }; - walk(path.join(repo, "target"), 0); - return best; -} - -export function project(): string { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-vscode-")); - fs.mkdirSync(path.join(root, "src")); - fs.writeFileSync(path.join(root, "mcpp.toml"), '[package]\nname = "app"\nversion = "0.1.0"\n'); - return root; -} diff --git a/mide-plugins/vscode-1.0/test/unit/locate.test.ts b/mide-plugins/vscode-1.0/test/unit/locate.test.ts deleted file mode 100644 index 3741858..0000000 --- a/mide-plugins/vscode-1.0/test/unit/locate.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Where the backend and mcppls are found. -import * as assert from "node:assert/strict"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { test } from "node:test"; - -import { locateBackend, okrunDir, onPath } from "../../src/backend/locate"; -import { mcpplsCandidates } from "../../src/backend/mcppls"; - -test("the configured path wins: a program, or a bundle run by okrun", () => { - const ext = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-ext-")); - assert.deepEqual(locateBackend({ configured: "/x/mcpp-ide", extensionRoot: ext }), { program: "/x/mcpp-ide", args: [], source: "setting" }); - const bin = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-okrun-")); - fs.writeFileSync(path.join(bin, "okrun"), ""); - assert.deepEqual(locateBackend({ configured: "/x/mcpp-ide.okb", extensionRoot: ext, platform: "linux", arch: "x64", env: { PATH: bin } }), - { program: path.join(bin, "okrun"), args: ["/x/mcpp-ide.okb"], source: "setting" }); -}); - -test("the bundle runs under the okrun carried for this system, else okrun on PATH", () => { - const ext = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-okb-")); - fs.mkdirSync(path.join(ext, "backend", "okrun", "linux-x86_64"), { recursive: true }); - fs.writeFileSync(path.join(ext, "backend", "mcpp-ide.okb"), ""); - fs.writeFileSync(path.join(ext, "backend", "okrun", "linux-x86_64", "okrun"), ""); - const linux = locateBackend({ extensionRoot: ext, platform: "linux", arch: "x64", env: { PATH: "" }, home: "/nonexistent" }); - assert.deepEqual(linux, { program: path.join(ext, "backend", "okrun", "linux-x86_64", "okrun"), args: [path.join(ext, "backend", "mcpp-ide.okb")], source: "bundle" }); - // No okrun carried for macOS: okrun on PATH runs the bundle. - const bin = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-okrun-")); - fs.writeFileSync(path.join(bin, "okrun"), ""); - const mac = locateBackend({ extensionRoot: ext, platform: "darwin", arch: "arm64", env: { PATH: bin }, home: "/nonexistent" }); - assert.deepEqual(mac?.args, [path.join(ext, "backend", "mcpp-ide.okb")]); - assert.equal(mac?.program, path.join(bin, "okrun")); - // No okrun at all: nothing in the package starts. - const native = locateBackend({ extensionRoot: ext, platform: "linux", arch: "arm64", env: { PATH: "" }, home: "/nonexistent" }); - assert.equal(native, undefined); - assert.equal(okrunDir("win32", "x64"), "windows-x86_64"); - assert.equal(okrunDir("darwin", "arm64"), "macos-aarch64"); -}); - -test("PATH lookup appends .exe on Windows", () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-path-")); - fs.writeFileSync(path.join(dir, "mcpp-ide.exe"), ""); - assert.equal(onPath("mcpp-ide", { PATH: dir }, "win32"), path.join(dir, "mcpp-ide.exe")); - assert.equal(onPath("mcpp-ide", { PATH: dir }, "linux"), undefined); -}); - -test("mcppls: the extension's payload is the candidate, for the backend to start its server", () => { - const ext = fs.mkdtempSync(path.join(os.tmpdir(), "mcppls-ext-")); - assert.deepEqual(mcpplsCandidates(ext, {}), [], "no payload, nothing"); - fs.mkdirSync(path.join(ext, "payload")); - fs.writeFileSync(path.join(ext, "payload", "payload.json"), JSON.stringify({ server: { path: "bin/mcppls.okb", runner: "bin/okrun" } })); - assert.deepEqual(mcpplsCandidates(ext, {}), [path.join(ext, "payload")]); - assert.deepEqual(mcpplsCandidates(ext, { MCPPLS_PAYLOAD: path.join(ext, "payload") }), [path.join(ext, "payload")]); - assert.deepEqual(mcpplsCandidates(undefined, { MCPPLS_SERVER: "/s/mcppls" }), ["/s/mcppls"]); -}); diff --git a/mide-plugins/vscode-1.0/test/unit/mapping.test.ts b/mide-plugins/vscode-1.0/test/unit/mapping.test.ts deleted file mode 100644 index c6ef829..0000000 --- a/mide-plugins/vscode-1.0/test/unit/mapping.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -// The protocol's values in VS Code's terms. -import * as assert from "node:assert/strict"; -import { test } from "node:test"; - -import { availableDebuggers, format, parseTestId, severityOf, testId, zeroBased } from "../../src/mapping"; - -test("positions are 1-based in mcpp and 0-based in VS Code", () => { - assert.deepEqual(zeroBased({ line: 1, column: 21 }), { line: 0, character: 20 }); - assert.deepEqual(zeroBased(undefined), { line: 0, character: 0 }); -}); - -test("severities", () => { - assert.equal(severityOf({ severity: "error" }), "error"); - assert.equal(severityOf({ severity: "note" }), "hint"); - assert.equal(severityOf({ severity: "info" }), "information"); -}); - -test("a test's identity round-trips, roots with :: included", () => { - const id = testId("/a::b", "m", "t"); - assert.deepEqual(parseTestId(id), { root: "/a::b", member: "m", test: "t" }); - assert.equal(parseTestId("x"), undefined); -}); - -test("the debuggers present, in order of preference", () => { - assert.deepEqual(availableDebuggers((id) => id === "ms-vscode.cpptools" || id === "vadimcn.vscode-lldb"), ["lldb", "cppdbg"]); - assert.equal(format("{0} and {1}", ["a", 2]), "a and 2"); -}); diff --git a/mide-plugins/vscode-1.0/tsconfig.json b/mide-plugins/vscode-1.0/tsconfig.json deleted file mode 100644 index 21db1ce..0000000 --- a/mide-plugins/vscode-1.0/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "Node", - "lib": ["ES2022"], - "rootDir": ".", - "outDir": "out", - "strict": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "sourceMap": true - }, - "include": ["src/**/*.ts", "test/**/*.ts"] -} diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json index 4b6227e..2c2d3b6 100644 --- a/mide-plugins/vscode/package.json +++ b/mide-plugins/vscode/package.json @@ -1362,7 +1362,7 @@ "test:e2e:one": "node dist/test/e2e/runTest.js", "gen:examples": "node tools/generate-library-examples.mjs", "test:graphs-ui": "npm run compile && node tools/test-graphs-ui.mjs", - "gen": "cd ../.. && mcpp run mcpp-ide-dev -- gen vscode --dev", + "gen": "cd ../.. && mcpp run mcpp-ide-dev -- gen vscode", "stage:backend": "node tools/stage-backend.mjs", "test:parity": "npm run compile && node --test \"dist/test/parity/*.test.js\"" }, diff --git a/mide-plugins/vscode/src/extension.ts b/mide-plugins/vscode/src/extension.ts index 7c73162..b66885b 100644 --- a/mide-plugins/vscode/src/extension.ts +++ b/mide-plugins/vscode/src/extension.ts @@ -225,7 +225,12 @@ async function autoConfigureModulesWizard( } } -export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> { +/** What the extension hands to whoever activates it (its own tests): the backend session. */ +export interface McppApi { + session: Session; +} + +export async function activate(extensionContext: vscode.ExtensionContext): Promise<McppApi> { const output = vscode.window.createOutputChannel("mcpp"); // Language first: everything below may want to speak to the user. @@ -587,6 +592,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi manifestWatcher.onDidChange(() => cliController.refreshStatus()), manifestWatcher.onDidDelete(() => cliController.refreshStatus()), ); + return { session }; } /** diff --git a/mide-plugins/vscode/test/artifacts.test.ts b/mide-plugins/vscode/test/artifacts.test.ts index f7c1bb6..4622c4a 100644 --- a/mide-plugins/vscode/test/artifacts.test.ts +++ b/mide-plugins/vscode/test/artifacts.test.ts @@ -610,87 +610,72 @@ test("the rename offer is only marked answered after the user answered it", () = ); }); -test("tag release 工作流校验版本并发布 VSIX", () => { - const workflow = readFileSync(path.join(root, ".github/workflows/release.yml"), "utf8"); - assert.match(workflow, /push:\s*\n\s+tags:\s*\n\s+- "v\*"/); +// The workflows are the repository's (mcpp-ide): the extension is mide-plugins/vscode of it. +const workflows = path.join(root, "..", "..", ".github", "workflows"); + +test("a release publishes the VSIX CI built, with its checksum", () => { + const workflow = readFileSync(path.join(workflows, "release.yml"), "utf8"); + assert.match(workflow, /push:\s*\n\s+tags: \['v\*'\]/); assert.match(workflow, /contents: write/); - assert.match(workflow, /npm ci/); - assert.match(workflow, /npm test/); - assert.match(workflow, /npm run package/); - assert.match(workflow, /GITHUB_REF_NAME.*v\$\{PACKAGE_VERSION\}/s); + // The release publishes what CI on the commit built and tested, not a second build. + assert.match(workflow, /uses: \.\/\.github\/workflows\/ci\.yml/); + assert.match(workflow, /artifacts\/mcpp-vscode-vsix\/\*\.vsix/); assert.match(workflow, /sha256sum/); assert.match(workflow, /gh release create/); - assert.match(workflow, /gh release upload.*--clobber/s); }); -test("the release workflow guards the published artefact against local leakage", () => { - const workflow = readFileSync(path.join(root, ".github/workflows/release.yml"), "utf8"); - assert.match(workflow, /校验 VSIX 体积与内容/); +test("the package job guards the published artefact against local leakage", () => { + const workflow = readFileSync(path.join(workflows, "ci.yml"), "utf8"); + assert.match(workflow, /The VSIX stays small and free of local state/); const forbidden = /for forbidden in ([^;\n]+)/.exec(workflow)?.[1].trim().split(/\s+/) ?? []; for (const directory of [".dev-profile/", ".agents/", ".agnets/", "test/", "tools/", "src/", "node_modules/"]) { assert.ok(forbidden.includes(directory), `${directory} must be excluded from the VSIX`); } assert.match(workflow, /unzip -t/); + // The backend the package carries: the bundle and every system's runner. + assert.match(workflow, /extension\/backend\/mcpp-ide\.okb/); + for (const system of ["linux-x86_64", "linux-aarch64", "macos-aarch64", "windows-x86_64"]) { + assert.match(workflow, new RegExp(`extension/backend/okrun/${system}/okrun`)); + } }); -test("tag release publishes Open VSX and leaves Marketplace upload manual", () => { - const workflow = readFileSync(path.join(root, ".github/workflows/release.yml"), "utf8"); - // 两个市场收到的都是 GitHub Release 上的那只 VSIX,不重新打包。Open VSX 的 - // secret 名就是仓库实际配置的 OPENVSX_TOKEN(曾写成 OVSX_PAT,首版必然失败)。 - assert.match(workflow, /npx ovsx publish "mcpp-vscode-\$\{PACKAGE_VERSION\}\.vsix" --pat "\$OPENVSX_TOKEN"/); +test("a release publishes Open VSX and leaves the Marketplace upload manual", () => { + const workflow = readFileSync(path.join(workflows, "release.yml"), "utf8"); + assert.match(workflow, /npx ovsx publish "\$GITHUB_WORKSPACE\/\$vsix" --pat "\$OPENVSX_TOKEN"/); assert.doesNotMatch(workflow, /npx vsce publish/); - // Open VSX 是必发目标:缺 token 显式失败。 + // Open VSX is a required destination: a missing token fails the release. assert.match(workflow, /OPENVSX_TOKEN: \$\{\{ secrets\.OPENVSX_TOKEN \}\}/); assert.match(workflow, /if \[ -z "\$\{OPENVSX_TOKEN:-\}" \]; then[\s\S]*?exit 1/); assert.match(workflow, /Upload the GitHub Release VSIX manually/); - // 发布用的是仓库锁定的 ovsx:npx 不在发布中途去网络解析。 const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; assert.ok(manifest.devDependencies?.ovsx, "ovsx must be a devDependency the release resolves offline"); }); test("CI runs the gates, the package checks, the drift check and every e2e variant", () => { - const workflow = readFileSync(path.join(root, ".github/workflows/ci.yml"), "utf8"); + const workflow = readFileSync(path.join(workflows, "ci.yml"), "utf8"); assert.match(workflow, /pull_request:/); - assert.match(workflow, /push:\s*\n\s+branches:\s*\n\s+- main/); - assert.match(workflow, /permissions:\s*\n\s+contents: read/); + assert.match(workflow, /push:\s*\n\s+branches: \[main\]/); assert.match(workflow, /concurrency:[\s\S]*cancel-in-progress: true/); assert.match(workflow, /node-version: \$\{\{ env.NODE_VERSION \}\}/); assert.match(workflow, /NODE_VERSION: 22/); - // Cross-platform confidence: the unit gates, the Extension Host e2e and the - // isolated install all run the same three-OS matrix — Linux, macOS ARM64 and + // The unit gates, the Extension Host e2e and the isolated install all run on Linux, macOS and // Windows, where the spawn and path handling actually differ. const osMatrixes = workflow.match(/os: \[ubuntu-latest, macos-14, windows-latest\]/g) ?? []; - assert.equal( - osMatrixes.length, - 3, - "gates, extension-host-e2e and isolated-install must all run on Linux, macOS and Windows", - ); - assert.match(workflow, /gates:/); - assert.match(workflow, /extension-host-e2e:/); - assert.match(workflow, /package:/); + assert.equal(osMatrixes.length, 3, "gates, the Extension Host e2e and the isolated install must run on three systems"); + for (const job of ["vscode-gates:", "vscode-drift:", "vscode:", "vscode-e2e:", "vscode-isolated-install:", "vscode-graph-browser:"]) { + assert.match(workflow, new RegExp(`^ ${job}`, "m"), job); + } assert.match(workflow, /npm ci/); assert.match(workflow, /npm test/); + assert.match(workflow, /npm run test:parity/); assert.match(workflow, /npm run package/); - assert.match(workflow, /unzip -t/); - // A local dev profile once leaked into the VSIX (64 MB); the guard is now explicit. - assert.match(workflow, /The VSIX stays small and free of local state/); - const forbidden = /for forbidden in ([^;\n]+)/.exec(workflow)?.[1].trim().split(/\s+/) ?? []; - for (const directory of [".dev-profile/", ".agents/", ".agnets/", "test/", "tools/", "src/", "node_modules/"]) { - assert.ok(forbidden.includes(directory), `${directory} must be excluded from the VSIX`); - } assert.match(workflow, /xvfb-run -a npm run test:e2e:one/); - // The snapshot drift gate only means something with an mcpp checkout present. - assert.match(workflow, /generated-drift:/); + // The manifest is spec/'s, and the snapshots are mcpp's. + assert.match(workflow, /mcpp-ide-dev -- gen vscode\n/); assert.match(workflow, /repository: mcpp-community\/mcpp/); assert.match(workflow, /MCPP_REPO: \$\{\{ github.workspace \}\}\/\.mcpp-source/); - // The end-to-end dependency resolution check. - assert.match(workflow, /graph-browser:/); assert.match(workflow, /npm run test:graphs-ui/); - assert.match(workflow, /isolated-install:/); - assert.match(workflow, /sunrisepeak.mcpp-language-server@/); - // The ubuntu runner ships no `code` on PATH, so the job downloads the CLI via - // the e2e tooling instead. A step that calls bare `code` again is the exact - // `code: command not found` failure this gate was added after. + assert.match(workflow, /sunrisepeak.mcpp-language-server/); assert.match(workflow, /tools\/ci-vscode-cli\.mjs/); assert.doesNotMatch(workflow, /^\s+code --/m); }); diff --git a/mide-plugins/vscode/test/e2e/fixtures/fake-mcpp.js b/mide-plugins/vscode/test/e2e/fixtures/fake-mcpp.js index b7e4b14..369c137 100644 --- a/mide-plugins/vscode/test/e2e/fixtures/fake-mcpp.js +++ b/mide-plugins/vscode/test/e2e/fixtures/fake-mcpp.js @@ -1,10 +1,17 @@ #!/usr/bin/env node +// The mcpp the e2e suite points the extension at: it records every command it is given (the suite +// reads the log), answers the two the suite drives (`build`, and the configure-only build the +// language service asks for), and answers mcpp's machine contract -- the version, the protocol, +// the toolchain, cache, index and package lists -- from the backend's recorded fixtures +// (MCPP_E2E_FIXTURES, protocol/fixtures). The backend reaches mcpp through this script as the +// extension does. const fs = require("node:fs"); const path = require("node:path"); const logPath = process.env.MCPP_E2E_LOG; -const command = process.argv.slice(2).join(" "); +const args = process.argv.slice(2); +const command = args.join(" "); if (typeof logPath === "string") { fs.appendFileSync(logPath, `${command}\n`); } @@ -19,5 +26,28 @@ if (command === "build --configure-only") { }])); process.exit(0); } +if (command === "build") { + process.exit(0); +} -process.exit(command === "build" ? 0 : 1); +const fixtures = process.env.MCPP_E2E_FIXTURES; +const answers = [ + [["--version"], "version.txt"], + [["--protocol-version"], "protocol-version.json"], + [["toolchain", "list"], "toolchain-list.json"], + [["cache", "list"], "cache-list.json"], + [["self", "env"], "self-env.json"], + [["index", "list"], "index-list.json"], + [["search"], "search.json"], + [["pack", "--list-formats"], "pack-formats.json"], + [["pack", "--help"], "pack-help.txt"], +]; +if (typeof fixtures === "string" && fixtures.length > 0) { + for (const [words, file] of answers) { + if (words.every((word, i) => args[i] === word || (i > 0 && args.includes(word)))) { + process.stdout.write(fs.readFileSync(path.join(fixtures, file), "utf8")); + process.exit(0); + } + } +} +process.exit(1); diff --git a/mide-plugins/vscode/test/e2e/runTest.ts b/mide-plugins/vscode/test/e2e/runTest.ts index 7c0596b..7480c54 100644 --- a/mide-plugins/vscode/test/e2e/runTest.ts +++ b/mide-plugins/vscode/test/e2e/runTest.ts @@ -1,4 +1,4 @@ -import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -42,6 +42,18 @@ async function runVariant(repositoryRoot: string, fixtureRoot: string, variant: copyFileSync(join(fixtureRoot, "project/mcpp.toml"), join(workspaceDir, "mcpp.toml")); copyFileSync(join(fixtureRoot, "project/main.cpp"), join(workspaceDir, "main.cpp")); copyFileSync(join(fixtureRoot, "project/build.mcpp"), join(workspaceDir, "build.mcpp")); + // The backend: the one this build produced when MCPP_IDE_BACKEND names it, as a user's settings + // would, else the bundle staged into the extension (npm run stage:backend), as a user's + // installation carries it. And the fake mcpp from the start: the backend reaches mcpp through it. + const backend = process.env.MCPP_IDE_BACKEND; + if (!backend && !existsSync(join(repositoryRoot, "backend", "mcpp-ide.okb"))) { + throw new Error("no backend: set MCPP_IDE_BACKEND, or stage the bundle (npm run stage:backend)"); + } + mkdirSync(join(userDataDir, "User"), { recursive: true }); + writeFileSync(join(userDataDir, "User", "settings.json"), JSON.stringify({ + ...(backend ? { "mcpp.ide.backendPath": backend } : {}), + "mcpp.path": fakeMcpp, + }, null, 2)); // `MCPP_E2E_CODE` runs the suite against an already-installed VS Code, which is // what makes the Extension Host tests runnable without the 135 MB download. @@ -68,6 +80,7 @@ async function runVariant(repositoryRoot: string, fixtureRoot: string, variant: ], extensionTestsEnv: { MCPP_E2E_FAKE_MCPP: fakeMcpp, + MCPP_E2E_FIXTURES: join(repositoryRoot, "..", "..", "protocol", "fixtures"), MCPP_E2E_LOG: mcppLogPath, MCPP_E2E_MCPPLS_LOG: mcpplsLogPath, MCPP_E2E_STUB: variant, diff --git a/mide-plugins/vscode/test/e2e/suite/extension.e2e.ts b/mide-plugins/vscode/test/e2e/suite/extension.e2e.ts index 5e85f28..d801876 100644 --- a/mide-plugins/vscode/test/e2e/suite/extension.e2e.ts +++ b/mide-plugins/vscode/test/e2e/suite/extension.e2e.ts @@ -121,6 +121,38 @@ suite(`mcpp extension smoke (mcppls stub: ${STUB})`, () => { await vscode.commands.executeCommand("mcpp.selfCheck"); }); + test("the backend starts and reaches mcpp through the configured path", async () => { + const extension = vscode.extensions.getExtension<{ session: { request<T>(method: string, params?: unknown): Promise<T> } }>( + "mcpp-community.mcpp-vscode"); + assert.ok(extension); + const api = await extension.activate(); + const exec = await api.session.request<{ exitCode: number; stdout: string }>("mcpp/exec", { args: ["--version"] }); + assert.equal(exec.exitCode, 0); + assert.match(exec.stdout, /^mcpp \d/); + const mcppLog = process.env.MCPP_E2E_LOG; + assert.ok(mcppLog); + assert.ok(lines(mcppLog).includes("--version"), "the backend ran the configured mcpp"); + }); + + test("the targets come from the backend, with no set while mcpp names none", async () => { + const api = await vscode.extensions.getExtension<{ session: { request<T>(method: string, params?: unknown): Promise<T> } }>( + "mcpp-community.mcpp-vscode")!.activate(); + const targets = await api.session.request<{ rows: { target: string; set?: boolean }[] }>("targets/list", {}); + assert.ok(targets.rows.some((row) => row.target === "x86_64-openkal")); + // The mock is mcpp 2026.10.27.1, before target sets (2026.10.30.1). + assert.ok(!targets.rows.some((row) => row.set === true)); + }); + + test("an mcpp launch configuration without a debugger extension says so and starts nothing", async () => { + const folder = vscode.workspace.workspaceFolders?.[0]; + const started = await vscode.debug.startDebugging(folder, { type: "mcpp", request: "launch", name: "mcpp: debug" }); + assert.equal(started, false); + }); + + test("mcpp.showCachePanel brings the cache view on screen", async () => { + await vscode.commands.executeCommand("mcpp.showCachePanel"); + }); + test("opening the settings panel does not throw", async () => { await vscode.commands.executeCommand("mcpp.openSettings"); }); diff --git a/src/bin/dev.cpp b/src/bin/dev.cpp index 86cb621..d8b61a1 100644 --- a/src/bin/dev.cpp +++ b/src/bin/dev.cpp @@ -5,12 +5,11 @@ // The golden fixtures under protocol/fixtures, from a real mcpp: the envelopes, streams and // records the backend reads. The mock mcpp and both adapters' tests replay them, so a change in // mcpp's contract shows up as a diff here first. -// mcpp-ide-dev gen vscode --out DIR [--stage] [--okb FILE] [--okrun DIR] [--backends DIR] -// The VS Code extension's declarations from spec/: package.json (mide-plugins/vscode/package.json -// with the generated commands, keybindings, menus and configuration), package.nls.json and -// package.nls.zh-cn.json. With --stage, also the files the extension ships (its out/, media, -// syntaxes, the webui, README, CHANGELOG, LICENSE, and the backend: see stage_backend): the -// directory `vsce package` packs. +// mcpp-ide-dev gen vscode +// The VS Code extension's declarations from spec/, written in place: mide-plugins/vscode's +// package.json (its commands, keybindings, menus and configuration), package.nls.json, +// package.nls.zh-cn.json and data/config-registry.json (the settings registry the extension +// reads). The rest of the manifest is the extension's own. CI regenerates and expects no change. // mcpp-ide-dev gen jetbrains --out DIR // The CLion plugin's declarations from spec/ (the IntelliJ platform's, editor id `jetbrains`): // META-INF/mcpp-actions.xml (included by plugin.xml) and the message bundles @@ -20,9 +19,10 @@ // entry in spec/i18n/runtime.zh-cn.json; every editor string a Chinese one in // manifest.zh-cn.json. // -// Nothing generated is written into the repository: the outputs go where the adapters' builds put -// them (mide-plugins/vscode/dist, mide-plugins/clion/build). The Neovim plugin (mide-plugins/nvim) -// declares nothing statically: it reads the command table from the backend at run time. +// The VS Code declarations are kept in the repository (mcpp-vscode packs its own directory, and its +// gates read the manifest); CI checks they are spec/'s. The CLion declarations go where its build +// puts them (mide-plugins/clion/build). The Neovim plugin (mide-plugins/nvim) declares nothing +// statically: it reads the command table from the backend at run time. import std; import nlohmann.json; import mcpplibs.os; @@ -258,80 +258,19 @@ void vscode_contributes(const Spec& spec, Json& contributes) { contributes["configuration"] = Json{{"title", "%mcpp.configuration.title%"}, {"properties", properties}}; } -void copy_tree(const fs::path& from, const fs::path& to, std::span<const std::string_view> skip = {}) { - std::error_code ec; - if (!fs::exists(from, ec)) return; - for (fs::recursive_directory_iterator it(from, ec), end; !ec && it != end; it.increment(ec)) { - const auto rel = fs::relative(it->path(), from, ec); - const auto relText = rel.generic_string(); - if (std::ranges::any_of(skip, [&](std::string_view s) { return relText == s || relText.starts_with(std::string(s) + "/"); })) { - if (it->is_directory(ec)) it.disable_recursion_pending(); - continue; - } - if (it->is_directory(ec)) { fs::create_directories(to / rel, ec); continue; } - fs::create_directories((to / rel).parent_path(), ec); - fs::copy_file(it->path(), to / rel, fs::copy_options::overwrite_existing, ec); - if (ec) throw std::runtime_error(std::format("copying {}: {}", it->path().generic_string(), ec.message())); - } -} - -// What an editor package carries of the backend (mcpp design 2026-10-28 part 2 §15): -// --okb FILE (MCPP_IDE_OKB) the ok-image bundle, as backend/mcpp-ide.okb: one for every system -// --okrun DIR (MCPP_IDE_OKRUN) okrun for each system and architecture, laid out -// <os>-<arch>/okrun[.exe], as backend/okrun/ -void stage_backend(std::span<const std::string> args, const fs::path& out) { - auto from = [&](std::string_view flag, const char* variable) { - auto v = option(args, flag); - if (!v) v = mcpplibs::os::env::get(variable); - return v && !v->empty() ? std::optional<fs::path>(*v) : std::nullopt; - }; - if (auto okb = from("--okb", "MCPP_IDE_OKB")) { - fs::create_directories(out / "backend"); - fs::copy_file(*okb, out / "backend" / "mcpp-ide.okb", fs::copy_options::overwrite_existing); - } - if (auto okrun = from("--okrun", "MCPP_IDE_OKRUN")) copy_tree(*okrun, out / "backend" / "okrun"); -} - -int gen_vscode(std::span<const std::string> args) { +int gen_vscode(std::span<const std::string>) { const auto root = repo(); const auto ext = plugin(root, "vscode"); - const auto out = fs::path(option(args, "--out").value_or((ext / "dist" / "extension").string())); const auto spec = load_spec(root); auto pkg = read_json(ext / "package.json"); vscode_contributes(spec, pkg["contributes"]); Json zh = spec.en; for (auto it = spec.zh.begin(); it != spec.zh.end(); ++it) zh[it.key()] = it.value(); - // --dev: the development manifest itself, its strings and the settings registry the extension - // reads (data/config-registry.json), written in place; CI regenerates and expects no change. - if (flag(args, "--dev")) { - spit(ext / "package.json", pkg.dump(2) + "\n"); - spit(ext / "package.nls.json", spec.en.dump(2) + "\n"); - spit(ext / "package.nls.zh-cn.json", zh.dump(2) + "\n"); - spit(ext / "data" / "config-registry.json", spec.settings.dump(2) + "\n"); - std::println("vscode declarations in {}", ext.generic_string()); - return 0; - } - // npm's own fields stay in the development manifest. - pkg.erase("scripts"); - pkg.erase("devDependencies"); - spit(out / "package.json", pkg.dump(2) + "\n"); - spit(out / "package.nls.json", spec.en.dump(2) + "\n"); - spit(out / "package.nls.zh-cn.json", zh.dump(2) + "\n"); - if (flag(args, "--stage")) { - copy_tree(ext / "out" / "src", out / "out" / "src"); - copy_tree(ext / "media", out / "media"); - copy_tree(ext / "syntaxes", out / "syntaxes"); - static constexpr std::array<std::string_view, 3> webuiSkip{"tests", "tsconfig.json", "js/types.d.ts"}; - copy_tree(root / "webui", out / "webui", webuiSkip); - for (const auto* f : {"README.md", "CHANGELOG.md", ".vscodeignore"}) - if (fs::exists(ext / f)) fs::copy_file(ext / f, out / f, fs::copy_options::overwrite_existing); - fs::copy_file(root / "LICENSE", out / "LICENSE.txt", fs::copy_options::overwrite_existing); - // The runtime strings, for what the extension says before its backend runs. - fs::create_directories(out / "i18n"); - fs::copy_file(root / "spec" / "i18n" / "runtime.zh-cn.json", out / "i18n" / "runtime.zh-cn.json", fs::copy_options::overwrite_existing); - stage_backend(args, out); - } - std::println("vscode declarations in {}", out.generic_string()); + spit(ext / "package.json", pkg.dump(2) + "\n"); + spit(ext / "package.nls.json", spec.en.dump(2) + "\n"); + spit(ext / "package.nls.zh-cn.json", zh.dump(2) + "\n"); + spit(ext / "data" / "config-registry.json", spec.settings.dump(2) + "\n"); + std::println("vscode declarations in {}", ext.generic_string()); return 0; } @@ -472,9 +411,9 @@ int check_i18n(std::span<const std::string>) { const auto zh = read_json(root / "spec" / "i18n" / "runtime.zh-cn.json"); std::set<std::string> missing; std::size_t seen = 0; - const std::array<std::pair<fs::path, std::vector<std::string>>, 5> trees{{ + // The VS Code extension carries its own strings (data/i18n, checked by its l10n gate). + const std::array<std::pair<fs::path, std::vector<std::string>>, 4> trees{{ {root / "src", {".cppm", ".cpp"}}, - {plugin(root, "vscode") / "src", {".ts"}}, {root / "webui", {".js"}}, {plugin(root, "clion") / "src" / "main" / "kotlin", {".kt"}}, {plugin(root, "nvim") / "lua", {".lua"}}, @@ -520,7 +459,7 @@ int main(int argc, char** argv) { std::vector<std::string> args(argv + 1, argv + argc); try { if (args.empty() || args[0] == "--help") { - std::println("mcpp-ide-dev capture | gen vscode|jetbrains --out DIR | check-i18n"); + std::println("mcpp-ide-dev capture | gen vscode | gen jetbrains --out DIR | check-i18n"); return args.empty() ? 2 : 0; } const std::span<const std::string> rest(args.begin() + 1, args.end()); From bd7e521e297baedebcfeeb90f8a5b11853d3fe11 Mon Sep 17 00:00:00 2001 From: Sunrisepeak <speakshen@163.com> Date: Fri, 30 Oct 2026 01:47:12 +0900 Subject: [PATCH 073/119] mcpp-vscode: only the staged backend at the extension's root is ignored, not src/ide/backend Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mide-plugins/vscode/.gitignore | 2 +- mide-plugins/vscode/src/ide/backend/client.ts | 164 ++++++++++++++++++ mide-plugins/vscode/src/ide/backend/locate.ts | 93 ++++++++++ mide-plugins/vscode/src/ide/backend/mcppls.ts | 15 ++ 4 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 mide-plugins/vscode/src/ide/backend/client.ts create mode 100644 mide-plugins/vscode/src/ide/backend/locate.ts create mode 100644 mide-plugins/vscode/src/ide/backend/mcppls.ts diff --git a/mide-plugins/vscode/.gitignore b/mide-plugins/vscode/.gitignore index 535e842..c11d0ed 100644 --- a/mide-plugins/vscode/.gitignore +++ b/mide-plugins/vscode/.gitignore @@ -14,4 +14,4 @@ dist/ .agents/prototypes/ # The backend the package carries (tools/stage-backend.mjs). -backend/ +/backend/ diff --git a/mide-plugins/vscode/src/ide/backend/client.ts b/mide-plugins/vscode/src/ide/backend/client.ts new file mode 100644 index 0000000..daec36f --- /dev/null +++ b/mide-plugins/vscode/src/ide/backend/client.ts @@ -0,0 +1,164 @@ +// The backend's wire, as the adapter speaks it: `mcpp-ide serve` started as a child process, +// JSON-RPC 2.0 framed as the LSP base protocol on its standard streams. No editor API here, so the +// unit tests and the panel tests drive a real backend through the same code. +import { spawn, type ChildProcess } from "node:child_process"; +import { EventEmitter } from "node:events"; + +export interface RpcError { + code: number; + message: string; + data?: unknown; +} + +export class BackendError extends Error { + constructor(public readonly method: string, public readonly error: RpcError) { + super(error.message); + } +} + +type Pending = { resolve: (v: unknown) => void; reject: (e: unknown) => void; method: string }; + +/** Splits a byte stream into the bodies of Content-Length frames. */ +export class FrameReader { + private buffer = Buffer.alloc(0); + + feed(chunk: Buffer): string[] { + this.buffer = Buffer.concat([this.buffer, chunk]); + const out: string[] = []; + for (;;) { + const headerEnd = this.buffer.indexOf("\r\n\r\n"); + if (headerEnd < 0) break; + const headers = this.buffer.subarray(0, headerEnd).toString("ascii"); + const match = /content-length:\s*(\d+)/i.exec(headers); + if (!match) { + this.buffer = this.buffer.subarray(headerEnd + 4); + continue; + } + const length = Number(match[1]); + if (this.buffer.length < headerEnd + 4 + length) break; + out.push(this.buffer.subarray(headerEnd + 4, headerEnd + 4 + length).toString("utf8")); + this.buffer = this.buffer.subarray(headerEnd + 4 + length); + } + return out; + } +} + +export function encode(message: unknown): Buffer { + const body = Buffer.from(JSON.stringify(message), "utf8"); + return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii"), body]); +} + +export interface BackendOptions { + program: string; + args?: string[]; + cwd?: string; + env?: NodeJS.ProcessEnv; + /** Lines the backend writes to its standard error (its log). */ + log?: (line: string) => void; +} + +/** + * One backend process. `request` sends a method and resolves with its result; `notifications` + * emits each notification by method name (and "*" with the method first). `exited` resolves when + * the process ends. + */ +export class Backend { + readonly notifications = new EventEmitter(); + readonly exited: Promise<number | null>; + private readonly child: ChildProcess; + private readonly pending = new Map<number, Pending>(); + private readonly reader = new FrameReader(); + private nextId = 1; + private alive = true; + + constructor(options: BackendOptions) { + this.notifications.setMaxListeners(0); + this.child = spawn(options.program, options.args ?? ["serve"], { + cwd: options.cwd, + env: options.env ?? process.env, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }); + this.child.stdout!.on("data", (chunk: Buffer) => { + for (const body of this.reader.feed(chunk)) this.dispatch(body); + }); + let errText = ""; + this.child.stderr!.on("data", (chunk: Buffer) => { + errText += chunk.toString("utf8"); + let nl: number; + while ((nl = errText.indexOf("\n")) >= 0) { + options.log?.(errText.slice(0, nl)); + errText = errText.slice(nl + 1); + } + }); + this.exited = new Promise((resolve) => { + const done = (code: number | null) => { + if (!this.alive) return; + this.alive = false; + for (const p of this.pending.values()) p.reject(new BackendError(p.method, { code: -32099, message: "the mcpp-ide backend ended" })); + this.pending.clear(); + resolve(code); + }; + this.child.on("exit", (code) => done(code)); + this.child.on("error", (e) => { + options.log?.(`mcpp-ide could not start: ${e.message}`); + done(null); + }); + }); + } + + get running(): boolean { + return this.alive; + } + + request<T = unknown>(method: string, params: unknown = {}): Promise<T> { + if (!this.alive) return Promise.reject(new BackendError(method, { code: -32099, message: "the mcpp-ide backend is not running" })); + const id = this.nextId++; + return new Promise<T>((resolve, reject) => { + this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject, method }); + this.child.stdin!.write(encode({ jsonrpc: "2.0", id, method, params })); + }); + } + + notify(method: string, params: unknown = {}): void { + if (this.alive) this.child.stdin!.write(encode({ jsonrpc: "2.0", method, params })); + } + + cancel(id: number): void { + this.notify("$/cancelRequest", { id }); + } + + async stop(): Promise<void> { + if (!this.alive) return; + try { + await Promise.race([this.request("shutdown"), new Promise((r) => setTimeout(r, 2000))]); + } catch { + // ended already + } + this.notify("exit"); + this.child.stdin!.end(); + const ended = await Promise.race([this.exited.then(() => true), new Promise<boolean>((r) => setTimeout(() => r(false), 3000))]); + if (!ended) this.child.kill(); + } + + private dispatch(body: string): void { + let message: { id?: number; method?: string; params?: unknown; result?: unknown; error?: RpcError }; + try { + message = JSON.parse(body); + } catch { + return; + } + if (message.id !== undefined && message.id !== null && (message.result !== undefined || message.error !== undefined)) { + const p = this.pending.get(message.id); + if (!p) return; + this.pending.delete(message.id); + if (message.error) p.reject(new BackendError(p.method, message.error)); + else p.resolve(message.result); + return; + } + if (message.method) { + this.notifications.emit(message.method, message.params); + this.notifications.emit("*", message.method, message.params); + } + } +} diff --git a/mide-plugins/vscode/src/ide/backend/locate.ts b/mide-plugins/vscode/src/ide/backend/locate.ts new file mode 100644 index 0000000..9638e12 --- /dev/null +++ b/mide-plugins/vscode/src/ide/backend/locate.ts @@ -0,0 +1,93 @@ +// Where the backend is, as the command that starts it (mcpp design 2026-10-28 part 2 §15): +// +// 1. mcpp.ide.backendPath: a program, or an ok-image bundle (`*.okb`) run by okrun; +// 2. the bundle the extension carries, `backend/mcpp-ide.okb`, run by the okrun it carries for +// this system (`backend/okrun/<os>-<arch>/okrun[.exe]`), else by okrun on PATH -- one package +// for every system and architecture; +// 3. the mcpp-ide xlings installed (its command runs okrun on its bundle), then PATH. +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +export interface Located { + program: string; + /** Arguments before the backend's own (`serve`): the bundle, when okrun runs one. */ + args: string[]; + source: "setting" | "bundle" | "xlings" | "PATH"; +} + +/** mcpp's names for a system and an architecture: "linux-x86_64", "macos-aarch64", "windows-x86_64". */ +export function okrunDir(platform: NodeJS.Platform = process.platform, arch: string = process.arch): string { + const system = platform === "win32" ? "windows" : platform === "darwin" ? "macos" : platform; + const machine = arch === "x64" ? "x86_64" : arch === "arm64" ? "aarch64" : arch; + return `${system}-${machine}`; +} + +function executable(name: string, platform: NodeJS.Platform): string { + return platform === "win32" ? `${name}.exe` : name; +} + +function isFile(p: string): boolean { + try { + return fs.statSync(p).isFile(); + } catch { + return false; + } +} + +// A package's archive need not keep the mode bits. +function runnable(p: string, platform: NodeJS.Platform): string { + if (platform !== "win32") { + try { fs.accessSync(p, fs.constants.X_OK); } catch { try { fs.chmodSync(p, 0o755); } catch { /* reported when it is started */ } } + } + return p; +} + +export function onPath(name: string, env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string | undefined { + const dirs = (env.PATH ?? env.Path ?? "").split(platform === "win32" ? ";" : ":").filter(Boolean); + for (const d of dirs) { + const candidate = path.join(d, executable(name, platform)); + if (isFile(candidate)) return candidate; + } + return undefined; +} + +export function locateBackend(options: { + configured?: string; + extensionRoot: string; + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; + arch?: string; + home?: string; +}): Located | undefined { + const platform = options.platform ?? process.platform; + const arch = options.arch ?? process.arch; + const env = options.env ?? process.env; + const bundledOkrun = path.join(options.extensionRoot, "backend", "okrun", okrunDir(platform, arch), executable("okrun", platform)); + const okrun = (): string | undefined => (isFile(bundledOkrun) ? runnable(bundledOkrun, platform) : onPath("okrun", env, platform)); + + const configured = options.configured?.trim(); + if (configured) { + if (configured.endsWith(".okb")) { + const runner = okrun(); + return runner ? { program: runner, args: [configured], source: "setting" } : undefined; + } + return { program: configured, args: [], source: "setting" }; + } + const bundle = path.join(options.extensionRoot, "backend", "mcpp-ide.okb"); + if (isFile(bundle)) { + const runner = okrun(); + if (runner) return { program: runner, args: [bundle], source: "bundle" }; + } + const exe = executable("mcpp-ide", platform); + const home = options.home ?? os.homedir(); + for (const candidate of [ + env.XLINGS_HOME ? path.join(env.XLINGS_HOME, "subos", "current", "bin", exe) : undefined, + path.join(home, ".xlings", "subos", "current", "bin", exe), + path.join(home, ".xlings", "subos", "default", "bin", exe), + ]) { + if (candidate && isFile(candidate)) return { program: candidate, args: [], source: "xlings" }; + } + const found = onPath("mcpp-ide", env, platform); + return found ? { program: found, args: [], source: "PATH" } : undefined; +} diff --git a/mide-plugins/vscode/src/ide/backend/mcppls.ts b/mide-plugins/vscode/src/ide/backend/mcppls.ts new file mode 100644 index 0000000..2e052b0 --- /dev/null +++ b/mide-plugins/vscode/src/ide/backend/mcppls.ts @@ -0,0 +1,15 @@ +// Where the mcppls VS Code extension keeps its server: its payload directory (MCPPLS_PAYLOAD, else +// `<extension>/payload`), whose payload.json's `server` the backend starts -- the runner and the okb, or a +// program -- and MCPPLS_SERVER. The backend reads its version (languageService/status). +import * as fs from "node:fs"; +import * as path from "node:path"; + +export function mcpplsCandidates(extensionPath: string | undefined, env: NodeJS.ProcessEnv = process.env): string[] { + const out: string[] = []; + if (env.MCPPLS_SERVER) out.push(env.MCPPLS_SERVER); + if (!extensionPath) return out; + const payload = env.MCPPLS_PAYLOAD ? path.resolve(env.MCPPLS_PAYLOAD) : path.join(extensionPath, "payload"); + // An installation without a payload answers nothing here; PATH and xlings are tried after. + if (fs.existsSync(path.join(payload, "payload.json"))) out.push(payload); + return out; +} From e21df38ea0ad516fb0dd9de54c4ac57cc16e5260 Mon Sep 17 00:00:00 2001 From: Sunrisepeak <speakshen@163.com> Date: Fri, 30 Oct 2026 01:51:03 +0900 Subject: [PATCH 074/119] mcpp-vscode: the build-script API and the mcpp.toml schema from mcpp 2026.10.29.1; the platform plane; the settings documentation for 75 settings Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mide-plugins/vscode/data/buildscript-api.json | 97 ++++++++-- mide-plugins/vscode/data/toml-schema.json | 43 ++++- mide-plugins/vscode/docs/settings.md | 175 +++++++++++++----- .../vscode/test/buildscript/api.test.ts | 3 +- .../vscode/tools/generate-toml-schema.mjs | 1 + 5 files changed, 262 insertions(+), 57 deletions(-) diff --git a/mide-plugins/vscode/data/buildscript-api.json b/mide-plugins/vscode/data/buildscript-api.json index 9b2f89d..5c6085a 100644 --- a/mide-plugins/vscode/data/buildscript-api.json +++ b/mide-plugins/vscode/data/buildscript-api.json @@ -1,7 +1,7 @@ { - "sourceVersion": "2026.10.17.1", - "sourceCommit": "684a306bced037014da08b038412f57c203d0bc9", - "protocolVersion": 15, + "sourceVersion": "2026.10.29.1", + "sourceCommit": "db3b8159480ce94df2ba7ec20c7f9e0654f97764", + "protocolVersion": 17, "cacheEpoch": 3, "roles": [ "source", @@ -131,8 +131,10 @@ "transform": "Verbatim", "mustExist": false, "since": 4, - "rules": [], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192" + "rules": [ + "R9.10" + ], + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192" }, { "wire": "runner-named", @@ -263,7 +265,7 @@ "R2.1", "R9.4" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212" }, { "wire": "diagnostic", @@ -299,7 +301,8 @@ "R5.1", "R5.3", "R6.4", - "R9.2" + "R9.2", + "R9.10" ], "docsUrl": null }, @@ -314,7 +317,7 @@ "rules": [ "R2.1" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" }, { "wire": "floor", @@ -327,7 +330,7 @@ "rules": [ "R2.1" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" }, { "wire": "pack-format", @@ -337,7 +340,9 @@ "transform": "Verbatim", "mustExist": false, "since": 9, - "rules": [], + "rules": [ + "R9.10" + ], "docsUrl": null }, { @@ -376,7 +381,7 @@ "R9.2", "R9.5" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11" }, { "wire": "runtime-search-dir", @@ -429,6 +434,76 @@ "R9.9" ], "docsUrl": null + }, + { + "wire": "link-start", + "tag": "link-start", + "slot": "LinkStart", + "scope": "LinkGlobal", + "transform": "Verbatim", + "mustExist": false, + "since": 16, + "rules": [ + "R9.2" + ], + "docsUrl": null + }, + { + "wire": "link-driver", + "tag": "link-driver", + "slot": "LinkDriver", + "scope": "LinkGlobal", + "transform": "Verbatim", + "mustExist": false, + "since": 17, + "rules": [], + "docsUrl": null + }, + { + "wire": "link-input", + "tag": "link-input", + "slot": "LinkInputs", + "scope": "LinkGlobal", + "transform": "Verbatim", + "mustExist": false, + "since": 17, + "rules": [], + "docsUrl": null + }, + { + "wire": "artifact-name", + "tag": "artifact-name", + "slot": "ArtifactNames", + "scope": "LinkGlobal", + "transform": "Verbatim", + "mustExist": false, + "since": 17, + "rules": [], + "docsUrl": null + }, + { + "wire": "seal", + "tag": "seal", + "slot": "Seal", + "scope": "LinkGlobal", + "transform": "Verbatim", + "mustExist": false, + "since": 17, + "rules": [ + "R9.2" + ], + "docsUrl": null + }, + { + "wire": "run-host", + "tag": "run-host", + "slot": "RunHost", + "scope": "RunGlobal", + "transform": "Verbatim", + "mustExist": false, + "since": 17, + "rules": [], + "docsUrl": null } ] } diff --git a/mide-plugins/vscode/data/toml-schema.json b/mide-plugins/vscode/data/toml-schema.json index 5c89ce8..fdbf1b0 100644 --- a/mide-plugins/vscode/data/toml-schema.json +++ b/mide-plugins/vscode/data/toml-schema.json @@ -1,6 +1,6 @@ { - "sourceVersion": "2026.10.17.1", - "sourceCommit": "684a306bced037014da08b038412f57c203d0bc9", + "sourceVersion": "2026.10.29.1", + "sourceCommit": "db3b8159480ce94df2ba7ec20c7f9e0654f97764", "sections": [ { "header": "[build]", @@ -54,6 +54,11 @@ "key": "cflags", "type": "array" }, + { + "key": "compile_commands", + "type": "string", + "unmodelled": true + }, { "key": "cxx_runtime", "type": "enum", @@ -251,6 +256,13 @@ "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#216-hooks--project-build-lifecycle-commands", "deprecatedBy": null }, + { + "header": "[host]", + "name": "host", + "plane": "platform", + "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", + "deprecatedBy": null + }, { "header": "[indices]", "name": "indices", @@ -496,6 +508,13 @@ } ] }, + { + "header": "[platform]", + "name": "platform", + "plane": "platform", + "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", + "deprecatedBy": null + }, { "header": "[profile]", "name": "profile", @@ -617,6 +636,21 @@ "key": "min_api_level", "type": "number" }, + { + "key": "platform", + "type": "string", + "unmodelled": true + }, + { + "key": "requires", + "type": "string", + "unmodelled": true + }, + { + "key": "run_under", + "type": "string", + "unmodelled": true + }, { "key": "runner", "type": "array", @@ -690,6 +724,11 @@ "key": "soname", "type": "string" }, + { + "key": "start", + "type": "string", + "unmodelled": true + }, { "key": "windows_auto_export", "type": "string", diff --git a/mide-plugins/vscode/docs/settings.md b/mide-plugins/vscode/docs/settings.md index a343afe..0107d95 100644 --- a/mide-plugins/vscode/docs/settings.md +++ b/mide-plugins/vscode/docs/settings.md @@ -2,7 +2,7 @@ <!-- Generated by `npm run gen:docs` from `data/config-registry.json`. Do not edit by hand. --> -69 settings, in 10 groups. All of them are read by the +75 settings, in 10 groups. All of them are read by the **mcpp** extension only. `mcppls.*` belongs to the C++ Modules extension and is never written by this one; the configuration panel says so on every screen. @@ -184,6 +184,62 @@ Show the Run and Test buttons in the editor title bar while the file belongs to | Applies | Takes effect immediately | | Since | 0.5.0 | +### `mcpp.build.onSave` + +Start a build of the file's project when a source or mcpp.toml is saved. A save while a build waits replaces it; a running build is cancelled and started again. + +保存源文件或 mcpp.toml 时构建其所在工程。构建在等待时再次保存会替换它;正在运行的构建会被取消并重新开始。 + +| | | +|---|---| +| Type | `boolean` | +| Default | `false` | +| Scope | per folder | +| Applies | Takes effect immediately | +| Since | 1.0.0 | + +### `mcpp.build.profile` + +The profile build, run, test and debug use: dev (unoptimised, debug information) or release. + +构建、运行、测试与调试使用的配置:dev(不优化,带调试信息)或 release。 + +| | | +|---|---| +| Type | `string`: `dev` \| `release` | +| Default | `"dev"` | +| Scope | per folder | +| Applies | Takes effect immediately | +| Since | 1.0.0 | + +### `mcpp.build.target` + +The target build, run, test, debug and pack use, as mcpp names it (x86_64-linux-musl, x86_64-openkal). Empty: this machine. The Targets panel lists the targets and the platform that configures each. + +构建、运行、测试、调试与打包使用的目标,按 mcpp 的命名(x86_64-linux-musl、x86_64-openkal)。留空:本机。目标面板列出各目标及配置它的平台。 + +| | | +|---|---| +| Type | `string` | +| Default | `""` | +| Scope | per folder | +| Applies | Takes effect immediately | +| Since | 1.0.0 | + +### `mcpp.debug.engine` + +The debug adapter a debug session uses. auto: CodeLLDB when it is installed, then the C/C++ extension's gdb or lldb, then lldb-dap. + +调试会话使用的调试适配器。auto:已安装 CodeLLDB 时用它,其次用 C/C++ 扩展的 gdb 或 lldb,再次用 lldb-dap。 + +| | | +|---|---| +| Type | `string`: `auto` \| `cppdbg` \| `lldb` \| `lldb-dap` \| `gdb` | +| Default | `"auto"` | +| Scope | per folder | +| Applies | Takes effect immediately | +| Since | 1.0.0 | + ## C++ Modules language service ### `mcpp.languageService.menuItems` — advanced @@ -343,6 +399,7 @@ mcpp.toml 语法错误的严重度。 | Scope | per folder | | Applies | Takes effect immediately | | Since | 0.5.0 | +| Deprecated | mcpp reports mcpp.toml problems itself, with their location; this setting no longer has an effect. | ### `mcpp.toml.diagnostics.unknownSection` @@ -357,6 +414,7 @@ mcpp 无法识别的段的严重度。 | Scope | per folder | | Applies | Takes effect immediately | | Since | 0.5.0 | +| Deprecated | mcpp reports mcpp.toml problems itself, with their location; this setting no longer has an effect. | ### `mcpp.toml.diagnostics.unknownKey` @@ -371,6 +429,7 @@ mcpp 无法识别的键的严重度。 | Scope | per folder | | Applies | Takes effect immediately | | Since | 0.5.0 | +| Deprecated | mcpp reports mcpp.toml problems itself, with their location; this setting no longer has an effect. | ### `mcpp.toml.diagnostics.planeSeparation` — advanced @@ -385,6 +444,7 @@ Severity used when a key is written in the wrong mcpp.toml plane, such as a buil | Scope | per folder | | Applies | Takes effect immediately | | Since | 0.5.0 | +| Deprecated | mcpp reports mcpp.toml problems itself, with their location; this setting no longer has an effect. | ### `mcpp.toml.diagnostics.legacyKeys` — advanced @@ -399,6 +459,7 @@ Severity used for pre-0.5 mcpp.toml keys that still work but should be migrated. | Scope | per folder | | Applies | Takes effect immediately | | Since | 0.5.0 | +| Deprecated | mcpp reports mcpp.toml problems itself, with their location; this setting no longer has an effect. | ### `mcpp.toml.indexCompletion` — advanced @@ -637,25 +698,25 @@ Default age in days used when pruning cache entries older than a threshold. ## Views -### `mcpp.views.enabled` +### `mcpp.views.project.show` -Turns every mcpp view off at once. The view contents disappear, so the container shows nothing; whether VS Code also removes the mcpp icon from the activity bar is up to VS Code itself, because an extension cannot mark its container as hidden-when-empty. Right-click the icon to hide it there. +Show the mcpp project view in the activity bar. Takes effect after the view container reloads. -一次性关闭全部 mcpp 视图。视图内容会消失,容器变成空的;mcpp 图标是否也从活动栏移除由 VS Code 决定——扩展无法把自己的容器标记为「空则隐藏」。要移除图标可在图标上右键。 +Show the mcpp project view in the activity bar. Takes effect after the view container reloads. | | | |---|---| | Type | `boolean` | | Default | `true` | | Scope | per window | -| Applies | Takes effect immediately | -| Since | 0.6.0 | +| Applies | Takes effect after the window reloads | +| Since | 0.5.0 | -### `mcpp.views.project.show` +### `mcpp.views.cache.show` -Show the mcpp project view in the activity bar. Takes effect after the view container reloads. +Show the mcpp cache view in the activity bar. Takes effect after the view container reloads. -Show the mcpp project view in the activity bar. Takes effect after the view container reloads. +Show the mcpp cache view in the activity bar. Takes effect after the view container reloads. | | | |---|---| @@ -665,6 +726,20 @@ Show the mcpp project view in the activity bar. Takes effect after the view cont | Applies | Takes effect after the window reloads | | Since | 0.5.0 | +### `mcpp.views.enabled` + +Turns every mcpp view off at once. The view contents disappear, so the container shows nothing; whether VS Code also removes the mcpp icon from the activity bar is up to VS Code itself, because an extension cannot mark its container as hidden-when-empty. Right-click the icon to hide it there. + +一次性关闭全部 mcpp 视图。视图内容会消失,容器变成空的;mcpp 图标是否也从活动栏移除由 VS Code 决定——扩展无法把自己的容器标记为「空则隐藏」。要移除图标可在图标上右键。 + +| | | +|---|---| +| Type | `boolean` | +| Default | `true` | +| Scope | per window | +| Applies | Takes effect immediately | +| Since | 0.6.0 | + ### `mcpp.views.library.show` Show the view that browses the package index mcpp has already refreshed locally. @@ -679,19 +754,33 @@ Show the view that browses the package index mcpp has already refreshed locally. | Applies | Takes effect after the window reloads | | Since | 0.6.0 | -### `mcpp.views.cache.show` +### `mcpp.library.indexPath` — advanced -Show the mcpp cache view in the activity bar. Takes effect after the view container reloads. +Leave empty to browse the official mcpp-index at <mcpp home>/indices/mcpplibs (legacy registry/data/mcpplibs is supported). MCPP_HOME takes precedence; otherwise the extension asks mcpp self env. Set a directory containing pkgs/ or a collection of indexes to browse a custom source. -Show the mcpp cache view in the activity bar. Takes effect after the view container reloads. +留空则读取 mcpp 主目录下 indices/mcpplibs 中的官方 mcpp-index,兼容旧位置 registry/data/mcpplibs。优先使用 MCPP_HOME,否则通过 mcpp self env 获取主目录。可指定带 pkgs/ 的目录或索引集合目录来浏览自定义来源。 + +| | | +|---|---| +| Type | `string` | +| Default | `""` | +| Scope | per window | +| Applies | Takes effect immediately | +| Since | 0.6.0 | + +### `mcpp.library.networkSearch` + +Off by default. Also searches other registries with mcpp search; remote results have limited previews and cannot be added directly. + +默认关闭。开启后额外用 mcpp search 搜索其它 registry;搜索结果仅提供有限预览,不能直接添加。 | | | |---|---| | Type | `boolean` | -| Default | `true` | +| Default | `false` | | Scope | per window | -| Applies | Takes effect after the window reloads | -| Since | 0.5.0 | +| Applies | Takes effect immediately | +| Since | 0.6.0 | ### `mcpp.views.languageServer.show` @@ -736,34 +825,6 @@ Age buckets used to group cache entries in the cache view, written as "1d", "7d" | Applies | Takes effect immediately | | Since | 0.5.0 | -### `mcpp.library.indexPath` — advanced - -Leave empty to browse the official mcpp-index at <mcpp home>/indices/mcpplibs (legacy registry/data/mcpplibs is supported). MCPP_HOME takes precedence; otherwise the extension asks mcpp self env. Set a directory containing pkgs/ or a collection of indexes to browse a custom source. - -留空则读取 mcpp 主目录下 indices/mcpplibs 中的官方 mcpp-index,兼容旧位置 registry/data/mcpplibs。优先使用 MCPP_HOME,否则通过 mcpp self env 获取主目录。可指定带 pkgs/ 的目录或索引集合目录来浏览自定义来源。 - -| | | -|---|---| -| Type | `string` | -| Default | `""` | -| Scope | per window | -| Applies | Takes effect immediately | -| Since | 0.6.0 | - -### `mcpp.library.networkSearch` - -Off by default. Also searches other registries with mcpp search; remote results have limited previews and cannot be added directly. - -默认关闭。开启后额外用 mcpp search 搜索其它 registry;搜索结果仅提供有限预览,不能直接添加。 - -| | | -|---|---| -| Type | `boolean` | -| Default | `false` | -| Scope | per window | -| Applies | Takes effect immediately | -| Since | 0.6.0 | - ## Interface ### `mcpp.ui.confirmDestructiveOnly` — advanced @@ -1015,6 +1076,34 @@ Deprecated: mcpp-vscode no longer reads this setting; use mcpp.toml.completion i | Since | 0.2.6 | | Deprecated | mcpp-vscode no longer reads this setting; use mcpp.toml.completion instead. | +### `mcpp.ide.backendPath` — advanced + +The mcpp-ide program the extension starts. Empty: the backend shipped with the extension, then the one xlings installed, then PATH. + +扩展启动的 mcpp-ide 程序。留空:先用扩展自带的后端,再用 xlings 安装的,最后用 PATH 中的。 + +| | | +|---|---| +| Type | `string` | +| Default | `""` | +| Scope | machine | +| Applies | restart | +| Since | 1.0.0 | + +### `mcpp.trust.denyEffects` — advanced + +What mcpp may not do while the workspace is not trusted (mcpp --deny): a run that needs one of them stops before it and says so. Trusting the workspace lifts the list. + +工作区未受信任时 mcpp 不得做的事(mcpp --deny):需要其中之一的运行会在发生前停止并说明原因。信任工作区后解除。 + +| | | +|---|---| +| Type | `array` | +| Default | `["exec-build-script","network"]` | +| Scope | per window | +| Applies | Takes effect immediately | +| Since | 1.0.0 | + ## Language `mcpp.ui.language` overrides the language of **runtime messages and this extension's diff --git a/mide-plugins/vscode/test/buildscript/api.test.ts b/mide-plugins/vscode/test/buildscript/api.test.ts index efab84d..82b8551 100644 --- a/mide-plugins/vscode/test/buildscript/api.test.ts +++ b/mide-plugins/vscode/test/buildscript/api.test.ts @@ -40,7 +40,8 @@ test("the snapshot loads with a usable shape", () => { assert.equal(typeof API.sourceCommit, "string"); assert.ok(API.protocolVersion >= 1); assert.ok(API.cacheEpoch >= 1); - assert.equal(API.directives.length, 31); + // mcpp 2026.10.29.1 answers 37 directives (31 when 0.9.0 was generated). + assert.equal(API.directives.length, 37); assert.equal(directives().length, apiJson.directives.length); }); diff --git a/mide-plugins/vscode/tools/generate-toml-schema.mjs b/mide-plugins/vscode/tools/generate-toml-schema.mjs index bfd4167..59226dd 100644 --- a/mide-plugins/vscode/tools/generate-toml-schema.mjs +++ b/mide-plugins/vscode/tools/generate-toml-schema.mjs @@ -126,6 +126,7 @@ const PLANE_LABELS = new Map([ ["条件", "condition"], ["产物元数据", "metadata"], ["生命周期", "lifecycle"], + ["平台", "platform"], ]); // Sections the plane table does not name. The table is the spec's own closure From 53b6d073b21d7d76095f774d3a33b576946a8c33 Mon Sep 17 00:00:00 2001 From: Sunrisepeak <speakshen@163.com> Date: Fri, 30 Oct 2026 01:57:31 +0900 Subject: [PATCH 075/119] mcpp-vscode: the backend locator's tests use the host's PATH conventions, so they hold on Windows Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mide-plugins/vscode/test/ide/locate.test.ts | 34 ++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/mide-plugins/vscode/test/ide/locate.test.ts b/mide-plugins/vscode/test/ide/locate.test.ts index 90950d8..4e66554 100644 --- a/mide-plugins/vscode/test/ide/locate.test.ts +++ b/mide-plugins/vscode/test/ide/locate.test.ts @@ -8,31 +8,37 @@ import { test } from "node:test"; import { locateBackend, okrunDir, onPath } from "../../src/ide/backend/locate"; import { mcpplsCandidates } from "../../src/ide/backend/mcppls"; +// PATH is this host's (its separator, its suffix), so these use the host's own system. +const host = process.platform; +const exe = host === "win32" ? ".exe" : ""; + test("the configured path wins: a program, or a bundle run by okrun", () => { const ext = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-ext-")); assert.deepEqual(locateBackend({ configured: "/x/mcpp-ide", extensionRoot: ext }), { program: "/x/mcpp-ide", args: [], source: "setting" }); const bin = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-okrun-")); - fs.writeFileSync(path.join(bin, "okrun"), ""); - assert.deepEqual(locateBackend({ configured: "/x/mcpp-ide.okb", extensionRoot: ext, platform: "linux", arch: "x64", env: { PATH: bin } }), - { program: path.join(bin, "okrun"), args: ["/x/mcpp-ide.okb"], source: "setting" }); + fs.writeFileSync(path.join(bin, `okrun${exe}`), ""); + assert.deepEqual(locateBackend({ configured: "/x/mcpp-ide.okb", extensionRoot: ext, platform: host, arch: "x64", env: { PATH: bin } }), + { program: path.join(bin, `okrun${exe}`), args: ["/x/mcpp-ide.okb"], source: "setting" }); }); test("the bundle runs under the okrun carried for this system, else okrun on PATH", () => { const ext = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-okb-")); - fs.mkdirSync(path.join(ext, "backend", "okrun", "linux-x86_64"), { recursive: true }); + const carried = okrunDir(host, "x64"); + fs.mkdirSync(path.join(ext, "backend", "okrun", carried), { recursive: true }); fs.writeFileSync(path.join(ext, "backend", "mcpp-ide.okb"), ""); - fs.writeFileSync(path.join(ext, "backend", "okrun", "linux-x86_64", "okrun"), ""); - const linux = locateBackend({ extensionRoot: ext, platform: "linux", arch: "x64", env: { PATH: "" }, home: "/nonexistent" }); - assert.deepEqual(linux, { program: path.join(ext, "backend", "okrun", "linux-x86_64", "okrun"), args: [path.join(ext, "backend", "mcpp-ide.okb")], source: "bundle" }); - // No okrun carried for macOS: okrun on PATH runs the bundle. + fs.writeFileSync(path.join(ext, "backend", "okrun", carried, `okrun${exe}`), ""); + const here = locateBackend({ extensionRoot: ext, platform: host, arch: "x64", env: { PATH: "" }, home: "/nonexistent" }); + assert.deepEqual(here, { program: path.join(ext, "backend", "okrun", carried, `okrun${exe}`), args: [path.join(ext, "backend", "mcpp-ide.okb")], source: "bundle" }); + // No okrun carried for the other architecture: okrun on PATH runs the bundle. const bin = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-okrun-")); - fs.writeFileSync(path.join(bin, "okrun"), ""); - const mac = locateBackend({ extensionRoot: ext, platform: "darwin", arch: "arm64", env: { PATH: bin }, home: "/nonexistent" }); - assert.deepEqual(mac?.args, [path.join(ext, "backend", "mcpp-ide.okb")]); - assert.equal(mac?.program, path.join(bin, "okrun")); + fs.writeFileSync(path.join(bin, `okrun${exe}`), ""); + const other = locateBackend({ extensionRoot: ext, platform: host, arch: "arm64", env: { PATH: bin }, home: "/nonexistent" }); + assert.deepEqual(other?.args, [path.join(ext, "backend", "mcpp-ide.okb")]); + assert.equal(other?.program, path.join(bin, `okrun${exe}`)); // No okrun at all: nothing in the package starts. - const native = locateBackend({ extensionRoot: ext, platform: "linux", arch: "arm64", env: { PATH: "" }, home: "/nonexistent" }); - assert.equal(native, undefined); + const none = locateBackend({ extensionRoot: ext, platform: host, arch: "arm64", env: { PATH: "" }, home: "/nonexistent" }); + assert.equal(none, undefined); + assert.equal(okrunDir("linux", "x64"), "linux-x86_64"); assert.equal(okrunDir("win32", "x64"), "windows-x86_64"); assert.equal(okrunDir("darwin", "arm64"), "macos-aarch64"); }); From 0ca145b11d689e735a84421cd99d91b4fc8dfecb Mon Sep 17 00:00:00 2001 From: Sunrisepeak <speakshen@163.com> Date: Fri, 30 Oct 2026 02:05:31 +0900 Subject: [PATCH 076/119] mcpp-clion: the trust, debug-session and debugger APIs that 2026.1 through 2026.3 share TrustedProjects.isProjectTrusted (com.intellij.ide.trustedProjects) for the deprecated, experimental isTrusted; XDebuggerManager.newSessionBuilder(...).environment(env).startSession() and its runContentDescriptor for the deprecated startSession and XDebugSession.getRunContentDescriptor; createDriverConfiguration(project, CPPEnvironment(toolchain)) -- the debugger the toolchain names, as CLion creates it, GDB, LLDB or a configured debug adapter -- for Toolchain.getDebuggerKind(), which 2026.3 schedules for removal. Checked against the 2026.1.5.1 and 263.7307.35 jars. plugin.xml says where the test tree lives in each version. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../mcppcommunity/mcppclion/McppSession.kt | 8 ++++---- .../mcppclion/run/McppDebugRunner.kt | 16 +++++++++------- .../clion/src/main/resources/META-INF/plugin.xml | 5 +++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index fce8163..1b7770b 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -7,7 +7,7 @@ import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import com.intellij.DynamicBundle -import com.intellij.ide.impl.isTrusted +import com.intellij.ide.trustedProjects.TrustedProjects import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.Disposable @@ -79,7 +79,7 @@ class McppSession(private val project: Project) : Disposable { started?.let { if (backend?.running == true || !it.isDone) return it } val future = CompletableFuture<JsonObject>() started = future - val configured = if (project.isTrusted()) McppSettings.get().string("mcpp.ide.backendPath") else "" + val configured = if (TrustedProjects.isProjectTrusted(project)) McppSettings.get().string("mcpp.ide.backendPath") else "" val located = Locate.backend(configured, pluginRoot()) if (located == null) { notify(t("The mcpp-ide backend was not found. Install it with `xlings install mcpp-ide`, or set mcpp.ide.backendPath."), NotificationType.ERROR) @@ -97,7 +97,7 @@ class McppSession(private val project: Project) : Disposable { add("client", JsonObject().apply { addProperty("name", ApplicationInfo.getInstance().versionName); addProperty("version", ApplicationInfo.getInstance().fullVersion) }) addProperty("editor", "jetbrains") addProperty("locale", DynamicBundle.getLocale().toLanguageTag()) - addProperty("trusted", project.isTrusted()) + addProperty("trusted", TrustedProjects.isProjectTrusted(project)) add("settings", McppSettings.get().values()) add("workspaceFolders", JsonArray().apply { project.basePath?.let { add(it) } }) add("languageService", JsonObject().apply { add("candidates", mcpplsCandidates()) }) @@ -158,7 +158,7 @@ class McppSession(private val project: Project) : Disposable { val b = backend ?: return b.request("settings/update", JsonObject().apply { add("settings", McppSettings.get().values()) - addProperty("trusted", project.isTrusted()) + addProperty("trusted", TrustedProjects.isProjectTrusted(project)) addProperty("locale", DynamicBundle.getLocale().toLanguageTag()) }) } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppDebugRunner.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppDebugRunner.kt index 6ea6daf..f82dcf3 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppDebugRunner.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppDebugRunner.kt @@ -19,9 +19,9 @@ import com.intellij.xdebugger.XDebugProcess import com.intellij.xdebugger.XDebugProcessStarter import com.intellij.xdebugger.XDebugSession import com.intellij.xdebugger.XDebuggerManager -import com.jetbrains.cidr.cpp.execution.debugger.backend.CLionGDBDriverConfiguration -import com.jetbrains.cidr.cpp.execution.debugger.backend.CLionLLDBDriverConfiguration +import com.jetbrains.cidr.cpp.toolchains.CPPEnvironment import com.jetbrains.cidr.cpp.toolchains.CPPToolchains +import com.jetbrains.cidr.cpp.toolchains.createDriverConfiguration import com.jetbrains.cidr.execution.TrivialRunParameters import com.jetbrains.cidr.execution.debugger.CidrLocalDebugProcess import io.github.mcppcommunity.mcppclion.McppSession @@ -40,14 +40,16 @@ class McppDebugRunner : GenericProgramRunner<RunnerSettings>() { ?: throw ExecutionException(session.t("No toolchain is set up in Settings | Build, Execution, Deployment | Toolchains; CLion's debugger comes from one.")) val launch = ProgressManager.getInstance().runProcessWithProgressSynchronously<com.google.gson.JsonObject, Exception>( { launch(c).get() }, session.t("mcpp: building for the debugger…"), true, project) - val lldb = toolchain.debuggerKind.isLLDB() - if (!launch["via"].isJsonNull && lldb) + // The debugger the toolchain names (GDB, LLDB, or a debug adapter configured in CLion), as CLion + // itself creates it. + val driver = createDriverConfiguration(project, CPPEnvironment(toolchain)) + // Said for either debugger: the sentence names what each one needs. + if (!launch["via"].isJsonNull) session.notify(session.t("This program runs through {0}. LLDB reads its symbols once `settings set plugin.jit-loader.gdb.enable on` is in ~/.lldbinit; GDB reads them as it is.", launch["via"].asString), NotificationType.INFORMATION) - val driver = if (lldb) CLionLLDBDriverConfiguration(project, toolchain) else CLionGDBDriverConfiguration(project, toolchain) val parameters = TrivialRunParameters(driver, commandLine(launch, pty = false)) - return XDebuggerManager.getInstance(project).startSession(env, object : XDebugProcessStarter() { + return XDebuggerManager.getInstance(project).newSessionBuilder(object : XDebugProcessStarter() { override fun start(xs: XDebugSession): XDebugProcess = CidrLocalDebugProcess(parameters, xs, TextConsoleBuilderFactory.getInstance().createBuilder(project)) - }).runContentDescriptor + }).environment(env).startSession().runContentDescriptor } } diff --git a/mide-plugins/clion/src/main/resources/META-INF/plugin.xml b/mide-plugins/clion/src/main/resources/META-INF/plugin.xml index a5ae59b..b697e43 100644 --- a/mide-plugins/clion/src/main/resources/META-INF/plugin.xml +++ b/mide-plugins/clion/src/main/resources/META-INF/plugin.xml @@ -21,8 +21,9 @@ <depends>com.intellij.modules.clion</depends> <depends>com.intellij.modules.cidr.debugger</depends> <depends>io.github.sunrisepeak.mcppls</depends> - <!-- Since 2026.1 the panels' browser and the test tree are plugins of their own; in 2025.2 they are - the platform's. Optional, so the classes are found either way. --> + <!-- The panels' browser is a module of its own (com.intellij.modules.jcef). The test tree is the + platform's in CLion 2026.1 and a plugin of its own from 2026.2 on (intellij.testRunner.plugin); + optional, so the classes are found either way. --> <depends optional="true" config-file="mcpp-jcef.xml">com.intellij.modules.jcef</depends> <depends optional="true" config-file="mcpp-testrunner.xml">intellij.testRunner.plugin</depends> From f0ef33e28b0abca6def1fd572df5bcfefab90c0f Mon Sep 17 00:00:00 2001 From: Sunrisepeak <speakshen@163.com> Date: Fri, 30 Oct 2026 02:12:43 +0900 Subject: [PATCH 077/119] mcpp-vscode: mcpp through the backend's mcpp/exec, mcppls through its languageService/exec, nothing else Only mcpp's commands go to mcpp/exec (runMcppCommand, behind runMcpp and the controller); the module graph's `mcppls query module` goes to the new languageService/exec, which runs the mcppls the backend found -- a payload's runner and image, as mcppls 0.0.18 and later ship, or a program. 0.9.0 looked for payload/bin/mcppls, which those payloads no longer carry, and fell back to PATH. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mide-plugins/vscode/package-lock.json | 485 ++++++++++++++++++ mide-plugins/vscode/package.json | 1 + mide-plugins/vscode/src/cli/controller.ts | 10 +- mide-plugins/vscode/src/cli/process.ts | 35 +- mide-plugins/vscode/src/extension.ts | 17 +- .../src/projects/dependencyGraphPanel.ts | 7 +- mide-plugins/vscode/test/artifacts.test.ts | 2 +- protocol/protocol.schema.json | 24 + src/server.cppm | 41 +- tests/test_protocol.cpp | 4 + 10 files changed, 604 insertions(+), 22 deletions(-) diff --git a/mide-plugins/vscode/package-lock.json b/mide-plugins/vscode/package-lock.json index 5be3e31..b01f1c5 100644 --- a/mide-plugins/vscode/package-lock.json +++ b/mide-plugins/vscode/package-lock.json @@ -16,6 +16,7 @@ "@vscode/codicons": "0.0.46-24", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "esbuild": "^0.25.12", "mocha": "^11.8.0", "ovsx": "^1.2.0", "playwright": "1.64.1", @@ -235,6 +236,448 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", @@ -2836,6 +3279,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json index 2c2d3b6..9a0c32d 100644 --- a/mide-plugins/vscode/package.json +++ b/mide-plugins/vscode/package.json @@ -1374,6 +1374,7 @@ "@vscode/codicons": "0.0.46-24", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "esbuild": "^0.25.12", "mocha": "^11.8.0", "ovsx": "^1.2.0", "playwright": "1.64.1", diff --git a/mide-plugins/vscode/src/cli/controller.ts b/mide-plugins/vscode/src/cli/controller.ts index 5276691..244cf32 100644 --- a/mide-plugins/vscode/src/cli/controller.ts +++ b/mide-plugins/vscode/src/cli/controller.ts @@ -21,7 +21,7 @@ import { } from "../projects/discovery"; import { updateEditorTitleButtonsContext } from "../projects/context"; import { createLogger, type Logger } from "../util/log"; -import { runProcess } from "./process"; +import { runMcppCommand } from "./process"; import { McppOperationRegistry, classifyTaskExit, @@ -582,7 +582,7 @@ export class McppCliController { try { const args = mcppCommandArguments("toolchain", "default", picked.spec); - const result = await runProcess( + const result = await runMcppCommand( this.mcppExecutable(project), args, workingDirectory(project), @@ -608,7 +608,7 @@ export class McppCliController { ); if (buildChoice === warningLabels.cleanAndBuild && project !== undefined) { const cleanArgs = mcppCommandArguments("clean"); - const cleanResult = await runProcess(this.mcppExecutable(project), cleanArgs, project.root); + const cleanResult = await runMcppCommand(this.mcppExecutable(project), cleanArgs, project.root); this.appendShortCommand(t("Clean old artifacts"), this.mcppExecutable(project), cleanArgs, cleanResult); await this.runProjectTask("build"); } @@ -821,7 +821,7 @@ export class McppCliController { run: async (name, cwd) => { const executable = this.mcppExecutable(undefined); const args = mcppCommandArguments("new", name); - const result = await runProcess(executable, args, cwd); + const result = await runMcppCommand(executable, args, cwd); this.appendShortCommand(t("New project"), executable, args, result); return result.exitCode; }, @@ -885,7 +885,7 @@ export class McppCliController { ): Promise<ToolchainInventory | undefined> { const executable = this.mcppExecutable(project); const args = mcppCommandArguments("toolchain", "list"); - const result = await runProcess(executable, args, workingDirectory(project)); + const result = await runMcppCommand(executable, args, workingDirectory(project)); this.appendShortCommand(t("Inspect the toolchain"), executable, args, result); if (result.exitCode !== 0) { this.reportCommandFailure( diff --git a/mide-plugins/vscode/src/cli/process.ts b/mide-plugins/vscode/src/cli/process.ts index 3da740d..78eb175 100644 --- a/mide-plugins/vscode/src/cli/process.ts +++ b/mide-plugins/vscode/src/cli/process.ts @@ -54,26 +54,45 @@ export function quoteWindowsArgument(argument: string): string { } /** - * Who runs mcpp. With a backend installed (`useMcppExecutor`, from 1.1.0 the mcpp-ide session), - * every query and write below is one `mcpp/exec` of the backend, which runs mcpp as it runs every - * command — the mcpp it located, the session's client identity, the effects an untrusted workspace - * denies — and the output reaches the same parsers it reached when the extension ran mcpp itself. + * Who runs mcpp and mcppls. With a backend installed (from 1.1.0 the mcpp-ide session), every mcpp + * command below is one `mcpp/exec` of the backend, which runs mcpp as it runs every command — the + * mcpp it located, the session's client identity, the effects an untrusted workspace denies — and + * every mcppls command one `languageService/exec`, which runs the mcppls the backend found (a + * payload's runner and image, or a program). The output reaches the same parsers it reached when + * the extension ran the programs itself. Any other program still runs here (`runProcess`). */ let mcppExecutor: ProcessRunner | undefined; +let mcpplsExecutor: ((args: string[], cwd?: string, options?: ProcessRunOptions) => Promise<ProcessResult>) | undefined; export function useMcppExecutor(executor: ProcessRunner | undefined): void { mcppExecutor = executor; } +export function useMcpplsExecutor(executor: typeof mcpplsExecutor): void { + mcpplsExecutor = executor; +} + +/** Runs mcpp: through the backend when one is installed, else as `executable`. No trust check. */ +export function runMcppCommand( + executable: string, + args: string[], + cwd?: string, + options: ProcessRunOptions = {}, +): Promise<ProcessResult> { + return mcppExecutor !== undefined ? mcppExecutor(executable, args, cwd, options) : runProcess(executable, args, cwd, options); +} + +/** Runs mcppls: through the backend when one is installed, else `mcppls` from PATH. */ +export function runMcppls(args: string[], cwd?: string, options: ProcessRunOptions = {}): Promise<ProcessResult> { + return mcpplsExecutor !== undefined ? mcpplsExecutor(args, cwd, options) : runProcess("mcppls", args, cwd, options); +} + export async function runProcess( executable: string, args: string[], cwd?: string, options: ProcessRunOptions = {}, ): Promise<ProcessResult> { - if (mcppExecutor !== undefined) { - return mcppExecutor(executable, args, cwd, options); - } try { const needsShell = spawnNeedsShell(executable); const result = await execFileAsync( @@ -133,5 +152,5 @@ export async function runMcpp( if (!trusted) { return undefined; } - return runProcess(executable, [...args], cwd, options); + return runMcppCommand(executable, [...args], cwd, options); } diff --git a/mide-plugins/vscode/src/extension.ts b/mide-plugins/vscode/src/extension.ts index b66885b..66451e2 100644 --- a/mide-plugins/vscode/src/extension.ts +++ b/mide-plugins/vscode/src/extension.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import { McppCliController, discoveryBoundaryFromSettings } from "./cli/controller"; -import { runMcpp, useMcppExecutor } from "./cli/process"; +import { runMcpp, useMcppExecutor, useMcpplsExecutor } from "./cli/process"; import { parseProtocolInfo } from "./cli/protocol"; import { buildSelfCheckText } from "./cli/selfCheck"; import { CLI_COMMANDS, LEGACY_LANGUAGE_SERVER_COMMANDS, LIBRARY_COMMANDS, TOOL_COMMANDS } from "./commands/ids"; @@ -332,7 +332,20 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi return { exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error) }; } }); - extensionContext.subscriptions.push({ dispose: () => useMcppExecutor(undefined) }); + useMcpplsExecutor(async (args, cwd, options = {}) => { + try { + const answer = await session.request<{ exitCode: number; stdout: string; stderr: string; timedOut: boolean }>("languageService/exec", { + args, + ...(cwd ? { cwd } : {}), + ...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}), + maxOutputBytes: Math.max(1, options.maxBufferMiB ?? 16) * 1024 * 1024, + }); + return { exitCode: answer.timedOut && answer.exitCode === 0 ? 1 : answer.exitCode, stdout: answer.stdout, stderr: answer.stderr }; + } catch (error) { + return { exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error) }; + } + }); + extensionContext.subscriptions.push({ dispose: () => { useMcppExecutor(undefined); useMcpplsExecutor(undefined); } }); extensionContext.subscriptions.push( session, jobs, diff --git a/mide-plugins/vscode/src/projects/dependencyGraphPanel.ts b/mide-plugins/vscode/src/projects/dependencyGraphPanel.ts index ec5a26e..f5711b2 100644 --- a/mide-plugins/vscode/src/projects/dependencyGraphPanel.ts +++ b/mide-plugins/vscode/src/projects/dependencyGraphPanel.ts @@ -5,6 +5,7 @@ import * as vscode from "vscode"; import { TOOL_COMMANDS } from "../commands/ids"; import { t } from "../i18n/t"; import { queryModuleGraph } from "../cli/moduleGraph"; +import { runMcppls } from "../cli/process"; import { cachedDescriptorText, loadSnapshot } from "../library/indexLocator"; import { descriptorDependencies, lockPackageVersions } from "../library/indexModel"; import type { ProjectSummary } from "../views/models"; @@ -78,10 +79,8 @@ export function registerDependencyGraph(context: vscode.ExtensionContext, projec view.name = readProjectSummary(view.root, manifest.split(/\r?\n/)).name ?? path.basename(view.root); let graph: VisualGraph; if (view.mode === "modules") { - const extension = vscode.extensions.getExtension("sunrisepeak.mcpp-language-server"); - const bundled = extension ? path.join(extension.extensionPath, "payload", "bin", process.platform === "win32" ? "mcppls.exe" : "mcppls") : undefined; - const executable = bundled && await exists(bundled) ? bundled : "mcppls"; - const raw = await queryModuleGraph(vscode.workspace.isTrusted, executable, view.root, undefined, signal); + // mcppls as the backend found it: the payload's runner and image (mcppls 0.0.18 on), or a program. + const raw = await queryModuleGraph(vscode.workspace.isTrusted, "mcppls", view.root, (_executable, args, cwd, options) => runMcppls(args, cwd, options), signal); const patterns = testDiscovery(manifest); const discovered = new Set<string>(); let discoveryLimited = false; for (const pattern of new Set(patterns.filter(pattern => !pattern.startsWith("!")).map(testCandidateGlob))) { diff --git a/mide-plugins/vscode/test/artifacts.test.ts b/mide-plugins/vscode/test/artifacts.test.ts index 4622c4a..9fd7777 100644 --- a/mide-plugins/vscode/test/artifacts.test.ts +++ b/mide-plugins/vscode/test/artifacts.test.ts @@ -528,7 +528,7 @@ test("新建工程先校验目标路径再确认创建,成功后只打开不 const flow = method.indexOf("runNewProjectFlow"); const exists = method.indexOf("existsSync", flow); const confirm = method.indexOf("showWarningMessage", flow); - const create = method.indexOf("runProcess", flow); + const create = method.indexOf("runMcppCommand", flow); const open = method.indexOf('executeCommand("vscode.openFolder"', flow); assert.ok(exists >= 0 && exists < confirm); assert.ok(confirm >= 0 && confirm < create); diff --git a/protocol/protocol.schema.json b/protocol/protocol.schema.json index e3203d6..7f198d5 100644 --- a/protocol/protocol.schema.json +++ b/protocol/protocol.schema.json @@ -293,6 +293,30 @@ } } }, + "languageService/exec": { + "description": "One command of mcppls (`query module`, for the module graph), run as the command languageService/status found -- a payload's runner and image, or a program -- ended past timeoutMs (default 120000) and keeping at most maxOutputBytes (default 16 MiB) of each output.", + "params": { + "type": "object", + "required": ["args"], + "properties": { + "args": { "type": "array", "items": { "type": "string" } }, + "cwd": { "type": "string" }, + "timeoutMs": { "type": "integer", "minimum": 1 }, + "maxOutputBytes": { "type": "integer", "minimum": 1 } + } + }, + "result": { + "type": "object", + "required": ["exitCode", "stdout", "stderr", "timedOut", "truncated"], + "properties": { + "exitCode": { "type": "integer" }, + "stdout": { "type": "string" }, + "stderr": { "type": "string" }, + "timedOut": { "type": "boolean" }, + "truncated": { "type": "boolean" } + } + } + }, "project/files": { "description": "What an editor reads of a project to describe it: the text of mcpp.toml and mcpp.lock (null when absent) and the number of source files under the root, counted as mcpp-vscode 0.9.0 counted them.", "params": { "type": "object", "required": ["root"], "properties": { "root": { "type": "string" } } }, diff --git a/src/server.cppm b/src/server.cppm index c320875..4138abe 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -36,7 +36,7 @@ inline constexpr int kProtocol = 1; // The methods `install` registers, for the protocol schema's test: a method // here and not in protocol/protocol.schema.json (or the other way) fails it. -inline constexpr std::array<std::string_view, 44> kMethods = { +inline constexpr std::array<std::string_view, 45> kMethods = { "initialize", "settings/update", "settings/registry", "settings/preset", "i18n/bundle", "selfCheck", "commands/list", "menu/quick", "languageService/status", "projects/list", "projects/forFile", "project/summary", "project/new", "result/last", @@ -45,7 +45,7 @@ inline constexpr std::array<std::string_view, 44> kMethods = { "toolchains/list", "toolchains/install", "toolchains/setDefault", "targets/list", "cache/inventory", "cache/plan", "cache/apply", "graph/packages", "graph/modules", "pack/formats", "pack/start", "manifest/check", "toml/complete", "toml/hover", "toml/symbols", - "buildscript/complete", "mcpp/exec", "project/files", "shutdown", "exit"}; + "buildscript/complete", "mcpp/exec", "project/files", "languageService/exec", "shutdown", "exit"}; // The notifications the backend sends. inline constexpr std::array<std::string_view, 13> kNotifications = { @@ -75,6 +75,7 @@ private: Result graph(const Json& p, bool modules); Result project_summary(const Json& p); Result mcpp_exec(const Json& p); + Result language_service_exec(const Json& p); Result project_new(const Json& p); Result manifest_check(const Json& p); Result language_service(const Json& p); @@ -366,6 +367,41 @@ std::expected<Json, rpc::Error> Session::mcpp_exec(const Json& p) { {"truncated", r->truncated}}; } +// One command of mcppls (`query module`, for the module graph), run as the command languageService/status +// found: a payload's runner and image, or a program. Bounded as mcpp/exec is. +std::expected<Json, rpc::Error> Session::language_service_exec(const Json& p) { + if (!p.contains("args") || !p["args"].is_array()) return std::unexpected(rpc::Error{rpc::INVALID_PARAMS, "args: an array of strings"}); + std::vector<std::string> candidates; + { + std::lock_guard lock(mutex_); + candidates = mcpplsCandidates_; + } + const auto found = languageservice::probe(candidates); + if (found.command.empty()) + return std::unexpected(rpc::Error{rpc::REQUEST_FAILED, t("mcppls was not found: C++ files have no completion, navigation or diagnostics from mcpp's build. Install the C++ Modules language server ({0} or later).", + {std::string(languageservice::kRequired)})}); + mcpplibs::os::process::spawn_options o; + o.program = found.command.front(); + for (std::size_t i = 1; i < found.command.size(); ++i) o.args.push_back(found.command[i].string()); + for (const auto& a : p["args"]) { + if (!a.is_string()) return std::unexpected(rpc::Error{rpc::INVALID_PARAMS, "args: an array of strings"}); + o.args.push_back(a.get<std::string>()); + } + o.in = mcpplibs::os::process::stdio::null; + if (p.contains("cwd") && p["cwd"].is_string()) o.cwd = p["cwd"].get<std::string>(); + mcpplibs::os::process::bounds limits; + limits.hard = std::chrono::milliseconds(p.contains("timeoutMs") && p["timeoutMs"].is_number_integer() && p["timeoutMs"].get<std::int64_t>() > 0 + ? p["timeoutMs"].get<std::int64_t>() : std::int64_t{120000}); + const auto limit = p.contains("maxOutputBytes") && p["maxOutputBytes"].is_number_integer() && p["maxOutputBytes"].get<std::int64_t>() > 0 + ? static_cast<std::size_t>(p["maxOutputBytes"].get<std::int64_t>()) : std::size_t{16} << 20; + limits.out_limit = limit; + limits.err_limit = limit; + auto r = mcpplibs::os::process::run(o, limits); + if (!r) return std::unexpected(rpc::Error{rpc::REQUEST_FAILED, r.error().message()}); + return Json{{"exitCode", r->status.shell_code()}, {"stdout", r->out}, {"stderr", r->err}, {"timedOut", r->timed_out}, + {"truncated", r->out_truncated || r->err_truncated}}; +} + std::expected<Json, rpc::Error> Session::build_database(const std::filesystem::path& root) { { std::lock_guard lock(mutex_); @@ -1017,6 +1053,7 @@ void Session::install() { }); s.on("project/summary", [this](const Json& p, const rpc::Context&) { return project_summary(p); }); s.on("mcpp/exec", [this](const Json& p, const rpc::Context&) { return mcpp_exec(p); }); + s.on("languageService/exec", [this](const Json& p, const rpc::Context&) { return language_service_exec(p); }); s.on("project/files", [](const Json& p, const rpc::Context&) -> Result { return projects::files(root_of(p)); }); s.on("project/new", [this](const Json& p, const rpc::Context&) { return project_new(p); }); s.on("result/last", [](const Json& p, const rpc::Context&) -> Result { return projects::last_result(root_of(p)); }); diff --git a/tests/test_protocol.cpp b/tests/test_protocol.cpp index d8052f6..84bce97 100644 --- a/tests/test_protocol.cpp +++ b/tests/test_protocol.cpp @@ -173,6 +173,10 @@ int main() { auto bounded = result_of("mcpp/exec", Json{{"args", Json::array({"--version"})}, {"maxOutputBytes", 3}}); CHECK(bounded["truncated"] == true && bounded["stdout"].get<std::string>().size() <= 3); CHECK(wire.call("mcpp/exec", Json{{"args", "--version"}}).contains("error")); + // mcppls's own commands run as the command the status found (here a program that answers --version). + result_of("languageService/status", Json{{"candidates", Json::array({mock.generic_string()})}}); + auto ls = result_of("languageService/exec", Json{{"args", Json::array({"--version"})}}); + CHECK(ls["exitCode"] == 0 && !ls["stdout"].get<std::string>().empty()); std::ofstream(root / "src" / "main.cpp") << "int main() {}\n"; std::ofstream(root / "src" / "notes.txt") << "not a source\n"; std::filesystem::create_directories(root / "target" / "obj"); From 237c8df80bed684629868e43047a8f989d1a8119 Mon Sep 17 00:00:00 2001 From: Sunrisepeak <speakshen@163.com> Date: Fri, 30 Oct 2026 02:33:21 +0900 Subject: [PATCH 078/119] mcpp-vscode: the pages are registered by one function, registerPages, that the extension and the workbench call The project tree, the libraries and a package's page, the cache, the settings page and the graphs are registered by src/pages.ts from what the extension activates; finding the current project moves to src/projects/current.ts. The extension's behaviour is unchanged. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../mcppclion/webui/WebuiPanel.kt | 182 ---------------- mide-plugins/vscode/src/cli/controller.ts | 10 +- mide-plugins/vscode/src/extension.ts | 198 +----------------- mide-plugins/vscode/src/pages.ts | 185 ++++++++++++++++ mide-plugins/vscode/src/projects/current.ts | 44 ++++ webui/index.html | 26 --- webui/js/app.js | 22 -- webui/js/core.js | 84 -------- webui/js/graph-layout.js | 119 ----------- webui/js/host.js | 62 ------ webui/js/panels/cache.js | 72 ------- webui/js/panels/graph.js | 113 ---------- webui/js/panels/libraries.js | 87 -------- webui/js/panels/pack.js | 67 ------ webui/js/panels/project.js | 92 -------- webui/js/panels/settings.js | 89 -------- webui/js/panels/targets.js | 63 ------ webui/js/types.d.ts | 43 ---- webui/styles.css | 71 ------- webui/tests/layout.test.js | 41 ---- webui/tests/load.js | 16 -- webui/tests/panels.test.js | 119 ----------- webui/tsconfig.json | 14 -- 23 files changed, 242 insertions(+), 1577 deletions(-) delete mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/WebuiPanel.kt create mode 100644 mide-plugins/vscode/src/pages.ts create mode 100644 mide-plugins/vscode/src/projects/current.ts delete mode 100644 webui/index.html delete mode 100644 webui/js/app.js delete mode 100644 webui/js/core.js delete mode 100644 webui/js/graph-layout.js delete mode 100644 webui/js/host.js delete mode 100644 webui/js/panels/cache.js delete mode 100644 webui/js/panels/graph.js delete mode 100644 webui/js/panels/libraries.js delete mode 100644 webui/js/panels/pack.js delete mode 100644 webui/js/panels/project.js delete mode 100644 webui/js/panels/settings.js delete mode 100644 webui/js/panels/targets.js delete mode 100644 webui/js/types.d.ts delete mode 100644 webui/styles.css delete mode 100644 webui/tests/layout.test.js delete mode 100644 webui/tests/load.js delete mode 100644 webui/tests/panels.test.js delete mode 100644 webui/tsconfig.json diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/WebuiPanel.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/WebuiPanel.kt deleted file mode 100644 index b2b7061..0000000 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/WebuiPanel.kt +++ /dev/null @@ -1,182 +0,0 @@ -// The shared panels (webui/) in a JCEF browser. The page speaks the backend's protocol through -// this host -- `window.mcppHostSend` (a JBCefJSQuery) out, `window.mcppHostReceive` in -- and asks -// it for what only the IDE does: open a file, confirm, store a setting, run a command. -package io.github.mcppcommunity.mcppclion.webui - -import com.google.gson.Gson -import com.google.gson.JsonElement -import com.google.gson.JsonNull -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import com.intellij.ide.BrowserUtil -import com.intellij.ide.actions.RevealFileAction -import com.intellij.notification.NotificationType -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.PathManager -import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.ide.CopyPasteManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.ui.JBColor -import com.intellij.ui.jcef.JBCefApp -import com.intellij.ui.jcef.JBCefBrowser -import com.intellij.ui.jcef.JBCefBrowserBase -import com.intellij.ui.jcef.JBCefJSQuery -import com.intellij.util.ui.UIUtil -import io.github.mcppcommunity.mcppclion.McppSession -import io.github.mcppcommunity.mcppclion.actions.Dispatch -import io.github.mcppcommunity.mcppclion.settings.McppSettings -import org.cef.browser.CefBrowser -import org.cef.browser.CefFrame -import org.cef.handler.CefLoadHandlerAdapter -import java.awt.Color -import java.awt.datatransfer.StringSelection -import java.io.File -import javax.swing.JComponent -import javax.swing.JLabel -import javax.swing.SwingConstants - -class WebuiPanel(private val project: Project, private val panel: String, private val params: JsonObject, parent: Disposable) : Disposable { - private val session = McppSession.get(project) - private val browser: JBCefBrowser? = if (JBCefApp.isSupported()) JBCefBrowser() else null - private val query: JBCefJSQuery? = browser?.let { JBCefJSQuery.create(it as JBCefBrowserBase) } - private val unsubscribe: () -> Unit - - val component: JComponent = browser?.component ?: JLabel(session.t("This IDE runs without JCEF; the mcpp panels need it."), SwingConstants.CENTER) - - init { - Disposer.register(parent, this) - unsubscribe = session.on("*") { message -> post(JsonObject().apply { addProperty("type", "notify"); add("method", message.asJsonObject["method"]); add("params", message.asJsonObject["params"]) }) } - if (browser != null && query != null) { - query.addHandler { text -> receive(text); null } - browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { - override fun onLoadEnd(b: CefBrowser, frame: CefFrame, status: Int) { - if (!frame.isMain) return - b.executeJavaScript("window.mcppHostSend = function (text) { ${query.inject("text")} }; window.mcppHostAttached && window.mcppHostAttached();", b.url, 0) - } - }, browser.cefBrowser) - browser.loadURL(page().toURI().toString()) - } - } - - // The page and its scripts, written once per plugin version where the browser can load them. - private fun page(): File { - // One directory per build of the plugin: its jar's name and time identify it. - val jar = runCatching { File(WebuiPanel::class.java.protectionDomain.codeSource.location.toURI()) }.getOrNull() - val version = jar?.takeIf { it.isFile }?.let { "${it.nameWithoutExtension}-${it.lastModified()}" } ?: "dev" - val dir = File(PathManager.getSystemPath(), "mcpp-clion/webui-$version") - val index = javaClass.getResource("/webui/index.html")?.readText() ?: error("webui/index.html is not in the plugin") - val files = Regex("\\{\\{base}}([\\w./-]+)").findAll(index).map { it.groupValues[1] }.toSet() - for (f in files) { - val target = File(dir, f) - if (target.isFile && version != "dev") continue - target.parentFile.mkdirs() - javaClass.getResourceAsStream("/webui/$f")?.use { input -> target.outputStream().use { input.copyTo(it) } } - } - val html = File(dir, "panel.html") - html.writeText(index.replace("{{base}}", "").replace("{{csp}}", "default-src 'self' 'unsafe-inline' file:") - .replace("{{nonce}}", "").replace("{{lang}}", com.intellij.DynamicBundle.getLocale().language)) - return html - } - - private fun css(c: Color) = "rgb(${c.red}, ${c.green}, ${c.blue})" - - private fun theme(): JsonObject = JsonObject().apply { - addProperty("--mcpp-fg", css(UIUtil.getLabelForeground())) - addProperty("--mcpp-bg", css(UIUtil.getPanelBackground())) - addProperty("--mcpp-muted", css(UIUtil.getContextHelpForeground())) - addProperty("--mcpp-border", css(JBColor.border())) - addProperty("--mcpp-card", css(UIUtil.getListBackground())) - addProperty("--mcpp-input", css(UIUtil.getTextFieldBackground())) - addProperty("--mcpp-accent", css(JBColor.namedColor("Button.default.startBackground", JBColor(0x3574F0, 0x3574F0)))) - addProperty("--mcpp-secondary", css(JBColor.namedColor("Button.startBackground", JBColor(0xEBECF0, 0x3C3F41)))) - addProperty("--mcpp-secondary-fg", css(UIUtil.getLabelForeground())) - addProperty("--mcpp-font", "\"${UIUtil.getLabelFont().family}\", system-ui, sans-serif") - addProperty("--mcpp-font-size", "${UIUtil.getLabelFont().size}px") - } - - private fun post(message: JsonObject) { - val b = browser ?: return - val js = "window.mcppHostReceive && window.mcppHostReceive(${message});" - ApplicationManager.getApplication().invokeLater { b.cefBrowser.executeJavaScript(js, b.cefBrowser.url, 0) } - } - - private fun receive(text: String) { - val m = runCatching { JsonParser.parseString(text).asJsonObject }.getOrNull() ?: return - when (m["type"]?.asString) { - "ready" -> post(JsonObject().apply { - addProperty("type", "init"); addProperty("panel", panel); add("params", params) - add("bundle", Gson().toJsonTree(session.strings)); add("theme", theme()); addProperty("editor", "jetbrains") - }) - "rpc" -> session.request(m["method"].asString, m["params"] ?: JsonObject()).whenComplete { result, error -> - if (error != null) post(JsonObject().apply { - addProperty("type", "rpcError"); add("id", m["id"]) - add("error", JsonObject().apply { addProperty("message", (error.cause ?: error).message ?: "failed") }) - }) - else post(JsonObject().apply { addProperty("type", "rpcResult"); add("id", m["id"]); add("result", result) }) - } - "host" -> ApplicationManager.getApplication().invokeLater { - val result = runCatching { host(m["action"].asString, m["params"]?.asJsonObject ?: JsonObject()) } - post(JsonObject().apply { - add("id", m["id"]) - result.onSuccess { addProperty("type", "hostResult"); add("result", it) } - result.onFailure { e -> addProperty("type", "hostError"); add("error", JsonObject().apply { addProperty("message", e.message ?: "failed") }) } - }) - } - } - } - - private fun host(action: String, p: JsonObject): JsonElement { - val settings = McppSettings.get() - return when (action) { - "settings" -> settings.values() - "setSetting" -> { settings.set(p["key"].asString, p["value"]); JsonParser.parseString("true") } - "resetSettings" -> { p.getAsJsonArray("keys").forEach { settings.set(it.asString, null) }; JsonParser.parseString("true") } - "migrateSettings" -> { - p.getAsJsonArray("renames").forEach { r -> - val o = r.asJsonObject - settings.get(o["from"].asString)?.let { v -> settings.set(o["to"].asString, v) } - } - JsonParser.parseString("true") - } - "openFile" -> { - val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(p["path"].asString) - if (file != null) OpenFileDescriptor(project, file, maxOf(0, (p["line"]?.asInt ?: 1) - 1), 0).navigate(true) - JsonParser.parseString((file != null).toString()) - } - "openExternal" -> { BrowserUtil.browse(p["url"].asString); JsonNull.INSTANCE } - "reveal" -> { RevealFileAction.openFile(File(p["path"].asString)); JsonNull.INSTANCE } - "copy" -> { CopyPasteManager.getInstance().setContents(StringSelection(p["text"].asString)); JsonNull.INSTANCE } - "notify" -> { - val type = when (p["level"]?.asString) { "error" -> NotificationType.ERROR; "warning" -> NotificationType.WARNING; else -> NotificationType.INFORMATION } - session.notify(p["message"].asString, type); JsonNull.INSTANCE - } - "openPanel" -> { - val rest = p.deepCopy().apply { remove("panel") } - McppPanels.show(project, p["panel"].asString, rest); JsonNull.INSTANCE - } - "runCommand" -> { Dispatch.run(project, p["command"].asString, p); JsonNull.INSTANCE } - "confirm" -> { - val detail = listOfNotNull(p["detail"]?.asString, p["preview"]?.asString?.takeIf { it.isNotBlank() }).joinToString("\n\n") - val yes = Messages.showOkCancelDialog(project, detail, p["title"].asString, session.t("Continue"), Messages.getCancelButton(), Messages.getWarningIcon()) == Messages.OK - var acknowledged = false - if (yes && p["acknowledge"]?.asBoolean == true) { - val ack = session.t("I understand this affects every mcpp project on this machine") - acknowledged = Messages.showOkCancelDialog(project, ack, p["title"].asString, ack, Messages.getCancelButton(), Messages.getWarningIcon()) == Messages.OK - } - JsonObject().apply { addProperty("confirmed", yes && (acknowledged || p["acknowledge"]?.asBoolean != true)); addProperty("acknowledged", acknowledged) } - } - "languageService" -> { Dispatch.languageServiceAction(project, p["action"]?.asString ?: "install"); JsonNull.INSTANCE } - else -> error("unknown host action $action") - } - } - - override fun dispose() { - unsubscribe() - query?.let { Disposer.dispose(it) } - browser?.let { Disposer.dispose(it) } - } -} diff --git a/mide-plugins/vscode/src/cli/controller.ts b/mide-plugins/vscode/src/cli/controller.ts index 244cf32..dc99625 100644 --- a/mide-plugins/vscode/src/cli/controller.ts +++ b/mide-plugins/vscode/src/cli/controller.ts @@ -14,11 +14,7 @@ import { type ToolchainInventory, type ToolchainItem, } from "./toolchain"; -import { - discoveryBoundaryOf, - type DiscoveryBoundary, - type McppProjectDiscovery, -} from "../projects/discovery"; +import { type McppProjectDiscovery } from "../projects/discovery"; import { updateEditorTitleButtonsContext } from "../projects/context"; import { createLogger, type Logger } from "../util/log"; import { runMcppCommand } from "./process"; @@ -250,9 +246,7 @@ function failureGuidanceText(kind: McppFailureKind): string | undefined { * passes the result into `findNearestMcppProject`, whose walk stays pure. The * resource is the workspace folder, because the setting is resource-scoped. */ -export function discoveryBoundaryFromSettings(resource?: vscode.Uri): DiscoveryBoundary { - return discoveryBoundaryOf(read<string>("mcpp.project.discoveryBoundary", resource)); -} +export { discoveryBoundaryFromSettings } from "../projects/current"; const SUCCESS_STATUS_MS = 6000; diff --git a/mide-plugins/vscode/src/extension.ts b/mide-plugins/vscode/src/extension.ts index 66451e2..f8c2641 100644 --- a/mide-plugins/vscode/src/extension.ts +++ b/mide-plugins/vscode/src/extension.ts @@ -1,11 +1,12 @@ import * as vscode from "vscode"; -import { McppCliController, discoveryBoundaryFromSettings } from "./cli/controller"; +import { McppCliController } from "./cli/controller"; +import { findCurrentProject } from "./projects/current"; import { runMcpp, useMcppExecutor, useMcpplsExecutor } from "./cli/process"; import { parseProtocolInfo } from "./cli/protocol"; import { buildSelfCheckText } from "./cli/selfCheck"; -import { CLI_COMMANDS, LEGACY_LANGUAGE_SERVER_COMMANDS, LIBRARY_COMMANDS, TOOL_COMMANDS } from "./commands/ids"; -import { findNearestMcppProject, type McppProjectDiscovery } from "./projects/discovery"; +import { CLI_COMMANDS, LEGACY_LANGUAGE_SERVER_COMMANDS, TOOL_COMMANDS } from "./commands/ids"; +import type { McppProjectDiscovery } from "./projects/discovery"; import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./projects/context"; import { createLanguageServerBridge, @@ -41,16 +42,11 @@ import { Session } from "./ide/session"; import { Testing } from "./ide/testing"; import { changedSettings, onDidChange as onConfigurationChanged, read } from "./config/access"; import { applyRenames, pendingRenames, renamePrompt } from "./config/migrate"; -import { registerSettingsPanel } from "./config/panel"; import { languagePreference, setLanguagePreference, t, type LanguagePreference } from "./i18n/t"; import { CACHE_VIEW_FOCUS_COMMAND } from "./cache/cachePanel"; -import { readCacheSnapshot, registerCacheView } from "./cache/cacheView"; +import { readCacheSnapshot } from "./cache/cacheView"; +import { registerPages } from "./pages"; import { registerLanguageServerCommands } from "./views/languageServerView"; -import { createLibraryDetailOpener } from "./library/detailPanel"; -import { loadSnapshot, invalidateIndexCache } from "./library/indexLocator"; -import { watchProjectFiles } from "./library/projectWatcher"; -import { registerLibraryView } from "./library/libraryView"; -import { registerProjectView } from "./views/projectView"; /** * The commands an extension declares in its own `package.json`, read without @@ -78,37 +74,6 @@ function applyLanguagePreference(): void { } -function findCurrentProject(): McppProjectDiscovery | undefined { - const activeEditor = vscode.window.activeTextEditor; - if (activeEditor !== undefined) { - const activeUri = activeEditor.document.uri; - if (activeUri.scheme !== "file") { - return undefined; - } - const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeUri); - if (workspaceFolder === undefined) { - return undefined; - } - return findNearestMcppProject( - activeUri.fsPath, - workspaceFolder.uri.fsPath, - discoveryBoundaryFromSettings(workspaceFolder.uri), - ); - } - - for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { - const project = findNearestMcppProject( - workspaceFolder.uri.fsPath, - workspaceFolder.uri.fsPath, - discoveryBoundaryFromSettings(workspaceFolder.uri), - ); - if (project !== undefined) { - return project; - } - } - return undefined; -} - function outputText(output: vscode.OutputChannel, line: string): void { try { output.appendLine(line); @@ -392,156 +357,15 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi void session.start(); // ── views ──────────────────────────────────────────────────────────────── - // Each view owns its own tree provider and its own commands; the project view - // reads the manifest, the cache view runs mcpp's read-only queries, and the - // C++ Modules view only forwards to mcppls. - const applyViewVisibility = (): void => { - // Literal keys, so the wiring gate can see them. - // `mcpp.views.enabled` is the master switch: with it off every view is hidden. - // Whether VS Code also drops the container's icon from the activity bar is - // up to VS Code itself — an extension cannot mark its own container - // hidden-when-empty (the setting's description says the same). The `when` - // clauses in package.json are negated (`!mcpp.sidebarHidden`) so the default - // state is visible — otherwise activation would never run to set the key back. - void vscode.commands.executeCommand("setContext", "mcpp.sidebarHidden", !read<boolean>("mcpp.views.enabled")); - void vscode.commands.executeCommand("setContext", "mcpp.views.project", read<boolean>("mcpp.views.project.show")); - void vscode.commands.executeCommand("setContext", "mcpp.views.library", read<boolean>("mcpp.views.library.show")); - void vscode.commands.executeCommand("setContext", "mcpp.views.cache", read<boolean>("mcpp.views.cache.show")); - }; - applyViewVisibility(); - extensionContext.subscriptions.push(onConfigurationChanged(applyViewVisibility)); - - // The language service is a block inside the project view, so its state source has - // to exist before the view is registered. + // The C++ Modules block of the project view reads mcppls through the bridge; the views themselves + // (the project tree, the libraries, the cache, the settings) are src/pages.ts's, which the + // workbench mcpp-clion runs registers in the same way. const languageService = registerLanguageServerCommands(extensionContext, { bridge, output }); - registerProjectView(extensionContext, { currentProject: findCurrentProject, languageService }); - // The library ecosystem: a sidebar view plus an editor-area package page. The - // detail page writes `mcpp.toml` through `mcpp add`, so it tells the view to - // refresh — no save event fires for a file written outside the editor. - // Keep the selected library project when an editor-area webview takes focus. - let libraryProjectRoot: string | undefined = vscode.window.activeTextEditor?.document.uri.scheme === "file" - || vscode.workspace.workspaceFolders?.length === 1 ? findCurrentProject()?.root : undefined; - const libraryProject = (): string | undefined => libraryProjectRoot; - const chooseLibraryProject = async (): Promise<string | undefined> => { - const manifests = await vscode.workspace.findFiles(MCPP_MANIFEST_GLOB, "**/{node_modules,target,.git}/**", 200); - const roots = [...new Set(manifests.map((uri) => uri.fsPath.slice(0, -"/mcpp.toml".length)))]; - const active = libraryProject(); - if (active && !roots.includes(active)) { roots.unshift(active); } - const selected = await vscode.window.showQuickPick(roots.map((root) => ({ label: vscode.workspace.asRelativePath(root), description: root, root })), { - title: t("Choose the target mcpp project"), placeHolder: t("Dependencies will be added to this project's mcpp.toml"), - }); - if (selected) { libraryProjectRoot = selected.root; watchSelectedLibraryProject(); refreshLibraries(); } - return selected?.root; - }; - const openLibraryDetail = createLibraryDetailOpener(extensionContext, { - mcppExecutable: (root) => cliController.mcppExecutable(root ? { root, manifestPath: `${root}/mcpp.toml` } : undefined), - projectRoot: libraryProject, - chooseProject: chooseLibraryProject, - onPackageOpened: (key) => library.select(key), - output, - isTrusted: () => vscode.workspace.isTrusted, - onDependenciesChanged: () => { void library.refresh(); }, - onProjectFilesRead: (root, manifest, lock) => { if (root === libraryProjectRoot) { selectedLibraryPoll?.observe(manifest, lock); } }, - }); - const library = registerLibraryView(extensionContext, { - projectRoot: libraryProject, - mcppExecutable: () => cliController.mcppExecutable(libraryProjectRoot ? { root: libraryProjectRoot, manifestPath: `${libraryProjectRoot}/mcpp.toml` } : undefined), - openDetail: openLibraryDetail, - output, - isTrusted: () => vscode.workspace.isTrusted, - }); - const dependencyWatcher = vscode.workspace.createFileSystemWatcher("**/mcpp.{toml,lock}"); - let selectedLibraryWatcher: vscode.Disposable | undefined; - let selectedLibraryPoll: ReturnType<typeof watchProjectFiles> | undefined; - let libraryTimer: NodeJS.Timeout | undefined; - const refreshLibraries = (): void => { - void selectedLibraryPoll?.check(); - if (libraryTimer) { clearTimeout(libraryTimer); } - libraryTimer = setTimeout(() => { - libraryTimer = undefined; - void library.refresh(); - void openLibraryDetail.refresh(); - }, 120); - }; - // A non-recursive RelativePattern also observes scratch/gitignored projects - // which a workspace's recursive watcher may intentionally exclude. - function watchSelectedLibraryProject(): void { - selectedLibraryWatcher?.dispose(); - selectedLibraryWatcher = undefined; - selectedLibraryPoll = undefined; - if (!libraryProjectRoot) { return; } - const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file(libraryProjectRoot), "mcpp.{toml,lock}")); - selectedLibraryPoll = watchProjectFiles(libraryProjectRoot, refreshLibraries); - selectedLibraryWatcher = vscode.Disposable.from(selectedLibraryPoll, watcher, watcher.onDidCreate(refreshLibraries), watcher.onDidChange(refreshLibraries), watcher.onDidDelete(refreshLibraries)); - } - watchSelectedLibraryProject(); - extensionContext.subscriptions.push( - { dispose: () => selectedLibraryWatcher?.dispose() }, - dependencyWatcher, - dependencyWatcher.onDidCreate(refreshLibraries), - dependencyWatcher.onDidChange(refreshLibraries), - dependencyWatcher.onDidDelete(refreshLibraries), - vscode.window.onDidChangeActiveTextEditor((editor) => { if (editor?.document.uri.scheme === "file") { const active = findCurrentProject()?.root; if (active && active !== libraryProjectRoot) { libraryProjectRoot = active; watchSelectedLibraryProject(); } refreshLibraries(); } }), - vscode.workspace.onDidChangeWorkspaceFolders(() => { libraryProjectRoot = undefined; watchSelectedLibraryProject(); refreshLibraries(); }), - vscode.workspace.onDidGrantWorkspaceTrust(refreshLibraries), - vscode.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration("mcpp.library") || event.affectsConfiguration("mcpp.path") || event.affectsConfiguration("mcpp.ui.language")) { refreshLibraries(); } - }), - { dispose: () => { if (libraryTimer) { clearTimeout(libraryTimer); } } }, - ); - // `registerLibraryView` registers its own webview provider; these are only the - // entry points a menu or the project view can name. - extensionContext.subscriptions.push( - vscode.commands.registerCommand(LIBRARY_COMMANDS.search, async () => { - await vscode.commands.executeCommand("mcpp.library.focus"); - }), - vscode.commands.registerCommand(LIBRARY_COMMANDS.openDetail, async (id: unknown) => { - if (typeof id === "string" && id.length > 0) { - await openLibraryDetail(id); - } - }), - vscode.commands.registerCommand(LIBRARY_COMMANDS.updateIndex, async () => { - if (!vscode.workspace.isTrusted) { - void vscode.window.showWarningMessage(t("This workspace is not trusted. mcpp commands may run external programs named by workspace settings; trust the workspace first.")); - return; - } - const root = libraryProject(); - const project = root ? { root, manifestPath: `${root}/mcpp.toml` } : undefined; - if (read<string>("mcpp.library.indexPath").trim()) { - output.appendLine(t("Updating the global mcpp index; a configured custom checkout is only reread.")); - } - library.setUpdateStatus(t("Updating mcpp index…")); - const result = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: t("Refreshing the mcpp Package Index…") }, - () => runMcpp(vscode.workspace.isTrusted, cliController.mcppExecutable(project), ["index", "update"], project?.root, { timeoutMs: 300_000 }), - ); - if (result === undefined || result.exitCode !== 0) { - library.setUpdateStatus(t("Index update failed; showing the last available local snapshot.")); - if (result?.stderr) { output.appendLine(result.stderr.trimEnd()); } - void vscode.window.showErrorMessage(t("mcpp index update failed with exit code {0}", result?.exitCode ?? -1)); - return; - } - invalidateIndexCache(); - await library.refresh(); - await openLibraryDetail.refresh(); - const snapshot = await loadSnapshot({ projectRoot: root }); - library.setUpdateStatus(t("Index updated; {0} local packages.", snapshot.entries.length)); - }), - ); - // There is no `mcpp.library.indexFound` context key and no `viewsWelcome` - // entry for the library view. `paneview.ts`'s base `ViewPane` answers - // `shouldShowWelcome() === false`; only the tree-shaped panes override it, so - // welcome content can never render inside a `"type": "webview"` view. The - // library view states its own empty case in the document instead, which is the - // only place VS Code will show it. - - registerCacheView(extensionContext, { + registerPages(extensionContext, { output, - currentProject: findCurrentProject, mcppExecutable: (project) => cliController.mcppExecutable(project), - isTrusted: () => vscode.workspace.isTrusted, + languageService, }); - registerSettingsPanel(extensionContext); // Text analysis only: no mcpp process, so both stay available in a restricted // workspace, which is exactly what the `limited` capability promises. diff --git a/mide-plugins/vscode/src/pages.ts b/mide-plugins/vscode/src/pages.ts new file mode 100644 index 0000000..48c91b0 --- /dev/null +++ b/mide-plugins/vscode/src/pages.ts @@ -0,0 +1,185 @@ +/** + * The pages: the views of the activity bar (the project tree, the libraries, the cache), the panels + * they open (a package's page, the settings, the graphs) and their commands, wired together. The + * extension registers them at activation; mcpp-clion's workbench (webui/) registers the same + * modules in the same way, so both IDEs show mcpp-vscode's pages. + */ +import * as vscode from "vscode"; + +import { registerCacheView } from "./cache/cacheView"; +import { runMcpp } from "./cli/process"; +import { LIBRARY_COMMANDS } from "./commands/ids"; +import { onDidChange as onConfigurationChanged, read } from "./config/access"; +import { registerSettingsPanel } from "./config/panel"; +import { t } from "./i18n/t"; +import { createLibraryDetailOpener } from "./library/detailPanel"; +import { invalidateIndexCache, loadSnapshot } from "./library/indexLocator"; +import { registerLibraryView } from "./library/libraryView"; +import { watchProjectFiles } from "./library/projectWatcher"; +import { MCPP_MANIFEST_GLOB } from "./projects/context"; +import { findCurrentProject } from "./projects/current"; +import type { McppProjectDiscovery } from "./projects/discovery"; +import type { LanguageServerBlockSource } from "./views/languageServerView"; +import { registerProjectView } from "./views/projectView"; + +export interface PagesDeps { + output: vscode.OutputChannel; + /** The mcpp the commands run (`mcpp.path` for the project's folder, else `mcpp`). */ + mcppExecutable: (project: McppProjectDiscovery | undefined) => string; + /** The project view's C++ Modules block; absent where no mcppls bridge is (the workbench). */ + languageService?: LanguageServerBlockSource; +} + +export function registerPages(context: vscode.ExtensionContext, deps: PagesDeps): void { + const output = deps.output; + // ── views ──────────────────────────────────────────────────────────────── + // Each view owns its own tree provider and its own commands; the project view + // reads the manifest, the cache view runs mcpp's read-only queries, and the + // C++ Modules view only forwards to mcppls. + const applyViewVisibility = (): void => { + // Literal keys, so the wiring gate can see them. + // `mcpp.views.enabled` is the master switch: with it off every view is hidden. + // Whether VS Code also drops the container's icon from the activity bar is + // up to VS Code itself — an extension cannot mark its own container + // hidden-when-empty (the setting's description says the same). The `when` + // clauses in package.json are negated (`!mcpp.sidebarHidden`) so the default + // state is visible — otherwise activation would never run to set the key back. + void vscode.commands.executeCommand("setContext", "mcpp.sidebarHidden", !read<boolean>("mcpp.views.enabled")); + void vscode.commands.executeCommand("setContext", "mcpp.views.project", read<boolean>("mcpp.views.project.show")); + void vscode.commands.executeCommand("setContext", "mcpp.views.library", read<boolean>("mcpp.views.library.show")); + void vscode.commands.executeCommand("setContext", "mcpp.views.cache", read<boolean>("mcpp.views.cache.show")); + }; + applyViewVisibility(); + context.subscriptions.push(onConfigurationChanged(applyViewVisibility)); + + // The language service is a block inside the project view, so its state source has + // to exist before the view is registered. + registerProjectView(context, { currentProject: findCurrentProject, languageService: deps.languageService }); + // The library ecosystem: a sidebar view plus an editor-area package page. The + // detail page writes `mcpp.toml` through `mcpp add`, so it tells the view to + // refresh — no save event fires for a file written outside the editor. + // Keep the selected library project when an editor-area webview takes focus. + let libraryProjectRoot: string | undefined = vscode.window.activeTextEditor?.document.uri.scheme === "file" + || vscode.workspace.workspaceFolders?.length === 1 ? findCurrentProject()?.root : undefined; + const libraryProject = (): string | undefined => libraryProjectRoot; + const chooseLibraryProject = async (): Promise<string | undefined> => { + const manifests = await vscode.workspace.findFiles(MCPP_MANIFEST_GLOB, "**/{node_modules,target,.git}/**", 200); + const roots = [...new Set(manifests.map((uri) => uri.fsPath.slice(0, -"/mcpp.toml".length)))]; + const active = libraryProject(); + if (active && !roots.includes(active)) { roots.unshift(active); } + const selected = await vscode.window.showQuickPick(roots.map((root) => ({ label: vscode.workspace.asRelativePath(root), description: root, root })), { + title: t("Choose the target mcpp project"), placeHolder: t("Dependencies will be added to this project's mcpp.toml"), + }); + if (selected) { libraryProjectRoot = selected.root; watchSelectedLibraryProject(); refreshLibraries(); } + return selected?.root; + }; + const openLibraryDetail = createLibraryDetailOpener(context, { + mcppExecutable: (root) => deps.mcppExecutable(root ? { root, manifestPath: `${root}/mcpp.toml` } : undefined), + projectRoot: libraryProject, + chooseProject: chooseLibraryProject, + onPackageOpened: (key) => library.select(key), + output, + isTrusted: () => vscode.workspace.isTrusted, + onDependenciesChanged: () => { void library.refresh(); }, + onProjectFilesRead: (root, manifest, lock) => { if (root === libraryProjectRoot) { selectedLibraryPoll?.observe(manifest, lock); } }, + }); + const library = registerLibraryView(context, { + projectRoot: libraryProject, + mcppExecutable: () => deps.mcppExecutable(libraryProjectRoot ? { root: libraryProjectRoot, manifestPath: `${libraryProjectRoot}/mcpp.toml` } : undefined), + openDetail: openLibraryDetail, + output, + isTrusted: () => vscode.workspace.isTrusted, + }); + const dependencyWatcher = vscode.workspace.createFileSystemWatcher("**/mcpp.{toml,lock}"); + let selectedLibraryWatcher: vscode.Disposable | undefined; + let selectedLibraryPoll: ReturnType<typeof watchProjectFiles> | undefined; + let libraryTimer: NodeJS.Timeout | undefined; + const refreshLibraries = (): void => { + void selectedLibraryPoll?.check(); + if (libraryTimer) { clearTimeout(libraryTimer); } + libraryTimer = setTimeout(() => { + libraryTimer = undefined; + void library.refresh(); + void openLibraryDetail.refresh(); + }, 120); + }; + // A non-recursive RelativePattern also observes scratch/gitignored projects + // which a workspace's recursive watcher may intentionally exclude. + function watchSelectedLibraryProject(): void { + selectedLibraryWatcher?.dispose(); + selectedLibraryWatcher = undefined; + selectedLibraryPoll = undefined; + if (!libraryProjectRoot) { return; } + const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file(libraryProjectRoot), "mcpp.{toml,lock}")); + selectedLibraryPoll = watchProjectFiles(libraryProjectRoot, refreshLibraries); + selectedLibraryWatcher = vscode.Disposable.from(selectedLibraryPoll, watcher, watcher.onDidCreate(refreshLibraries), watcher.onDidChange(refreshLibraries), watcher.onDidDelete(refreshLibraries)); + } + watchSelectedLibraryProject(); + context.subscriptions.push( + { dispose: () => selectedLibraryWatcher?.dispose() }, + dependencyWatcher, + dependencyWatcher.onDidCreate(refreshLibraries), + dependencyWatcher.onDidChange(refreshLibraries), + dependencyWatcher.onDidDelete(refreshLibraries), + vscode.window.onDidChangeActiveTextEditor((editor) => { if (editor?.document.uri.scheme === "file") { const active = findCurrentProject()?.root; if (active && active !== libraryProjectRoot) { libraryProjectRoot = active; watchSelectedLibraryProject(); } refreshLibraries(); } }), + vscode.workspace.onDidChangeWorkspaceFolders(() => { libraryProjectRoot = undefined; watchSelectedLibraryProject(); refreshLibraries(); }), + vscode.workspace.onDidGrantWorkspaceTrust(refreshLibraries), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("mcpp.library") || event.affectsConfiguration("mcpp.path") || event.affectsConfiguration("mcpp.ui.language")) { refreshLibraries(); } + }), + { dispose: () => { if (libraryTimer) { clearTimeout(libraryTimer); } } }, + ); + // `registerLibraryView` registers its own webview provider; these are only the + // entry points a menu or the project view can name. + context.subscriptions.push( + vscode.commands.registerCommand(LIBRARY_COMMANDS.search, async () => { + await vscode.commands.executeCommand("mcpp.library.focus"); + }), + vscode.commands.registerCommand(LIBRARY_COMMANDS.openDetail, async (id: unknown) => { + if (typeof id === "string" && id.length > 0) { + await openLibraryDetail(id); + } + }), + vscode.commands.registerCommand(LIBRARY_COMMANDS.updateIndex, async () => { + if (!vscode.workspace.isTrusted) { + void vscode.window.showWarningMessage(t("This workspace is not trusted. mcpp commands may run external programs named by workspace settings; trust the workspace first.")); + return; + } + const root = libraryProject(); + const project = root ? { root, manifestPath: `${root}/mcpp.toml` } : undefined; + if (read<string>("mcpp.library.indexPath").trim()) { + output.appendLine(t("Updating the global mcpp index; a configured custom checkout is only reread.")); + } + library.setUpdateStatus(t("Updating mcpp index…")); + const result = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: t("Refreshing the mcpp Package Index…") }, + () => runMcpp(vscode.workspace.isTrusted, deps.mcppExecutable(project), ["index", "update"], project?.root, { timeoutMs: 300_000 }), + ); + if (result === undefined || result.exitCode !== 0) { + library.setUpdateStatus(t("Index update failed; showing the last available local snapshot.")); + if (result?.stderr) { output.appendLine(result.stderr.trimEnd()); } + void vscode.window.showErrorMessage(t("mcpp index update failed with exit code {0}", result?.exitCode ?? -1)); + return; + } + invalidateIndexCache(); + await library.refresh(); + await openLibraryDetail.refresh(); + const snapshot = await loadSnapshot({ projectRoot: root }); + library.setUpdateStatus(t("Index updated; {0} local packages.", snapshot.entries.length)); + }), + ); + // There is no `mcpp.library.indexFound` context key and no `viewsWelcome` + // entry for the library view. `paneview.ts`'s base `ViewPane` answers + // `shouldShowWelcome() === false`; only the tree-shaped panes override it, so + // welcome content can never render inside a `"type": "webview"` view. The + // library view states its own empty case in the document instead, which is the + // only place VS Code will show it. + + registerCacheView(context, { + output, + currentProject: findCurrentProject, + mcppExecutable: (project) => deps.mcppExecutable(project), + isTrusted: () => vscode.workspace.isTrusted, + }); + registerSettingsPanel(context); +} diff --git a/mide-plugins/vscode/src/projects/current.ts b/mide-plugins/vscode/src/projects/current.ts new file mode 100644 index 0000000..82a0394 --- /dev/null +++ b/mide-plugins/vscode/src/projects/current.ts @@ -0,0 +1,44 @@ +/** + * Which mcpp project the user is in: the nearest one above the active editor's file, within its + * workspace folder unless `mcpp.project.discoveryBoundary` says otherwise, else the first one a + * workspace folder holds. The extension and the workbench mcpp-clion runs the pages in ask the same. + */ +import * as vscode from "vscode"; + +import { read } from "../config/access"; +import { discoveryBoundaryOf, findNearestMcppProject, type DiscoveryBoundary, type McppProjectDiscovery } from "./discovery"; + +export function discoveryBoundaryFromSettings(resource?: vscode.Uri): DiscoveryBoundary { + return discoveryBoundaryOf(read<string>("mcpp.project.discoveryBoundary", resource)); +} + +export function findCurrentProject(): McppProjectDiscovery | undefined { + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor !== undefined) { + const activeUri = activeEditor.document.uri; + if (activeUri.scheme !== "file") { + return undefined; + } + const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeUri); + if (workspaceFolder === undefined) { + return undefined; + } + return findNearestMcppProject( + activeUri.fsPath, + workspaceFolder.uri.fsPath, + discoveryBoundaryFromSettings(workspaceFolder.uri), + ); + } + + for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { + const project = findNearestMcppProject( + workspaceFolder.uri.fsPath, + workspaceFolder.uri.fsPath, + discoveryBoundaryFromSettings(workspaceFolder.uri), + ); + if (project !== undefined) { + return project; + } + } + return undefined; +} diff --git a/webui/index.html b/webui/index.html deleted file mode 100644 index 62021e6..0000000 --- a/webui/index.html +++ /dev/null @@ -1,26 +0,0 @@ -<!DOCTYPE html> -<!-- The shared panels. The host fills the placeholders: its content security policy, the base URL - of this directory, the script nonce, the language. --> -<html lang="{{lang}}"> -<head> - <meta charset="utf-8"> - <meta http-equiv="Content-Security-Policy" content="{{csp}}"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel="stylesheet" href="{{base}}styles.css"> - <title>mcpp - - -
      - - - - - - - - - - - - - diff --git a/webui/js/app.js b/webui/js/app.js deleted file mode 100644 index 29b04c1..0000000 --- a/webui/js/app.js +++ /dev/null @@ -1,22 +0,0 @@ -// The panel's start: tell the host it is ready, receive which panel to show and with what (the -// project root, a graph kind, a package id), the language's strings and the editor's look, then -// mount it. A later `init` replaces the panel (an editor reuses one view for several). -(function (root) { - "use strict"; - const ui = root.McppUi; - let unmount = null; - ui.on("$init", (m) => { - ui.setBundle(m.bundle || {}); - if (m.theme && typeof m.theme === "object") - for (const [k, v] of Object.entries(m.theme)) root.document.documentElement.style.setProperty(k, v); - const el = root.document.getElementById("app"); - if (typeof unmount === "function") unmount(); - ui.clear(el); - const panel = ui.panels[m.panel]; - if (!panel) { el.appendChild(ui.empty(ui.t("Unknown panel {0}", m.panel))); return; } - root.document.title = panel.title(m.params || {}); - unmount = panel.mount(el, m.params || {}) || null; - }); - if (root.document.readyState === "loading") root.document.addEventListener("DOMContentLoaded", ui.ready); - else ui.ready(); -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/core.js b/webui/js/core.js deleted file mode 100644 index c68b93a..0000000 --- a/webui/js/core.js +++ /dev/null @@ -1,84 +0,0 @@ -// The panels' shared helpers: the namespace, the runtime strings, formatting, and a small DOM -// builder. Classic scripts on one global (`McppUi`), so the same files load in a VS Code webview and -// in a JCEF page opened from disk, with no bundler between the repository and either editor. -(function (root) { - "use strict"; - const ui = (root.McppUi = root.McppUi || /** @type {McppUiApi} */ ({})); - ui.panels = ui.panels || {}; - - // The runtime strings: English keys, the bundle the backend answers for the session's language. - let bundle = {}; - ui.setBundle = (b) => { bundle = b && typeof b === "object" ? b : {}; }; - ui.t = (key, ...args) => { - let text = Object.prototype.hasOwnProperty.call(bundle, key) ? bundle[key] : key; - args.forEach((a, i) => { text = text.split(`{${i}}`).join(String(a)); }); - return text; - }; - - ui.formatBytes = (bytes) => { - const units = ["B", "KiB", "MiB", "GiB", "TiB"]; - let v = Math.max(0, Number(bytes) || 0); - let u = 0; - while (v >= 1024 && u + 1 < units.length) { v /= 1024; ++u; } - return u === 0 ? `${Math.round(v)} B` : `${v.toFixed(1)} ${units[u]}`; - }; - - // "3 days ago" from seconds (or milliseconds, when the value is that large) since the epoch. - ui.ago = (when, now = Date.now()) => { - if (when === undefined || when === null) return ""; - let ms = Number(when); - if (ms < 1e12) ms *= 1000; - const s = Math.max(0, Math.round((now - ms) / 1000)); - if (s < 60) return ui.t("just now"); - if (s < 3600) return ui.t("{0} min ago", Math.round(s / 60)); - if (s < 86400) return ui.t("{0} h ago", Math.round(s / 3600)); - return ui.t("{0} days ago", Math.round(s / 86400)); - }; - - ui.debounce = (fn, ms) => { - let timer; - return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; - }; - - // h("div.row#id", {onclick, title, ...}, children...) -- children are nodes, strings or arrays. - ui.h = (spec, props, ...children) => { - const doc = root.document; - const m = /^([a-z0-9-]+)?((?:[.#][\w-]+)*)$/i.exec(spec) || []; - const el = doc.createElement(m[1] || "div"); - for (const part of (m[2] || "").match(/[.#][\w-]+/g) || []) { - if (part[0] === ".") el.classList.add(part.slice(1)); else el.id = part.slice(1); - } - if (props && (typeof props !== "object" || props.nodeType || Array.isArray(props))) { children.unshift(props); props = null; } - for (const [k, v] of Object.entries(props || {})) { - if (v === undefined || v === null || v === false) continue; - if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2), v); - else if (k === "class") el.className = v; - else if (k === "style" && typeof v === "object") Object.assign(el.style, v); - else if (k === "dataset") Object.assign(el.dataset, v); - else if (k in el && typeof v !== "string") el[k] = v; - else el.setAttribute(k, v === true ? "" : String(v)); - } - const add = (c) => { - if (c === undefined || c === null || c === false) return; - if (Array.isArray(c)) { c.forEach(add); return; } - el.appendChild(c.nodeType ? c : doc.createTextNode(String(c))); - }; - children.forEach(add); - return el; - }; - - ui.clear = (el) => { while (el.firstChild) el.removeChild(el.firstChild); return el; }; - - // A status badge: ok | warning | error | info | running. - ui.badge = (state, text) => ui.h(`span.badge.badge-${state}`, text); - - ui.button = (text, onclick, opts = {}) => - ui.h(`button${opts.secondary ? ".secondary" : ""}`, { onclick, disabled: !!opts.disabled, title: opts.title }, text); - - ui.section = (title, ...children) => ui.h("section.card", ui.h("h2", title), ...children); - - ui.empty = (text) => ui.h("p.empty", text); - - ui.errorBox = (error) => - ui.h("div.error", ui.h("strong", ui.t("mcpp could not answer")), ui.h("pre", (error && (error.message || String(error))) || "")); -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/graph-layout.js b/webui/js/graph-layout.js deleted file mode 100644 index aeb8e5f..0000000 --- a/webui/js/graph-layout.js +++ /dev/null @@ -1,119 +0,0 @@ -// A layered layout for the dependency graphs: every edge points from a node to one it depends on, -// layers are assigned by longest path from the roots (cycles broken at the edge that closes them), -// and the order within a layer is improved by barycentre sweeps. Pure: nodes and edges in, positions -// out; the graph panel draws them. -(function (root) { - "use strict"; - const ui = (root.McppUi = root.McppUi || /** @type {McppUiApi} */ ({})); - - // nodes: [{id}], edges: [{from, to}] -> {nodes: [{id, x, y, layer}], edges: [{from, to, points}], width, height} - function layout(nodes, edges, opts = {}) { - const nodeW = opts.nodeWidth || 180, nodeH = opts.nodeHeight || 28, gapX = opts.gapX || 24, gapY = opts.gapY || 64; - const ids = nodes.map((n) => n.id); - const index = new Map(ids.map((id, i) => [id, i])); - const out = ids.map(() => []), inn = ids.map(() => []); - const kept = []; - for (const e of edges) { - const a = index.get(e.from), b = index.get(e.to); - if (a === undefined || b === undefined || a === b) continue; - kept.push([a, b]); - } - // Break cycles: a DFS marks back edges, which are drawn but do not constrain the layers. - const state = new Array(ids.length).fill(0); - const back = new Set(); - const adj = ids.map(() => []); - kept.forEach(([a, b], k) => adj[a].push([b, k])); - for (let s = 0; s < ids.length; ++s) { - if (state[s]) continue; - const stack = [[s, 0]]; - state[s] = 1; - while (stack.length) { - const top = stack[stack.length - 1]; - const [v, i] = top; - if (i < adj[v].length) { - top[1]++; - const [w, k] = adj[v][i]; - if (state[w] === 1) back.add(k); - else if (state[w] === 0) { state[w] = 1; stack.push([w, 0]); } - } else { state[v] = 2; stack.pop(); } - } - } - kept.forEach(([a, b], k) => { if (!back.has(k)) { out[a].push(b); inn[b].push(a); } }); - // Longest path from the roots (nodes nothing depends on): a dependency sits below its users. - const layer = new Array(ids.length).fill(0); - const indeg = inn.map((l) => l.length); - const queue = []; - indeg.forEach((d, i) => { if (d === 0) queue.push(i); }); - for (let q = 0; q < queue.length; ++q) { - const v = queue[q]; - for (const w of out[v]) { - layer[w] = Math.max(layer[w], layer[v] + 1); - if (--indeg[w] === 0) queue.push(w); - } - } - const layers = []; - layer.forEach((l, i) => { (layers[l] = layers[l] || []).push(i); }); - for (let l = 0; l < layers.length; ++l) if (!layers[l]) layers[l] = []; - // Barycentre sweeps, down then up. - const pos = new Array(ids.length).fill(0); - const place = () => layers.forEach((row) => row.forEach((v, i) => { pos[v] = i; })); - place(); - const sweep = (rows, neighbours) => { - for (const row of rows) { - const key = new Map(row.map((v) => { - const ns = neighbours[v]; - return [v, ns.length ? ns.reduce((s, w) => s + pos[w], 0) / ns.length : pos[v]]; - })); - row.sort((a, b) => key.get(a) - key.get(b) || a - b); - row.forEach((v, i) => { pos[v] = i; }); - } - }; - for (let k = 0; k < (opts.sweeps || 4); ++k) { - sweep(layers.slice(1), inn); - sweep(layers.slice(0, -1).reverse(), out); - } - const widest = Math.max(1, ...layers.map((r) => r.length)); - const width = widest * (nodeW + gapX) + gapX; - const placed = nodes.map((n, i) => { - const row = layers[layer[i]]; - const offset = (width - row.length * (nodeW + gapX) - gapX) / 2; - return Object.assign({}, n, { - layer: layer[i], - x: offset + gapX + pos[i] * (nodeW + gapX), - y: gapY / 2 + layer[i] * (nodeH + gapY), - width: nodeW, - height: nodeH, - }); - }); - const routed = kept.map(([a, b], k) => { - const p = placed[a], q = placed[b]; - return { - from: ids[a], to: ids[b], back: back.has(k), - points: [[p.x + nodeW / 2, p.y + nodeH], [q.x + nodeW / 2, q.y]], - }; - }); - return { nodes: placed, edges: routed, width, height: layers.length * (nodeH + gapY) + gapY / 2 }; - } - - // The part of a graph around one node: it, what it depends on and what depends on it, `depth` - // steps each way. Large module graphs are read this way rather than all at once. - function neighbourhood(nodes, edges, focus, depth = 1) { - const keep = new Set([focus]); - let frontier = new Set([focus]); - for (let d = 0; d < depth; ++d) { - const next = new Set(); - for (const e of edges) { - if (frontier.has(e.from) && !keep.has(e.to)) next.add(e.to); - if (frontier.has(e.to) && !keep.has(e.from)) next.add(e.from); - } - next.forEach((v) => keep.add(v)); - frontier = next; - } - return { - nodes: nodes.filter((n) => keep.has(n.id)), - edges: edges.filter((e) => keep.has(e.from) && keep.has(e.to)), - }; - } - - ui.graphLayout = { layout, neighbourhood }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/host.js b/webui/js/host.js deleted file mode 100644 index 315b7df..0000000 --- a/webui/js/host.js +++ /dev/null @@ -1,62 +0,0 @@ -// The bridge to the editor that hosts the panel. The panel speaks the backend's protocol through -// it (`rpc`), receives the backend's notifications (`on`), and asks the editor for what only the -// editor can do (`host`: open a file, confirm, write a setting, start a terminal). -// -// VS Code: `acquireVsCodeApi().postMessage` out, `window` "message" events in. -// JetBrains (JCEF): `window.mcppHostSend(text)` out (a JBCefJSQuery the plugin injects), and the -// plugin calls `window.mcppHostReceive(message)` in. -(function (root) { - "use strict"; - const ui = (root.McppUi = root.McppUi || /** @type {McppUiApi} */ ({})); - let send; - const vscode = typeof root.acquireVsCodeApi === "function" ? root.acquireVsCodeApi() : undefined; - if (vscode) send = (m) => vscode.postMessage(m); - else send = (m) => { if (typeof root.mcppHostSend === "function") root.mcppHostSend(JSON.stringify(m)); else queue.push(m); }; - const queue = []; - let nextId = 1; - const pending = new Map(); - const listeners = new Map(); - - function receive(m) { - if (typeof m === "string") { try { m = JSON.parse(m); } catch { return; } } - if (!m || typeof m !== "object") return; - if (m.type === "rpcResult" || m.type === "hostResult") { - const p = pending.get(m.id); pending.delete(m.id); - if (p) p.resolve(m.result); - } else if (m.type === "rpcError" || m.type === "hostError") { - const p = pending.get(m.id); pending.delete(m.id); - if (p) p.reject(Object.assign(new Error((m.error && m.error.message) || "failed"), { data: m.error && m.error.data, code: m.error && m.error.code })); - } else if (m.type === "notify") { - for (const fn of listeners.get(m.method) || []) fn(m.params); - for (const fn of listeners.get("*") || []) fn(m.method, m.params); - } else if (m.type === "init") { - for (const fn of listeners.get("$init") || []) fn(m); - } - } - if (vscode) root.addEventListener("message", (e) => receive(e.data)); - root.mcppHostReceive = receive; - // The JCEF query is injected after the page loads: whatever was sent before it goes then. - root.mcppHostAttached = () => { while (queue.length) send(queue.shift()); }; - - function request(type, fields) { - const id = nextId++; - return new Promise((resolve, reject) => { - pending.set(id, { resolve, reject }); - send(Object.assign({ type, id }, fields)); - }); - } - - ui.rpc = (method, params = {}) => request("rpc", { method, params }); - ui.host = (action, params = {}) => request("host", { action, params }); - ui.on = (method, fn) => { - if (!listeners.has(method)) listeners.set(method, new Set()); - listeners.get(method).add(fn); - return () => listeners.get(method).delete(fn); - }; - ui.ready = () => send({ type: "ready" }); - ui.state = { - get: () => (vscode ? vscode.getState() : root.__mcppState) || {}, - set: (s) => { if (vscode) vscode.setState(s); else root.__mcppState = s; }, - }; - ui.editor = vscode ? "vscode" : "jetbrains"; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/panels/cache.js b/webui/js/panels/cache.js deleted file mode 100644 index 6337ac7..0000000 --- a/webui/js/panels/cache.js +++ /dev/null @@ -1,72 +0,0 @@ -// The cache panel: what mcpp's shared build cache holds (`mcpp cache list`), by kind, by package and -// by age, and the cleanups. A cleanup is a plan the backend makes (its argv, its danger level, its -// preview); the editor confirms it natively, at the level the plan asks for. -(function (root) { - "use strict"; - const ui = root.McppUi; - const { h, t } = ui; - - // plan -> preview -> the editor's confirmation -> apply. Used by the panel and nothing else: the - // editors' own commands run the same three steps against the same plan. - async function cleanup(action, rootDir) { - const plan = await ui.rpc("cache/plan", { action, root: rootDir }); - if (plan.refusedUntrusted) { - await ui.host("notify", { level: "warning", message: t("this action needs a trusted workspace") }); - return false; - } - let confirmation = {}; - if (plan.level > 0) { - let preview = ""; - if (plan.preview && plan.preview.source === "mcpp") preview = plan.preview.text || ""; - else if (plan.preview && plan.preview.entries) - preview = t("About {0} would be freed ({1} entries).", plan.preview.bytesText, plan.preview.count); - const answer = await ui.host("confirm", { title: plan.title, detail: plan.detail, preview, level: plan.level, acknowledge: plan.acknowledge }); - if (!answer || !answer.confirmed) return false; - confirmation = { confirmed: true, previewShown: !!plan.preview, acknowledged: !!answer.acknowledged }; - } - await ui.rpc("cache/apply", { action, root: rootDir, confirmation }); - return true; - } - ui.cleanup = cleanup; - - function bars(rows, total, label) { - return h("table.bars", rows.map((r) => h("tr", - h("td", label(r)), - h("td.num", ui.formatBytes(r.bytes)), - h("td.bar", h("div", { style: { width: `${total ? Math.max(1, Math.round((100 * r.bytes) / total)) : 0}%` } }))))); - } - - ui.panels.cache = { - title: () => t("Cache"), - mount(el, params) { - const rootDir = params.root; - async function render() { - ui.clear(el).appendChild(h("p.muted", t("Loading…"))); - let inv; - try { inv = await ui.rpc("cache/inventory", { root: rootDir }); } catch (e) { ui.clear(el).appendChild(ui.errorBox(e)); return; } - ui.clear(el); - el.append( - h("header.panel-head", - h("h1", t("Cache")), - h("p", h("strong", inv.totalText), " ", t("in {0} entries", inv.totalEntries), inv.root ? h("span.muted", ` · ${inv.root}`) : null), - h("div.actions", ui.button(t("Refresh"), render, { secondary: true }))), - ui.section(t("By kind"), bars(inv.byKind, inv.totalBytes, (r) => `${r.kind} (${r.entries})`)), - ui.section(t("Largest packages"), inv.topLabels.length ? bars(inv.topLabels, inv.totalBytes, (r) => r.label) : ui.empty(t("none"))), - ui.section(t("By last use"), bars(inv.ageBuckets, inv.totalBytes, (b) => - b.toDays === undefined ? t("older than {0} days", b.fromDays) : t("{0}–{1} days", b.fromDays, b.toDays))), - inv.incomplete.length ? ui.section(t("Incomplete entries"), h("ul.list", inv.incomplete.map((e) => h("li", e.label || e.key)))) : null, - ui.section(t("Clean up"), - h("div.actions.wrap", - [["stale", t("Clean stale artifacts")], ["project", t("Clean project artifacts")], ["cacheGc", t("Collect to a budget")], - ["cachePrune", t("Prune by age")], ["cacheDeps", t("Drop package entries")], ["cacheStd", t("Drop standard library entries")], - ["cacheVerify", t("Verify")], ["cacheLegacy", t("Remove the pre-v1 cache")], ["cacheAll", t("Drop everything")]] - .map(([action, label]) => ui.button(label, async () => { - try { if (await cleanup(action, rootDir)) setTimeout(render, 500); } catch (e) { el.prepend(ui.errorBox(e)); } - }, { secondary: action !== "stale", disabled: action === "project" && !rootDir }))))); - } - const off = ui.on("job/finished", (p) => { if (p.kind === "clean" || p.kind === "command") render(); }); - render(); - return off; - }, - }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/panels/graph.js b/webui/js/panels/graph.js deleted file mode 100644 index 88ce573..0000000 --- a/webui/js/panels/graph.js +++ /dev/null @@ -1,113 +0,0 @@ -// The dependency graphs: packages (which set sees which) and modules (who provides, who imports), -// from mcpp's build database through the backend's view models. Drawn as SVG with a layered layout; -// a large graph opens on the neighbourhood of one node, chosen by search or by a click. -(function (root) { - "use strict"; - const ui = root.McppUi; - const { h, t } = ui; - const SVG = "http://www.w3.org/2000/svg"; - const svg = (tag, attrs = {}, ...children) => { - const el = root.document.createElementNS(SVG, tag); - for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v)); - children.forEach((c) => c && el.appendChild(c)); - return el; - }; - // Beyond this many nodes the whole graph is shown only when asked for. - const WHOLE_LIMIT = 150; - - ui.panels.graph = { - title: (params) => (params.graph === "modules" ? t("Module graph") : t("Dependency graph")), - mount(el, params) { - const kind = params.graph === "modules" ? "modules" : "packages"; - const rootDir = params.root; - let data = { nodes: [], edges: [] }; - let focus = null, depth = 1, whole = false; - let zoom = 1, panX = 0, panY = 0; - const search = h("input", { type: "search", placeholder: t("Find a node") }); - const list = h("datalist#graph-nodes"); - search.setAttribute("list", "graph-nodes"); - const depthSel = h("select", [1, 2, 3].map((d) => h("option", { value: d }, t("{0} step(s) around", d)))); - const canvas = h("div.graph-canvas"); - const status = h("p.muted"); - - function draw() { - ui.clear(canvas); - let view = data; - if (focus) view = ui.graphLayout.neighbourhood(data.nodes, data.edges, focus, depth); - else if (data.nodes.length > WHOLE_LIMIT && !whole) { - status.textContent = t("{0} nodes: choose one to see its neighbourhood, or show the whole graph.", data.nodes.length); - canvas.appendChild(h("div.actions", ui.button(t("Show the whole graph"), () => { whole = true; draw(); }, { secondary: true }))); - return; - } - status.textContent = focus ? t("{0} and {1} node(s) around it", focus, view.nodes.length - 1) - : t("{0} nodes, {1} edges", view.nodes.length, view.edges.length); - const lay = ui.graphLayout.layout(view.nodes, view.edges, { nodeWidth: kind === "modules" ? 200 : 180 }); - const g = svg("g"); - const defs = svg("defs", {}, svg("marker", { id: "arrow", viewBox: "0 0 10 10", refX: 9, refY: 5, markerWidth: 6, markerHeight: 6, orient: "auto-start-reverse" }, - svg("path", { d: "M 0 0 L 10 5 L 0 10 z", class: "arrow" }))); - for (const e of lay.edges) { - const [[x1, y1], [x2, y2]] = e.points; - const my = (y1 + y2) / 2; - g.appendChild(svg("path", { d: `M ${x1} ${y1} C ${x1} ${my}, ${x2} ${my}, ${x2} ${y2}`, class: e.back ? "edge back" : "edge", "marker-end": "url(#arrow)" })); - } - for (const n of lay.nodes) { - const label = n.id.length > 28 ? `${n.id.slice(0, 26)}…` : n.id; - const node = svg("g", { class: `node kind-${n.kind || "set"}${n.id === focus ? " focus" : ""}`, transform: `translate(${n.x},${n.y})`, tabindex: 0 }, - svg("rect", { width: n.width, height: n.height, rx: 4 }), - svg("text", { x: 8, y: n.height / 2 + 4 }, root.document.createTextNode(label))); - const title = svg("title"); - title.textContent = [n.id, n.source || "", n.set ? `(${n.set})` : ""].filter(Boolean).join("\n"); - node.appendChild(title); - node.addEventListener("click", () => { focus = n.id; draw(); }); - node.addEventListener("dblclick", () => { if (n.source) ui.host("openFile", { path: n.source }); }); - node.addEventListener("keydown", (ev) => { if (ev.key === "Enter") { focus = n.id; draw(); } }); - g.appendChild(node); - } - const s = svg("svg", { width: "100%", height: "100%" }, defs, g); - const apply = () => g.setAttribute("transform", `translate(${panX},${panY}) scale(${zoom})`); - zoom = Math.min(1, (canvas.clientWidth || 800) / lay.width, (canvas.clientHeight || 600) / lay.height) || 1; - panX = Math.max(0, ((canvas.clientWidth || 800) - lay.width * zoom) / 2); panY = 8; - apply(); - s.addEventListener("wheel", (ev) => { - ev.preventDefault(); - const next = Math.max(0.05, Math.min(4, zoom * (ev.deltaY < 0 ? 1.1 : 1 / 1.1))); - const r = s.getBoundingClientRect(), x = ev.clientX - r.left, y = ev.clientY - r.top; - panX = x - ((x - panX) * next) / zoom; panY = y - ((y - panY) * next) / zoom; zoom = next; apply(); - }, { passive: false }); - let drag = null; - s.addEventListener("pointerdown", (ev) => { if (ev.target === s) { drag = [ev.clientX - panX, ev.clientY - panY]; s.setPointerCapture(ev.pointerId); } }); - s.addEventListener("pointermove", (ev) => { if (drag) { panX = ev.clientX - drag[0]; panY = ev.clientY - drag[1]; apply(); } }); - s.addEventListener("pointerup", () => { drag = null; }); - canvas.appendChild(s); - } - - async function load() { - status.textContent = t("Loading…"); - try { data = await ui.rpc(kind === "modules" ? "graph/modules" : "graph/packages", { root: rootDir }); } - catch (e) { status.textContent = ""; ui.clear(canvas).appendChild(ui.errorBox(e)); return; } - ui.clear(list); - data.nodes.forEach((n) => list.appendChild(h("option", { value: n.id }))); - if (params.focus) focus = params.focus; - draw(); - } - search.addEventListener("change", () => { if (data.nodes.some((n) => n.id === search.value)) { focus = search.value; draw(); } }); - depthSel.addEventListener("change", () => { depth = Number(depthSel.value); draw(); }); - el.append( - h("header.panel-head", - h("h1", kind === "modules" ? t("Module graph") : t("Dependency graph")), - h("div.toolbar", search, list, depthSel, - ui.button(t("Overview"), () => { focus = null; draw(); }, { secondary: true }), - ui.button(t("Refresh"), load, { secondary: true }), - ui.button(t("Copy as Mermaid"), () => { - const lines = ["graph TD"]; - data.edges.forEach((e) => lines.push(` ${JSON.stringify(e.from)} --> ${JSON.stringify(e.to)}`.replace(/"/g, ""))); - ui.host("copy", { text: lines.join("\n") }); - }, { secondary: true })), - status), - canvas); - const off = ui.on("result/changed", (p) => { if (p.root === rootDir) load(); }); - load(); - return off; - }, - }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/panels/libraries.js b/webui/js/panels/libraries.js deleted file mode 100644 index 71f54f8..0000000 --- a/webui/js/panels/libraries.js +++ /dev/null @@ -1,87 +0,0 @@ -// The libraries panel: the packages of every index mcpp reads (`mcpp search`), filtered and sorted -// by the backend, and one package's page. "Add" runs `mcpp add` on the project, which writes -// mcpp.toml; nothing here edits the manifest itself. -(function (root) { - "use strict"; - const ui = root.McppUi; - const { h, t } = ui; - - function detail(el, pkg, rootDir, back) { - ui.clear(el); - const versions = pkg.versions || []; - const select = h("select", versions.map((v) => h("option", { value: v }, v))); - el.append( - h("header.panel-head", - h("a.back", { href: "#", onclick: (e) => { e.preventDefault(); back(); } }, `← ${t("All packages")}`), - h("h1", pkg.id), - h("p", pkg.description || ""), - h("dl.facts", - h("dt", t("Index")), h("dd", pkg.index || ""), - h("dt", t("Dependency name")), h("dd", h("code", pkg.dependencyName || pkg.name)), - h("dt", t("Licences")), h("dd", (pkg.licenses || []).join(", ") || "—"), - pkg.repo ? [h("dt", t("Repository")), h("dd", h("a", { href: "#", onclick: (e) => { e.preventDefault(); ui.host("openExternal", { url: pkg.repo }); } }, pkg.repo))] : null), - h("div.actions", - select, - ui.button(t("Add to mcpp.toml"), async () => { - if (!rootDir) { await ui.host("notify", { level: "warning", message: t("No mcpp project in this workspace") }); return; } - await ui.rpc("library/add", { root: rootDir, dependency: pkg.dependencyName || pkg.name, version: select.value }); - }, { disabled: !rootDir }))), - ui.section(t("Versions"), h("ul.list", versions.map((v) => h("li", h("code", v)))))); - } - - ui.panels.libraries = { - title: () => t("Libraries"), - mount(el, params) { - const rootDir = params.root; - const saved = ui.state.get().libraries || {}; - const search = h("input", { type: "search", placeholder: t("Search packages"), value: saved.keyword || "" }); - const sort = h("select", h("option", { value: "name" }, t("by name")), h("option", { value: "versions" }, t("by versions"))); - sort.value = saved.sort || "name"; - const index = h("select", h("option", { value: "" }, t("every index"))); - const results = h("div.results"); - const status = h("p.muted"); - let lastQuery = 0; - - async function query(refresh = false) { - const id = ++lastQuery; - ui.state.set(Object.assign(ui.state.get(), { libraries: { keyword: search.value, sort: sort.value } })); - status.textContent = t("Searching…"); - let r; - try { - r = await ui.rpc("library/search", { keyword: search.value, sort: sort.value, index: index.value, limit: 300, refresh, root: rootDir }); - } catch (e) { - if (id === lastQuery) { status.textContent = ""; ui.clear(results).appendChild(ui.errorBox(e)); } - return; - } - if (id !== lastQuery) return; - if (index.options.length === 1) for (const name of r.indexes || []) index.appendChild(h("option", { value: name }, name)); - status.textContent = t("{0} packages", r.total); - ui.clear(results); - if (!r.packages.length) { results.appendChild(ui.empty(t("No package matches."))); return; } - results.appendChild(h("ul.list.packages", r.packages.map((p) => - h("li.package", { onclick: () => detail(el, p, rootDir, () => { ui.clear(el); mountList(); }) }, - h("div", h("strong", p.id), h("span.muted", ` ${p.latest || ""}`)), - h("div.muted", p.description || ""))))); - } - - function mountList() { - el.append( - h("header.panel-head", - h("h1", t("Libraries")), - h("div.toolbar", search, sort, index, - ui.button(t("Refresh the index"), async () => { await ui.rpc("library/updateIndex", { root: rootDir }); }, { secondary: true }))), - status, results); - query(); - } - search.addEventListener("input", ui.debounce(() => query(), 200)); - sort.addEventListener("change", () => query()); - index.addEventListener("change", () => query()); - const off = ui.on("job/finished", (p) => { if ((p.envelope && p.envelope.kind) || p.kind === "command") query(true); }); - if (params.id) { - ui.rpc("library/search", { id: params.id, root: rootDir }).then((p) => detail(el, p, rootDir, () => { ui.clear(el); mountList(); }), - (e) => el.appendChild(ui.errorBox(e))); - } else mountList(); - return off; - }, - }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/panels/pack.js b/webui/js/panels/pack.js deleted file mode 100644 index 8f20c6e..0000000 --- a/webui/js/panels/pack.js +++ /dev/null @@ -1,67 +0,0 @@ -// The pack panel: the formats mcpp can produce for this project (`mcpp pack --list-formats`, with -// the plugins that provide them), the mode, the target, and the result of a pack: what was written -// and where. -(function (root) { - "use strict"; - const ui = root.McppUi; - const { h, t } = ui; - - ui.panels.pack = { - title: () => t("Pack"), - mount(el, params) { - const rootDir = params.root; - const format = h("select"); - const mode = h("select", h("option", { value: "" }, t("the project's default"))); - const linkage = h("select", h("option", { value: "" }, t("the project's default"))); - const profile = h("select", ["release", "dev"].map((p) => h("option", { value: p }, p))); - const target = h("input", { type: "text", placeholder: t("this machine") }); - const output = h("pre.log"); - const result = h("div"); - let jobId = null; - const go = ui.button(t("Pack"), async () => { - ui.clear(output); ui.clear(result); - try { - const r = await ui.rpc("pack/start", { root: rootDir, pkgFormat: format.value || undefined, mode: mode.value || undefined, - linkage: linkage.value || undefined, profile: profile.value, target: target.value }); - jobId = r.jobId; go.disabled = true; - } catch (e) { result.appendChild(ui.errorBox(e)); } - }); - const offs = [ - ui.on("job/output", (p) => { if (p.jobId === jobId) output.textContent += p.text; }), - ui.on("job/finished", (p) => { - if (p.jobId !== jobId) return; - go.disabled = false; jobId = null; - ui.clear(result); - const data = (p.envelope && p.envelope.data) || {}; - result.appendChild(ui.section(t("Result"), - h("p", ui.badge(p.status === "ok" ? "ok" : "error", p.status)), - (p.diagnostics || []).map((d) => h("p.error", d.message)), - data.outputs || data.artifacts ? h("ul.list", (data.outputs || data.artifacts).map((o) => { - const path = typeof o === "string" ? o : o.path; - return h("li", h("code", path), ui.button(t("Reveal"), () => ui.host("reveal", { path }), { secondary: true })); - })) : h("pre", JSON.stringify(data, null, 2)))); - }), - ]; - el.append(h("header.panel-head", h("h1", t("Pack")), h("p.muted", rootDir || "")), - ui.section(t("What to produce"), - h("dl.facts", - h("dt", t("Format")), h("dd", format), - h("dt", t("Mode")), h("dd", mode), - h("dt", t("Linkage")), h("dd", linkage), - h("dt", t("Profile")), h("dd", profile), - h("dt", t("Target")), h("dd", target)), - h("div.actions", go)), - result, output); - ui.rpc("pack/formats", { root: rootDir }).then((r) => { - format.appendChild(h("option", { value: "" }, t("the project's default"))); - for (const f of (r && r.formats) || []) - format.appendChild(h("option", { value: f.name }, f.builtin ? f.name : `${f.name} (${(f.providers || []).join(", ")})`)); - for (const m of (r && r.modes) || []) mode.appendChild(h("option", { value: m }, m)); - for (const l of (r && r.linkages) || []) linkage.appendChild(h("option", { value: l }, l)); - linkage.disabled = !((r && r.linkages) || []).length; - }, (e) => result.appendChild(ui.errorBox(e))); - ui.host("settings", {}).then((s) => { if (s && s["mcpp.build.target"]) target.value = s["mcpp.build.target"]; }); - return () => offs.forEach((off) => off()); - }, - }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/panels/project.js b/webui/js/panels/project.js deleted file mode 100644 index f623a91..0000000 --- a/webui/js/panels/project.js +++ /dev/null @@ -1,92 +0,0 @@ -// The project panel: what the project builds (executables, tests, libraries), for which target and -// platform, how its last build went, and the language service that answers its sources. The actions -// are the command table's: the editor runs them as it runs the same commands from its menus. -(function (root) { - "use strict"; - const ui = root.McppUi; - const { h, t } = ui; - - function languageServiceCard(status, refresh) { - if (!status) return null; - const state = status.state === "ok" ? "ok" : status.state === "missing" ? "error" : "warning"; - return ui.section( - t("C++ Modules language service"), - h("p", ui.badge(state, status.state === "ok" ? `mcppls ${status.version}` : t("needs attention")), " ", status.message), - h("div.actions", - (status.actions || []).map((a) => ui.button(a.title, () => ui.host("languageService", { action: a.id }))), - ui.button(t("Check again"), refresh, { secondary: true }))); - } - - function list(title, rows, render) { - return ui.section(`${title} (${rows.length})`, rows.length ? h("ul.list", rows.map(render)) : ui.empty(t("none"))); - } - - ui.panels.project = { - title: () => t("Project"), - mount(el, params) { - const rootDir = params.root; - let jobRunning = null; - const command = (id) => () => ui.host("runCommand", { command: id, root: rootDir }); - - async function render() { - ui.clear(el); - if (!rootDir) { el.appendChild(ui.empty(t("No mcpp project in this workspace"))); return; } - el.appendChild(h("p.muted", t("Loading…"))); - let summary, ls; - try { - [summary, ls] = await Promise.all([ui.rpc("project/summary", { root: rootDir }), ui.rpc("languageService/status", {})]); - } catch (e) { - ui.clear(el).appendChild(ui.errorBox(e)); - return; - } - ui.clear(el); - const name = rootDir.split(/[\\/]/).filter(Boolean).pop(); - const target = summary.target || ""; - el.appendChild(h("header.panel-head", - h("h1", name), - h("p.muted", rootDir), - h("div.actions", - ui.button(t("Build"), command("mcpp.build"), { disabled: !!jobRunning }), - ui.button(t("Run"), command("mcpp.run")), - ui.button(t("Debug"), command("mcpp.debug")), - ui.button(t("Test"), command("mcpp.test"), { disabled: !!jobRunning }), - ui.button(t("Pack"), () => ui.host("openPanel", { panel: "pack", root: rootDir }), { secondary: true }), - ui.button(t("Clean target"), command("mcpp.clean"), { secondary: true }), - jobRunning ? ui.button(t("Cancel"), () => ui.rpc("job/cancel", { jobId: jobRunning }), { secondary: true }) : null))); - - const facts = h("dl.facts", - h("dt", t("Target")), h("dd", target || t("this machine"), " ", ui.h("a", { href: "#", onclick: (e) => { e.preventDefault(); ui.host("openPanel", { panel: "targets", root: rootDir }); } }, t("change"))), - h("dt", t("Platform")), h("dd", summary.platformText || t("reported after the first build")), - summary.hostPlatformText ? [h("dt", t("Host platform")), h("dd", summary.hostPlatformText)] : null, - h("dt", t("Toolchain")), h("dd", summary.toolchain || "—")); - if (summary.lastBuild) { - const b = summary.lastBuild; - facts.append(h("dt", t("Last build")), - h("dd", ui.badge(b.status === "ok" ? "ok" : b.status === "cancelled" ? "info" : "error", b.status), " ", - ui.ago(b.finishedAt), b.client ? ` · ${b.client}` : "")); - } - el.appendChild(ui.section(t("Overview"), facts, summary.error ? ui.errorBox({ message: summary.error }) : null)); - el.appendChild(list(t("Executables"), summary.targets || [], (r) => - h("li", h("span", r.name), h("span.muted", r.configuration || "")))); - el.appendChild(list(t("Tests"), summary.tests || [], (r) => - h("li", h("span", r.name), ui.button(t("Run"), () => ui.host("runCommand", { command: "mcpp.test", root: rootDir, args: { filter: r.name } }), { secondary: true })))); - el.appendChild(list(t("Libraries"), summary.libraries || [], (r) => h("li", h("span", r.name), h("span.muted", r.kind)))); - el.appendChild(h("div.actions", - ui.button(t("Dependency graph"), () => ui.host("openPanel", { panel: "graph", root: rootDir, graph: "packages" }), { secondary: true }), - ui.button(t("Module graph"), () => ui.host("openPanel", { panel: "graph", root: rootDir, graph: "modules" }), { secondary: true }), - ui.button(t("Libraries"), () => ui.host("openPanel", { panel: "libraries", root: rootDir }), { secondary: true }))); - const card = languageServiceCard(ls, render); - if (card) el.appendChild(card); - } - - const offs = [ - ui.on("job/started", (p) => { if (p.root === rootDir) { jobRunning = p.jobId; render(); } }), - ui.on("job/finished", (p) => { if (p.root === rootDir || p.jobId === jobRunning) { jobRunning = null; render(); } }), - ui.on("result/changed", (p) => { if (p.root === rootDir) render(); }), - ui.on("manifest/changed", (p) => { if (p.root === rootDir) render(); }), - ]; - render(); - return () => offs.forEach((off) => off()); - }, - }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/panels/settings.js b/webui/js/panels/settings.js deleted file mode 100644 index 921d2b4..0000000 --- a/webui/js/panels/settings.js +++ /dev/null @@ -1,89 +0,0 @@ -// The settings panel: every setting of the registry (spec/settings.json), grouped, with what it -// takes; a change is written by the editor into its own settings store, and the backend's resolution -// (rejections, renamed keys) is shown beside it. Presets apply several at once. -(function (root) { - "use strict"; - const ui = root.McppUi; - const { h, t } = ui; - - function control(entry, value, write) { - if (entry.type === "boolean") - return h("input", { type: "checkbox", checked: !!value, onchange: (e) => write(e.target.checked) }); - if (entry.enum) - return h("select", { onchange: (e) => write(e.target.value) }, entry.enum.map((v) => h("option", { value: v, selected: v === value }, v))); - if (entry.type === "number") - return h("input", { type: "number", value: value ?? "", min: entry.minimum, max: entry.maximum, onchange: (e) => write(Number(e.target.value)) }); - if (entry.type === "array") - return h("textarea", { rows: 2, placeholder: t("one per line"), onchange: (e) => write(e.target.value.split("\n").map((s) => s.trim()).filter(Boolean)) }, - (value || []).join("\n")); - return h("input", { type: "text", value: value ?? "", onchange: (e) => write(e.target.value) }); - } - - ui.panels.settings = { - title: () => t("Settings"), - mount(el) { - const filter = h("input", { type: "search", placeholder: t("Filter settings") }); - const advanced = h("input", { type: "checkbox" }); - const body = h("div"); - let registry, values = {}, resolved = {}; - - async function load() { - ui.clear(body).appendChild(h("p.muted", t("Loading…"))); - try { - [registry, values] = await Promise.all([ui.rpc("settings/registry", {}), ui.host("settings", {})]); - resolved = await ui.rpc("settings/update", { settings: values }); - } catch (e) { ui.clear(body).appendChild(ui.errorBox(e)); return; } - draw(); - } - - function draw() { - ui.clear(body); - const needle = filter.value.toLowerCase(); - const rejected = new Map((resolved.rejected || []).map((r) => [r.key, r])); - const groups = new Map(); - for (const s of registry.settings) { - if (s.tier === "advanced" && !advanced.checked) continue; - if (needle && !`${s.key} ${s.title} ${s.description}`.toLowerCase().includes(needle)) continue; - if (!groups.has(s.group)) groups.set(s.group, []); - groups.get(s.group).push(s); - } - if ((resolved.renames || []).length) - body.appendChild(ui.section(t("Renamed settings"), - h("ul.list", resolved.renames.map((r) => h("li", h("code", r.from), " → ", h("code", r.to)))), - ui.button(t("Move them"), async () => { await ui.host("migrateSettings", { renames: resolved.renames }); load(); }))); - const groupTitle = new Map((registry.groups || []).map((g) => [g.id, g.title])); - for (const [group, entries] of groups) { - body.appendChild(ui.section(groupTitle.get(group) || group, entries.map((s) => { - const value = Object.prototype.hasOwnProperty.call(values, s.key) ? values[s.key] : s.default; - const bad = rejected.get(s.key); - return h("div.setting", - h("label", h("strong", s.title || s.key), h("code.muted", ` ${s.key}`)), - h("p.muted", s.description), - control(s, value, async (v) => { - await ui.host("setSetting", { key: s.key, value: v }); - values[s.key] = v; - resolved = await ui.rpc("settings/update", { settings: values }); - draw(); - }), - s.applies === "restart" ? h("p.note", t("Takes effect after a restart.")) : null, - bad ? h("p.error", t("Not accepted ({0}); expected {1}. The default is used.", bad.reason, bad.expectation)) : null); - }))); - } - if ((registry.presets || []).length) - body.appendChild(ui.section(t("Presets"), h("div.actions.wrap", registry.presets.map((p) => - ui.button(p.title || p.id, async () => { - const preset = await ui.rpc("settings/preset", { id: p.id }); - const entries = Object.entries(preset.values || {}); - // An empty preset is every setting at its default. - if (!entries.length) await ui.host("resetSettings", { keys: registry.settings.map((s) => s.key) }); - for (const [k, v] of entries) await ui.host("setSetting", { key: k, value: v }); - load(); - }, { secondary: true, title: p.description || "" }))))); - } - filter.addEventListener("input", ui.debounce(draw, 150)); - advanced.addEventListener("change", draw); - el.append(h("header.panel-head", h("h1", t("Settings")), h("div.toolbar", filter, h("label", advanced, ` ${t("Show advanced settings")}`))), body); - load(); - }, - }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/panels/targets.js b/webui/js/panels/targets.js deleted file mode 100644 index 3b0cf5d..0000000 --- a/webui/js/panels/targets.js +++ /dev/null @@ -1,63 +0,0 @@ -// The targets panel: every target mcpp knows, the toolchains installed for it, and the platform -// that configures it (mcpp's platforms: a built-in convention, a pinned platform package, or one the -// project or this machine chose), with each of the platform's fields and where its value came from. -// Choosing a target writes `mcpp.build.target`; build, run, test, debug and pack then use it. -(function (root) { - "use strict"; - const ui = root.McppUi; - const { h, t } = ui; - - function fields(platform) { - const f = (platform && platform.fields) || {}; - const keys = Object.keys(f); - if (!keys.length) return null; - return h("table.fields", keys.map((k) => { - const v = f[k] && Object.prototype.hasOwnProperty.call(f[k], "value") ? f[k].value : f[k]; - return h("tr", h("td", h("code", k)), h("td", typeof v === "string" ? v : JSON.stringify(v)), h("td.muted", (f[k] && f[k].source) || "")); - })); - } - - ui.panels.targets = { - title: () => t("Targets and Platforms"), - mount(el, params) { - const rootDir = params.root; - async function render() { - ui.clear(el).appendChild(h("p.muted", t("Loading…"))); - let r; - try { r = await ui.rpc("targets/list", { root: rootDir }); } catch (e) { ui.clear(el).appendChild(ui.errorBox(e)); return; } - ui.clear(el); - el.appendChild(h("header.panel-head", - h("h1", t("Targets and Platforms")), - h("p", t("This machine: {0}", r.host), r.hostPlatformText ? h("span.muted", ` · ${t("host platform")}: ${r.hostPlatformText}`) : null), - r.lastPlan && r.lastPlan.configuration ? h("p.muted", t("Last build: {0}, {1}", r.lastPlan.configuration.triple, r.lastPlan.platformText || t("host conventions"))) : null)); - const rows = r.rows.map((row) => { - const details = h("details", - h("summary", row.platformText || t("platform not reported by this mcpp")), - fields(row.platform) || h("p.muted", t("No platform fields reported."))); - const choose = row.selected ? ui.badge("ok", t("selected")) - : ui.button(t("Use"), async () => { await ui.host("setSetting", { key: "mcpp.build.target", value: row.host ? "" : row.target }); render(); }, { secondary: true }); - const toolchains = row.toolchains.length - ? row.toolchains.map((tc) => h("div", tc.spec || "—", " ", ui.badge(tc.status === "installed" ? "ok" : "info", tc.status), tc.default ? ` ${t("(default)")}` : "")) - : h("span.muted", "—"); - const install = !row.installed && row.pin - ? ui.button(t("Install"), () => ui.rpc("toolchains/install", { spec: row.pin, target: row.target, root: rootDir }), { secondary: true }) - : null; - return h(`tr${row.selected ? ".selected" : ""}`, - h("td", h("strong", row.target), row.host ? h("span.muted", ` ${t("(this machine)")}`) : null, row.note ? h("div.muted", row.note) : null), - h("td", toolchains, install), - h("td", details), - h("td", choose)); - }); - el.appendChild(h("table.targets", - h("thead", h("tr", h("th", t("Target")), h("th", t("Toolchains")), h("th", t("Platform")), h("th", ""))), - h("tbody", rows))); - } - const offs = [ - ui.on("job/finished", (p) => { if (p.kind === "toolchain") render(); }), - ui.on("job/plan", (p) => { if (p.root === rootDir) render(); }), - ]; - render(); - return () => offs.forEach((off) => off()); - }, - }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/webui/js/types.d.ts b/webui/js/types.d.ts deleted file mode 100644 index 1c1d8e4..0000000 --- a/webui/js/types.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// The panels' one global, for `tsc --checkJs` (vscode: npm run check:webui). Not loaded at run time. -interface McppPanel { - title(params: any): string; - mount(el: HTMLElement, params: any): void | (() => void); -} - -interface McppUiApi { - panels: Record; - setBundle(b: Record): void; - t(key: string, ...args: unknown[]): string; - formatBytes(bytes: number): string; - ago(when: number | null | undefined, now?: number): string; - debounce(fn: (...args: any[]) => void, ms: number): (...args: any[]) => void; - h(spec: string, props?: any, ...children: any[]): any; - clear(el: E): E; - badge(state: string, text: string): HTMLElement; - button(text: string, onclick: (ev?: Event) => unknown, opts?: { secondary?: boolean; disabled?: boolean; title?: string }): HTMLButtonElement; - section(title: string, ...children: any[]): HTMLElement; - empty(text: string): HTMLElement; - errorBox(error: any): HTMLElement; - rpc(method: string, params?: unknown): Promise; - host(action: string, params?: unknown): Promise; - on(method: string, fn: (...args: any[]) => void): () => void; - ready(): void; - state: { get(): any; set(s: any): void }; - editor: "vscode" | "jetbrains"; - graphLayout: { - layout(nodes: any[], edges: any[], opts?: any): { nodes: any[]; edges: any[]; width: number; height: number }; - neighbourhood(nodes: any[], edges: any[], focus: string, depth?: number): { nodes: any[]; edges: any[] }; - }; - cleanup(action: string, root?: string): Promise; -} - -interface Window { - McppUi: McppUiApi; - acquireVsCodeApi?: () => { postMessage(m: unknown): void; getState(): any; setState(s: any): void }; - mcppHostSend?: (text: string) => void; - mcppHostReceive: (m: unknown) => void; - mcppHostAttached: () => void; - __mcppState: any; -} - -declare var McppUi: McppUiApi; diff --git a/webui/styles.css b/webui/styles.css deleted file mode 100644 index 7e4f110..0000000 --- a/webui/styles.css +++ /dev/null @@ -1,71 +0,0 @@ -/* The panels' look. Colours come from the editor: VS Code's theme variables, or the --mcpp-* ones - the JetBrains host sets from its own theme in `init`; the fallbacks are a neutral dark theme. */ -:root { - --fg: var(--mcpp-fg, var(--vscode-foreground, #cccccc)); - --bg: var(--mcpp-bg, var(--vscode-editor-background, #1e1e1e)); - --muted: var(--mcpp-muted, var(--vscode-descriptionForeground, #9d9d9d)); - --border: var(--mcpp-border, var(--vscode-panel-border, #3c3c3c)); - --card: var(--mcpp-card, var(--vscode-sideBar-background, #252526)); - --accent: var(--mcpp-accent, var(--vscode-button-background, #0e639c)); - --accent-fg: var(--mcpp-accent-fg, var(--vscode-button-foreground, #ffffff)); - --secondary: var(--mcpp-secondary, var(--vscode-button-secondaryBackground, #3a3d41)); - --secondary-fg: var(--mcpp-secondary-fg, var(--vscode-button-secondaryForeground, #ffffff)); - --input: var(--mcpp-input, var(--vscode-input-background, #3c3c3c)); - --ok: var(--mcpp-ok, #89d185); - --warn: var(--mcpp-warn, #e2c08d); - --err: var(--mcpp-err, var(--vscode-errorForeground, #f48771)); - --font: var(--mcpp-font, var(--vscode-font-family, system-ui, sans-serif)); - --mono: var(--mcpp-mono, var(--vscode-editor-font-family, ui-monospace, monospace)); - --size: var(--mcpp-font-size, var(--vscode-font-size, 13px)); -} -* { box-sizing: border-box; } -body { margin: 0; padding: 12px 16px; color: var(--fg); background: var(--bg); font: var(--size)/1.45 var(--font); } -h1 { font-size: 1.4em; margin: 0 0 4px; } -h2 { font-size: 1.05em; margin: 0 0 8px; } -code, pre { font-family: var(--mono); } -a { color: var(--accent); } -.muted, .empty { color: var(--muted); } -.panel-head { margin-bottom: 12px; } -.card { background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; margin: 10px 0; } -.actions, .toolbar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-top: 8px; } -button { background: var(--accent); color: var(--accent-fg); border: 0; border-radius: 3px; padding: 4px 10px; font: inherit; cursor: pointer; } -button.secondary { background: var(--secondary); color: var(--secondary-fg); } -button:disabled { opacity: .5; cursor: default; } -input, select, textarea { background: var(--input); color: var(--fg); border: 1px solid var(--border); border-radius: 3px; padding: 3px 6px; font: inherit; } -input[type="search"] { min-width: 220px; } -textarea { width: 100%; font-family: var(--mono); } -.badge { display: inline-block; padding: 0 6px; border-radius: 8px; font-size: .85em; border: 1px solid currentColor; } -.badge-ok { color: var(--ok); } .badge-warning { color: var(--warn); } .badge-error { color: var(--err); } .badge-info, .badge-running { color: var(--muted); } -.error { color: var(--err); } -.error pre { white-space: pre-wrap; color: var(--fg); } -.note { color: var(--warn); } -dl.facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; margin: 0; } -dl.facts dt { color: var(--muted); } -dl.facts dd { margin: 0; } -ul.list { list-style: none; padding: 0; margin: 0; } -ul.list li { display: flex; justify-content: space-between; gap: 8px; padding: 3px 0; border-bottom: 1px solid var(--border); } -ul.list li:last-child { border-bottom: 0; } -li.package { display: block !important; cursor: pointer; } -li.package:hover { background: var(--card); } -table { border-collapse: collapse; width: 100%; } -td, th { padding: 4px 6px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--border); } -table.targets tr.selected { background: var(--card); } -table.bars td.num { text-align: right; white-space: nowrap; width: 7em; } -table.bars td.bar { width: 40%; } -table.bars td.bar div { height: 8px; background: var(--accent); border-radius: 2px; } -table.fields td { border: 0; padding: 1px 6px; } -.setting { padding: 6px 0; border-bottom: 1px solid var(--border); } -.setting:last-child { border-bottom: 0; } -.setting p { margin: 2px 0 4px; } -.graph-canvas { position: relative; height: calc(100vh - 150px); min-height: 320px; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; } -.graph-canvas svg { display: block; } -.node rect { fill: var(--card); stroke: var(--border); } -.node.focus rect { stroke: var(--accent); stroke-width: 2; } -.node.kind-external rect { stroke-dasharray: 4 3; } -.node text { fill: var(--fg); font-size: 12px; font-family: var(--mono); pointer-events: none; } -.node { cursor: pointer; } -.edge { fill: none; stroke: var(--muted); stroke-width: 1; } -.edge.back { stroke: var(--warn); stroke-dasharray: 4 3; } -.arrow { fill: var(--muted); } -pre.log { max-height: 40vh; overflow: auto; background: var(--card); padding: 8px; border-radius: 4px; white-space: pre-wrap; } -details summary { cursor: pointer; } diff --git a/webui/tests/layout.test.js b/webui/tests/layout.test.js deleted file mode 100644 index ac6ac93..0000000 --- a/webui/tests/layout.test.js +++ /dev/null @@ -1,41 +0,0 @@ -// The layered layout and the neighbourhood: every edge that is not part of a cycle points down a -// layer, nodes of a layer do not overlap, and a neighbourhood holds exactly the nodes in reach. -const assert = require("node:assert/strict"); -const { test } = require("node:test"); -const { load } = require("./load"); - -const ui = load(["js/core.js", "js/graph-layout.js"]); - -test("edges point from a layer to a lower one; cycles are drawn, not obeyed", () => { - const nodes = ["app", "lib", "std", "a", "b"].map((id) => ({ id })); - const edges = [{ from: "app", to: "lib" }, { from: "lib", to: "std" }, { from: "app", to: "std" }, { from: "a", to: "b" }, { from: "b", to: "a" }]; - const l = ui.graphLayout.layout(nodes, edges); - const at = new Map(l.nodes.map((n) => [n.id, n])); - for (const e of l.edges) if (!e.back) assert.ok(at.get(e.from).layer < at.get(e.to).layer, `${e.from} -> ${e.to}`); - assert.equal(l.edges.filter((e) => e.back).length, 1); - const byLayer = new Map(); - for (const n of l.nodes) { - const row = byLayer.get(n.layer) || []; - for (const m of row) assert.ok(Math.abs(m.x - n.x) >= n.width, "overlap"); - row.push(n); - byLayer.set(n.layer, row); - } - assert.ok(l.width > 0 && l.height > 0); -}); - -test("a neighbourhood is the nodes within reach, both ways", () => { - const nodes = ["a", "b", "c", "d"].map((id) => ({ id })); - const edges = [{ from: "a", to: "b" }, { from: "b", to: "c" }, { from: "c", to: "d" }]; - const one = ui.graphLayout.neighbourhood(nodes, edges, "b", 1); - assert.deepEqual(one.nodes.map((n) => n.id).sort(), ["a", "b", "c"]); - const two = ui.graphLayout.neighbourhood(nodes, edges, "b", 2); - assert.equal(two.nodes.length, 4); -}); - -test("strings and sizes", () => { - ui.setBundle({ "{0} packages": "{0} 个包" }); - assert.equal(ui.t("{0} packages", 3), "3 个包"); - assert.equal(ui.t("missing {0}", "x"), "missing x"); - assert.equal(ui.formatBytes(1536), "1.5 KiB"); - assert.equal(ui.formatBytes(10), "10 B"); -}); diff --git a/webui/tests/load.js b/webui/tests/load.js deleted file mode 100644 index f4d379d..0000000 --- a/webui/tests/load.js +++ /dev/null @@ -1,16 +0,0 @@ -// Loads the panels' classic scripts into a context: Node's own global for the pure ones, a jsdom -// window for the ones that render. -const fs = require("node:fs"); -const path = require("node:path"); -const vm = require("node:vm"); - -const webui = path.resolve(__dirname, ".."); - -function load(files, context) { - const g = context || { console, setTimeout, clearTimeout }; - if (!context) { g.globalThis = g; vm.createContext(g); } - for (const f of files) vm.runInContext(fs.readFileSync(path.join(webui, f), "utf8"), g, { filename: f }); - return g.McppUi; -} - -module.exports = { load, webui }; diff --git a/webui/tests/panels.test.js b/webui/tests/panels.test.js deleted file mode 100644 index 56f3eea..0000000 --- a/webui/tests/panels.test.js +++ /dev/null @@ -1,119 +0,0 @@ -// Every panel, mounted in a DOM against the real backend over the mock mcpp: the host is played the -// way the JetBrains plugin plays it (window.mcppHostSend out, window.mcppHostReceive in), and each -// panel must render what the golden fixtures hold without an error box. -const assert = require("node:assert/strict"); -const fs = require("node:fs"); -const os = require("node:os"); -const path = require("node:path"); -const { test, before, after } = require("node:test"); - -const repo = path.resolve(__dirname, "..", ".."); -// The VS Code adapter holds the DOM, the wire and the harness the panels are tested with. -const vscodeDir = path.join(repo, "mide-plugins", "vscode"); -const { JSDOM } = require(path.join(vscodeDir, "node_modules", "jsdom")); -const { Backend } = require(path.join(vscodeDir, "out", "src", "backend", "client.js")); -const { built } = require(path.join(vscodeDir, "out", "test", "unit", "harness.js")); - -const scripts = ["core.js", "host.js", "graph-layout.js", "panels/project.js", "panels/libraries.js", "panels/cache.js", - "panels/settings.js", "panels/graph.js", "panels/pack.js", "panels/targets.js", "app.js"].map((f) => path.join(repo, "webui", "js", f)); - -let backend, root, skip; -const hostCalls = []; - -before(async () => { - const program = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); - if (!program || !mock) { skip = "build the backend first (mcpp build)"; return; } - root = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-webui-")); - fs.mkdirSync(path.join(root, "src")); - fs.writeFileSync(path.join(root, "mcpp.toml"), '[package]\nname = "app"\nversion = "0.1.0"\n'); - backend = new Backend({ program }); - await backend.request("initialize", { editor: "jetbrains", settings: { "mcpp.path": mock }, workspaceFolders: [root] }); -}); - -after(async () => { if (backend) await backend.stop(); }); - -function mount(panel, params) { - const dom = new JSDOM('
      ', { runScripts: "outside-only", pretendToBeVisual: true }); - const w = dom.window; - w.mcppHostSend = async (text) => { - const m = JSON.parse(text); - if (m.type === "ready") { w.mcppHostReceive({ type: "init", panel, params, bundle: {} }); return; } - if (m.type === "rpc") { - try { w.mcppHostReceive({ type: "rpcResult", id: m.id, result: await backend.request(m.method, m.params) }); } - catch (e) { w.mcppHostReceive({ type: "rpcError", id: m.id, error: { message: e.message } }); } - return; - } - if (m.type === "host") { - hostCalls.push(m.action); - const answers = { settings: {}, setSetting: true, confirm: { confirmed: false } }; - w.mcppHostReceive({ type: "hostResult", id: m.id, result: answers[m.action] ?? true }); - } - }; - for (const f of scripts) w.eval(fs.readFileSync(f, "utf8")); - return w; -} - -async function settle(w, predicate, what) { - for (let i = 0; i < 200; i++) { - if (predicate(w.document)) return; - await new Promise((r) => setTimeout(r, 25)); - } - assert.fail(`${what}: ${w.document.getElementById("app").textContent.slice(0, 400)}`); -} - -const noError = (w) => assert.equal(w.document.querySelector(".error"), null, w.document.querySelector(".error")?.textContent); - -test("project", async (t) => { - if (skip) return t.skip(skip); - const w = mount("project", { root }); - await settle(w, (d) => d.body.textContent.includes("Tests (1)"), "the project's test"); - assert.ok(w.document.body.textContent.includes("app")); - noError(w); -}); - -test("targets", async (t) => { - if (skip) return t.skip(skip); - const w = mount("targets", { root }); - await settle(w, (d) => d.querySelectorAll("table.targets tbody tr").length > 0, "target rows"); - assert.equal(w.document.querySelectorAll("tr.selected").length, 1); - noError(w); -}); - -test("libraries", async (t) => { - if (skip) return t.skip(skip); - const w = mount("libraries", { root }); - await settle(w, (d) => d.querySelectorAll("li.package").length > 0, "packages"); - w.document.querySelector("li.package").dispatchEvent(new w.MouseEvent("click", { bubbles: true })); - await settle(w, (d) => d.querySelector("select") && d.body.textContent.includes("Add to mcpp.toml"), "a package page"); - noError(w); -}); - -test("cache", async (t) => { - if (skip) return t.skip(skip); - const w = mount("cache", { root }); - await settle(w, (d) => d.querySelectorAll("table.bars tr").length > 0, "cache bars"); - noError(w); -}); - -test("settings", async (t) => { - if (skip) return t.skip(skip); - const w = mount("settings", {}); - await settle(w, (d) => d.body.textContent.includes("mcpp.path"), "the settings"); - noError(w); -}); - -test("graphs", async (t) => { - if (skip) return t.skip(skip); - for (const graph of ["packages", "modules"]) { - const w = mount("graph", { root, graph }); - await settle(w, (d) => d.querySelectorAll(".node").length > 0, `${graph} nodes`); - noError(w); - } -}); - -test("pack", async (t) => { - if (skip) return t.skip(skip); - const w = mount("pack", { root }); - await settle(w, (d) => d.querySelectorAll("select option").length > 2, "pack formats"); - noError(w); -}); diff --git a/webui/tsconfig.json b/webui/tsconfig.json deleted file mode 100644 index 0ab6899..0000000 --- a/webui/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "noEmit": true, - "target": "ES2022", - "lib": ["ES2022", "DOM"], - "strict": false, - "noImplicitAny": false, - "skipLibCheck": true, - "types": [] - }, - "include": ["js/**/*.js", "js/types.d.ts"] -} From ec94f6f68a075980d4a52d13a6a731436d9de68e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 02:33:21 +0900 Subject: [PATCH 079/119] webui: the workbench -- mcpp-vscode's pages, unchanged, in a browser host webui/src answers what the pages ask of VS Code (the vscode module) and of Node (fs, path, os, crypto, url, util) through a bridge to the IDE that hosts the page: requests through mcppHostSend/mcppHostReceive, synchronous file reads through a prompt the host answers. Views are tabs in the manifest's order and names, webviews are iframes with the page's own CSP, nonce and the IDE's --vscode-* theme. tools/bundle-webui.mjs bundles it with esbuild beside the pages' media, the codicons and mermaid, and lists the files in files.txt. The previous panels (webui/js) are removed: the workbench shows the pages VS Code shows. webui/tests/workbench.test.mjs runs the workbench in Chromium over a real mcpp-ide serve and the mock mcpp: the tabs, the project tree, the cache page, the libraries over an index and a package's page, a settings write, and the graph page, with no page error and no CSP refusal. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mide-plugins/vscode/.gitignore | 1 + mide-plugins/vscode/.vscodeignore | 1 + mide-plugins/vscode/package-lock.json | 56 ++- mide-plugins/vscode/package.json | 7 +- mide-plugins/vscode/tools/bundle-webui.mjs | 60 +++ webui/.gitignore | 2 + webui/index.html | 12 + webui/src/bridge.ts | 106 ++++++ webui/src/main.ts | 85 +++++ webui/src/node/buffer.ts | 2 + webui/src/node/child_process.ts | 9 + webui/src/node/crypto.ts | 31 ++ webui/src/node/fs.ts | 118 ++++++ webui/src/node/os.ts | 9 + webui/src/node/path.ts | 110 ++++++ webui/src/node/process.ts | 10 + webui/src/node/url.ts | 15 + webui/src/node/util.ts | 7 + webui/src/vscode.ts | 422 ++++++++++++++++++++ webui/src/workbench.ts | 424 +++++++++++++++++++++ webui/tests/workbench.test.mjs | 199 ++++++++++ webui/tsconfig.json | 34 ++ webui/workbench.css | 38 ++ 23 files changed, 1748 insertions(+), 10 deletions(-) create mode 100644 mide-plugins/vscode/tools/bundle-webui.mjs create mode 100644 webui/.gitignore create mode 100644 webui/index.html create mode 100644 webui/src/bridge.ts create mode 100644 webui/src/main.ts create mode 100644 webui/src/node/buffer.ts create mode 100644 webui/src/node/child_process.ts create mode 100644 webui/src/node/crypto.ts create mode 100644 webui/src/node/fs.ts create mode 100644 webui/src/node/os.ts create mode 100644 webui/src/node/path.ts create mode 100644 webui/src/node/process.ts create mode 100644 webui/src/node/url.ts create mode 100644 webui/src/node/util.ts create mode 100644 webui/src/vscode.ts create mode 100644 webui/src/workbench.ts create mode 100644 webui/tests/workbench.test.mjs create mode 100644 webui/tsconfig.json create mode 100644 webui/workbench.css diff --git a/mide-plugins/vscode/.gitignore b/mide-plugins/vscode/.gitignore index c11d0ed..e89c910 100644 --- a/mide-plugins/vscode/.gitignore +++ b/mide-plugins/vscode/.gitignore @@ -15,3 +15,4 @@ dist/ # The backend the package carries (tools/stage-backend.mjs). /backend/ +.webui-test/ diff --git a/mide-plugins/vscode/.vscodeignore b/mide-plugins/vscode/.vscodeignore index 483bb46..14e9820 100644 --- a/mide-plugins/vscode/.vscodeignore +++ b/mide-plugins/vscode/.vscodeignore @@ -6,6 +6,7 @@ # 本地验收 profile(tools/dev-profile.mjs 生成)与 e2e 的 VS Code 缓存都不属于产物 .dev-profile/** .vscode-test/** +.webui-test/** docs/** src/** test/** diff --git a/mide-plugins/vscode/package-lock.json b/mide-plugins/vscode/package-lock.json index b01f1c5..a2b06c1 100644 --- a/mide-plugins/vscode/package-lock.json +++ b/mide-plugins/vscode/package-lock.json @@ -10,12 +10,14 @@ "license": "Apache-2.0", "devDependencies": { "@mermaid-js/tiny": "12.2.0", + "@noble/hashes": "^1.8.0", "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", "@types/vscode": "^1.91.0", "@vscode/codicons": "0.0.46-24", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "buffer": "^6.0.3", "esbuild": "^0.25.12", "mocha": "^11.8.0", "ovsx": "^1.2.0", @@ -1425,6 +1427,19 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@node-rs/crc32": { "version": "1.10.8", "resolved": "https://registry.npmjs.org/@node-rs/crc32/-/crc32-1.10.8.tgz", @@ -2412,8 +2427,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/binaryextensions": { "version": "6.11.0", @@ -2444,6 +2458,32 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2492,9 +2532,9 @@ "license": "ISC" }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, "funding": [ { @@ -2511,10 +2551,9 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-crc32": { @@ -3874,8 +3913,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "7.0.6", diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json index 9a0c32d..5d7cb47 100644 --- a/mide-plugins/vscode/package.json +++ b/mide-plugins/vscode/package.json @@ -1364,16 +1364,21 @@ "test:graphs-ui": "npm run compile && node tools/test-graphs-ui.mjs", "gen": "cd ../.. && mcpp run mcpp-ide-dev -- gen vscode", "stage:backend": "node tools/stage-backend.mjs", - "test:parity": "npm run compile && node --test \"dist/test/parity/*.test.js\"" + "test:parity": "npm run compile && node --test \"dist/test/parity/*.test.js\"", + "bundle:webui": "node tools/bundle-webui.mjs", + "check:webui": "tsc -p ../../webui/tsconfig.json", + "test:webui": "npm run compile && node tools/bundle-webui.mjs --out .webui-test && node --test ../../webui/tests/workbench.test.mjs" }, "devDependencies": { "@mermaid-js/tiny": "12.2.0", + "@noble/hashes": "^1.8.0", "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", "@types/vscode": "^1.91.0", "@vscode/codicons": "0.0.46-24", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "buffer": "^6.0.3", "esbuild": "^0.25.12", "mocha": "^11.8.0", "ovsx": "^1.2.0", diff --git a/mide-plugins/vscode/tools/bundle-webui.mjs b/mide-plugins/vscode/tools/bundle-webui.mjs new file mode 100644 index 0000000..ae4bb41 --- /dev/null +++ b/mide-plugins/vscode/tools/bundle-webui.mjs @@ -0,0 +1,60 @@ +/** + * The workbench mcpp-clion shows (webui/): mcpp-vscode's page modules, unchanged, with `vscode` and + * Node's built-ins answered by webui/src, bundled into one script, beside the shell page, its + * stylesheet, the codicons, and the files the pages load (media/, dist/vendor/), into + * webui/dist (or the directory named by --out), with files.txt listing them (what mcpp-clion + * extracts from its jar). Nothing it writes is kept in the repository. + */ +import { build } from "esbuild"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const extension = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const webui = path.resolve(extension, "..", "..", "webui"); +const at = process.argv.indexOf("--out"); +const out = at > 0 ? path.resolve(process.argv[at + 1]) : path.join(webui, "dist"); +const shim = (name) => path.join(webui, "src", ...name.split("/")); + +fs.rmSync(out, { recursive: true, force: true }); +fs.mkdirSync(out, { recursive: true }); +await build({ + entryPoints: [shim("main.ts")], + bundle: true, + format: "iife", + platform: "browser", + target: "chrome120", + outfile: path.join(out, "workbench.js"), + alias: { + vscode: shim("vscode.ts"), + "node:fs": shim("node/fs.ts"), + "node:path": shim("node/path.ts"), + "node:os": shim("node/os.ts"), + "node:crypto": shim("node/crypto.ts"), + "node:url": shim("node/url.ts"), + "node:util": shim("node/util.ts"), + "node:child_process": shim("node/child_process.ts"), + }, + inject: [shim("node/process.ts"), shim("node/buffer.ts")], + nodePaths: [path.join(extension, "node_modules")], + logLevel: "warning", + legalComments: "eof", +}); +for (const file of ["index.html", "workbench.css"]) fs.copyFileSync(path.join(webui, file), path.join(out, file)); +const codicons = path.join(extension, "node_modules", "@vscode", "codicons", "dist"); +for (const file of ["codicon.css", "codicon.ttf"]) fs.copyFileSync(path.join(codicons, file), path.join(out, file)); +fs.cpSync(path.join(extension, "media"), path.join(out, "media"), { recursive: true }); +fs.mkdirSync(path.join(out, "dist", "vendor"), { recursive: true }); +fs.copyFileSync(path.join(extension, "node_modules", "@mermaid-js", "tiny", "dist", "mermaid.tiny.js"), path.join(out, "dist", "vendor", "mermaid.js")); +fs.copyFileSync(path.join(extension, "node_modules", "@mermaid-js", "tiny", "LICENSE"), path.join(out, "dist", "vendor", "MERMAID-LICENSE")); +const listed = []; +const list = (dir) => { + for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const p = path.join(dir, e.name); + if (e.isDirectory()) list(p); + else listed.push(path.relative(out, p).split(path.sep).join("/")); + } +}; +list(out); +fs.writeFileSync(path.join(out, "files.txt"), `${listed.join("\n")}\n`); +console.log(`bundle-webui: ${path.relative(process.cwd(), out) || "."}`); diff --git a/webui/.gitignore b/webui/.gitignore new file mode 100644 index 0000000..0ae8788 --- /dev/null +++ b/webui/.gitignore @@ -0,0 +1,2 @@ +# The bundled workbench (mide-plugins/vscode: npm run bundle:webui). +dist/ diff --git a/webui/index.html b/webui/index.html new file mode 100644 index 0000000..064ad3e --- /dev/null +++ b/webui/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/webui/src/bridge.ts b/webui/src/bridge.ts new file mode 100644 index 0000000..61554f2 --- /dev/null +++ b/webui/src/bridge.ts @@ -0,0 +1,106 @@ +/** + * The bridge to the IDE that hosts the workbench (mcpp-clion's JCEF browser). + * + * Asynchronous requests travel through `window.mcppHostSend(text)` (a JBCefJSQuery the plugin + * injects) and come back through `window.mcppHostReceive(message)`; the IDE's notifications arrive + * the same way. A synchronous request -- what a synchronous file read of the page code needs -- is + * a `prompt()` the plugin answers through its JS dialog handler, which is the one synchronous + * channel a Chromium page has to its host. + */ + +type Pending = { resolve: (value: unknown) => void; reject: (error: Error) => void }; + +const pending = new Map(); +const listeners = new Map void>>(); +const queue: string[] = []; +let nextId = 1; + +declare global { + interface Window { + mcppHostSend?: (text: string) => void; + mcppHostReceive?: (message: unknown) => void; + mcppHostAttached?: () => void; + } +} + +function send(message: unknown): void { + const text = JSON.stringify(message); + if (typeof window.mcppHostSend === "function") window.mcppHostSend(text); + else queue.push(text); +} + +window.mcppHostAttached = () => { + while (queue.length > 0) window.mcppHostSend?.(queue.shift()!); +}; + +window.mcppHostReceive = (raw: unknown) => { + let message = raw as { type?: string; id?: number; result?: unknown; error?: { message?: string; code?: number; data?: unknown }; method?: string; params?: unknown }; + if (typeof raw === "string") { + try { message = JSON.parse(raw); } catch { return; } + } + if (!message || typeof message !== "object") return; + if (message.type === "result" || message.type === "error") { + const p = pending.get(message.id ?? -1); + pending.delete(message.id ?? -1); + if (!p) return; + if (message.type === "result") p.resolve(message.result); + else p.reject(Object.assign(new Error(message.error?.message ?? "failed"), { code: message.error?.code, data: message.error?.data })); + } else if (message.type === "notify" && typeof message.method === "string") { + for (const fn of listeners.get(message.method) ?? []) fn(message.params); + for (const fn of listeners.get("*") ?? []) fn({ method: message.method, params: message.params }); + } +}; + +/** An action of the IDE: `rpc` reaches the backend, everything else the IDE itself. */ +export function request(action: string, params: unknown = {}): Promise { + const id = nextId++; + return new Promise((resolve, reject) => { + pending.set(id, { resolve: resolve as (value: unknown) => void, reject }); + send({ type: "request", id, action, params }); + }); +} + +/** A method of the backend (`mcpp-ide serve`). */ +export function rpc(method: string, params: unknown = {}): Promise { + return request("rpc", { method, params }); +} + +/** The IDE's answer, now: for the synchronous file reads of the page code. */ +export function requestSync(action: string, params: unknown = {}): T { + const answer = window.prompt(`mcpp-host:${JSON.stringify({ action, params })}`, ""); + if (answer === null) throw new Error(`the IDE did not answer ${action}`); + const parsed = JSON.parse(answer) as { ok: boolean; result?: T; error?: string; code?: string }; + if (!parsed.ok) throw Object.assign(new Error(parsed.error ?? `${action} failed`), { code: parsed.code }); + return parsed.result as T; +} + +/** The IDE's notifications (`*` for every one): the backend's, and the IDE's own. */ +export function on(method: string, fn: (params: any) => void): { dispose(): void } { + if (!listeners.has(method)) listeners.set(method, new Set()); + listeners.get(method)!.add(fn); + return { dispose: () => listeners.get(method)?.delete(fn) }; +} + +/** What the IDE told the workbench when it loaded. */ +export interface HostInfo { + /** "linux" | "darwin" | "win32" */ + platform: string; + arch: string; + /** The IDE's language: "en" | "zh-cn" | ... */ + language: string; + home: string; + tmp: string; + env: Record; + /** The workbench's own files (media/, dist/vendor/) as a URL. */ + extensionUri: string; + workspaceFolders: string[]; + trusted: boolean; +} + +export let info: HostInfo = { + platform: "linux", arch: "x64", language: "en", home: "/", tmp: "/tmp", env: {}, extensionUri: "", workspaceFolders: [], trusted: false, +}; + +export function setInfo(next: HostInfo): void { + info = next; +} diff --git a/webui/src/main.ts b/webui/src/main.ts new file mode 100644 index 0000000..7f8df15 --- /dev/null +++ b/webui/src/main.ts @@ -0,0 +1,85 @@ +/** + * The workbench's start: the IDE says where it is (its system, language, folders, trust, theme), + * mcpp and mcppls are routed through the backend as the extension routes them, and the pages are + * registered by mcpp-vscode's own `registerPages` -- the same modules, wired the same way. + */ +import manifest from "../../mide-plugins/vscode/package.json"; +import { useMcppExecutor, useMcpplsExecutor } from "../../mide-plugins/vscode/src/cli/process"; +import { setLanguagePreference, type LanguagePreference } from "../../mide-plugins/vscode/src/i18n/t"; +import { read } from "../../mide-plugins/vscode/src/config/access"; +import { onDidChange } from "../../mide-plugins/vscode/src/config/access"; +import { registerPages } from "../../mide-plugins/vscode/src/pages"; +import { on, request, rpc, setInfo, type HostInfo } from "./bridge"; +import { configure as configurePath } from "./node/path"; +import { Uri, commands, loadSettings, window } from "./vscode"; +import { workbench } from "./workbench"; + +interface Init { + info: HostInfo; + theme: Record; + themeKind: "vscode-light" | "vscode-dark" | "vscode-high-contrast"; + /** `window`: the views as tabs; `settings`: the settings page alone (the IDE's settings dialog). */ + mode?: "window" | "settings"; +} + +interface Exec { exitCode: number; stdout: string; stderr: string; timedOut: boolean } + +async function start(): Promise { + const init = await request("init"); + setInfo(init.info); + configurePath(); + workbench.setTheme(init.theme, init.themeKind); + on("theme", (t: { theme: Record; themeKind: Init["themeKind"] }) => workbench.setTheme(t.theme, t.themeKind)); + await loadSettings(); + const language = () => setLanguagePreference(read("mcpp.ui.language")); + language(); + onDidChange(language); + + const exec = (method: string) => async (args: string[], cwd?: string, options: { timeoutMs?: number; maxBufferMiB?: number } = {}) => { + try { + const answer = await rpc(method, { + args, ...(cwd ? { cwd } : {}), ...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}), + maxOutputBytes: Math.max(1, options.maxBufferMiB ?? 16) * 1024 * 1024, + }); + return { exitCode: answer.timedOut && answer.exitCode === 0 ? 1 : answer.exitCode, stdout: answer.stdout, stderr: answer.stderr }; + } catch (error) { + return { exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error) }; + } + }; + useMcppExecutor((_executable, args, cwd, options) => exec("mcpp/exec")(args, cwd, options)); + useMcpplsExecutor(exec("languageService/exec")); + + const context = { + subscriptions: [] as Array<{ dispose(): unknown }>, + extensionUri: Uri.parse(init.info.extensionUri), + extension: { packageJSON: manifest }, + }; + registerPages(context as never, { + output: window.createOutputChannel("mcpp"), + // The backend runs the mcpp it located; the name is only what a message would say. + mcppExecutable: () => "mcpp", + }); + if (init.mode === "settings") { + document.body.classList.add("settings-only"); + void commands.executeCommand("mcpp.openSettings"); + } else { + workbench.showFirst(); + } + on("show", (p: { view: string; args?: unknown }) => { + // A panel the IDE asks for: the command that opens it, as the menus run it. + const open: Record = { + settings: "mcpp.openSettings", + "graph:packages": "mcpp.project.showDependencyGraph", + "graph:modules": "mcpp.project.showModuleDependencyGraph", + library: "mcpp.library.openDetail", + }; + const command = open[p.view]; + if (command) void commands.executeCommand(command, ...(p.args === undefined ? [] : [p.args])); + else workbench.focus(p.view); + }); +} + +start().catch((error) => { + document.body.textContent = `mcpp: ${error instanceof Error ? error.message : String(error)}`; + request("log", { text: `workbench: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n` }).catch(() => undefined); +}); diff --git a/webui/src/node/buffer.ts b/webui/src/node/buffer.ts new file mode 100644 index 0000000..98f5619 --- /dev/null +++ b/webui/src/node/buffer.ts @@ -0,0 +1,2 @@ +/** The `Buffer` global the page code uses (the graph's exports). */ +export { Buffer } from "buffer"; diff --git a/webui/src/node/child_process.ts b/webui/src/node/child_process.ts new file mode 100644 index 0000000..37c1af9 --- /dev/null +++ b/webui/src/node/child_process.ts @@ -0,0 +1,9 @@ +/** + * `node:child_process` for the page code. The workbench starts no program itself: mcpp and mcppls + * run through the backend (the process module's executors), and anything else is refused. + */ +export function execFile(file: string, _args: unknown, _options: unknown, callback?: (error: Error) => void): void { + const done = typeof _options === "function" ? (_options as (error: Error) => void) : callback; + done?.(new Error(`the workbench does not start programs (${file})`)); +} +export default { execFile }; diff --git a/webui/src/node/crypto.ts b/webui/src/node/crypto.ts new file mode 100644 index 0000000..2c63397 --- /dev/null +++ b/webui/src/node/crypto.ts @@ -0,0 +1,31 @@ +/** `node:crypto` for the page code: SHA-256 digests and random bytes. */ +import { sha256 } from "@noble/hashes/sha256"; +import { Buffer } from "buffer"; + +export function randomBytes(size: number): Buffer { + const bytes = new Uint8Array(size); + globalThis.crypto.getRandomValues(bytes); + return Buffer.from(bytes); +} + +class Hash { + private readonly chunks: Uint8Array[] = []; + constructor(algorithm: string) { + if (algorithm !== "sha256") throw new Error(`the workbench has no ${algorithm} digest`); + } + update(data: string | Uint8Array): this { + this.chunks.push(typeof data === "string" ? new TextEncoder().encode(data) : data); + return this; + } + digest(encoding?: "hex" | "base64"): string | Buffer { + const all = Buffer.concat(this.chunks); + const out = Buffer.from(sha256(all)); + return encoding ? out.toString(encoding) : out; + } +} + +export function createHash(algorithm: string): Hash { + return new Hash(algorithm); +} + +export default { randomBytes, createHash }; diff --git a/webui/src/node/fs.ts b/webui/src/node/fs.ts new file mode 100644 index 0000000..e1384e3 --- /dev/null +++ b/webui/src/node/fs.ts @@ -0,0 +1,118 @@ +/** + * `node:fs` for the page code: the IDE reads the files. A synchronous call is answered through the + * bridge's synchronous channel, a promise through its asynchronous one; a directory listing brings + * each entry's status along, so the `lstatSync` that follows a `readdirSync` (the walk of target/) + * costs nothing more. + */ +import { request, requestSync } from "../bridge"; + +interface StatJson { + type: "file" | "directory" | "symlink" | "other"; + size: number; + mtimeMs: number; + ino: number; +} + +interface EntryJson extends StatJson { + name: string; +} + +class Stats { + readonly size: number; + readonly mtimeMs: number; + readonly mtime: Date; + readonly ino: number | bigint; + readonly mtimeNs: bigint | number; + constructor(private readonly s: StatJson, bigint = false) { + this.size = s.size; + this.mtimeMs = s.mtimeMs; + this.mtime = new Date(s.mtimeMs); + this.ino = bigint ? BigInt(s.ino) : s.ino; + this.mtimeNs = bigint ? BigInt(Math.round(s.mtimeMs)) * 1000000n : s.mtimeMs * 1e6; + } + isFile(): boolean { return this.s.type === "file"; } + isDirectory(): boolean { return this.s.type === "directory"; } + isSymbolicLink(): boolean { return this.s.type === "symlink"; } +} + +class Dirent { + constructor(readonly name: string, private readonly type: StatJson["type"]) {} + isFile(): boolean { return this.type === "file"; } + isDirectory(): boolean { return this.type === "directory"; } + isSymbolicLink(): boolean { return this.type === "symlink"; } +} + +/** The statuses a listing brought, by full name: `lstatSync` reads them first. */ +const listed = new Map(); + +function enoent(path: string): Error { + return Object.assign(new Error(`ENOENT: no such file or directory, '${path}'`), { code: "ENOENT", path }); +} + +function childName(dir: string, name: string): string { + return /[\\/]$/.test(dir) ? `${dir}${name}` : `${dir}${dir.includes("\\") && !dir.includes("/") ? "\\" : "/"}${name}`; +} + +export function existsSync(path: string): boolean { + try { return requestSync("fs.stat", { path, follow: true }) !== null; } catch { return false; } +} + +export function statSync(path: string, options?: { bigint?: boolean }): Stats { + const s = requestSync("fs.stat", { path, follow: true }); + if (s === null) throw enoent(path); + return new Stats(s, options?.bigint === true); +} + +export function lstatSync(path: string, options?: { bigint?: boolean }): Stats { + const known = listed.get(path); + if (known) return new Stats(known, options?.bigint === true); + const s = requestSync("fs.stat", { path, follow: false }); + if (s === null) throw enoent(path); + return new Stats(s, options?.bigint === true); +} + +export function readFileSync(path: string, _encoding?: unknown): string { + const text = requestSync("fs.read", { path }); + if (text === null) throw enoent(path); + return text; +} + +export function readdirSync(path: string, options?: { withFileTypes?: boolean }): Array { + const entries = requestSync("fs.list", { path }); + if (entries === null) throw enoent(path); + for (const e of entries) listed.set(childName(path, e.name), e); + return options?.withFileTypes ? entries.map((e) => new Dirent(e.name, e.type)) : entries.map((e) => e.name); +} + +export const promises = { + async readFile(path: string, _encoding?: unknown): Promise { + const text = await request("fs.read", { path }); + if (text === null) throw enoent(path); + return text; + }, + async writeFile(path: string, data: string | Uint8Array): Promise { + const base64 = typeof data !== "string"; + const text = typeof data === "string" ? data : btoa(String.fromCharCode(...data)); + await request("fs.write", { path, text, base64 }); + }, + async readdir(path: string, options?: { withFileTypes?: boolean }): Promise> { + const entries = await request("fs.list", { path }); + if (entries === null) throw enoent(path); + return options?.withFileTypes ? entries.map((e) => new Dirent(e.name, e.type)) : entries.map((e) => e.name); + }, + async stat(path: string): Promise { + const s = await request("fs.stat", { path, follow: true }); + if (s === null) throw enoent(path); + return new Stats(s); + }, + async access(path: string): Promise { + if ((await request("fs.stat", { path, follow: true })) === null) throw enoent(path); + }, + async realpath(path: string): Promise { + const real = await request("fs.realpath", { path }); + if (real === null) throw enoent(path); + return real; + }, +}; + +export default { existsSync, statSync, lstatSync, readFileSync, readdirSync, promises }; diff --git a/webui/src/node/os.ts b/webui/src/node/os.ts new file mode 100644 index 0000000..6195b09 --- /dev/null +++ b/webui/src/node/os.ts @@ -0,0 +1,9 @@ +/** `node:os` for the page code: the IDE's machine. */ +import { info } from "../bridge"; + +export function homedir(): string { return info.home; } +export function tmpdir(): string { return info.tmp; } +export function platform(): string { return info.platform; } +export function arch(): string { return info.arch; } +export const EOL = "\n"; +export default { homedir, tmpdir, platform, arch, EOL }; diff --git a/webui/src/node/path.ts b/webui/src/node/path.ts new file mode 100644 index 0000000..bf6c3fc --- /dev/null +++ b/webui/src/node/path.ts @@ -0,0 +1,110 @@ +/** + * `node:path` for the page code in the workbench: the conventions of the system the IDE runs on + * (Windows names with `\` and a drive, POSIX names otherwise), as Node's own module answers them on + * that system, for the functions the page code calls. + */ +import { info } from "../bridge"; + +const windows = (): boolean => info.platform === "win32"; + +function splitRoot(p: string): [root: string, rest: string] { + if (!windows()) return p.startsWith("/") ? ["/", p.slice(1)] : ["", p]; + const drive = /^[A-Za-z]:/.exec(p); + if (drive) { + const after = p.slice(2); + return /^[\\/]/.test(after) ? [`${drive[0]}\\`, after.slice(1)] : [drive[0], after]; + } + const unc = /^[\\/]{2}[^\\/]+[\\/][^\\/]+/.exec(p); + if (unc) return [`${unc[0].replace(/\//g, "\\")}\\`, p.slice(unc[0].length + 1)]; + return /^[\\/]/.test(p) ? ["\\", p.slice(1)] : ["", p]; +} + +function parts(rest: string): string[] { + return rest.split(windows() ? /[\\/]+/ : /\/+/).filter((s) => s.length > 0); +} + +export function normalize(p: string): string { + if (p.length === 0) return "."; + const [root, rest] = splitRoot(p); + const out: string[] = []; + for (const part of parts(rest)) { + if (part === ".") continue; + if (part === "..") { + if (out.length > 0 && out[out.length - 1] !== "..") out.pop(); + else if (!root) out.push(".."); + continue; + } + out.push(part); + } + const trailing = /[\\/]$/.test(p) && out.length > 0 ? sep : ""; + const body = out.join(sep); + if (!root) return (body || ".") + trailing; + return root + body + trailing; +} + +export function isAbsolute(p: string): boolean { + if (!windows()) return p.startsWith("/"); + return /^[A-Za-z]:[\\/]/.test(p) || /^[\\/]/.test(p); +} + +export function join(...segments: string[]): string { + const joined = segments.filter((s) => s.length > 0).join(sep); + return joined.length === 0 ? "." : normalize(joined); +} + +export function resolve(...segments: string[]): string { + let resolved = ""; + for (let i = segments.length - 1; i >= 0 && !isAbsolute(resolved); i--) { + const s = segments[i]; + if (s.length === 0) continue; + resolved = resolved ? `${s}${sep}${resolved}` : s; + } + if (!isAbsolute(resolved)) resolved = `${info.workspaceFolders[0] ?? info.home}${sep}${resolved}`; + const n = normalize(resolved); + return n.length > 1 && /[\\/]$/.test(n) && !/^[A-Za-z]:\\$/.test(n) ? n.slice(0, -1) : n; +} + +export function relative(from: string, to: string): string { + const a = resolve(from), b = resolve(to); + const fold = (s: string) => (windows() ? s.toLowerCase() : s); + const [ra, xa] = splitRoot(a), [rb, xb] = splitRoot(b); + if (fold(ra) !== fold(rb)) return b; + const pa = parts(xa), pb = parts(xb); + let i = 0; + while (i < pa.length && i < pb.length && fold(pa[i]) === fold(pb[i])) i++; + return [...pa.slice(i).map(() => ".."), ...pb.slice(i)].join(sep); +} + +export function dirname(p: string): string { + const [root, rest] = splitRoot(p); + const ps = parts(rest); + if (ps.length <= 1) return root || "."; + return root + ps.slice(0, -1).join(sep); +} + +export function basename(p: string, ext?: string): string { + const [, rest] = splitRoot(p); + const ps = parts(rest); + const base = ps.length > 0 ? ps[ps.length - 1] : ""; + return ext && base.endsWith(ext) && base !== ext ? base.slice(0, -ext.length) : base; +} + +export function extname(p: string): string { + const base = basename(p); + const dot = base.lastIndexOf("."); + return dot <= 0 ? "" : base.slice(dot); +} + +export let sep = "/"; +export let delimiter = ":"; + +/** Called once the IDE has said which system it runs on. */ +export function configure(): void { + sep = windows() ? "\\" : "/"; + delimiter = windows() ? ";" : ":"; + api.sep = sep; + api.delimiter = delimiter; +} + +const api = { normalize, isAbsolute, join, resolve, relative, dirname, basename, extname, sep, delimiter }; +export default api; diff --git a/webui/src/node/process.ts b/webui/src/node/process.ts new file mode 100644 index 0000000..fa18662 --- /dev/null +++ b/webui/src/node/process.ts @@ -0,0 +1,10 @@ +/** The `process` global the page code reads: the IDE's system and environment. */ +import { info } from "../bridge"; + +export const process = { + get platform(): string { return info.platform; }, + get arch(): string { return info.arch; }, + get env(): Record { return info.env; }, + cwd: (): string => info.workspaceFolders[0] ?? info.home, + versions: {} as Record, +}; diff --git a/webui/src/node/url.ts b/webui/src/node/url.ts new file mode 100644 index 0000000..dd20551 --- /dev/null +++ b/webui/src/node/url.ts @@ -0,0 +1,15 @@ +/** `node:url` for the page code: file URLs. */ +import { info } from "../bridge"; + +export function fileURLToPath(url: string | URL): string { + const u = typeof url === "string" ? new URL(url) : url; + const p = decodeURIComponent(u.pathname); + return info.platform === "win32" ? p.replace(/^\/([A-Za-z]:)/, "$1").replace(/\//g, "\\") : p; +} + +export function pathToFileURL(p: string): URL { + const forward = p.replace(/\\/g, "/"); + return new URL(`file://${forward.startsWith("/") ? "" : "/"}${encodeURI(forward)}`); +} + +export default { fileURLToPath, pathToFileURL }; diff --git a/webui/src/node/util.ts b/webui/src/node/util.ts new file mode 100644 index 0000000..dfe04ba --- /dev/null +++ b/webui/src/node/util.ts @@ -0,0 +1,7 @@ +/** `node:util` for the page code: promisify, which the process module applies at load. */ +export function promisify void>(fn: T): (...args: any[]) => Promise { + return (...args: any[]) => new Promise((resolve, reject) => fn(...args, (error: unknown, ...values: unknown[]) => { + if (error) reject(error); else resolve(values.length > 1 ? values : values[0]); + })); +} +export default { promisify }; diff --git a/webui/src/vscode.ts b/webui/src/vscode.ts new file mode 100644 index 0000000..b96d13d --- /dev/null +++ b/webui/src/vscode.ts @@ -0,0 +1,422 @@ +/** + * The part of VS Code's extension API that mcpp-vscode's pages use, for the workbench in an IDE that + * is not VS Code (mcpp-clion's JCEF browser). The pages are mcpp-vscode's own modules, unchanged; + * what they ask of VS Code is answered here: views and panels by the workbench (workbench.ts), + * dialogs, files, settings, the clipboard and the IDE's own commands by the IDE (bridge.ts). + */ +import zhCn from "../../mide-plugins/vscode/l10n/bundle.l10n.zh-cn.json"; +import { info, on, request } from "./bridge"; +import { workbench } from "./workbench"; + +// ── events and disposables ────────────────────────────────────────────────────────────────────── + +export class Disposable { + constructor(private readonly fn: () => void) {} + static from(...items: Array<{ dispose(): unknown }>): Disposable { + return new Disposable(() => items.forEach((i) => i.dispose())); + } + dispose(): void { + this.fn(); + } +} + +export type Event = (listener: (e: T) => unknown) => Disposable; + +export class EventEmitter { + private readonly listeners = new Set<(e: T) => unknown>(); + readonly event: Event = (listener) => { + this.listeners.add(listener); + return new Disposable(() => this.listeners.delete(listener)); + }; + fire(value: T): void { + for (const l of [...this.listeners]) { + try { l(value); } catch (e) { console.error(e); } + } + } + dispose(): void { + this.listeners.clear(); + } +} + +// ── values ────────────────────────────────────────────────────────────────────────────────────── + +export class Uri { + private constructor(readonly scheme: string, readonly authority: string, readonly path: string, readonly query: string, readonly fragment: string) {} + + static parse(text: string): Uri { + const m = /^([a-zA-Z][\w+.-]*):(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/.exec(text); + if (!m) return new Uri("file", "", text, "", ""); + return new Uri(m[1], m[2] ?? "", decodeURIComponent(m[3] ?? ""), m[4] ?? "", m[5] ?? ""); + } + + static file(fsPath: string): Uri { + let p = fsPath.replace(/\\/g, "/"); + if (/^[A-Za-z]:/.test(p)) p = `/${p}`; + return new Uri("file", "", p, "", ""); + } + + static joinPath(base: Uri, ...segments: string[]): Uri { + const joined = [base.path.replace(/\/+$/, ""), ...segments.map((s) => s.replace(/^\/+|\/+$/g, ""))].join("/"); + return base.with({ path: joined }); + } + + get fsPath(): string { + const p = /^\/[A-Za-z]:/.test(this.path) ? this.path.slice(1) : this.path; + return info.platform === "win32" ? p.replace(/\//g, "\\") : p; + } + + with(change: { scheme?: string; path?: string; query?: string; fragment?: string }): Uri { + return new Uri(change.scheme ?? this.scheme, this.authority, change.path ?? this.path, change.query ?? this.query, change.fragment ?? this.fragment); + } + + toString(): string { + const authority = this.scheme === "file" || this.authority ? `//${this.authority}` : ""; + const path = this.path.split("/").map(encodeURIComponent).join("/").replace(/%3A/g, ":"); + return `${this.scheme}:${authority}${path}${this.query ? `?${this.query}` : ""}${this.fragment ? `#${this.fragment}` : ""}`; + } + + toJSON(): string { + return this.toString(); + } +} + +export class ThemeColor { + constructor(readonly id: string) {} +} + +export class ThemeIcon { + constructor(readonly id: string, readonly color?: ThemeColor) {} +} + +export enum TreeItemCollapsibleState { None = 0, Collapsed = 1, Expanded = 2 } + +export class TreeItem { + id?: string; + description?: string; + tooltip?: string; + iconPath?: ThemeIcon; + contextValue?: string; + command?: { command: string; title: string; arguments?: unknown[] }; + accessibilityInformation?: { label: string }; + constructor(public label: string, public collapsibleState: TreeItemCollapsibleState = TreeItemCollapsibleState.None) {} +} + +export class RelativePattern { + readonly baseUri: Uri; + readonly base: string; + constructor(base: Uri | string | { uri: Uri }, readonly pattern: string) { + this.baseUri = typeof base === "string" ? Uri.file(base) : base instanceof Uri ? base : base.uri; + this.base = this.baseUri.fsPath; + } +} + +export enum ViewColumn { Active = -1, Beside = -2, One = 1 } +export enum ProgressLocation { SourceControl = 1, Window = 10, Notification = 15 } +export enum ConfigurationTarget { Global = 1, Workspace = 2, WorkspaceFolder = 3 } +export enum StatusBarAlignment { Left = 1, Right = 2 } + +// ── settings ──────────────────────────────────────────────────────────────────────────────────── + +/** Every mcpp.* value, by level: the IDE's own store, read once and kept current. */ +interface SettingLevels { user: Record; workspace: Record } +let levels: SettingLevels = { user: {}, workspace: {} }; +const configurationChanged = new EventEmitter<{ affectsConfiguration(section: string): boolean }>(); + +export async function loadSettings(): Promise { + levels = await request("settings.read"); +} + +on("settingsChanged", (changed: { keys: string[]; levels: SettingLevels }) => { + levels = changed.levels; + configurationChanged.fire({ affectsConfiguration: (section) => changed.keys.some((k) => k === section || k.startsWith(`${section}.`)) }); +}); + +function configuration(section?: string) { + const full = (key: string) => (section ? `${section}.${key}` : key); + return { + get(key: string, fallback?: T): T | undefined { + const k = full(key); + return (levels.workspace[k] ?? levels.user[k] ?? fallback) as T | undefined; + }, + has(key: string): boolean { + const k = full(key); + return k in levels.workspace || k in levels.user; + }, + inspect(key: string) { + const k = full(key); + return { key: k, globalValue: levels.user[k] as T | undefined, workspaceValue: levels.workspace[k] as T | undefined, workspaceFolderValue: undefined }; + }, + async update(key: string, value: unknown, target?: ConfigurationTarget | boolean): Promise { + const k = full(key); + const level = target === ConfigurationTarget.Global || target === true ? "user" : "workspace"; + levels = await request("settings.write", { key: k, value: value ?? null, level }); + configurationChanged.fire({ affectsConfiguration: (s) => k === s || k.startsWith(`${s}.`) }); + }, + }; +} + +// ── documents ─────────────────────────────────────────────────────────────────────────────────── + +const contentProviders = new Map }>(); +const saved = new EventEmitter<{ uri: Uri; fileName: string; languageId: string }>(); +on("documentSaved", (p: { path: string }) => saved.fire({ uri: Uri.file(p.path), fileName: p.path, languageId: "" })); + +class TextDocument { + constructor(readonly uri: Uri, private readonly text: string) {} + get fileName(): string { return this.uri.fsPath; } + getText(): string { return this.text; } +} + +async function openTextDocument(target: Uri | string): Promise { + const uri = typeof target === "string" ? Uri.file(target) : target; + const provider = contentProviders.get(uri.scheme); + if (provider) return new TextDocument(uri, await provider.provideTextDocumentContent(uri)); + return new TextDocument(uri, (await request("fs.read", { path: uri.fsPath })) ?? ""); +} + +async function showTextDocument(target: Uri | TextDocument, options?: { selection?: { start: { line: number } } }): Promise { + const doc = target instanceof TextDocument ? target : undefined; + const uri = doc ? doc.uri : (target as Uri); + const line = options?.selection?.start?.line; + if (uri.scheme === "file") return request("openFile", { path: uri.fsPath, line: line === undefined ? undefined : line + 1 }); + const text = doc ? doc.getText() : (await openTextDocument(uri)).getText(); + return request("openText", { name: uri.path.split("/").pop() ?? uri.path, text }); +} + +// ── file watchers ─────────────────────────────────────────────────────────────────────────────── + +/** A glob over `/`-separated names: `**`, `*`, `?` and `{a,b}`. */ +/** A glob as VS Code reads one: `**`, `*`, `?`, and `{a,b}` whose alternatives are globs again. */ +function globSource(glob: string): string { + let re = ""; + for (let i = 0; i < glob.length; i++) { + const c = glob[i]; + if (c === "*" && glob[i + 1] === "*") { + i++; + if (glob[i + 1] === "/") { re += "(?:.*/)?"; i++; } else re += ".*"; + } + else if (c === "*") re += "[^/]*"; + else if (c === "?") re += "[^/]"; + else if (c === "{") { const end = glob.indexOf("}", i); re += `(?:${glob.slice(i + 1, end).split(",").map(globSource).join("|")})`; i = end; } + else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&"); + } + return re; +} + +function globToRegExp(glob: string): RegExp { + return new RegExp(`^${globSource(glob)}$`); +} + +const watchers = new Set<{ match(path: string): boolean; created: EventEmitter; changed: EventEmitter; deleted: EventEmitter }>(); +on("fileChanged", (p: { path: string; kind: "created" | "changed" | "deleted" }) => { + for (const w of watchers) { + if (!w.match(p.path)) continue; + (p.kind === "created" ? w.created : p.kind === "deleted" ? w.deleted : w.changed).fire(Uri.file(p.path)); + } +}); + +function createFileSystemWatcher(pattern: string | RelativePattern) { + const slash = (s: string) => s.replace(/\\/g, "/"); + const base = typeof pattern === "string" ? "" : slash(pattern.base).replace(/\/$/, ""); + const re = globToRegExp(typeof pattern === "string" ? pattern : pattern.pattern); + const w = { + match: (path: string) => { + const p = slash(path); + if (base && !p.startsWith(`${base}/`)) return false; + const rel = base ? p.slice(base.length + 1) : p; + return re.test(rel) || (!base && re.test(p.split("/").slice(-rel.split("/").length).join("/"))) || re.test(p.replace(/^.*\//, "")); + }, + created: new EventEmitter(), changed: new EventEmitter(), deleted: new EventEmitter(), + }; + watchers.add(w); + request("watch", { base: base || undefined, pattern: typeof pattern === "string" ? pattern : pattern.pattern }).catch(() => undefined); + return { + onDidCreate: w.created.event, onDidChange: w.changed.event, onDidDelete: w.deleted.event, + dispose: () => watchers.delete(w), + }; +} + +// ── commands ──────────────────────────────────────────────────────────────────────────────────── + +const registry = new Map unknown>(); + +export const commands = { + registerCommand(id: string, fn: (...args: unknown[]) => unknown): Disposable { + registry.set(id, fn); + return new Disposable(() => registry.delete(id)); + }, + async executeCommand(id: string, ...args: unknown[]): Promise { + const own = registry.get(id); + if (own) return (await own(...args)) as T; + if (id === "setContext") { workbench.setContext(String(args[0]), args[1]); return undefined; } + if (id.endsWith(".focus")) { workbench.focus(id.slice(0, -".focus".length)); return undefined; } + if (id === "workbench.action.openSettings") { return (await commands.executeCommand("mcpp.openSettings")) as T; } + // Everything else is the IDE's: mcpp's commands (spec/commands.json), and the editor's own. + return request("command", { id, args }); + }, + async getCommands(): Promise { + return [...registry.keys()]; + }, +}; + +// ── window ────────────────────────────────────────────────────────────────────────────────────── + +type MessageItem = string | { title: string; isCloseAffordance?: boolean }; + +async function showMessage(level: "info" | "warning" | "error", message: string, ...rest: unknown[]): Promise { + const options = rest.length > 0 && typeof rest[0] === "object" && rest[0] !== null && !("title" in (rest[0] as object)) ? (rest.shift() as { modal?: boolean; detail?: string }) : {}; + const items = rest.filter((i) => i !== undefined) as MessageItem[]; + const titles = items.map((i) => (typeof i === "string" ? i : i.title)); + const chosen = await request("message", { level, message, detail: options.detail, modal: options.modal === true, items: titles }); + return chosen === null || chosen < 0 ? undefined : items[chosen]; +} + +const activeEditorChanged = new EventEmitter(); +let activeEditorPath: string | undefined; +on("activeEditor", (p: { path?: string }) => { + activeEditorPath = p.path; + activeEditorChanged.fire(window.activeTextEditor); +}); + +export const window = { + showInformationMessage: (message: string, ...rest: unknown[]) => showMessage("info", message, ...rest), + showWarningMessage: (message: string, ...rest: unknown[]) => showMessage("warning", message, ...rest), + showErrorMessage: (message: string, ...rest: unknown[]) => showMessage("error", message, ...rest), + async showInputBox(options: { title?: string; prompt?: string; value?: string; placeHolder?: string; validateInput?: (v: string) => unknown } = {}): Promise { + for (;;) { + const value = await request("input", { title: options.title, prompt: options.prompt, value: options.value, placeHolder: options.placeHolder }); + if (value === null) return undefined; + const problem = options.validateInput ? await options.validateInput(value) : undefined; + if (!problem) return value; + await showMessage("warning", typeof problem === "string" ? problem : String((problem as { message?: string }).message ?? problem)); + options = { ...options, value }; + } + }, + async showQuickPick(items: readonly T[] | Promise, options: { title?: string; placeHolder?: string } = {}): Promise { + const list = await items; + const rows = list.map((i) => (typeof i === "string" ? { label: i } : { label: i.label, description: i.description, detail: i.detail })); + const chosen = await request("pick", { title: options.title, placeHolder: options.placeHolder, items: rows }); + return chosen === null || chosen < 0 ? undefined : list[chosen]; + }, + async showSaveDialog(options: { defaultUri?: Uri; filters?: Record; title?: string } = {}): Promise { + const path = await request("saveDialog", { path: options.defaultUri?.fsPath, filters: options.filters, title: options.title }); + return path === null ? undefined : Uri.file(path); + }, + async withProgress(options: { location: ProgressLocation; title?: string; cancellable?: boolean }, task: (progress: { report(v: { message?: string; increment?: number }): void }, token: { isCancellationRequested: boolean; onCancellationRequested: Event }) => Thenable | Promise): Promise { + const id = await request("progress.start", { title: options.title ?? "" }); + const cancelled = new EventEmitter(); + try { + return await task( + { report: (v) => { if (v.message) request("progress.report", { id, message: v.message }).catch(() => undefined); } }, + { isCancellationRequested: false, onCancellationRequested: cancelled.event }, + ); + } finally { + request("progress.end", { id }).catch(() => undefined); + } + }, + showTextDocument, + get activeTextEditor(): { document: { uri: Uri; fileName: string } } | undefined { + return activeEditorPath ? { document: { uri: Uri.file(activeEditorPath), fileName: activeEditorPath } } : undefined; + }, + onDidChangeActiveTextEditor: activeEditorChanged.event, + createStatusBarItem() { + // The IDE's status bar is its own; the workbench shows none. + return { text: "", tooltip: "", command: undefined as unknown, show() {}, hide() {}, dispose() {} }; + }, + createOutputChannel(name: string) { + return { + name, + append: (text: string) => { request("log", { text }).catch(() => undefined); }, + appendLine: (text: string) => { request("log", { text: `${text}\n` }).catch(() => undefined); }, + show: () => { request("log.show").catch(() => undefined); }, + clear() {}, hide() {}, dispose() {}, replace() {}, + }; + }, + registerWebviewViewProvider: (id: string, provider: { resolveWebviewView(view: unknown, context: unknown, token: unknown): unknown }) => + workbench.registerView(id, provider), + createWebviewPanel: (viewType: string, title: string, _column: unknown, options: unknown) => workbench.createPanel(viewType, title, options), + createTreeView: (id: string, options: { treeDataProvider: unknown }) => + workbench.registerTree(id, options.treeDataProvider, (command, args) => commands.executeCommand(command, ...args)), + registerTreeDataProvider: (id: string, provider: unknown) => + workbench.registerTree(id, provider, (command, args) => commands.executeCommand(command, ...args)), +}; + +// ── workspace ─────────────────────────────────────────────────────────────────────────────────── + +const trustGranted = new EventEmitter(); +const foldersChanged = new EventEmitter(); +on("trust", (p: { trusted: boolean }) => { info.trusted = p.trusted; if (p.trusted) trustGranted.fire(); }); + +function folders() { + return info.workspaceFolders.map((f, index) => ({ uri: Uri.file(f), name: f.split(/[\\/]/).pop() ?? f, index })); +} + +export const workspace = { + get isTrusted(): boolean { return info.trusted; }, + get workspaceFolders() { return folders(); }, + getWorkspaceFolder(uri: Uri) { + const p = uri.fsPath.replace(/\\/g, "/"); + return folders().find((f) => p === f.uri.fsPath.replace(/\\/g, "/") || p.startsWith(`${f.uri.fsPath.replace(/\\/g, "/")}/`)); + }, + getConfiguration: (section?: string, _resource?: unknown) => configuration(section), + onDidChangeConfiguration: configurationChanged.event, + onDidSaveTextDocument: saved.event, + onDidGrantWorkspaceTrust: trustGranted.event, + onDidChangeWorkspaceFolders: foldersChanged.event, + openTextDocument, + registerTextDocumentContentProvider(scheme: string, provider: { provideTextDocumentContent(uri: Uri): string | Promise }): Disposable { + contentProviders.set(scheme, provider); + return new Disposable(() => contentProviders.delete(scheme)); + }, + createFileSystemWatcher, + async findFiles(pattern: string | RelativePattern, exclude?: string, max?: number): Promise { + const base = typeof pattern === "string" ? info.workspaceFolders[0] : pattern.base; + const glob = typeof pattern === "string" ? pattern : pattern.pattern; + const found = await request("findFiles", { base, pattern: glob, exclude, max }); + return found.map((p) => Uri.file(p)); + }, + fs: { + async writeFile(uri: Uri, bytes: Uint8Array): Promise { + let text = ""; + for (let i = 0; i < bytes.length; i += 0x8000) text += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + await request("fs.write", { path: uri.fsPath, text: btoa(text), base64: true }); + }, + async readFile(uri: Uri): Promise { + return new TextEncoder().encode((await request("fs.read", { path: uri.fsPath })) ?? ""); + }, + }, +}; + +// ── the rest ──────────────────────────────────────────────────────────────────────────────────── + +export const env = { + get language(): string { return info.language; }, + clipboard: { writeText: (text: string) => request("clipboard", { text }).then(() => undefined) }, + openExternal: (uri: Uri) => request("openExternal", { url: uri.toString() }), +}; + +export const extensions = { + // The C++ Modules language service is the IDE's (the mcppls plugin); no VS Code extension is. + getExtension: (_id: string) => undefined, + onDidChange: new EventEmitter().event, +}; + +export const tasks = { + // A build in the IDE ends in the backend's result record: the pages refresh as on a task's end. + onDidEndTaskProcess: (listener: (e: unknown) => unknown) => on("result/changed", () => listener({ execution: { task: { definition: { type: "mcpp" } } }, exitCode: 0 })), +}; + +function format(template: string, args: unknown[]): string { + return args.reduce((text, a, i) => text.split(`{${i}}`).join(String(a)), template); +} + +export const l10n = { + t(message: string | { message: string; args?: unknown[] }, ...args: unknown[]): string { + const text = typeof message === "string" ? message : message.message; + const values = typeof message === "string" ? args : (message.args ?? []); + const bundle = info.language.toLowerCase().startsWith("zh") ? (zhCn as Record) : {}; + return format(bundle[text] ?? text, values); + }, +}; + +export const version = "1.91.0"; diff --git a/webui/src/workbench.ts b/webui/src/workbench.ts new file mode 100644 index 0000000..ba609f5 --- /dev/null +++ b/webui/src/workbench.ts @@ -0,0 +1,424 @@ +/** + * The workbench: what VS Code's own window does for mcpp-vscode's pages, in one browser page. + * + * Views (the project tree, the libraries, the cache) and panels (the settings, a package's page, + * the graphs) are tabs. A webview's document is an iframe whose page is the document the page code + * assigned, with `acquireVsCodeApi()` and the theme's `--vscode-*` variables put in front of it, so + * the page runs as it runs in VS Code: messages both ways, state across reloads. A tree view renders + * the provider's items as VS Code's tree does, with the inline actions the manifest names. + */ +import manifest from "../../mide-plugins/vscode/package.json"; +import nlsEn from "../../mide-plugins/vscode/package.nls.json"; +import nlsZh from "../../mide-plugins/vscode/package.nls.zh-cn.json"; +import { info, request } from "./bridge"; + +type Listener = (e: T) => unknown; + +class Emitter { + private readonly ls = new Set>(); + event = (l: Listener) => { this.ls.add(l); return { dispose: () => this.ls.delete(l) }; }; + fire(v: T): void { for (const l of [...this.ls]) { try { l(v); } catch (e) { console.error(e); } } } +} + +/** A manifest string (`%key%`) in the IDE's language. */ +function nls(text: string | undefined): string { + if (!text) return ""; + const m = /^%(.+)%$/.exec(text); + if (!m) return text; + const zh = info.language.toLowerCase().startsWith("zh"); + return ((zh ? (nlsZh as Record)[m[1]] : undefined) ?? (nlsEn as Record)[m[1]] ?? text); +} + +// ── context keys and `when` ───────────────────────────────────────────────────────────────────── + +const context = new Map(); +const contextChanged = new Emitter(); + +/** The `when` forms the manifest uses: `a`, `!a`, `a == b`, `a != b`, `a =~ /re/`, `&&`, `||`. */ +function when(expression: string | undefined, extra: Record = {}): boolean { + if (!expression) return true; + const value = (name: string): unknown => (name in extra ? extra[name] : context.get(name)); + return expression.split("||").some((alternative) => alternative.split("&&").every((raw) => { + const term = raw.trim(); + let m = /^(\S+)\s*=~\s*\/(.*)\/([a-z]*)$/.exec(term); + if (m) return new RegExp(m[2], m[3]).test(String(value(m[1]) ?? "")); + m = /^(\S+)\s*(==|!=)\s*(.+)$/.exec(term); + if (m) { + const right = m[3].trim().replace(/^'(.*)'$/, "$1"); + const equal = String(value(m[1])) === right; + return m[2] === "==" ? equal : !equal; + } + if (term.startsWith("!")) return !value(term.slice(1).trim()); + return Boolean(value(term)); + })); +} + +// ── the frame of a webview ────────────────────────────────────────────────────────────────────── + +let theme: Record = {}; +let themeKind: "vscode-light" | "vscode-dark" | "vscode-high-contrast" = "vscode-light"; +const frames = new Map(); +let nextFrame = 1; + +window.addEventListener("message", (e: MessageEvent) => { + const data = e.data as { __mcppWebview?: string; message?: unknown; state?: unknown; kind?: string }; + if (!data || typeof data.__mcppWebview !== "string") return; + const webview = frames.get(data.__mcppWebview); + if (!webview) return; + if (data.kind === "state") webview.state = data.state; + else webview.received.fire(data.message); +}); + +class Webview { + readonly id = `w${nextFrame++}`; + readonly received = new Emitter(); + readonly onDidReceiveMessage = this.received.event; + options: unknown = {}; + state: unknown = undefined; + private document = ""; + constructor(readonly frame: HTMLIFrameElement) { + frames.set(this.id, this); + } + + /** Where the page's own files are, as a CSP source: `file:` (a scheme) for the files the IDE extracted. */ + get cspSource(): string { + const url = new URL(info.extensionUri); + return url.protocol === "file:" ? "file:" : url.origin; + } + + asWebviewUri(uri: { toString(): string }): { toString(): string } { + return uri; + } + + get html(): string { + return this.document; + } + + set html(document: string) { + this.document = document; + const nonce = /'nonce-([^']+)'/.exec(document)?.[1] ?? ""; + const vars = Object.entries(theme).map(([k, v]) => `${k}: ${v};`).join(" "); + const prelude = `` + + ``; + const withPrelude = /]*>/i.test(document) ? document.replace(/]*>/i, (h) => `${h}${prelude}`) : `${prelude}${document}`; + this.frame.srcdoc = withPrelude; + } + + async postMessage(message: unknown): Promise { + this.frame.contentWindow?.postMessage(message, "*"); + return true; + } +} + +// ── tabs ──────────────────────────────────────────────────────────────────────────────────────── + +interface Tab { + key: string; + title: string; + closable: boolean; + /** A view of the manifest: shown while its `when` holds. */ + when?: string; + body: HTMLElement; + button?: HTMLButtonElement; + shown(): void; + hidden(): void; + closed(): void; +} + +const tabs: Tab[] = []; +let current: Tab | undefined; +let strip: HTMLElement; +let stage: HTMLElement; + +function layout(): void { + if (strip) return; + strip = document.createElement("nav"); + strip.className = "tabs"; + stage = document.createElement("main"); + stage.className = "stage"; + document.body.append(strip, stage); +} + +function renderStrip(): void { + layout(); + strip.replaceChildren(); + for (const tab of tabs) { + if (!when(tab.when)) continue; + const b = document.createElement("button"); + b.className = `tab${tab === current ? " active" : ""}`; + b.textContent = tab.title; + b.onclick = () => select(tab); + if (tab.closable) { + const x = document.createElement("span"); + x.className = "codicon codicon-close close"; + x.title = "Close"; + x.onclick = (e) => { e.stopPropagation(); close(tab); }; + b.append(x); + } + tab.button = b; + strip.append(b); + } +} + +function select(tab: Tab): void { + layout(); + if (current === tab) { renderStrip(); return; } + current?.hidden(); + if (current) current.body.hidden = true; + current = tab; + tab.body.hidden = false; + tab.shown(); + renderStrip(); +} + +function add(tab: Tab): void { + layout(); + tab.body.hidden = true; + stage.append(tab.body); + tabs.push(tab); + renderStrip(); +} + +function close(tab: Tab): void { + const i = tabs.indexOf(tab); + if (i < 0) return; + tabs.splice(i, 1); + tab.body.remove(); + tab.closed(); + if (current === tab) { + current = undefined; + const next = tabs[Math.max(0, i - 1)]; + if (next) select(next); + } + renderStrip(); +} + +contextChanged.event(() => { + renderStrip(); + if (current && !when(current.when)) { + const next = tabs.find((t) => when(t.when)); + if (next) select(next); + } +}); + +/** The views the manifest contributes, in its order, with their titles and `when`. */ +const declaredViews: Array<{ id: string; name?: string; when?: string; type?: string }> = + ((manifest as { contributes?: { views?: Record> } }).contributes?.views?.mcpp) ?? []; + +function viewTab(id: string, body: HTMLElement, shown: () => void, hidden: () => void): Tab { + const declared = declaredViews.find((v) => v.id === id); + const tab: Tab = { key: id, title: nls(declared?.name) || id, closable: false, when: declared?.when, body, shown, hidden, closed() {} }; + // The manifest's order, whatever order the pages registered in. + const order = (key: string) => { const i = declaredViews.findIndex((v) => v.id === key); return i < 0 ? 1000 : i; }; + const at = tabs.findIndex((t) => !t.closable && order(t.key) > order(id)); + layout(); + body.hidden = true; + stage.append(body); + if (at < 0) tabs.splice(tabs.filter((t) => !t.closable).length, 0, tab); else tabs.splice(at, 0, tab); + renderStrip(); + return tab; +} + +// ── tree views ────────────────────────────────────────────────────────────────────────────────── + +interface TreeItemLike { + label: string; id?: string; description?: string; tooltip?: string; contextValue?: string; collapsibleState?: number; + iconPath?: { id: string; color?: { id: string } }; + command?: { command: string; arguments?: unknown[] }; +} +interface Provider { + getChildren(element?: T): T[] | Promise; + getTreeItem(element: T): TreeItemLike | Promise; + onDidChangeTreeData?: (l: () => unknown) => unknown; +} + +const inlineActions: Array<{ command: string; when?: string; group?: string }> = + ((manifest as { contributes?: { menus?: Record> } }).contributes?.menus?.["view/item/context"]) ?? []; +const commandIcons = new Map( + ((manifest as { contributes?: { commands?: Array<{ command: string; title: string; icon?: string }> } }).contributes?.commands ?? []) + .map((c) => [c.command, { title: nls(c.title), icon: c.icon }]), +); + +function treeView(id: string, provider: Provider, run: (command: string, args: unknown[]) => void) { + const body = document.createElement("section"); + body.className = "tree"; + const expanded = new Map(); + const changedVisibility = new Emitter<{ visible: boolean }>(); + let visible = false; + const draw = async (): Promise => { + const rows: HTMLElement[] = []; + const walk = async (element: T | undefined, depth: number): Promise => { + for (const child of await provider.getChildren(element)) { + const item = await provider.getTreeItem(child); + const key = item.id ?? `${depth}:${item.label}`; + const collapsible = (item.collapsibleState ?? 0) !== 0; + const open = expanded.get(key) ?? item.collapsibleState === 2; + const row = document.createElement("div"); + row.className = "row"; + row.style.paddingLeft = `${8 + depth * 16}px`; + row.title = item.tooltip ?? ""; + row.setAttribute("role", "treeitem"); + if (collapsible) row.setAttribute("aria-expanded", String(open)); + const twistie = document.createElement("span"); + twistie.className = `twistie codicon ${collapsible ? (open ? "codicon-chevron-down" : "codicon-chevron-right") : ""}`; + row.append(twistie); + if (item.iconPath?.id) { + const icon = document.createElement("span"); + icon.className = `icon codicon codicon-${item.iconPath.id}`; + if (item.iconPath.color) icon.style.color = `var(--vscode-${item.iconPath.color.id.replace(/\./g, "-")})`; + row.append(icon); + } + const label = document.createElement("span"); + label.className = "label"; + label.textContent = item.label; + row.append(label); + if (item.description) { + const d = document.createElement("span"); + d.className = "description"; + d.textContent = item.description; + row.append(d); + } + for (const action of inlineActions) { + if (!(action.group ?? "").startsWith("inline")) continue; + if (!when(action.when, { view: id, viewItem: item.contextValue ?? "" })) continue; + const declared = commandIcons.get(action.command); + const b = document.createElement("span"); + const icon = /^\$\((.+)\)$/.exec(declared?.icon ?? "")?.[1]; + b.className = `action codicon ${icon ? `codicon-${icon}` : "codicon-arrow-right"}`; + b.title = declared?.title ?? action.command; + b.onclick = (e) => { e.stopPropagation(); run(action.command, [child]); }; + row.append(b); + } + row.onclick = () => { + if (collapsible) { expanded.set(key, !open); void draw(); return; } + if (item.command) run(item.command.command, item.command.arguments ?? []); + }; + rows.push(row); + if (collapsible && open) await walk(child, depth + 1); + } + }; + await walk(undefined, 0); + body.replaceChildren(...rows); + }; + provider.onDidChangeTreeData?.(() => { if (visible) void draw(); }); + const tab = viewTab(id, body, () => { visible = true; changedVisibility.fire({ visible: true }); void draw(); }, () => { visible = false; changedVisibility.fire({ visible: false }); }); + return { + get visible() { return visible; }, + onDidChangeVisibility: changedVisibility.event, + reveal: async () => select(tab), + dispose: () => close(tab), + title: tab.title, + description: undefined as string | undefined, + message: undefined as string | undefined, + badge: undefined as unknown, + }; +} + +// ── webview views and panels ──────────────────────────────────────────────────────────────────── + +function frame(): HTMLIFrameElement { + const f = document.createElement("iframe"); + f.className = "webview"; + return f; +} + +export const workbench = { + /** The IDE's theme, as the `--vscode-*` variables the pages read, and the theme's kind. */ + setTheme(vars: Record, kind: typeof themeKind): void { + theme = vars; + themeKind = kind; + document.documentElement.setAttribute("style", Object.entries(vars).map(([k, v]) => `${k}: ${v}`).join("; ")); + document.body.classList.remove("vscode-light", "vscode-dark", "vscode-high-contrast"); + document.body.classList.add(kind); + }, + + setContext(key: string, value: unknown): void { + context.set(key, value); + contextChanged.fire(); + }, + + /** Brings a view or a panel on screen: the IDE's request, or a page's `.focus`. */ + focus(key: string): void { + const tab = tabs.find((t) => t.key === key) ?? tabs.find((t) => t.key.endsWith(`.${key}`)); + if (tab) select(tab); + }, + + /** The first view whose `when` holds, when nothing is on screen yet. */ + showFirst(): void { + if (current) return; + const tab = tabs.find((t) => when(t.when)); + if (tab) select(tab); + }, + + registerView(id: string, provider: { resolveWebviewView(view: unknown, context: unknown, token: unknown): unknown }) { + const body = document.createElement("section"); + body.className = "view"; + const f = frame(); + body.append(f); + const webview = new Webview(f); + const changed = new Emitter(); + const disposed = new Emitter(); + let visible = false; + let resolved = false; + const view = { + webview, + get visible() { return visible; }, + onDidChangeVisibility: changed.event, + onDidDispose: disposed.event, + show: () => workbench.focus(id), + title: undefined as string | undefined, + description: undefined as string | undefined, + }; + viewTab(id, body, () => { + visible = true; + if (!resolved) { resolved = true; void provider.resolveWebviewView(view, { state: undefined }, { isCancellationRequested: false }); } + changed.fire(); + }, () => { visible = false; changed.fire(); }); + return { dispose() {} }; + }, + + createPanel(viewType: string, title: string, options: unknown) { + const body = document.createElement("section"); + body.className = "panel"; + const f = frame(); + body.append(f); + const webview = new Webview(f); + webview.options = options; + const disposed = new Emitter(); + const viewState = new Emitter<{ webviewPanel: unknown }>(); + let visible = false; + let isDisposed = false; + const panel = { + viewType, + webview, + get title() { return tab.title; }, + set title(t: string) { tab.title = t; renderStrip(); }, + get visible() { return visible; }, + get active() { return visible; }, + iconPath: undefined as unknown, + onDidDispose: disposed.event, + onDidChangeViewState: viewState.event, + reveal: () => select(tab), + dispose: () => { if (!isDisposed) close(tab); }, + }; + const tab: Tab = { + key: `${viewType}#${webview.id}`, title, closable: true, body, + shown: () => { visible = true; viewState.fire({ webviewPanel: panel }); }, + hidden: () => { visible = false; viewState.fire({ webviewPanel: panel }); }, + closed: () => { isDisposed = true; visible = false; frames.delete(webview.id); disposed.fire(); }, + }; + add(tab); + select(tab); + return panel; + }, + + /** A tree view; a row's command goes through `run`, where the page's own commands go. */ + registerTree(id: string, provider: unknown, run: (command: string, args: unknown[]) => Promise) { + return treeView(id, provider as Provider, (command, args) => { + run(command, args).catch((e) => request("log", { text: `${e}\n` })); + }); + }, +}; diff --git a/webui/tests/workbench.test.mjs b/webui/tests/workbench.test.mjs new file mode 100644 index 0000000..ca021a2 --- /dev/null +++ b/webui/tests/workbench.test.mjs @@ -0,0 +1,199 @@ +// The workbench as mcpp-clion shows it, in Chromium (the engine of JCEF), with a host that answers +// the bridge as Workbench.kt does -- the backend's methods over a real `mcpp-ide serve` answering for +// the mock mcpp, files from disk, settings in memory, dialogs recorded -- and the synchronous channel +// as the JS dialog handler answers it. mcpp-vscode's pages must run in it unchanged: the views as +// tabs, the project tree, the cache page over `mcpp cache list`, the libraries over an index and a +// package's page, the settings page writing a setting, and the graph page. +// +// Run from mide-plugins/vscode: npm run test:webui (it bundles the workbench and compiles the +// extension first). MCPP_IDE_BACKEND and MCPP_IDE_MOCK_MCPP name the programs; else the newest of +// the repository's target/ are taken. +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { createRequire } from "node:module"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repo = path.resolve(here, "..", ".."); +const extension = path.join(repo, "mide-plugins", "vscode"); +const require = createRequire(path.join(extension, "package.json")); +const { chromium } = require("playwright"); +const { Backend } = require(path.join(extension, "dist", "src", "ide", "backend", "client.js")); + +function built(name) { + const env = name === "mcpp-ide" ? process.env.MCPP_IDE_BACKEND : process.env.MCPP_IDE_MOCK_MCPP; + if (env) return env; + const exe = process.platform === "win32" ? `${name}.exe` : name; + let best, bestTime = 0; + const walk = (dir, depth) => { + if (depth > 4) return; + let entries = []; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } + for (const e of entries) { + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p, depth + 1); + else if (e.name === exe && path.basename(dir) === "bin") { const t = fs.statSync(p).mtimeMs; if (t > bestTime) { best = p; bestTime = t; } } + } + }; + walk(path.join(repo, "target"), 0); + return best; +} + +/** A project, a home with one index descriptor, and a target/ with artifacts to measure. */ +function fixture() { + const base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "mcpp-workbench-"))); + const root = path.join(base, "app"); + fs.mkdirSync(path.join(root, "src"), { recursive: true }); + fs.writeFileSync(path.join(root, "mcpp.toml"), '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies.compat]\nargparse = "3.2"\n'); + fs.writeFileSync(path.join(root, "src", "main.cpp"), "int main() {}\n"); + fs.mkdirSync(path.join(root, "target", "x86_64-linux-gnu", "obj"), { recursive: true }); + fs.writeFileSync(path.join(root, "target", "x86_64-linux-gnu", "obj", "main.o"), Buffer.alloc(4096)); + const home = path.join(base, "home"); + fs.mkdirSync(path.join(home, "indices", "mcpplibs", "pkgs", "c"), { recursive: true }); + fs.writeFileSync(path.join(home, "indices", "mcpplibs", "pkgs", "c", "compat.argparse.lua"), + 'package = { name = "argparse", namespace = "compat", description = "Argument parser", xpm = { linux = { ["3.2"] = {} }, macosx = { ["3.2"] = {} }, windows = { ["3.2"] = {} } } }'); + return { base, root, home }; +} + +function statJson(p, follow) { + try { + const s = follow ? fs.statSync(p) : fs.lstatSync(p); + return { type: s.isSymbolicLink() ? "symlink" : s.isDirectory() ? "directory" : s.isFile() ? "file" : "other", size: s.size, mtimeMs: s.mtimeMs, ino: Number(s.ino % 4294967296) }; + } catch { return null; } +} + +/** What Workbench.kt answers, for the bridge's actions. */ +function host({ backend, root, home, dist, recorded }) { + const settings = { user: { "mcpp.library.indexPath": "" }, workspace: {} }; + const files = (action, p) => { + switch (action) { + case "fs.stat": return statJson(p.path, p.follow !== false); + case "fs.read": try { return fs.readFileSync(p.path, "utf8"); } catch { return null; } + case "fs.list": try { return fs.readdirSync(p.path).map((name) => ({ name, ...statJson(path.join(p.path, name), false) })); } catch { return null; } + case "fs.realpath": try { return fs.realpathSync(p.path); } catch { return null; } + case "fs.write": fs.mkdirSync(path.dirname(p.path), { recursive: true }); fs.writeFileSync(p.path, p.base64 ? Buffer.from(p.text, "base64") : p.text); return true; + } + throw new Error(`unknown file action ${action}`); + }; + return { + files, + async handle(action, p) { + recorded.push({ action, params: p }); + switch (action) { + case "init": + return { + info: { + platform: process.platform, arch: process.arch, language: "en", home: os.homedir(), tmp: os.tmpdir(), + env: { MCPP_HOME: home }, extensionUri: `${pathToFileURL(dist).href}/`, workspaceFolders: [root], trusted: true, + }, + theme: { "--vscode-foreground": "#333", "--vscode-editor-background": "#fff", "--vscode-font-family": "sans-serif", "--vscode-font-size": "13px" }, + themeKind: "vscode-light", + mode: "window", + }; + case "rpc": return backend.request(p.method, p.params); + case "settings.read": return settings; + case "settings.write": settings[p.level === "user" ? "user" : "workspace"][p.key] = p.value; if (p.value === null) delete settings[p.level === "user" ? "user" : "workspace"][p.key]; return settings; + case "progress.start": return 1; + case "message": case "input": case "pick": case "saveDialog": return null; + case "findFiles": return []; + default: + if (action.startsWith("fs.")) return files(action, p); + return null; + } + }, + }; +} + +test("mcpp-vscode's pages run in the workbench, as mcpp-clion shows them", { timeout: 180_000 }, async (t) => { + const program = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); + if (!program || !mock) { t.skip("build the backend first (mcpp build)"); return; } + const dist = process.env.MCPP_WEBUI_DIST ?? path.join(extension, ".webui-test"); + assert.ok(fs.existsSync(path.join(dist, "workbench.js")), "bundle the workbench first (npm run bundle:webui -- --out .webui-test)"); + const { base, root, home } = fixture(); + const backend = new Backend({ program, env: { ...process.env, HOME: home } }); + await backend.request("initialize", { editor: "jetbrains", settings: { "mcpp.path": mock }, workspaceFolders: [root], trusted: true }); + const recorded = []; + const h = host({ backend, root, home, dist, recorded }); + const browser = await chromium.launch(); + const page = await browser.newPage(); + const errors = []; + const consoleLines = []; + page.on("pageerror", (e) => errors.push(String(e))); + page.on("console", (m) => consoleLines.push(`${m.type()}: ${m.text()}`)); + page.on("dialog", async (dialog) => { + const message = dialog.message(); + if (dialog.type() !== "prompt" || !message.startsWith("mcpp-host:")) { await dialog.dismiss(); return; } + const { action, params } = JSON.parse(message.slice("mcpp-host:".length)); + let answer; + try { answer = { ok: true, result: h.files(action, params) }; } catch (e) { answer = { ok: false, error: String(e) }; } + await dialog.accept(JSON.stringify(answer)); + }); + await page.exposeBinding("mcppHostSend", async (_source, text) => { + const m = JSON.parse(text); + let reply; + try { reply = { type: "result", id: m.id, result: (await h.handle(m.action, m.params)) ?? null }; } + catch (e) { reply = { type: "error", id: m.id, error: { message: e instanceof Error ? e.message : String(e) } }; } + await page.evaluate((r) => window.mcppHostReceive(r), reply).catch(() => undefined); + }); + const notify = (method, params) => page.evaluate(([m, p]) => window.mcppHostReceive({ type: "notify", method: m, params: p }), [method, params]); + try { + await page.goto(pathToFileURL(path.join(dist, "index.html")).href); + // The views, in the manifest's order and names. + await page.waitForSelector(".tabs .tab"); + assert.deepEqual(await page.$$eval(".tabs .tab", (tabs) => tabs.map((t) => t.textContent.trim())), ["Project", "Libraries", "Cache"]); + + // The project tree: 0.9.0's rows for this project. + await page.waitForSelector(".tree .row"); + const rows = await page.$$eval(".tree .row .label", (labels) => labels.map((l) => l.textContent)); + assert.ok(rows.length > 0, "the project tree has rows"); + + // The cache page, over `mcpp cache list` through the backend. + await page.click(".tabs .tab:nth-child(3)"); + const cacheFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); + await page.waitForFunction((f) => f.contentDocument?.body?.textContent?.includes("Global build cache"), cacheFrame, { timeout: 30_000 }); + const cacheText = await cacheFrame.evaluate((f) => f.contentDocument.body.textContent); + assert.match(cacheText, /Project cache/); + + // The libraries page, over the index under MCPP_HOME. + await page.click(".tabs .tab:nth-child(2)"); + const libraryFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); + await page.waitForFunction((f) => f.contentDocument?.querySelector("[data-row]")?.textContent?.includes("argparse"), libraryFrame, { timeout: 30_000 }); + + // A package's page: the row opens it as a panel, a tab of its own. + await libraryFrame.evaluate((f) => f.contentDocument.querySelector("[data-row]").click()); + await page.waitForFunction(() => document.querySelectorAll(".tabs .tab .close").length === 1, null, { timeout: 15_000 }); + const detailFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); + await page.waitForFunction((f) => f.contentDocument?.body?.textContent?.includes("Argument parser"), detailFrame, { timeout: 30_000 }); + + // The settings page; a change it makes reaches the IDE's store. + await notify("show", { view: "settings" }); + await page.waitForFunction(() => [...document.querySelectorAll(".tabs .tab")].some((t) => t.textContent.includes("settings")), null, { timeout: 15_000 }); + const settingsFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); + await page.waitForFunction((f) => f.contentDocument?.querySelector("input[type=checkbox][data-key]"), settingsFrame, { timeout: 30_000 }); + const writesBefore = recorded.filter((r) => r.action === "settings.write").length; + await settingsFrame.evaluate((f) => f.contentDocument.querySelector("input[type=checkbox][data-key]").click()); + const deadline = Date.now() + 10_000; + while (recorded.filter((r) => r.action === "settings.write").length === writesBefore && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100)); + assert.ok(recorded.filter((r) => r.action === "settings.write").length > writesBefore, "the settings page wrote a setting"); + + // The dependency graph page opens and names the project. + await notify("show", { view: "graph:packages" }); + const graphFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); + await page.waitForFunction((f) => f.contentDocument?.querySelector("#project-name")?.textContent === "app", graphFrame, { timeout: 30_000 }); + + assert.deepEqual(errors, [], "the workbench raised no error"); + assert.deepEqual(consoleLines.filter((l) => l.startsWith("error:")), [], "no page logged an error (a CSP refusal among them)"); + } catch (error) { + const body = await page.evaluate(() => document.body.innerText).catch(() => ""); + const logs = recorded.filter((r) => r.action === "log").map((r) => r.params.text); + console.error(`-- page --\n${body}\n-- errors --\n${errors.join("\n")}\n-- console --\n${consoleLines.join("\n")}\n-- log --\n${logs.join("")}`); + throw error; + } finally { + await browser.close(); + await backend.stop(); + fs.rmSync(base, { recursive: true, force: true }); + } +}); diff --git a/webui/tsconfig.json b/webui/tsconfig.json new file mode 100644 index 0000000..89fe55f --- /dev/null +++ b/webui/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": [ + "ES2022", + "DOM" + ], + "strict": true, + "noEmit": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [ + "node" + ], + "baseUrl": ".", + "paths": { + "@noble/hashes/*": [ + "../mide-plugins/vscode/node_modules/@noble/hashes/*" + ], + "buffer": [ + "../mide-plugins/vscode/node_modules/buffer" + ] + }, + "typeRoots": [ + "../mide-plugins/vscode/node_modules/@types" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/webui/workbench.css b/webui/workbench.css new file mode 100644 index 0000000..ebb8141 --- /dev/null +++ b/webui/workbench.css @@ -0,0 +1,38 @@ +/* The workbench's own chrome: a tab per view and panel, as the IDE's tool window shows tabs, in the + theme's colours (the --vscode-* variables the IDE sets on the root). */ +html, body { height: 100%; margin: 0; } +body { + display: flex; flex-direction: column; overflow: hidden; + color: var(--vscode-foreground); background: var(--vscode-sideBar-background, var(--vscode-editor-background)); + font-family: var(--vscode-font-family); font-size: var(--vscode-font-size); +} +.tabs { + display: flex; flex: none; gap: 2px; overflow-x: auto; padding: 0 4px; + border-bottom: 1px solid var(--vscode-panel-border); +} +.tab { + display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border: 0; border-bottom: 2px solid transparent; + background: none; color: var(--vscode-descriptionForeground); font: inherit; cursor: pointer; white-space: nowrap; +} +.tab:hover { color: var(--vscode-foreground); } +.tab.active { color: var(--vscode-foreground); border-bottom-color: var(--vscode-focusBorder); } +.tab .close { font-size: 12px; opacity: 0.6; } +.tab .close:hover { opacity: 1; } +.stage { flex: 1; min-height: 0; position: relative; } +.stage > section { position: absolute; inset: 0; overflow: auto; } +.stage > section[hidden] { display: none; } +iframe.webview { width: 100%; height: 100%; border: 0; display: block; background: var(--vscode-editor-background); } +.tree { padding: 4px 0; } +.tree .row { + display: flex; align-items: center; gap: 4px; height: 22px; padding-right: 8px; cursor: pointer; white-space: nowrap; +} +.tree .row:hover { background: var(--vscode-list-hoverBackground); } +.tree .twistie { width: 16px; flex: none; } +.tree .icon { flex: none; } +.tree .label { overflow: hidden; text-overflow: ellipsis; } +.tree .description { color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; margin-left: 4px; font-size: 0.92em; } +.tree .action { margin-left: auto; opacity: 0; } +.tree .row:hover .action { opacity: 0.8; } +.tree .action:hover { opacity: 1; } +/* The IDE's settings dialog shows the settings page alone. */ +body.settings-only .tabs { display: none; } From 1ebe209e32349052f1a01de55e928cb04b390e97 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 02:33:21 +0900 Subject: [PATCH 080/119] mcpp-clion: the mcpp tool window and the settings page are the workbench in JCEF Workbench.kt hosts webui's bundle in a JBCefBrowser and answers its bridge in the IDE's terms: the backend's methods, the settings store, dialogs, popups, progress, editors, the clipboard, the browser, files, mcpp's commands; the theme is given as the --vscode-* variables. The tool window holds one workbench; Settings | Tools | mcpp opens its settings page alone. Selecting a target and packing are the IDE's popups, packing as a job over the backend's pack/formats. Gradle bundles the workbench with the extension's tools/bundle-webui.mjs; CI installs the extension's dependencies, runs the workbench test in Chromium, and checks that the plugin's jar carries every file the workbench lists. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 24 +- mide-plugins/clion/build.gradle.kts | 25 +- .../mcppcommunity/mcppclion/McppSession.kt | 5 + .../mcppclion/actions/Dispatch.kt | 48 +- .../mcppclion/settings/McppConfigurable.kt | 9 +- .../mcppclion/webui/McppPanels.kt | 68 ++- .../mcppclion/webui/Workbench.kt | 481 ++++++++++++++++++ spec/i18n/runtime.zh-cn.json | 6 +- 8 files changed, 610 insertions(+), 56 deletions(-) create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db1ef50..d9d4e3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -381,9 +381,10 @@ jobs: if-no-files-found: warn # The CLion plugin (mide-plugins/clion; the protocol's editor `jetbrains`, the IntelliJ platform): - # its tests (the wire and a session over the mock mcpp), the archive carrying the bundle and every - # system's okrun, and the Plugin Verifier against the oldest CLion it claims (2026.1) and the one it - # is built on. + # its tests (the wire and a session over the mock mcpp), the workbench it shows (mcpp-vscode's pages, + # bundled by the extension's tools) in Chromium over the backend built here, the archive carrying + # the workbench, the bundle and every system's okrun, and the Plugin Verifier against the oldest + # CLion it claims (2026.1) and the one it is built on. clion: name: CLion plugin needs: backend @@ -395,6 +396,14 @@ jobs: steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-mcpp + - uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: mide-plugins/vscode/package-lock.json + - name: The extension's dependencies (the workbench's bundler) + working-directory: mide-plugins/vscode + run: npm ci - name: Gradle (xim:gradle brings its JDK) run: xlings install gradle -y -g - uses: actions/cache@v5 @@ -418,6 +427,11 @@ jobs: echo "MCPP_IDE_OKB=$GITHUB_WORKSPACE/backend/mcpp-ide.okb" echo "MCPP_IDE_OKRUN=$GITHUB_WORKSPACE/backend/okrun" } >> "$GITHUB_ENV" + - name: The workbench in Chromium, over the backend + working-directory: mide-plugins/vscode + run: | + npx playwright install --with-deps chromium + npm run test:webui - name: Test and package working-directory: mide-plugins/clion run: gradle --no-daemon test buildPlugin @@ -431,6 +445,10 @@ jobs: grep -q "mcpp-clion/backend/mcpp-ide.okb" files.txt for p in linux-x86_64/okrun linux-aarch64/okrun macos-aarch64/okrun windows-x86_64/okrun.exe; do grep -q "mcpp-clion/backend/okrun/$p" files.txt; done [ "$(grep -c '\.okb$' files.txt)" = 1 ] + jar="$(grep -o 'mcpp-clion/lib/mcpp-clion-[^ ]*\.jar' files.txt | head -1)" + unzip -p build/distributions/mcpp-clion-*.zip "$jar" > plugin.jar + unzip -l plugin.jar | tee jar.txt | grep -q "webui/files.txt" + for f in $(unzip -p plugin.jar webui/files.txt); do grep -q " webui/$f$" jar.txt; done ls -l build/distributions/*.zip - uses: actions/upload-artifact@v5 with: diff --git a/mide-plugins/clion/build.gradle.kts b/mide-plugins/clion/build.gradle.kts index 2e58ddf..8f50af4 100644 --- a/mide-plugins/clion/build.gradle.kts +++ b/mide-plugins/clion/build.gradle.kts @@ -1,5 +1,5 @@ // mcpp-clion: mcpp's project features in CLion -- run configurations, the test tree, debugging, the -// console's links, and the shared panels (../../webui) -- over the mcpp-ide backend (`mcpp-ide serve`). +// console's links, and mcpp-vscode's pages in a workbench (../../webui) -- over the mcpp-ide backend (`mcpp-ide serve`). // // Language features are mcppls's: this plugin depends on the mcppls CLion plugin and registers no // language service. The declarations generated from ../../spec (actions, message bundles) are written @@ -89,10 +89,23 @@ val generateDeclarations = tasks.register("generateDeclarations") { if (dev != null) commandLine(dev, "gen", "jetbrains", "--out", out.absolutePath) else commandLine("mcpp", "run", "mcpp-ide-dev", "--", "gen", "jetbrains", "--out", out.absolutePath) } -// ../../webui, as it is: the panels JCEF loads. -val copyWebui = tasks.register("copyWebui") { - from(repositoryRoot.resolve("webui")) { exclude("tests/**", "tsconfig.json", "js/types.d.ts") } - into(layout.buildDirectory.dir("generated/webui/webui")) +// The workbench JCEF loads: mcpp-vscode's pages over ../../webui, bundled by the extension's +// tools/bundle-webui.mjs (Node; `npm ci` in ../vscode first), with webui/files.txt naming its files. +val webuiOut = layout.buildDirectory.dir("generated/webui/webui") +val bundleWebui = tasks.register("bundleWebui") { + val extension = repositoryRoot.resolve("mide-plugins/vscode") + inputs.dir(repositoryRoot.resolve("webui/src")) + inputs.files(repositoryRoot.resolve("webui/index.html"), repositoryRoot.resolve("webui/workbench.css")) + inputs.dir(extension.resolve("src")) + inputs.dir(extension.resolve("media")) + inputs.file(extension.resolve("package.json")) + outputs.dir(webuiOut) + workingDir = extension + doFirst { + if (!extension.resolve("node_modules/esbuild").isDirectory) + throw GradleException("the workbench is bundled with the extension's dependencies: run `npm ci` in ${extension}") + } + commandLine("node", "tools/bundle-webui.mjs", "--out", webuiOut.get().asFile.absolutePath) } sourceSets { main { @@ -100,7 +113,7 @@ sourceSets { resources.srcDir(layout.buildDirectory.dir("generated/webui")) } } -tasks.processResources { dependsOn(generateDeclarations, copyWebui) } +tasks.processResources { dependsOn(generateDeclarations, bundleWebui) } // What the plugin carries of the backend (mcpp design 2026-10-28 part 2 §15), as the VS Code // extension carries it: MCPP_IDE_OKB, the ok-image bundle (backend/mcpp-ide.okb, one for every diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index 1b7770b..a23a9ce 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -163,6 +163,11 @@ class McppSession(private val project: Project) : Disposable { }) } + /** What the pages write to their output channel: the IDE's log. */ + fun log(text: String) { + text.trimEnd().lines().forEach { log.info("[mcpp] $it") } + } + fun notify(message: String, type: NotificationType = NotificationType.INFORMATION) { NotificationGroupManager.getInstance().getNotificationGroup("mcpp").createNotification("mcpp", message, type).notify(project) } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt index 8bc8e52..c651d2a 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt @@ -164,7 +164,50 @@ object Dispatch { } } - private fun selectTarget(project: Project) { + /** The pack's three choices (format, mode, linkage), as VS Code asks them, then the job in a console. */ + fun pack(project: Project) { + val session = McppSession.get(project) + val root = session.root ?: return + session.request("pack/formats", JsonObject().apply { addProperty("root", root) }).thenAccept { r -> + val o = r.asJsonObject + val dflt = session.t("the project's default") + val formats = listOf("") + (o.getAsJsonArray("formats")?.map { it.asJsonObject["name"].asString } ?: emptyList()) + val modes = listOf("") + (o.getAsJsonArray("modes")?.map { it.asString } ?: emptyList()) + val linkages = o.getAsJsonArray("linkages")?.map { it.asString } ?: emptyList() + fun choose(title: String, values: List, next: (String) -> Unit) { + val step = object : BaseListPopupStep(title, values) { + override fun getTextFor(value: String): String = value.ifEmpty { dflt } + override fun onChosen(value: String, finalChoice: Boolean): PopupStep<*>? { + ApplicationManager.getApplication().invokeLater { next(value) } + return FINAL_CHOICE + } + } + JBPopupFactory.getInstance().createListPopup(step).showCenteredInCurrentWindow(project) + } + ApplicationManager.getApplication().invokeLater { + choose(session.t("mcpp: Pack (1/3): the package format"), formats) { format -> + choose(session.t("mcpp: Pack (2/3): what the machine that runs it provides"), modes) { mode -> + val start = { linkage: String -> + val params = JsonObject().apply { + addProperty("kind", "pack"); addProperty("root", root); addProperty("color", true) + if (format.isNotEmpty()) addProperty("pkgFormat", format) + if (mode.isNotEmpty()) addProperty("mode", mode) + if (linkage.isNotEmpty()) addProperty("linkage", linkage) + McppSettings.get().string("mcpp.build.target").takeIf { it.isNotBlank() }?.let { addProperty("target", it) } + } + val handler = io.github.mcppcommunity.mcppclion.run.JobProcessHandler(session, params) + com.intellij.execution.RunContentExecutor(project, handler).withTitle(session.t("mcpp: Pack")).withActivateToolWindow(true).run() + } + if (linkages.isEmpty()) start("") + else choose(session.t("mcpp: Pack (3/3): the libraries inside the program or beside it"), listOf("") + linkages, start) + } + } + } + } + } + + /** The targets mcpp knows, and the target sets it names (`openkal (aarch64, x86_64)`). */ + fun selectTarget(project: Project) { val session = McppSession.get(project) session.request("targets/list", JsonObject().apply { session.root?.let { addProperty("root", it) } }).thenAccept { r -> val rows = r.asJsonObject.getAsJsonArray("rows").map { it.asJsonObject } @@ -172,7 +215,8 @@ object Dispatch { val step = object : BaseListPopupStep(session.t("Select the build target"), rows) { override fun getTextFor(row: JsonObject): String { val platform = row["platformText"]?.asString?.takeIf { it.isNotEmpty() }?.let { " · $it" } ?: "" - return (if (row["selected"].asBoolean) "✓ " else "") + row["target"].asString + platform + val name = if (row["set"]?.asBoolean == true) row["label"]?.asString ?: row["target"].asString else row["target"].asString + return (if (row["selected"].asBoolean) "✓ " else "") + name + platform } override fun onChosen(row: JsonObject, finalChoice: Boolean): PopupStep<*>? { McppSettings.get().set("mcpp.build.target", com.google.gson.JsonPrimitive(if (row["host"].asBoolean) "" else row["target"].asString)) diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/settings/McppConfigurable.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/settings/McppConfigurable.kt index eaa2b73..84dbef2 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/settings/McppConfigurable.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/settings/McppConfigurable.kt @@ -1,12 +1,11 @@ -// Settings | Tools | mcpp: the shared settings panel, the same one VS Code shows. +// Settings | Tools | mcpp: mcpp-vscode's settings page, in the workbench (Workbench.kt). package io.github.mcppcommunity.mcppclion.settings -import com.google.gson.JsonObject import com.intellij.openapi.Disposable import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer -import io.github.mcppcommunity.mcppclion.webui.WebuiPanel +import io.github.mcppcommunity.mcppclion.webui.Workbench import javax.swing.JComponent class McppConfigurable(private val project: Project) : Configurable { @@ -17,10 +16,10 @@ class McppConfigurable(private val project: Project) : Configurable { override fun createComponent(): JComponent { val d = Disposer.newDisposable("mcpp settings") disposable = d - return WebuiPanel(project, "settings", JsonObject(), d).component + return Workbench(project, "settings", d).component } - // Every change is stored as it is made (the panel writes through the host). + // Every change is stored as it is made (the page writes through the workbench). override fun isModified(): Boolean = false override fun apply() {} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/McppPanels.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/McppPanels.kt index 22eee42..91644d8 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/McppPanels.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/McppPanels.kt @@ -1,64 +1,54 @@ -// The panels' places in the IDE: the "mcpp" tool window holds the project, libraries, cache and -// targets panels; any other panel (a graph, pack, settings, a package's page) opens there as a tab -// of its own, which a second request for the same panel selects again. +// The "mcpp" tool window: the workbench (Workbench.kt), whose tabs are mcpp-vscode's views -- the +// project tree, the libraries, the cache -- and the panels they open. A request for a panel (the +// command table's `panel` actions) brings it on screen there; the targets and packing are the IDE's +// own popups, as VS Code's are its quick picks. package io.github.mcppcommunity.mcppclion.webui import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.openapi.wm.ToolWindowManager import com.intellij.ui.content.ContentFactory -import io.github.mcppcommunity.mcppclion.McppSession +import io.github.mcppcommunity.mcppclion.actions.Dispatch import java.io.File object McppPanels { const val TOOL_WINDOW = "mcpp" - private val defaults = listOf("project", "libraries", "cache", "targets") - - fun title(session: McppSession, panel: String, params: JsonObject): String = when (panel) { - "project" -> session.t("Project") - "libraries", "library" -> params["id"]?.asString ?: session.t("Libraries") - "cache" -> session.t("Cache") - "targets" -> session.t("Targets and Platforms") - "settings" -> session.t("Settings") - "pack" -> session.t("Pack") - "graph" -> if (params["graph"]?.asString == "modules") session.t("Module graph") else session.t("Dependency graph") - else -> panel - } - - private fun key(panel: String, params: JsonObject) = "$panel:${params["graph"]?.asString ?: ""}:${params["id"]?.asString ?: ""}" - - fun add(project: Project, window: ToolWindow, panel: String, params: JsonObject, closeable: Boolean) { - val session = McppSession.get(project) - val content = ContentFactory.getInstance().createContent(null, title(session, panel, params), false) - val view = WebuiPanel(project, if (panel == "library") "libraries" else panel, params, content) - content.component = view.component - content.isCloseable = closeable - content.putUserData(KEY, key(panel, params)) - window.contentManager.addContent(content) - } + private val WORKBENCH = Key.create("mcpp.workbench") fun show(project: Project, panel: String, params: JsonObject = JsonObject()) { + when (panel) { + "targets" -> { Dispatch.selectTarget(project); return } + "pack" -> { Dispatch.pack(project); return } + } val window = ToolWindowManager.getInstance(project).getToolWindow(TOOL_WINDOW) ?: return - if (!params.has("root")) McppSession.get(project).root?.let { params.addProperty("root", it) } - val wanted = key(panel, params) - val existing = window.contentManager.contents.firstOrNull { it.getUserData(KEY) == wanted } - if (existing == null) add(project, window, panel, params, closeable = panel !in defaults) - val content = window.contentManager.contents.first { it.getUserData(KEY) == wanted } - window.contentManager.setSelectedContent(content) - window.activate(null) + window.activate { + val workbench = window.contentManager.contents.firstNotNullOfOrNull { it.getUserData(WORKBENCH) } ?: return@activate + when (panel) { + "project" -> workbench.show("mcpp.project") + "cache" -> workbench.show("mcpp.cache") + "settings" -> workbench.show("settings") + "graph" -> workbench.show("graph:${params["graph"]?.asString ?: "packages"}") + "libraries", "library" -> params["id"]?.asString?.let { workbench.show("library", JsonPrimitive(it)) } ?: workbench.show("mcpp.library") + else -> workbench.show(panel) + } + } } - private val KEY = com.intellij.openapi.util.Key.create("mcpp.panel") - class Factory : ToolWindowFactory, DumbAware { override suspend fun isApplicableAsync(project: Project): Boolean = project.basePath?.let { File(it, "mcpp.toml").isFile } ?: false override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val params = JsonObject().apply { McppSession.get(project).root?.let { addProperty("root", it) } } - for (panel in defaults) add(project, toolWindow, panel, params.deepCopy(), closeable = false) + val content = ContentFactory.getInstance().createContent(null, "", false) + val workbench = Workbench(project, "window", toolWindow.disposable) + content.component = workbench.component + content.isCloseable = false + content.putUserData(WORKBENCH, workbench) + toolWindow.contentManager.addContent(content) } } } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt new file mode 100644 index 0000000..6698de5 --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt @@ -0,0 +1,481 @@ +// The workbench (../../webui): mcpp-vscode's pages -- the project tree, the libraries and a package's +// page, the cache, the settings, the graphs -- in a JCEF browser, as the same modules VS Code runs. +// What the pages ask of VS Code is answered here, in the IDE's own terms: the backend's methods, the +// settings store, dialogs and popups, editors, the clipboard, the browser, files, mcpp's commands. +// +// Asynchronous requests arrive through `window.mcppHostSend` (a JBCefJSQuery) and are answered +// through `window.mcppHostReceive`; a synchronous one (a page's synchronous file read) is a +// `prompt("mcpp-host:...")` answered by the JS dialog handler, the one synchronous channel a page has. +package io.github.mcppcommunity.mcppclion.webui + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.intellij.ide.BrowserUtil +import com.intellij.ide.trustedProjects.TrustedProjects +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.fileChooser.FileChooserFactory +import com.intellij.openapi.fileChooser.FileSaverDescriptor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileDocumentManagerListener +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.ide.CopyPasteManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.BulkFileListener +import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent +import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent +import com.intellij.openapi.vfs.newvfs.events.VFileEvent +import com.intellij.testFramework.LightVirtualFile +import com.intellij.ui.JBColor +import com.intellij.ui.jcef.JBCefApp +import com.intellij.ui.jcef.JBCefBrowser +import com.intellij.ui.jcef.JBCefBrowserBase +import com.intellij.ui.jcef.JBCefJSQuery +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import io.github.mcppcommunity.mcppclion.McppSession +import io.github.mcppcommunity.mcppclion.actions.Dispatch +import io.github.mcppcommunity.mcppclion.settings.McppSettings +import org.cef.browser.CefBrowser +import org.cef.browser.CefFrame +import org.cef.callback.CefJSDialogCallback +import org.cef.handler.CefJSDialogHandler +import org.cef.handler.CefJSDialogHandlerAdapter +import org.cef.handler.CefLoadHandlerAdapter +import org.cef.misc.BoolRef +import java.awt.Color +import java.awt.datatransfer.StringSelection +import java.io.File +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes +import java.util.Base64 +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.SwingConstants + +/** + * One workbench. [mode] `window` shows the views as tabs (the "mcpp" tool window); `settings` opens + * the settings panel alone (Settings | Tools | mcpp). + */ +class Workbench(private val project: Project, private val mode: String, parent: Disposable) : Disposable { + private val session = McppSession.get(project) + private val browser: JBCefBrowser? = if (JBCefApp.isSupported()) JBCefBrowser() else null + private val query: JBCefJSQuery? = browser?.let { JBCefJSQuery.create(it as JBCefBrowserBase) } + private val unsubscribe = mutableListOf<() -> Unit>() + private var loaded = false + private val watched = Regex("/(mcpp\\.toml|mcpp\\.lock|[^/]+\\.(cpp|cc|cxx|cppm|ixx|mpp|ccm))$") + private val waiting = mutableListOf() + + val component: JComponent = browser?.component + ?: JLabel(session.t("This IDE runs without JCEF; the mcpp panels need it."), SwingConstants.CENTER) + + init { + Disposer.register(parent, this) + unsubscribe += session.on("*") { m -> notify(m.asJsonObject["method"].asString, m.asJsonObject["params"] ?: JsonNull.INSTANCE) } + if (browser != null && query != null) { + query.addHandler { text -> receive(text); null } + browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { + override fun onLoadEnd(b: CefBrowser, frame: CefFrame, status: Int) { + if (!frame.isMain) return + b.executeJavaScript("window.mcppHostSend = function (text) { ${query.inject("text")} }; window.mcppHostAttached && window.mcppHostAttached();", b.url, 0) + ApplicationManager.getApplication().invokeLater { loaded = true; waiting.forEach(::post); waiting.clear() } + } + }, browser.cefBrowser) + browser.jbCefClient.addJSDialogHandler(object : CefJSDialogHandlerAdapter() { + override fun onJSDialog(b: CefBrowser, origin: String, type: CefJSDialogHandler.JSDialogType, message: String, defaultPrompt: String, + callback: CefJSDialogCallback, suppress: BoolRef): Boolean { + if (type != CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_PROMPT || !message.startsWith("mcpp-host:")) return false + callback.Continue(true, sync(message.removePrefix("mcpp-host:")).toString()) + return true + } + }, browser.cefBrowser) + val bus = project.messageBus.connect(this) + bus.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { + override fun selectionChanged(event: FileEditorManagerEvent) { + notify("activeEditor", JsonObject().apply { event.newFile?.takeIf { it.isInLocalFileSystem }?.let { addProperty("path", it.path) } }) + } + }) + bus.subscribe(FileDocumentManagerListener.TOPIC, object : FileDocumentManagerListener { + override fun beforeDocumentSaving(document: com.intellij.openapi.editor.Document) { + FileDocumentManager.getInstance().getFile(document)?.let { f -> notify("documentSaved", JsonObject().apply { addProperty("path", f.path) }) } + } + }) + bus.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener { + override fun after(events: List) { + for (e in events) { + // What the pages watch: the manifests and the module sources (the graphs). + val path = e.path + if (!watched.containsMatchIn(path)) continue + val kind = when (e) { is VFileCreateEvent -> "created"; is VFileDeleteEvent -> "deleted"; else -> "changed" } + notify("fileChanged", JsonObject().apply { addProperty("path", path); addProperty("kind", kind) }) + } + } + }) + ApplicationManager.getApplication().messageBus.connect(this).subscribe(McppSettings.CHANGED, McppSettings.Listener { key -> + notify("settingsChanged", JsonObject().apply { add("keys", JsonArray().apply { add(key) }); add("levels", levels()) }) + }) + browser.loadURL(page().toURI().toString()) + } + } + + /** The workbench's files, written once per build of the plugin where the browser can load them. */ + private fun page(): File { + val jar = runCatching { File(Workbench::class.java.protectionDomain.codeSource.location.toURI()) }.getOrNull() + val version = jar?.takeIf { it.isFile }?.let { "${it.nameWithoutExtension}-${it.lastModified()}" } ?: "dev" + val dir = File(PathManager.getSystemPath(), "mcpp-clion/workbench-$version") + val index = javaClass.getResource("/webui/files.txt")?.readText() ?: error("the workbench is not in the plugin (webui/files.txt)") + for (f in index.lines().filter { it.isNotBlank() }) { + val target = File(dir, f) + if (target.isFile && version != "dev") continue + target.parentFile.mkdirs() + javaClass.getResourceAsStream("/webui/$f")?.use { input -> target.outputStream().use { input.copyTo(it) } } + } + return File(dir, "index.html") + } + + /** Asks the workbench to bring a view or a panel on screen (McppPanels.show). */ + fun show(view: String, args: JsonElement? = null) { + notify("show", JsonObject().apply { addProperty("view", view); args?.let { add("args", it) } }) + } + + private fun notify(method: String, params: JsonElement) { + post(JsonObject().apply { addProperty("type", "notify"); addProperty("method", method); add("params", params) }) + } + + private fun post(message: JsonObject) { + val b = browser ?: return + ApplicationManager.getApplication().invokeLater { + if (!loaded) { waiting += message; return@invokeLater } + b.cefBrowser.executeJavaScript("window.mcppHostReceive && window.mcppHostReceive($message);", b.cefBrowser.url, 0) + } + } + + private fun answer(id: JsonElement, result: CompletableFuture) { + result.whenComplete { value, error -> + post(JsonObject().apply { + add("id", id) + if (error == null) { addProperty("type", "result"); add("result", value ?: JsonNull.INSTANCE) } + else { addProperty("type", "error"); add("error", JsonObject().apply { addProperty("message", (error.cause ?: error).message ?: "failed") }) } + }) + } + } + + private fun receive(text: String) { + val m = runCatching { JsonParser.parseString(text).asJsonObject }.getOrNull() ?: return + if (m["type"]?.asString != "request") return + val action = m["action"]?.asString ?: return + val params = m["params"]?.takeIf { it.isJsonObject }?.asJsonObject ?: JsonObject() + answer(m["id"], handle(action, params)) + } + + // ── synchronous requests: files ────────────────────────────────────────────────────────── + + private fun sync(text: String): JsonObject = runCatching { + val m = JsonParser.parseString(text).asJsonObject + val params = m["params"]?.takeIf { it.isJsonObject }?.asJsonObject ?: JsonObject() + JsonObject().apply { addProperty("ok", true); add("result", files(m["action"].asString, params)) } + }.getOrElse { e -> JsonObject().apply { addProperty("ok", false); addProperty("error", e.message ?: "failed") } } + + private fun stat(path: Path, follow: Boolean): JsonElement { + val options = if (follow) arrayOf() else arrayOf(LinkOption.NOFOLLOW_LINKS) + val a = runCatching { Files.readAttributes(path, BasicFileAttributes::class.java, *options) }.getOrNull() ?: return JsonNull.INSTANCE + return JsonObject().apply { + addProperty("type", when { a.isSymbolicLink -> "symlink"; a.isDirectory -> "directory"; a.isRegularFile -> "file"; else -> "other" }) + addProperty("size", a.size()) + addProperty("mtimeMs", a.lastModifiedTime().toMillis()) + addProperty("ino", (a.fileKey()?.hashCode() ?: path.hashCode()).toLong() and 0xffffffffL) + } + } + + private fun files(action: String, p: JsonObject): JsonElement { + val path = Path.of(p["path"].asString) + return when (action) { + "fs.stat" -> stat(path, p["follow"]?.asBoolean ?: true) + "fs.read" -> if (Files.isRegularFile(path)) JsonPrimitive(Files.readString(path)) else JsonNull.INSTANCE + "fs.list" -> if (!Files.isDirectory(path)) JsonNull.INSTANCE else JsonArray().also { out -> + Files.list(path).use { stream -> + stream.forEach { child -> + val s = stat(child, false) + if (s.isJsonObject) out.add(s.asJsonObject.apply { addProperty("name", child.fileName.toString()) }) + } + } + } + "fs.realpath" -> if (Files.exists(path)) JsonPrimitive(path.toRealPath().toString()) else JsonNull.INSTANCE + "fs.write" -> { + val text = p["text"].asString + Files.createDirectories(path.parent) + if (p["base64"]?.asBoolean == true) Files.write(path, Base64.getDecoder().decode(text)) else Files.writeString(path, text) + LocalFileSystem.getInstance().refreshAndFindFileByPath(path.toString()) + JsonPrimitive(true) + } + else -> error("unknown file action $action") + } + } + + // ── asynchronous requests ──────────────────────────────────────────────────────────────── + + private fun done(value: JsonElement = JsonNull.INSTANCE) = CompletableFuture.completedFuture(value) + + private fun onEdt(block: (CompletableFuture) -> Unit): CompletableFuture { + val f = CompletableFuture() + ApplicationManager.getApplication().invokeLater { runCatching { block(f) }.onFailure { f.completeExceptionally(it) } } + return f + } + + private fun levels(): JsonObject = JsonObject().apply { + add("user", McppSettings.get().values()) + add("workspace", JsonObject()) + } + + private fun handle(action: String, p: JsonObject): CompletableFuture = when (action) { + "init" -> done(JsonObject().apply { + add("info", info()) + add("theme", Theme.variables()) + addProperty("themeKind", if (JBColor.isBright()) "vscode-light" else "vscode-dark") + addProperty("mode", mode) + }) + "rpc" -> session.request(p["method"].asString, p["params"] ?: JsonObject()) + "settings.read" -> done(levels()) + "settings.write" -> { + McppSettings.get().set(p["key"].asString, p["value"]?.takeIf { !it.isJsonNull }) + done(levels()) + } + "message" -> message(p) + "input" -> onEdt { f -> + val value = Messages.showInputDialog(project, p["prompt"]?.asString ?: "", p["title"]?.asString ?: "mcpp", null, p["value"]?.asString ?: "", null) + f.complete(value?.let { JsonPrimitive(it) } ?: JsonNull.INSTANCE) + } + "pick" -> onEdt { f -> pick(p, f) } + "saveDialog" -> onEdt { f -> + val filters = p["filters"]?.takeIf { it.isJsonObject }?.asJsonObject?.entrySet()?.flatMap { e -> e.value.asJsonArray.map { it.asString } }?.toTypedArray() ?: emptyArray() + val start = p["path"]?.asString?.let { File(it) } + val dialog = FileChooserFactory.getInstance().createSaveFileDialog(FileSaverDescriptor(p["title"]?.asString ?: "mcpp", "", *filters), project) + val chosen = dialog.save(start?.parentFile?.toPath(), start?.name) + f.complete(chosen?.file?.path?.let { JsonPrimitive(it) } ?: JsonNull.INSTANCE) + } + "progress.start" -> progressStart(p["title"]?.asString ?: "mcpp") + "progress.report" -> { progress[p["id"].asInt]?.text2 = p["message"]?.asString; done() } + "progress.end" -> { progressEnd[p["id"].asInt]?.complete(Unit); done() } + "openFile" -> onEdt { f -> + val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(p["path"].asString) + if (file != null) OpenFileDescriptor(project, file, maxOf(0, (p["line"]?.takeIf { !it.isJsonNull }?.asInt ?: 1) - 1), 0).navigate(true) + f.complete(JsonPrimitive(file != null)) + } + "openText" -> onEdt { f -> + val file = LightVirtualFile(p["name"].asString, p["text"].asString).apply { isWritable = false } + FileEditorManager.getInstance(project).openFile(file, true) + f.complete(JsonPrimitive(true)) + } + "clipboard" -> { CopyPasteManager.getInstance().setContents(StringSelection(p["text"].asString)); done() } + "openExternal" -> { BrowserUtil.browse(p["url"].asString); done(JsonPrimitive(true)) } + "log" -> { session.log(p["text"].asString); done() } + "log.show" -> done() + "command" -> onEdt { f -> + val id = p["id"].asString + if (session.commands.any { it["id"].asString == id }) { + val args = p["args"]?.takeIf { it.isJsonArray }?.asJsonArray + val first = args?.firstOrNull() + Dispatch.run(project, id, if (first != null && first.isJsonObject) first.asJsonObject.deepCopy() else JsonObject()) + } + f.complete(JsonNull.INSTANCE) + } + "watch" -> done() + "findFiles" -> CompletableFuture.supplyAsync { findFiles(p) } + "fs.read", "fs.list", "fs.stat", "fs.realpath", "fs.write" -> CompletableFuture.supplyAsync { files(action, p) } + else -> CompletableFuture.failedFuture(IllegalArgumentException("unknown workbench action $action")) + } + + private fun info(): JsonObject = JsonObject().apply { + val os = System.getProperty("os.name").lowercase() + addProperty("platform", when { os.startsWith("windows") -> "win32"; os.startsWith("mac") -> "darwin"; else -> "linux" }) + addProperty("arch", if (System.getProperty("os.arch").lowercase().let { it == "aarch64" || it == "arm64" }) "arm64" else "x64") + addProperty("language", com.intellij.DynamicBundle.getLocale().toLanguageTag().lowercase().let { if (it.startsWith("zh")) "zh-cn" else it }) + addProperty("home", System.getProperty("user.home")) + addProperty("tmp", System.getProperty("java.io.tmpdir")) + add("env", Gson().toJsonTree(System.getenv())) + addProperty("extensionUri", page().parentFile.toURI().toString()) + add("workspaceFolders", JsonArray().apply { project.basePath?.let { add(it) } }) + addProperty("trusted", TrustedProjects.isProjectTrusted(project)) + } + + private fun message(p: JsonObject): CompletableFuture = onEdt { f -> + val items = p["items"]?.asJsonArray?.map { it.asString } ?: emptyList() + val text = listOfNotNull(p["message"]?.asString, p["detail"]?.takeIf { !it.isJsonNull }?.asString).joinToString("\n\n") + val modal = p["modal"]?.asBoolean == true + if (modal || items.isNotEmpty() && p["level"]?.asString == "warning") { + val icon = when (p["level"]?.asString) { "error" -> Messages.getErrorIcon(); "warning" -> Messages.getWarningIcon(); else -> Messages.getInformationIcon() } + val buttons = (items + session.t("Cancel")).toTypedArray() + val chosen = Messages.showDialog(project, text, "mcpp", buttons, items.size, icon) + f.complete(if (chosen in items.indices) JsonPrimitive(chosen) else JsonNull.INSTANCE) + return@onEdt + } + val type = when (p["level"]?.asString) { "error" -> NotificationType.ERROR; "warning" -> NotificationType.WARNING; else -> NotificationType.INFORMATION } + val n = NotificationGroupManager.getInstance().getNotificationGroup("mcpp").createNotification(text, type) + items.forEachIndexed { i, title -> + n.addAction(NotificationAction.createSimpleExpiring(title) { f.complete(JsonPrimitive(i)) }) + } + n.whenExpired { f.complete(JsonNull.INSTANCE) } + n.notify(project) + if (items.isEmpty()) f.complete(JsonNull.INSTANCE) + } + + private fun pick(p: JsonObject, f: CompletableFuture) { + val rows = p["items"].asJsonArray.map { it.asJsonObject } + val labels = rows.map { r -> listOfNotNull(r["label"]?.asString, r["description"]?.takeIf { !it.isJsonNull }?.asString).joinToString(" · ") } + val step = object : BaseListPopupStep(p["title"]?.takeIf { !it.isJsonNull }?.asString ?: p["placeHolder"]?.takeIf { !it.isJsonNull }?.asString, labels) { + override fun onChosen(selectedValue: String, finalChoice: Boolean): PopupStep<*>? { + f.complete(JsonPrimitive(labels.indexOf(selectedValue))) + return FINAL_CHOICE + } + override fun isSpeedSearchEnabled(): Boolean = true + override fun canceled() { f.complete(JsonNull.INSTANCE) } + } + JBPopupFactory.getInstance().createListPopup(step).showCenteredInCurrentWindow(project) + } + + private val progress = ConcurrentHashMap() + private val progressEnd = ConcurrentHashMap>() + private val nextProgress = AtomicInteger(1) + + private fun progressStart(title: String): CompletableFuture { + val id = nextProgress.getAndIncrement() + val end = CompletableFuture() + progressEnd[id] = end + ApplicationManager.getApplication().invokeLater { + ProgressManager.getInstance().run(object : Task.Backgroundable(project, title, false) { + override fun run(indicator: ProgressIndicator) { + progress[id] = indicator + indicator.isIndeterminate = true + runCatching { end.get() } + progress.remove(id) + progressEnd.remove(id) + } + }) + } + return done(JsonPrimitive(id)) + } + + private fun findFiles(p: JsonObject): JsonElement { + val base = Path.of(p["base"]?.takeIf { !it.isJsonNull }?.asString ?: project.basePath ?: return JsonArray()) + val matcher = FileSystems.getDefault().getPathMatcher("glob:${p["pattern"].asString}") + val skip = Regex("(^|/)(node_modules|target|\\.git|\\.mcpp|build)(/|$)") + val max = p["max"]?.takeIf { !it.isJsonNull }?.asInt ?: 10000 + val out = JsonArray() + if (!Files.isDirectory(base)) return out + Files.walk(base).use { stream -> + stream.filter { Files.isRegularFile(it) }.filter { !skip.containsMatchIn(base.relativize(it).toString().replace('\\', '/')) } + .filter { matcher.matches(base.relativize(it)) }.limit(max.toLong()).forEach { out.add(it.toString()) } + } + return out + } + + override fun dispose() { + unsubscribe.forEach { it() } + progressEnd.values.forEach { it.complete(Unit) } + query?.let { Disposer.dispose(it) } + browser?.let { Disposer.dispose(it) } + } +} + +/** + * The IDE's theme as the `--vscode-*` variables the pages' stylesheets read: VS Code's names, the + * IDE's colours and fonts. + */ +object Theme { + private fun css(c: Color) = if (c.alpha == 255) "rgb(${c.red}, ${c.green}, ${c.blue})" else "rgba(${c.red}, ${c.green}, ${c.blue}, ${c.alpha / 255.0})" + private fun named(key: String, light: Int, dark: Int) = JBColor.namedColor(key, JBColor(light, dark)) + private fun pick(light: String, dark: String) = if (JBColor.isBright()) light else dark + + fun variables(): JsonObject = JsonObject().apply { + val fg = UIUtil.getLabelForeground() + val muted = UIUtil.getContextHelpForeground() + val border = JBColor.border() + val scheme = EditorColorsManager.getInstance().globalScheme + val editorBg = scheme.defaultBackground + val panel = UIUtil.getPanelBackground() + val inputBg = UIUtil.getTextFieldBackground() + val inputFg = UIUtil.getTextFieldForeground() + val accent = named("Button.default.startBackground", 0x3574F0, 0x3574F0) + val accentFg = named("Button.default.foreground", 0xFFFFFF, 0xFFFFFF) + val secondary = named("Button.startBackground", 0xEBECF0, 0x3C3F41) + val link = JBUI.CurrentTheme.Link.Foreground.ENABLED + val hover = JBUI.CurrentTheme.List.Hover.background(true) + val selection = UIUtil.getListSelectionBackground(true) + val selectionFg = UIUtil.getListSelectionForeground(true) + val popup = named("PopupMenu.background", 0xFFFFFF, 0x2B2D30) + val warning = named("Component.warningFocusColor", 0xE2A53A, 0xE2A53A) + val error = named("Label.errorForeground", 0xC7222D, 0xF75464) + val font = UIUtil.getLabelFont() + fun v(name: String, value: String) = addProperty("--vscode-$name", value) + v("foreground", css(fg)); v("editorForeground", css(fg)); v("icon-foreground", css(fg)) + v("descriptionForeground", css(muted)); v("editorCodeLens-foreground", css(muted)); v("input-placeholderForeground", css(muted)) + v("editor-background", css(editorBg)); v("sideBar-background", css(panel)); v("editorWidget-background", css(popup)) + v("textCodeBlock-background", css(ColorUtilCompat.blend(editorBg, fg, 0.06))) + for (b in listOf("panel-border", "contrastBorder", "widget-border", "menu-border", "sideBarSectionHeader-border", "input-border", "dropdown-border")) v(b, css(border)) + v("focusBorder", css(JBUI.CurrentTheme.Focus.focusColor())) + v("textLink-foreground", css(link)); v("textLink-activeForeground", css(link)) + v("input-background", css(inputBg)); v("input-foreground", css(inputFg)) + v("dropdown-background", css(inputBg)); v("dropdown-foreground", css(inputFg)) + v("checkbox-background", css(inputBg)); v("checkbox-selectBackground", css(inputBg)) + v("inputOption-activeBackground", css(ColorUtilCompat.blend(inputBg, accent, 0.25))); v("inputOption-activeBorder", css(accent)); v("inputOption-activeForeground", css(fg)) + v("inputValidation-warningBackground", css(ColorUtilCompat.blend(inputBg, warning, 0.2))); v("inputValidation-warningBorder", css(warning)) + v("inputValidation-infoBackground", css(ColorUtilCompat.blend(inputBg, accent, 0.15))); v("inputValidation-infoBorder", css(accent)) + v("button-background", css(accent)); v("button-foreground", css(accentFg)); v("button-hoverBackground", css(ColorUtilCompat.blend(accent, Color.BLACK, 0.1))) + v("button-secondaryBackground", css(secondary)); v("button-secondaryForeground", css(fg)); v("button-secondaryHoverBackground", css(ColorUtilCompat.blend(secondary, fg, 0.08))) + v("button-border", "transparent"); v("button-separator", "transparent") + v("list-hoverBackground", css(hover)); v("list-activeSelectionBackground", css(selection)); v("list-activeSelectionForeground", css(selectionFg)) + v("list-inactiveSelectionBackground", css(UIUtil.getListSelectionBackground(false))) + v("toolbar-hoverBackground", css(JBUI.CurrentTheme.ActionButton.hoverBackground())); v("toolbar-activeBackground", css(JBUI.CurrentTheme.ActionButton.pressedBackground())) + v("badge-background", css(named("Counter.background", 0xDFE1E5, 0x4E5157))); v("badge-foreground", css(named("Counter.foreground", 0x000000, 0xDFE1E5))) + v("menu-background", css(popup)); v("menu-foreground", css(fg)) + v("menu-selectionBackground", css(selection)); v("menu-selectionForeground", css(selectionFg)) + v("sideBarSectionHeader-background", css(panel)); v("sideBarSectionHeader-foreground", css(fg)) + v("widget-shadow", "rgba(0, 0, 0, 0.36)") + v("errorForeground", css(error)); v("editorWarning-foreground", css(warning)) + v("charts-red", pick("#e51400", "#f14c4c")); v("charts-blue", pick("#1a85ff", "#3794ff")); v("charts-yellow", pick("#bf8803", "#cca700")) + v("charts-orange", pick("#d18616", "#d18616")); v("charts-green", pick("#388a34", "#89d185")); v("charts-purple", pick("#652d90", "#b180d7")) + v("testing-iconPassed", pick("#388a34", "#73c991")) + v("symbolIcon-keywordForeground", css(fg)); v("symbolIcon-interfaceForeground", pick("#1a85ff", "#75beff")); v("symbolIcon-operatorForeground", css(fg)) + v("mcpp-cacheOkForeground", pick("#388a34", "#89d185")); v("mcpp-cacheStaleForeground", pick("#bf8803", "#e2c08d")) + v("font-family", "\"${font.family}\", system-ui, sans-serif"); v("font-size", "${font.size}px") + v("editor-font-family", "\"${scheme.editorFontName}\", monospace") + } +} + +/** Colour arithmetic the theme needs (a mix of two colours). */ +private object ColorUtilCompat { + fun blend(a: Color, b: Color, t: Double): Color = Color( + (a.red + (b.red - a.red) * t).toInt().coerceIn(0, 255), + (a.green + (b.green - a.green) * t).toInt().coerceIn(0, 255), + (a.blue + (b.blue - a.blue) * t).toInt().coerceIn(0, 255), + ) +} diff --git a/spec/i18n/runtime.zh-cn.json b/spec/i18n/runtime.zh-cn.json index 9978670..25e0ae0 100644 --- a/spec/i18n/runtime.zh-cn.json +++ b/spec/i18n/runtime.zh-cn.json @@ -760,5 +760,9 @@ "{0}–{1} day(s) ago": "{0}–{1} 天前", "{0}–{1} days": "{0}–{1} 天", "…": "…", - "✓ Has examples": "✓ 有示例" + "✓ Has examples": "✓ 有示例", + "mcpp: Pack": "mcpp:打包", + "mcpp: Pack (1/3): the package format": "mcpp:打包(1/3):包格式", + "mcpp: Pack (2/3): what the machine that runs it provides": "mcpp:打包(2/3):运行它的机器提供什么", + "mcpp: Pack (3/3): the libraries inside the program or beside it": "mcpp:打包(3/3):库在程序里还是在程序旁边" } From 9c7d58f0c60eb885d14e6dd843107e5dd88d3cb6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 02:34:22 +0900 Subject: [PATCH 081/119] mcpp-vscode: an mcpp launch without a debugger extension is refused at once; the install action stays on the message Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mide-plugins/vscode/src/ide/debug.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mide-plugins/vscode/src/ide/debug.ts b/mide-plugins/vscode/src/ide/debug.ts index 69b696f..e6c0cc3 100644 --- a/mide-plugins/vscode/src/ide/debug.ts +++ b/mide-plugins/vscode/src/ide/debug.ts @@ -32,9 +32,10 @@ export class Debugging implements vscode.DebugConfigurationProvider, vscode.Disp const available = availableDebuggers((id) => vscode.extensions.getExtension(id) !== undefined); if (available.length === 0) { const install = t("Install CodeLLDB"); - const choice = await vscode.window.showErrorMessage( - t("No debugger extension is installed: install CodeLLDB, the C/C++ extension or lldb-dap."), install); - if (choice === install) await vscode.commands.executeCommand("workbench.extensions.search", "@id:vadimcn.vscode-lldb"); + // The session is refused now; the message's action is the user's, whenever they take it. + void vscode.window.showErrorMessage( + t("No debugger extension is installed: install CodeLLDB, the C/C++ extension or lldb-dap."), install) + .then((choice) => choice === install ? vscode.commands.executeCommand("workbench.extensions.search", "@id:vadimcn.vscode-lldb") : undefined); return undefined; } try { From dd4faf7becf6bdca7426f69cf980fc3753aa9976 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 02:37:26 +0900 Subject: [PATCH 082/119] 0.2.0: the backend, mcpp-clion and mcpp-nvim 0.2.0, mcpp-vscode 1.1.0; the changelogs, the READMEs and the architecture of the pages Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 31 +++++++++++++++++++ README.md | 12 +++++--- README.zh-CN.md | 10 +++--- docs/architecture.md | 42 +++++++++++++++++++++----- mcpp.toml | 2 +- mide-plugins/clion/README.md | 11 +++++-- mide-plugins/clion/gradle.properties | 2 +- mide-plugins/nvim/lua/mcpp/session.lua | 2 +- mide-plugins/vscode/CHANGELOG.md | 15 ++++++++- mide-plugins/vscode/README.md | 22 ++++++++++---- mide-plugins/vscode/README.zh-CN.md | 17 ++++++++--- src/server.cppm | 2 +- 12 files changed, 132 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3553332..70cbdab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 0.2.0 + +mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 §10). + +- **mcpp-vscode 1.1.0 is mcpp-vscode 0.9.0 over the backend.** 0.9.0's extension is imported with + its history (`mide-plugins/vscode`); every view, page, command, menu, setting and string a user + sees is 0.9.0's. What 1.0 added stays, in 0.9.0's style: debugging (`mcpp.debug`, the `mcpp` + launch type), the test tree, the target picker with target-set rows, packing, build on save, + cancellable jobs. mcpp is run through the backend's `mcpp/exec`, mcppls through its + `languageService/exec`; the extension starts no other process for mcpp or mcppls. +- **Parity gates against 0.9.0.** The manifest's contributions and strings contain 0.9.0's exactly + (`spec/` states them; `mcpp-ide-dev gen vscode` writes the manifest); every page's document, + rendered by the extension's generators, is byte-identical to the snapshot rendered by 0.9.0's; + the Extension Host suite runs over the bundled backend. +- **One page library for VS Code and CLion.** The workbench (`webui/`) runs mcpp-vscode's page + modules unchanged in a browser host: the `vscode` module and Node's built-ins are answered by + the host's bridge, views are tabs, webviews are iframes with the IDE's `--vscode-*` theme. + mcpp-clion's tool window and its settings page are this workbench in JCEF; the earlier panels + (`webui/js`) are removed. A Chromium test runs the workbench over a real backend. +- **Backend methods.** `mcpp/exec` (a bounded mcpp run: timeout, output limit, truncation reported), + `languageService/exec` (the located mcppls with arguments), `project/files` (manifest, lock and + the source count 0.9.0 shows); `targets` rows for target sets (`set`, `members`, `label`) when mcpp + is 2026.10.30.1 or later. +- **mcpp-clion on 2026.1 through 2026.3.** Trust through `TrustedProjects.isProjectTrusted`, debug + sessions through `XDebuggerManager.newSessionBuilder`, the debugger driver through + `createDriverConfiguration(project, CPPEnvironment(toolchain))`; packing is a job over the + backend's `pack/formats`. +- **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on + three systems, manifest drift, parity, Extension Host on three systems, isolated install, the + graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. + ## 0.1.1 mcpp-vscode 1.0.1, mcpp-clion 0.1.1 and the first mcpp-nvim (0.1.1), with mcppls 0.0.19. diff --git a/README.md b/README.md index c2b8294..0dd2375 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ different tool. mcpp-ide makes them once. Its backend, `mcpp-ide serve`, is a C++ program that reads only mcpp's machine contract (event streams, result records, launch descriptors, JSON queries) and answers the -editors over JSON-RPC. The VS Code extension and the CLion plugin map those answers onto their own -UI and share one set of web panels; the Neovim plugin maps them onto buffers, floating windows and -the quickfix list. The editor plugins are `mide-plugins/` of this repository. Language features of +editors over JSON-RPC. The VS Code extension's views and pages are mcpp-vscode 0.9.0's; the CLion +plugin shows the same pages in a workbench of its own (JCEF), and the Neovim plugin maps the answers +onto buffers, floating windows and the quickfix list. The editor plugins are `mide-plugins/` of this repository. Language features of C++ files come from [mcppls](https://github.com/Sunrisepeak/mcpp-language-server); mcpp-ide registers none. @@ -30,7 +30,8 @@ registers none. | Debug | CodeLLDB, C/C++ (gdb, lldb), lldb-dap | CLion's GDB or LLDB | nvim-dap (codelldb, lldb-dap, cppdbg, gdb) | | Diagnostics | Problems, from mcpp's located diagnostics | Console links to the reported location | `vim.diagnostic`, `:Mcpp problems` in the quickfix list | | mcpp.toml | Problems from mcpp; completion, hover, outline from mcpp's key registry | Problems from mcpp | Problems from mcpp | -| Targets and platforms, libraries, graphs, cache, pack, settings | Shared web panels | The same panels (JCEF) | Text in floating windows, `vim.ui.select`; settings in `setup()` | +| Libraries, a package's page, graphs, cache, settings | mcpp-vscode's views and pages | The same pages, in a JCEF workbench | Text in floating windows, `vim.ui.select`; settings in `setup()` | +| Targets (target sets among them), pack | Quick picks | Popups | `vim.ui.select` | | Language features | mcppls (installed with the extension) | mcppls CLion plugin (a dependency) | mcppls-nvim (`xlings install mcppls-nvim`) | - **One answer everywhere** -- what a build runs, how a test is reported, what a cleanup costs: the backend's @@ -69,7 +70,8 @@ Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it ```bash mcpp build # the backend, the mock mcpp, mcpp-ide-dev mcpp test # the backend's tests -cd mide-plugins/vscode && npm ci && npm test # the extension's and the panels' tests +cd mide-plugins/vscode && npm ci && npm test # the extension's tests, the parity gates against 0.9.0 among them +npm run test:webui # the workbench in Chromium, over the backend npm run package # dist/mcpp-vscode-.vsix cd ../clion && gradle buildPlugin verifyPlugin # build/distributions/mcpp-clion-.zip cd ../.. && nvim --headless --clean -l mide-plugins/nvim/tests/smoke.lua # the Neovim plugin's tests diff --git a/README.zh-CN.md b/README.zh-CN.md index 58218bc..eb7d88d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,8 +12,8 @@ 每个编辑器各写一遍,这些决定就会分歧;从一个编辑器换到另一个,用户遇到的就是另一个工具。 mcpp-ide 只做一次。它的后端 `mcpp-ide serve` 是一个 C++ 程序,只读取 mcpp 的机器契约(事件流、结果记录、启动描述、 -JSON 查询),通过 JSON-RPC 回答编辑器。VS Code 扩展与 CLion 插件把这些回答映射到各自的界面,并共用一套网页面板; -Neovim 插件把它们映射到缓冲区、浮动窗口与 quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。 +JSON 查询),通过 JSON-RPC 回答编辑器。VS Code 扩展的视图与页面即 mcpp-vscode 0.9.0 的视图与页面;CLion 插件在自己的 +工作台(JCEF)中显示同一套页面;Neovim 插件把这些回答映射到缓冲区、浮动窗口与 quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。 C++ 文件的语言功能来自 [mcppls](https://github.com/Sunrisepeak/mcpp-language-server);mcpp-ide 不注册任何语言服务。 ## 功能 @@ -25,7 +25,8 @@ C++ 文件的语言功能来自 [mcppls](https://github.com/Sunrisepeak/mcpp-lan | 调试 | CodeLLDB、C/C++(gdb、lldb)、lldb-dap | CLion 的 GDB 或 LLDB | nvim-dap(codelldb、lldb-dap、cppdbg、gdb) | | 诊断 | 问题面板,来自 mcpp 带位置的诊断 | 控制台链接到报告的位置 | `vim.diagnostic`,`:Mcpp problems` 填入 quickfix 列表 | | mcpp.toml | 问题来自 mcpp;补全、悬停、大纲来自 mcpp 的键注册表 | 问题来自 mcpp | 问题来自 mcpp | -| 目标与平台、库、依赖图、缓存、打包、设置 | 共享网页面板 | 同一套面板(JCEF) | 浮动窗口中的文本、`vim.ui.select`;设置在 `setup()` 中 | +| 库、包页面、依赖图、缓存、设置 | mcpp-vscode 的视图与页面 | 同一套页面,位于 JCEF 工作台 | 浮动窗口中的文本、`vim.ui.select`;设置在 `setup()` 中 | +| 目标(含目标集合)、打包 | 快速选择 | 弹出列表 | `vim.ui.select` | | 语言功能 | mcppls(随扩展一并安装) | mcppls CLion 插件(依赖) | mcppls-nvim(`xlings install mcppls-nvim`) | - **处处同一答案**——构建运行什么、测试如何报告、清理的代价:都由后端决定 @@ -62,7 +63,8 @@ x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 ```bash mcpp build # 后端、mock mcpp、mcpp-ide-dev mcpp test # 后端测试 -cd mide-plugins/vscode && npm ci && npm test # 扩展与面板的测试 +cd mide-plugins/vscode && npm ci && npm test # 扩展的测试,含对 0.9.0 的一致性门 +npm run test:webui # 工作台在 Chromium 中、经后端运行 npm run package # dist/mcpp-vscode-.vsix cd ../clion && gradle buildPlugin verifyPlugin # build/distributions/mcpp-clion-.zip cd ../.. && nvim --headless --clean -l mide-plugins/nvim/tests/smoke.lua # Neovim 插件的测试 diff --git a/docs/architecture.md b/docs/architecture.md index e901a75..4e9c7f9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,18 +6,44 @@ |---|---|---| | mcpp | the machine contract: event streams, result records, launch descriptors, JSON queries, `--deny` (mcpp docs/50) | any IDE concept | | mcpp-ide backend (`src/`) | sessions and projects (which mcpp.toml a file belongs to), watching (result records, manifests), jobs (one at a time per project, cancellation, a newer job replacing a waiting one), view models (project, targets and platforms, cache, libraries, graphs), cleanup plans and their danger levels, settings validation and migration, the command table, runtime strings, the self-check, the handshakes with mcpp (`--protocol-version`) and mcppls (`--version`) | parsing mcpp.toml, resolving dependencies | -| adapters (`mide-plugins/`) | declarations generated from `spec/`, the protocol mapped onto native UI, the panels' host, the backend process, finding the backend, workspace trust | any decision | +| adapters (`mide-plugins/`) | declarations generated from `spec/`, the protocol mapped onto native UI, the pages' host, the backend process, finding the backend, workspace trust | any decision | -The panels (`webui/`) are one set of classic scripts that the VS Code extension and the CLion -plugin load: a VS Code webview and a JCEF page. A panel speaks the backend's protocol through its -host and asks the host only for what the editor does (open a file, confirm, store a setting). +## The pages + +The views and pages a user sees -- the project tree, the libraries and a package's page, the cache, +the settings page, the dependency and module graphs -- are mcpp-vscode's +(`mide-plugins/vscode/src`), as mcpp-vscode 0.9.0 drew them; `registerPages` registers them. They +read projects and the index from disk and run mcpp through the backend's `mcpp/exec` and mcppls +through its `languageService/exec`; the extension starts no other process for either. + +The workbench (`webui/`) runs the same modules in a browser host. `webui/src` answers what they ask +of VS Code (the `vscode` module: settings, commands, dialogs, views, webviews) and of Node (`fs`, +`path`, `os`, `crypto`) through a bridge to the IDE; `tools/bundle-webui.mjs` bundles them into one +script. Views are tabs in the manifest's order and names; a webview is an iframe with the page's +own document, its CSP and nonce, and the IDE's colours as the `--vscode-*` variables. The bridge +carries requests through `mcppHostSend`/`mcppHostReceive`, and a synchronous file read as a +`prompt` the host answers. mcpp-clion hosts the workbench in JCEF (`webui/Workbench.kt`). + +| Bridge action | What it stands for in VS Code | What mcpp-clion answers (Workbench.kt) | +|---|---|---| +| `rpc` | the backend's method | the backend's method | +| `settings.read`, `settings.write` | the configuration | the plugin's settings store | +| `message`, `input`, `pick`, `saveDialog`, `progress.*` | notifications, input box, quick pick, save dialog, progress | notifications or dialogs, input dialog, list popup, file saver, background task | +| `openFile`, `openText`, `clipboard`, `openExternal` | editors, the clipboard, the browser | editors, the clipboard, the browser | +| `command` | the command | the command table's action | +| `fs.*`, `findFiles` | the file system | the file system | + +The parity gates hold the pages to 0.9.0: the manifest's contributions and strings contain 0.9.0's +exactly; every page's document, rendered by the extension's generators, is byte-identical to the +snapshot 0.9.0's generators render (`test/parity`); a Chromium test runs the workbench over a real +backend (`webui/tests`). ## The adapters | Directory | Plugin | `editor` | Declares | Holds | |---|---|---|---|---| -| `mide-plugins/vscode` | mcpp-vscode | `vscode` | package.json's contributes, from `mcpp-ide-dev gen vscode` | tasks, the Testing view, debug configurations, Problems, the panels' webviews | -| `mide-plugins/clion` | mcpp-clion | `jetbrains` (the IntelliJ platform) | actions and message bundles, from `mcpp-ide-dev gen jetbrains` | run configurations, the test tree, CLion's debugger, the panels in JCEF | +| `mide-plugins/vscode` | mcpp-vscode | `vscode` | package.json's contributes, from `mcpp-ide-dev gen vscode` | the pages, tasks, the Testing view, debug configurations, Problems | +| `mide-plugins/clion` | mcpp-clion | `jetbrains` (the IntelliJ platform) | actions and message bundles, from `mcpp-ide-dev gen jetbrains` | run configurations, the test tree, CLion's debugger, the workbench in JCEF | | `mide-plugins/nvim` | mcpp-nvim | `nvim` | nothing: `:Mcpp` reads the command table from the backend | jobs in a buffer, a terminal for the program, `vim.diagnostic` and the quickfix list, the view models as text, nvim-dap | `spec/commands.json` says which editors offer a command (`editors`, every editor when absent; a @@ -75,8 +101,8 @@ system and starts what they carry for it (`.github/scripts/check-package.sh`). ## Generated declarations `spec/` is the source of everything the editors declare statically. `mcpp-ide-dev gen vscode` writes -the extension's package.json (commands, keybindings, menus, configuration) and its message bundles -into `mide-plugins/vscode/dist`; `mcpp-ide-dev gen jetbrains` writes the CLion plugin's actions and +the extension's package.json (commands, keybindings, menus, configuration), its message bundles and +its settings registry in place, in `mide-plugins/vscode` (CI regenerates them and expects no change); `mcpp-ide-dev gen jetbrains` writes the CLion plugin's actions and message bundles into `mide-plugins/clion/build`. The backend embeds the same files (build.mcpp), so the commands an editor registers at run time are the ones it declared; which rows an editor offers is one rule (`spec::offered_in`) for both. The Neovim plugin declares nothing and offers what the diff --git a/mcpp.toml b/mcpp.toml index 02c90d2..22f4491 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -9,7 +9,7 @@ # M2/M3), where the system it runs upon is answered when the image is loaded. [package] name = "mcpp-ide" -version = "0.1.1" +version = "0.2.0" description = "The IDE backend for mcpp: sessions, jobs, records and view models over mcpp's control contract" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/mide-plugins/clion/README.md b/mide-plugins/clion/README.md index 45c6371..6f01fa8 100644 --- a/mide-plugins/clion/README.md +++ b/mide-plugins/clion/README.md @@ -8,8 +8,10 @@ mcpp's project features in CLion 2026.1 and later, over the mcpp-ide backend: - **debugging** under CLion's own GDB or LLDB (the default toolchain's); an ok image runs under `okrun`, whose images reach GDB through the JIT interface; - **console links** to `file:line:column`; -- the **mcpp tool window**: project, libraries, cache, targets and platforms, and the graphs, pack - and settings panels (Settings | Tools | mcpp); +- the **mcpp tool window**: mcpp-vscode's views and pages -- the project tree, the libraries and a + package's page, the cache, the dependency and module graphs -- in a workbench (JCEF) whose tabs + are the views; the settings page is Settings | Tools | mcpp; +- **Select the Build Target** (target sets among the rows) and **Pack** as popups, packing as a job; - the **Tools | mcpp** menu, generated from `spec/commands.json`. Language features of C++ files come from the mcppls CLion plugin, which this plugin depends on; it @@ -20,6 +22,7 @@ registers no language service. ```bash xlings install gradle # Gradle and its JDK mcpp build # in the repository root: the backend and mcpp-ide-dev +(cd ../vscode && npm ci) # the extension's dependencies: the build bundles the workbench with them gradle buildPlugin # build/distributions/mcpp-clion-.zip gradle verifyPlugin # CLion 2026.1 and the version in gradle.properties ``` @@ -32,4 +35,6 @@ docs/architecture.md, Distribution. Without them the plugin finds the backend th ## Tests `gradle test` runs the wire's tests, and a session over the mock mcpp when `MCPP_IDE_BACKEND` and -`MCPP_IDE_MOCK_MCPP` name the programs `mcpp build` produced. +`MCPP_IDE_MOCK_MCPP` name the programs `mcpp build` produced. `npm run test:webui` in +`../vscode` runs the workbench in Chromium over the backend, answering its bridge as Workbench.kt +does. diff --git a/mide-plugins/clion/gradle.properties b/mide-plugins/clion/gradle.properties index f36b18e..ce4fe38 100644 --- a/mide-plugins/clion/gradle.properties +++ b/mide-plugins/clion/gradle.properties @@ -2,7 +2,7 @@ # where it installs (2026.1 and later); the verifier checks both ends. platformType = CL platformVersion = 2026.2.3 -pluginVersion = 0.1.1 +pluginVersion = 0.2.0 # The mcppls CLion plugin this one depends on (io.github.sunrisepeak.mcppls): # this machine's package of the release, fetched for the sandbox, the tests and the verifier. mcpplsPluginVersion = 0.0.19 diff --git a/mide-plugins/nvim/lua/mcpp/session.lua b/mide-plugins/nvim/lua/mcpp/session.lua index d8da8b2..15ece58 100644 --- a/mide-plugins/nvim/lua/mcpp/session.lua +++ b/mide-plugins/nvim/lua/mcpp/session.lua @@ -11,7 +11,7 @@ local uv = vim.uv or vim.loop local is_windows = vim.fn.has('win32') == 1 -- mcpp-nvim's version: mcpp-ide's, whose release carries it. -M.version = '0.1.1' +M.version = '0.2.0' -- Neovim's version, as its release names it. function M.nvim_version() diff --git a/mide-plugins/vscode/CHANGELOG.md b/mide-plugins/vscode/CHANGELOG.md index f900f9f..b1e0fb8 100644 --- a/mide-plugins/vscode/CHANGELOG.md +++ b/mide-plugins/vscode/CHANGELOG.md @@ -2,7 +2,20 @@ ## 1.1.0 -- The extension is mcpp-vscode 0.9.0 again in everything a user sees and does, over the mcpp-ide backend. +- The extension is mcpp-vscode 0.9.0 in everything a user sees and does: the same views (Project, + Libraries, Cache), pages (a package's page, the settings page, the dependency and module graphs), + commands, menus, keybindings, settings and strings. 1.0's simplified panels are gone. +- mcpp runs through the mcpp-ide backend (`mcpp/exec`) and mcppls through it + (`languageService/exec`); the backend is bundled (`backend/`, an ok image with okrun for Linux, + macOS and Windows) and located as 1.0 located it (`mcpp.backendPath`, the bundle, PATH). +- Kept from 1.0, in 0.9.0's style: **Debug** (`mcpp.debug`, the `mcpp` launch type through CodeLLDB, + the C/C++ extension or lldb-dap), the **Testing** tree, **Select Target** with rows for target + sets (mcpp 2026.10.30.1 and later), **Pack**, build on save (`mcpp.build.onSave`), the build's + profile and target (`mcpp.build.profile`, `mcpp.build.target`), and cancelling a running job. +- A launch without a debugger extension is refused at once; the message offers to install CodeLLDB. +- The build-script API and the `mcpp.toml` schema follow mcpp 2026.10.29.1, the platform plane + included. +- Releases are published to Open VSX beside the GitHub release. ## 0.9.0 - 2026-10-20 diff --git a/mide-plugins/vscode/README.md b/mide-plugins/vscode/README.md index 620facd..3c3d752 100644 --- a/mide-plugins/vscode/README.md +++ b/mide-plugins/vscode/README.md @@ -19,7 +19,7 @@ its own and only **forwards** to it ([docs/architecture.md](docs/architecture.md | --- | --- | --- | | [mcpp](https://github.com/mcpp-community/mcpp) | The C++23 build tool this extension drives | — | | [mcpp-language-server](https://github.com/Sunrisepeak/mcpp-language-server) | The C++ Modules language service; a hard dependency of this extension | [Marketplace](https://marketplace.visualstudio.com/items?itemName=sunrisepeak.mcpp-language-server) · [Open VSX](https://open-vsx.org/extension/sunrisepeak/mcpp-language-server) | -| mcpp-vscode (this extension) | The VS Code front end | [Marketplace](https://marketplace.visualstudio.com/items?itemName=mcpp-community.mcpp-vscode) · [Open VSX](https://open-vsx.org/extension/mcpp-community/mcpp-vscode) · [Releases](https://github.com/mcpp-community/mcpp-vscode/releases) | +| mcpp-vscode (this extension) | The VS Code front end | [Marketplace](https://marketplace.visualstudio.com/items?itemName=mcpp-community.mcpp-vscode) · [Open VSX](https://open-vsx.org/extension/mcpp-community/mcpp-vscode) · [Releases](https://github.com/mcpp-community/mcpp-ide/releases) | See [dependency graph navigation and exports](docs/dependency-graphs.md) for entry groups, importer depth, refresh behavior and SVG/PNG export. @@ -58,8 +58,16 @@ Opening a project never runs `mcpp` on its own and never downloads a toolchain. - **Libraries** — browse the official mcpp-index offline, choose a target project and dependency type, and view available versions and verified example code. Other registry hits have separate previews. See [Libraries](docs/libraries.md). - **Cache views and cleanup** — project artifacts and the shared build cache, previewed before anything is deleted: [docs/cache.md](docs/cache.md). - **C++ Modules** — the other extension's state and actions, in the project view and the status-bar menu, forwarded not reimplemented: [docs/commands.md](docs/commands.md). +- **Debug, tests, targets, packing** — **mcpp: Debug** and the `mcpp` launch type (CodeLLDB, the C/C++ extension or lldb-dap), the Testing tree, **mcpp: Select the Build Target** (target sets among its rows with mcpp 2026.10.30.1 and later), **mcpp: Pack**, and build on save (`mcpp.build.onSave`). - **Settings and diagnostics** — a registry-backed settings panel (`mcpp.openSettings`) and a copyable environment self-check (`mcpp.selfCheck`): [docs/settings.md](docs/settings.md). `mcpp.ui.language` overrides our messages and panels only; palette titles always follow VS Code ([docs/architecture.md](docs/architecture.md#language)). +## The backend + +The extension carries the [mcpp-ide](https://github.com/mcpp-community/mcpp-ide) backend +(`backend/`: one ok-image bundle and okrun for Linux x64/arm64, macOS arm64 and Windows x64) and runs +mcpp and mcppls through it; `mcpp.backendPath` names another. The CLion plugin shows the same pages +over the same backend. + ## Troubleshooting Look first at the **mcpp** output channel, then run **mcpp: Environment Self-check** @@ -69,13 +77,15 @@ service — are in [docs/troubleshooting.md](docs/troubleshooting.md). ## Development -`npm ci`, `npm run compile`, `npm test` (the gates first, then the unit tests), `npm run test:e2e`, -`npm run package`, and `node tools/dev-profile.mjs` for a throwaway profile under `.dev-profile/` +`npm ci`, `npm run compile`, `npm test` (the gates first, then the unit tests, the parity gates +against 0.9.0 among them), `npm run test:e2e`, `npm run test:webui` (the CLion workbench in +Chromium), `npm run gen` (the declarations from the repository's `spec/`), `npm run package`, and `node tools/dev-profile.mjs` for a throwaway profile under `.dev-profile/` that never touches your real one. -Release: bump the version in `package.json` and `package-lock.json`, commit, and push a tag -matching the version exactly. The workflow runs the tests, packages one VSIX, and publishes that -same artifact to GitHub Releases and Open VSX. Upload that VSIX manually through the Visual Studio Marketplace publisher portal. +Release: the extension is released with mcpp-ide (a `v` tag of that repository). The +workflow runs the tests, packages one VSIX with the backend, and publishes that same artifact to +GitHub Releases and Open VSX. Upload that VSIX manually through the Visual Studio Marketplace +publisher portal. ## License diff --git a/mide-plugins/vscode/README.zh-CN.md b/mide-plugins/vscode/README.zh-CN.md index 70a1035..bc97ffb 100644 --- a/mide-plugins/vscode/README.zh-CN.md +++ b/mide-plugins/vscode/README.zh-CN.md @@ -19,7 +19,7 @@ C++ 模块语义——诊断、补全、悬停、定义、引用、模块图— | --- | --- | --- | | [mcpp](https://github.com/mcpp-community/mcpp) | 本扩展驱动的 C++23 构建工具 | — | | [mcpp-language-server](https://github.com/Sunrisepeak/mcpp-language-server) | C++ Modules 语言服务;本扩展的硬依赖 | [Marketplace](https://marketplace.visualstudio.com/items?itemName=sunrisepeak.mcpp-language-server) · [Open VSX](https://open-vsx.org/extension/sunrisepeak/mcpp-language-server) | -| mcpp-vscode(本扩展) | VS Code 前端 | [Marketplace](https://marketplace.visualstudio.com/items?itemName=mcpp-community.mcpp-vscode) · [Open VSX](https://open-vsx.org/extension/mcpp-community/mcpp-vscode) · [Releases](https://github.com/mcpp-community/mcpp-vscode/releases) | +| mcpp-vscode(本扩展) | VS Code 前端 | [Marketplace](https://marketplace.visualstudio.com/items?itemName=mcpp-community.mcpp-vscode) · [Open VSX](https://open-vsx.org/extension/mcpp-community/mcpp-vscode) · [Releases](https://github.com/mcpp-community/mcpp-ide/releases) | 模块入口分组、父节点深度、局部展开、刷新和 SVG/PNG 导出见 [依赖图使用说明](docs/dependency-graphs.md)。 @@ -58,8 +58,15 @@ C++ 模块语义——诊断、补全、悬停、定义、引用、模块图— - **库生态** —— 离线浏览本机已刷新的包索引;详情页展示真实示例代码、版本矩阵,以及工程是否已依赖它。 - **缓存视图与清理** —— 工程产物与共享缓存,删除前一律先给出预览:[docs/cache.md](docs/cache.md)。 - **C++ Modules** —— 对方扩展的状态与操作,集中在工程视图与状态栏快捷菜单,只转发不重写:[docs/commands.md](docs/commands.md)。 +- **调试、测试、目标、打包** —— **mcpp: 调试**与 `mcpp` 启动类型(CodeLLDB、C/C++ 扩展或 lldb-dap)、测试树、**mcpp: 选择构建目标**(mcpp 2026.10.30.1 起含目标集合行)、**mcpp: 打包**,以及保存时构建(`mcpp.build.onSave`)。 - **设置与诊断** —— 注册表驱动的设置面板(`mcpp.openSettings`)与可复制的环境自检(`mcpp.selfCheck`):[docs/settings.md](docs/settings.md)。`mcpp.ui.language` 只覆盖本扩展的运行时提示与面板;命令面板标题始终跟随 VS Code([docs/architecture.md](docs/architecture.md#language))。 +## 后端 + +扩展携带 [mcpp-ide](https://github.com/mcpp-community/mcpp-ide) 后端(`backend/`:一个 ok 映像包,以及 +Linux x64/arm64、macOS arm64、Windows x64 的 okrun),mcpp 与 mcppls 都经后端运行;`mcpp.backendPath` +可指定另一个后端。CLion 插件在同一后端上显示同一套页面。 + ## 故障排查 先看 **mcpp** 输出频道,再运行 **mcpp: 环境自检**(`mcpp.selfCheck`)。常见情形——依赖缺失、 @@ -68,13 +75,13 @@ C++ 模块语义——诊断、补全、悬停、定义、引用、模块图— ## 开发 -`npm ci`、`npm run compile`、`npm test`(先门禁后单测)、`npm run test:e2e`、 +`npm ci`、`npm run compile`、`npm test`(先门禁后单测,含对 0.9.0 的一致性门)、`npm run test:e2e`、 +`npm run test:webui`(CLion 工作台在 Chromium 中运行)、`npm run gen`(由仓库的 `spec/` 生成声明)、 `npm run package`,以及 `node tools/dev-profile.mjs`(在 `.dev-profile/` 下建一次性 profile, 不会碰你自己的)。 -发布:把 `package.json` 与 `package-lock.json` 的版本号改好并提交,推送与版本完全一致的 -tag。工作流会跑测试、打包同一只 VSIX,并发布到 GitHub Releases 与 Open VSX。官方 Marketplace -使用 Release 中的同一 VSIX 手动上传。 +发布:扩展随 mcpp-ide 发布(该仓库的 `v` tag)。工作流运行测试、打包携带后端的同一只 VSIX, +并发布到 GitHub Releases 与 Open VSX。官方 Marketplace 使用 Release 中的同一 VSIX 手动上传。 ## 许可证 diff --git a/src/server.cppm b/src/server.cppm index 4138abe..7c22ede 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -31,7 +31,7 @@ export namespace mcppide::server { using Json = nlohmann::json; -inline constexpr std::string_view kVersion = "0.1.1"; +inline constexpr std::string_view kVersion = "0.2.0"; inline constexpr int kProtocol = 1; // The methods `install` registers, for the protocol schema's test: a method From 3e02ac8f13fe119ea1779cdfb4deef69c5ed7a7b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 02:53:02 +0900 Subject: [PATCH 083/119] mcpp-vscode: stage-backend finds the extension's directory on Windows (fileURLToPath, not a URL's pathname) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mide-plugins/vscode/tools/stage-backend.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mide-plugins/vscode/tools/stage-backend.mjs b/mide-plugins/vscode/tools/stage-backend.mjs index 4484419..453c423 100644 --- a/mide-plugins/vscode/tools/stage-backend.mjs +++ b/mide-plugins/vscode/tools/stage-backend.mjs @@ -7,8 +7,9 @@ */ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; -const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const okb = process.env.MCPP_IDE_OKB; const okrun = process.env.MCPP_IDE_OKRUN; if (!okb || !fs.statSync(okb, { throwIfNoEntry: false })?.isFile()) { From bfc37a8a55d5bc8702ae735d696e0311a6d7ddef Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 03:20:45 +0900 Subject: [PATCH 084/119] mcpp-clion: the save dialog's extensions as the descriptor's extension filter, not the deprecated constructor Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../io/github/mcppcommunity/mcppclion/webui/Workbench.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt index 6698de5..82d8e47 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt @@ -276,9 +276,13 @@ class Workbench(private val project: Project, private val mode: String, parent: } "pick" -> onEdt { f -> pick(p, f) } "saveDialog" -> onEdt { f -> - val filters = p["filters"]?.takeIf { it.isJsonObject }?.asJsonObject?.entrySet()?.flatMap { e -> e.value.asJsonArray.map { it.asString } }?.toTypedArray() ?: emptyArray() + val filters = p["filters"]?.takeIf { it.isJsonObject }?.asJsonObject?.entrySet().orEmpty() val start = p["path"]?.asString?.let { File(it) } - val dialog = FileChooserFactory.getInstance().createSaveFileDialog(FileSaverDescriptor(p["title"]?.asString ?: "mcpp", "", *filters), project) + val descriptor = FileSaverDescriptor(p["title"]?.asString ?: "mcpp", "") + // VS Code's filters (label -> extensions) as the chooser's one filter: the first label, every extension. + if (filters.isNotEmpty()) + descriptor.withExtensionFilter(filters.first().key, *filters.flatMap { e -> e.value.asJsonArray.map { it.asString } }.toTypedArray()) + val dialog = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, project) val chosen = dialog.save(start?.parentFile?.toPath(), start?.name) f.complete(chosen?.file?.path?.let { JsonPrimitive(it) } ?: JsonNull.INSTANCE) } From 3e062aa4fd141fdf65b8b2f1e0757c1ccaef8a6e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 06:00:02 +0900 Subject: [PATCH 085/119] mcpp-clion: the plugin and tool window icons are the mcpp logo, not text; CI checks the icons Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 12 ++++++++++++ CHANGELOG.md | 3 +++ .../clion/src/main/resources/META-INF/pluginIcon.svg | 2 +- mide-plugins/clion/src/main/resources/icons/mcpp.svg | 2 +- .../clion/src/main/resources/icons/mcpp_dark.svg | 1 - 5 files changed, 17 insertions(+), 3 deletions(-) delete mode 100644 mide-plugins/clion/src/main/resources/icons/mcpp_dark.svg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9d4e3c..ae65668 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -450,6 +450,18 @@ jobs: unzip -l plugin.jar | tee jar.txt | grep -q "webui/files.txt" for f in $(unzip -p plugin.jar webui/files.txt); do grep -q " webui/$f$" jar.txt; done ls -l build/distributions/*.zip + # The plugin's icon and its tool window's are the mcpp logo, an embedded image (design 2026-10-29 + # §10.4), not text; the VS Code extension's logo is mcpp-vscode 0.9.0's, byte for byte. + - name: The icons are the mcpp logo + run: | + [ "$(md5sum < mide-plugins/vscode/media/logo.png | cut -d' ' -f1)" = f94579030616a45ca26694120daba050 ] \ + || { echo "::error::media/logo.png is not the mcpp logo"; exit 1; } + cd mide-plugins/clion + for f in META-INF/pluginIcon.svg icons/mcpp.svg; do + svg="$(unzip -p plugin.jar "$f")" + grep -q 'data:image/png;base64,' <<< "$svg" || { echo "::error::$f embeds no image of the logo"; exit 1; } + ! grep -q 'm++ + diff --git a/mide-plugins/clion/src/main/resources/icons/mcpp.svg b/mide-plugins/clion/src/main/resources/icons/mcpp.svg index 5366045..df879d9 100644 --- a/mide-plugins/clion/src/main/resources/icons/mcpp.svg +++ b/mide-plugins/clion/src/main/resources/icons/mcpp.svg @@ -1 +1 @@ -m+ + diff --git a/mide-plugins/clion/src/main/resources/icons/mcpp_dark.svg b/mide-plugins/clion/src/main/resources/icons/mcpp_dark.svg deleted file mode 100644 index 5366045..0000000 --- a/mide-plugins/clion/src/main/resources/icons/mcpp_dark.svg +++ /dev/null @@ -1 +0,0 @@ -m+ From 998154b9d9ee2c497bbec92f84e8ca65af43f463 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 02:38:19 +0900 Subject: [PATCH 086/119] CI and documentation on mcpp 2026.10.30.1: one pack for the target set openkal (tar and okb), runs_on, the .exe launcher Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 2 +- .github/workflows/ci.yml | 14 ++++---------- README.md | 2 +- README.zh-CN.md | 2 +- docs/architecture.md | 4 ++-- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index 614779f..9642377 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -2,7 +2,7 @@ # The released mcpp CI installs and uses: the control contract the fixtures were captured from # (protocol/fixtures/version.txt) and the platforms (openkal-platform 0.17.3) the backend's ok images # are built on. -MCPP_VERSION=2026.10.27.1 +MCPP_VERSION=2026.10.30.1 # A ref of mcpp-community/mcpp CI builds mcpp from and uses instead, for a contract not yet released. # Empty: the release above is used. MCPP_SOURCE_REF= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae65668..4d9bca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,9 +60,7 @@ jobs: set -euo pipefail version="$(sed -n 's/^version *= *"\(.*\)"/\1/p' mcpp.toml | head -1)" package="mcpp-ide-$version-openkal" - mcpp pack mcpp-ide --target x86_64-openkal --mode self-contained --systems linux,macos,windows \ - --pkg-format tar -o "$package.tar.gz" - mcpp pack mcpp-ide --target x86_64-openkal --pkg-format okb + mcpp pack mcpp-ide --target openkal --mode self-contained --systems all --pkg-format tar,okb -o target/dist/ mkdir -p backend unpacked cp "target/dist/$package.tar.gz" backend/ cp "target/dist/mcpp-ide-$version.okb" backend/mcpp-ide.okb @@ -71,9 +69,9 @@ jobs: python3 - "unpacked/$package/mcpp-package.json" <<'PY' import json, sys d = json.load(open(sys.argv[1])) - print("legs", d["legs"], "systems", d["systems"]) + print("legs", d["legs"], "runs_on", d["runs_on"]) want = {"linux-x86_64", "linux-aarch64", "macos-aarch64", "windows-x86_64"} - assert want <= set(d["systems"]), d["systems"] + assert want <= set(d["runs_on"]["self"]), d["runs_on"] PY "unpacked/$package/bin/mcpp-ide" --version backend/okrun/linux-x86_64/okrun backend/mcpp-ide.okb --version @@ -551,9 +549,5 @@ jobs: dir="$(mktemp -d)" tar -xzf backend/mcpp-ide-*-openkal.tar.gz -C "$dir" root="$(echo "$dir"/mcpp-ide-*-openkal)" - if [ "$RUNNER_OS" = Windows ]; then - (cd "$root/bin" && cmd //c "mcpp-ide.cmd --version") | tee said.txt - else - "$root/bin/mcpp-ide" --version | tee said.txt - fi + "$root/bin/mcpp-ide" --version | tee said.txt grep -q '^mcpp-ide ' said.txt diff --git a/README.md b/README.md index 0dd2375..8683186 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | VS Code-compatible editors | Open VSX, or the `.vsix` | install the `.vsix` from the extensions view | | CLion 2026.1+ | JetBrains Marketplace (`mcpp-clion`), or `mcpp-clion-.zip` | Settings → Plugins → Install Plugin from Disk; the mcppls CLion plugin (its package for this system carries the server) first | | Neovim 0.10+ | `mide-plugins/nvim` of the release's source tree | `xlings install mcpp-ide-nvim` (the backend with it, set up at start); or the directory on the runtimepath and `require('mcpp').setup()` ([its README](mide-plugins/nvim/README.md)) | -| Agents, other clients | `mcpp-ide--openkal.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.cmd` on Windows), or `okrun mcpp-ide-.okb serve` | +| Agents, other clients | `mcpp-ide--openkal.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle (`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.17.3) and okrun for diff --git a/README.zh-CN.md b/README.zh-CN.md index eb7d88d..08a9acd 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -52,7 +52,7 @@ C++ 文件的语言功能来自 [mcppls](https://github.com/Sunrisepeak/mcpp-lan | 兼容 VS Code 的编辑器 | Open VSX,或 `.vsix` | 在扩展视图中安装 `.vsix` | | CLion 2026.1+ | JetBrains Marketplace(`mcpp-clion`),或 `mcpp-clion-.zip` | Settings → Plugins → Install Plugin from Disk;先安装 mcppls CLion 插件(本系统的包自带服务端) | | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | -| Agent、其他客户端 | `mcpp-ide--openkal.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.cmd`),或 `okrun mcpp-ide-.okb serve` | +| Agent、其他客户端 | `mcpp-ide--openkal.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | 每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.17.3 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: diff --git a/docs/architecture.md b/docs/architecture.md index 4e9c7f9..8bbc0ab 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -81,13 +81,13 @@ build through mcpp's result record, and an older or missing mcppls is reported w The backend is one program for every editor, and one file for every system: an ok image per architecture (`mcpp build --target -openkal`, openkal-platform's toolchain and C library), -packed as one ok-image bundle (`mcpp pack mcpp-ide --target x86_64-openkal --pkg-format okb`). Each +packed as one ok-image bundle (`--pkg-format okb`). Each editor package carries that bundle and the runner that starts it on each system: | Layout in the package | Holds | Started as | |---|---|---| | `backend/mcpp-ide.okb` | the x86_64 and aarch64 images | | -| `backend/okrun/-/okrun[.exe]` | okrun for `linux-x86_64`, `linux-aarch64`, `macos-aarch64`, `windows-x86_64`, from the universal self-contained package (`mcpp pack --mode self-contained --systems linux,macos,windows`: each okrun built over the platform's hosted arrangement) | `okrun mcpp-ide.okb serve`: the okrun carried for this system, else okrun on PATH | +| `backend/okrun/-/okrun[.exe]` | okrun for `linux-x86_64`, `linux-aarch64`, `macos-aarch64`, `windows-x86_64`, from the universal self-contained package (`--mode self-contained --systems all`: each okrun built over the platform's hosted arrangement); one command packs both, `mcpp pack mcpp-ide --target openkal --mode self-contained --systems all --pkg-format tar,okb`, and the package's `runs_on.self` names the systems it runs on | `okrun mcpp-ide.okb serve`: the okrun carried for this system, else okrun on PATH | Before it, a configured `mcpp.ide.backendPath` (a program or an `.okb`) is used; after it, an installed `mcpp-ide` (xlings, whose command runs okrun on its bundle) or one on PATH. The Neovim From e06830b9421081e625ff6087d74e76e6d5dc3ee9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 08:18:04 +0900 Subject: [PATCH 087/119] mcpplibs.os 0.2.3 and openkal-platform 0.18.0; CI builds mcpp from release/2026.10.30.1 until it is released Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 6 +++--- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 4 ++++ README.md | 2 +- README.zh-CN.md | 2 +- mcpp.toml | 7 ++++--- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index 9642377..d7e42f7 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,11 +1,11 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. # The released mcpp CI installs and uses: the control contract the fixtures were captured from -# (protocol/fixtures/version.txt) and the platforms (openkal-platform 0.17.3) the backend's ok images +# (protocol/fixtures/version.txt) and the platforms (openkal-platform 0.18.0) the backend's ok images # are built on. -MCPP_VERSION=2026.10.30.1 +MCPP_VERSION=2026.10.27.1 # A ref of mcpp-community/mcpp CI builds mcpp from and uses instead, for a contract not yet released. # Empty: the release above is used. -MCPP_SOURCE_REF= +MCPP_SOURCE_REF=release/2026.10.30.1 # The LLVM the backend builds with on macOS and Windows (the targets mcpp reaches there with it). LLVM_VERSION=23.1.3 # The xlings mcpp 2026.10.27.1 bundles, which reads the current index. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d9bca5..6637c2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-14, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.17.3' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.18.0' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd2c71..eb0c92b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 sessions through `XDebuggerManager.newSessionBuilder`, the debugger driver through `createDriverConfiguration(project, CPPEnvironment(toolchain))`; packing is a job over the backend's `pack/formats`. +- **mcpp 2026.10.30.1.** One pack of the target set `openkal` writes the editor packages' tree and + okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads + `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`. The backend is on mcpplibs.os 0.2.3 and + openkal-platform 0.18.0 (openkal-linux 0.18.3, mcpp#857). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. diff --git a/README.md b/README.md index 8683186..f40bc83 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.17.3) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.18.0) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index 08a9acd..120f8b2 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -54,7 +54,7 @@ C++ 文件的语言功能来自 [mcppls](https://github.com/Sunrisepeak/mcpp-lan | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.17.3 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.18.0 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.toml b/mcpp.toml index 22f4491..7fb757d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -63,9 +63,10 @@ kind = "bin" main = "src/bin/dev.cpp" [dependencies.mcpplibs] -# os 0.2.1: an ok image's C library, C++ run-time and loader are its platform's, so the package -# declares no openkal-tools of its own beside the platform's. -os = "0.2.1" +# os 0.2.3: openkal-linux 0.18.3 (a program is found in a directory the process may search but not +# read, mcpp#857). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, +# so the package declares no openkal-tools of its own beside the platform's. +os = "0.2.3" [dependencies.nlohmann] json = "3.12.0" From 734d8567364ae41439c23a677c136ebcfed342aa Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 09:02:30 +0900 Subject: [PATCH 088/119] CI and the release read the tree's name as pack writes it (mcpp-ide--openkal-bundle-all) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 8 +++++--- .github/workflows/release.yml | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- docs/architecture.md | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6637c2b..759a014 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,8 +59,10 @@ jobs: run: | set -euo pipefail version="$(sed -n 's/^version *= *"\(.*\)"/\1/p' mcpp.toml | head -1)" - package="mcpp-ide-$version-openkal" mcpp pack mcpp-ide --target openkal --mode self-contained --systems all --pkg-format tar,okb -o target/dist/ + # The tree's name carries the mode (`mcpp-ide--openkal-bundle-all`, mcpp SPEC-014). + tarball="$(ls target/dist/mcpp-ide-"$version"-openkal*.tar.gz)" + package="$(basename "$tarball" .tar.gz)" mkdir -p backend unpacked cp "target/dist/$package.tar.gz" backend/ cp "target/dist/mcpp-ide-$version.okb" backend/mcpp-ide.okb @@ -547,7 +549,7 @@ jobs: run: | set -euo pipefail dir="$(mktemp -d)" - tar -xzf backend/mcpp-ide-*-openkal.tar.gz -C "$dir" - root="$(echo "$dir"/mcpp-ide-*-openkal)" + tar -xzf backend/mcpp-ide-*-openkal*.tar.gz -C "$dir" + root="$(echo "$dir"/mcpp-ide-*-openkal*)" "$root/bin/mcpp-ide" --version | tee said.txt grep -q '^mcpp-ide ' said.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6604718..5c5e708 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: mkdir -p dist cp artifacts/mcpp-vscode-vsix/*.vsix artifacts/mcpp-clion-zip/*.zip dist/ cp artifacts/backend/mcpp-ide.okb "dist/mcpp-ide-${version}.okb" - cp "artifacts/backend/mcpp-ide-${version}-openkal.tar.gz" dist/ + cp "artifacts/backend/mcpp-ide-${version}"-openkal*.tar.gz dist/ (cd dist && sha256sum * > SHA256SUMS && cat SHA256SUMS) gh release create "v${version}" dist/* --target "$GITHUB_SHA" --title "mcpp-ide ${version}" --notes-file CHANGELOG.md # Open VSX receives the exact VSIX attached to the GitHub Release; the Visual Studio diff --git a/README.md b/README.md index f40bc83..41f141f 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | VS Code-compatible editors | Open VSX, or the `.vsix` | install the `.vsix` from the extensions view | | CLion 2026.1+ | JetBrains Marketplace (`mcpp-clion`), or `mcpp-clion-.zip` | Settings → Plugins → Install Plugin from Disk; the mcppls CLion plugin (its package for this system carries the server) first | | Neovim 0.10+ | `mide-plugins/nvim` of the release's source tree | `xlings install mcpp-ide-nvim` (the backend with it, set up at start); or the directory on the runtimepath and `require('mcpp').setup()` ([its README](mide-plugins/nvim/README.md)) | -| Agents, other clients | `mcpp-ide--openkal.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | +| Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle (`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.18.0) and okrun for diff --git a/README.zh-CN.md b/README.zh-CN.md index 120f8b2..94d0e43 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -52,7 +52,7 @@ C++ 文件的语言功能来自 [mcppls](https://github.com/Sunrisepeak/mcpp-lan | 兼容 VS Code 的编辑器 | Open VSX,或 `.vsix` | 在扩展视图中安装 `.vsix` | | CLion 2026.1+ | JetBrains Marketplace(`mcpp-clion`),或 `mcpp-clion-.zip` | Settings → Plugins → Install Plugin from Disk;先安装 mcppls CLion 插件(本系统的包自带服务端) | | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | -| Agent、其他客户端 | `mcpp-ide--openkal.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | +| Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | 每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.18.0 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: diff --git a/docs/architecture.md b/docs/architecture.md index 8bbc0ab..83be62c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -94,8 +94,8 @@ installed `mcpp-ide` (xlings, whose command runs okrun on its bundle) or one on plugin carries no package of the backend: it starts setup()'s `backend`, else `mcpp-ide` on PATH, and `xlings install mcpp-ide-nvim` installs it from the release's source tree, with the backend's package as its dependency. CI packs the -bundle and the universal package (`mcpp-ide--openkal.tar.gz`, a release asset) once, on -Linux, takes okrun from that package, opens both editor packages and the universal package on every +bundle and the target set's tree (`mcpp-ide--openkal-bundle-all.tar.gz`, a release asset) in +one pack on Linux, takes okrun from that tree, opens both editor packages and the tree on every system and starts what they carry for it (`.github/scripts/check-package.sh`). ## Generated declarations From 9fed44088589a9581cbbed2e8fd76387b57094bc Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 19:44:24 +0900 Subject: [PATCH 089/119] =?UTF-8?q?checkpoint=201:=20the=20project's=20mcp?= =?UTF-8?q?p,=20asked=20in=20the=20project;=20the=20backend's=20lifecycle,?= =?UTF-8?q?=20its=20log=20and=20the=20IDE=20report=20(mcpp=20design=202026?= =?UTF-8?q?-10-30=20part=202=20=C2=A73,=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each project is asked in its own directory (mcpp self env through the shim or mcpp.path), with XLINGS_PROJECT_DIR removed from every run of mcpp; features by what mcpp states or by a table frozen at 2026.10.30.1, never by version; pinned-missing with the shim's words and its install command; untrusted workspaces not asked; asked again after a job's envelope names another version, on focus and on a manifest change; at most two at once. project/mcpp, project/refresh, capabilities/explain, report/collect, project/mcppChanged. VS Code and CLion restart an ended backend after 1, 4 and 16 s and stop after a fourth end in three minutes (degraded mode, restart and report actions); Export IDE Report in every editor; mcpp.backend.logLevel; VS Code's status item names the project's mcpp. Tests: test_probe over the mock playing a shim per directory; the Neovim smoke writes the report. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 19 ++ .../mcppcommunity/mcppclion/McppSession.kt | 50 +++- .../mcppclion/actions/Dispatch.kt | 25 ++ mide-plugins/nvim/lua/mcpp/commands.lua | 21 ++ mide-plugins/nvim/lua/mcpp/init.lua | 9 + mide-plugins/nvim/tests/smoke.lua | 9 + mide-plugins/vscode/data/config-registry.json | 19 ++ mide-plugins/vscode/data/i18n/zh-cn.json | 13 +- mide-plugins/vscode/package.json | 19 ++ mide-plugins/vscode/package.nls.json | 5 +- mide-plugins/vscode/package.nls.zh-cn.json | 5 +- mide-plugins/vscode/src/cli/controller.ts | 22 +- mide-plugins/vscode/src/commands/ids.ts | 1 + mide-plugins/vscode/src/commands/menu.ts | 1 + mide-plugins/vscode/src/extension.ts | 43 ++++ mide-plugins/vscode/src/ide/protocol.ts | 13 ++ mide-plugins/vscode/src/ide/session.ts | 60 ++++- .../vscode/test/config/registry.test.ts | 12 +- .../vscode/test/config/wiring.test.ts | 5 + protocol/protocol.schema.json | 45 +++- spec/commands.json | 2 + spec/i18n/manifest.en.json | 5 +- spec/i18n/manifest.zh-cn.json | 5 +- spec/i18n/runtime.zh-cn.json | 8 +- spec/settings.json | 19 ++ src/bin/mockmcpp.cpp | 42 +++- src/mcpp.cppm | 86 +++++++ src/server.cppm | 215 +++++++++++++++++- tests/test_probe.cpp | 150 ++++++++++++ 29 files changed, 886 insertions(+), 42 deletions(-) create mode 100644 tests/test_probe.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index eb0c92b..1065d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 §10). +- **The project's mcpp (mcpp design 2026-10-30 part 2 §4).** Each project is asked in its own + directory, `mcpp self env --format json` through the program a version manager's shim stands for + (or `mcpp.path`), with `XLINGS_PROJECT_DIR` removed from every run of mcpp, so a project pinned in + `.xlings.json` is described by the mcpp that builds it. A feature is turned on by what the project's + mcpp states (`features`, mcpp 2026.10.31.1) or, for an earlier mcpp, by a table frozen at that + release, never by comparing versions. A pinned version that is not installed is `pinned-missing`, + with the shim's words and its install command; an untrusted workspace's project is not asked. A + job whose envelope names another version, the editor regaining focus and a changed manifest each + make the project be asked again; never more than two at once. Methods `project/mcpp`, + `project/refresh`, `capabilities/explain` (why a feature is not offered: which party, which + capability, from which version) and `report/collect`; notification `project/mcppChanged`. +- **The backend's lifecycle and what it says (§3).** VS Code and CLion start an ended backend again + after 1, 4 and 16 seconds; a fourth end within three minutes leaves them in the degraded mode with + two actions, restarting the backend and exporting the IDE report. **mcpp: Export IDE Report** + (every editor; Neovim `:Mcpp exportReport [path]`) writes the backend, every project's mcpp, + mcppls's status, the settings in effect, recent jobs and the backend's recent log to a file the + person names. `mcpp.backend.logLevel` decides what the backend writes to the editor's log. VS + Code's status item names the project's mcpp version, and its tooltip the program and whether + `mcpp.path` chose it. - **mcpp-vscode 1.1.0 is mcpp-vscode 0.9.0 over the backend.** 0.9.0's extension is imported with its history (`mide-plugins/vscode`); every view, page, command, menu, setting and string a user sees is 0.9.0's. What 1.0 added stays, in 0.9.0's style: debugging (`mcpp.debug`, the `mcpp` diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index a23a9ce..e734947 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -1,6 +1,8 @@ // The session with the backend for one project: started when an mcpp project opens, given the // IDE's settings, trust, language and the project's directory, kept current as they change, and -// restarted once when it ends. Everything else in the plugin reaches the backend through it. +// started again after 1, 4 and 16 seconds when it ends; a fourth end within three minutes leaves the +// plugin in its degraded mode (design 2026-10-30 part 2 §3.1). Everything else in the plugin +// reaches the backend through it. package io.github.mcppcommunity.mcppclion import com.google.gson.JsonArray @@ -32,13 +34,23 @@ class McppSession(private val project: Project) : Disposable { @Volatile private var bundle: Map = emptyMap() @Volatile var commands: List = emptyList(); private set private val listeners = CopyOnWriteArrayList Unit>>() - private var restarts = 0 + /** When the backend ended, within the last three minutes. */ + private val exits = ArrayDeque() + @Volatile var degraded = false; private set init { project.messageBus.connect(this).subscribe(McppSettings.CHANGED, McppSettings.Listener { key -> if (key == "mcpp.ide.backendPath") restart() else pushSettings() }) on("manifest/changed") { p -> p.asJsonObject["root"]?.asString?.let { refreshTests(it) } } + // The IDE regained focus: a `xlings use` or a changed pin happened outside it (§4.2). + com.intellij.openapi.application.ApplicationManager.getApplication().messageBus.connect(this).subscribe( + com.intellij.openapi.application.ApplicationActivationListener.TOPIC, + object : com.intellij.openapi.application.ApplicationActivationListener { + override fun applicationActivated(ideFrame: com.intellij.openapi.wm.IdeFrame) { + if (backend?.running == true) backend?.request("project/refresh", JsonObject()) + } + }) } val root: String? get() = projects().firstOrNull()?.get("root")?.asString ?: project.basePath?.takeIf { File(it, "mcpp.toml").isFile } @@ -119,21 +131,45 @@ class McppSession(private val project: Project) : Disposable { private fun onExit(code: Int) { log.info("mcpp-ide: the backend ended ($code)") backend = null - if (restarts++ < 1) { start(); return } - notify(t("The mcpp-ide backend ended."), NotificationType.ERROR) + val now = System.currentTimeMillis() + synchronized(exits) { + exits.addLast(now) + while (exits.isNotEmpty() && now - exits.first() >= 3 * 60_000) exits.removeFirst() + } + val delays = longArrayOf(1000, 4000, 16000) + val count = synchronized(exits) { exits.size } + if (count <= delays.size) { + log.info("mcpp-ide: starting it again in ${delays[count - 1] / 1000} s") + com.intellij.util.concurrency.AppExecutorUtil.getAppScheduledExecutorService() + .schedule({ started = null; start() }, delays[count - 1], java.util.concurrent.TimeUnit.MILLISECONDS) + return + } + degraded = true + val n = NotificationGroupManager.getInstance().getNotificationGroup("mcpp").createNotification("mcpp", + t("The mcpp-ide backend ended four times in three minutes; build, run, test and the project pages go on without it."), + NotificationType.ERROR) + n.addAction(com.intellij.notification.NotificationAction.createSimpleExpiring(t("Restart the backend")) { restart() }) + n.addAction(com.intellij.notification.NotificationAction.createSimpleExpiring(t("Export IDE Report")) { + io.github.mcppcommunity.mcppclion.actions.Dispatch.exportReport(project) + }) + n.notify(project) } fun restart() { val old = backend backend = null started = null - restarts = 0 + synchronized(exits) { exits.clear() } + degraded = false old?.close() start() } - fun request(method: String, params: JsonElement = JsonObject()): CompletableFuture = - start().thenCompose { backend?.request(method, params) ?: CompletableFuture.failedFuture(IllegalStateException(t("the mcpp-ide backend is not running"))) } + fun request(method: String, params: JsonElement = JsonObject()): CompletableFuture { + if (degraded && backend?.running != true) + return CompletableFuture.failedFuture(IllegalStateException(t("the mcpp-ide backend is not running"))) + return start().thenCompose { backend?.request(method, params) ?: CompletableFuture.failedFuture(IllegalStateException(t("the mcpp-ide backend is not running"))) } + } // The tests of each project by the file holding their `main` (mcpp test --list), for the gutter // and the run configurations from context, which must not wait for mcpp. diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt index c651d2a..bd82a44 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt @@ -3,7 +3,9 @@ // does what the row's `action` says, in the IDE's own terms (run configurations, dialogs, panels). package io.github.mcppcommunity.mcppclion.actions +import com.google.gson.JsonElement import com.google.gson.JsonObject +import java.util.concurrent.CompletableFuture import com.intellij.execution.ProgramRunnerUtil import com.intellij.execution.RunManager import com.intellij.execution.executors.DefaultDebugExecutor @@ -111,11 +113,34 @@ object Dispatch { ApplicationManager.getApplication().invokeLater { Messages.showInfoMessage(project, text, t("Environment Self-check")) } } "languageService" -> languageService(project) + "report" -> exportReport(project) "rpc" -> job(project, action["method"].asString, JsonObject().apply { session.root?.let { addProperty("root", it) } }) else -> session.notify(t("This command is not available in this IDE."), NotificationType.INFORMATION) } } + /** + * THE IDE REPORT (design 2026-10-30 part 2 §3.2): what a bug report needs, written where the + * person chooses. Without a backend (the degraded mode) it says so, and still writes. + */ + fun exportReport(project: Project) { + val session = McppSession.get(project) + val collected = if (session.degraded) CompletableFuture.completedFuture(JsonObject().apply { + addProperty("kind", "mcpp-ide.report"); addProperty("version", 1); addProperty("degraded", true) + }) else session.request("report/collect", JsonObject().apply { session.root?.let { addProperty("root", it) } }) + collected.whenComplete { report, error -> + val document = report ?: JsonObject().apply { addProperty("kind", "mcpp-ide.report"); addProperty("version", 1); addProperty("error", error?.message ?: "") } + ApplicationManager.getApplication().invokeLater { + val stamp = java.time.Instant.now().toString().replace(':', '-').replace('.', '-') + val dialog = com.intellij.openapi.fileChooser.FileChooserFactory.getInstance().createSaveFileDialog( + com.intellij.openapi.fileChooser.FileSaverDescriptor(session.t("Export IDE Report"), "", "json"), project) + val target = dialog.save(project.basePath?.let { java.nio.file.Path.of(it) }, "mcpp-ide-report-$stamp.json") ?: return@invokeLater + target.file.writeText(com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(document) + "\n") + session.notify(session.t("The IDE report is written to {0}", target.file.path)) + } + } + } + /** A job whose narration is not a console's: its end is a notification. */ private fun job(project: Project, method: String, params: JsonObject) { val session = McppSession.get(project) diff --git a/mide-plugins/nvim/lua/mcpp/commands.lua b/mide-plugins/nvim/lua/mcpp/commands.lua index ce51cff..c645315 100644 --- a/mide-plugins/nvim/lua/mcpp/commands.lua +++ b/mide-plugins/nvim/lua/mcpp/commands.lua @@ -347,6 +347,27 @@ function actions.selfCheck() vim.notify(table.concat(lines, '\n'), level) end +-- The IDE report (design 2026-10-30 part 2 §3.2): what a bug report needs, written to a file the +-- person names (default: beside the project). +function actions.report(_, args, root) + local r, err = session.call('report/collect', root and { root = root } or {}) + if err then + r = { kind = 'mcpp-ide.report', version = 1, error = err.message or tostring(err) } + end + local default = (root or vim.fn.getcwd()) .. '/mcpp-ide-report-' .. os.date('!%Y%m%dT%H%M%SZ') .. '.json' + local path = (args and args[1]) or flow.input({ prompt = t('Export IDE Report') .. ': ', default = default }) + if not path or path == '' then + return + end + local file = io.open(path, 'w') + if not file then + return vim.notify('mcpp: ' .. t('cannot write {0}', path), vim.log.levels.ERROR) + end + file:write(vim.json.encode(r), '\n') + file:close() + vim.notify('mcpp: ' .. t('The IDE report is written to {0}', path), vim.log.levels.INFO) +end + -- Which mcppls answers C++ files (languageService/status), said: in Neovim a missing or older mcppls -- comes with its own plugin, mcppls-nvim. function M.language_service(s) diff --git a/mide-plugins/nvim/lua/mcpp/init.lua b/mide-plugins/nvim/lua/mcpp/init.lua index aecc703..c55081b 100644 --- a/mide-plugins/nvim/lua/mcpp/init.lua +++ b/mide-plugins/nvim/lua/mcpp/init.lua @@ -123,6 +123,15 @@ function M.setup(opts) }) vim.api.nvim_create_autocmd('User', { group = group, pattern = 'McppStatus', callback = on_started }) vim.api.nvim_create_autocmd('VimLeavePre', { group = group, callback = session.stop }) + -- The editor regained focus: a `xlings use` or a changed pin happened outside it (§4.2). + vim.api.nvim_create_autocmd('FocusGained', { + group = group, + callback = function() + if session.state.ready then + session.request('project/refresh', {}, function() end) + end + end, + }) if session.state.ready then session.push_settings() elseif vim.v.vim_did_enter == 1 then diff --git a/mide-plugins/nvim/tests/smoke.lua b/mide-plugins/nvim/tests/smoke.lua index c457c0a..4ca1fa5 100644 --- a/mide-plugins/nvim/tests/smoke.lua +++ b/mide-plugins/nvim/tests/smoke.lua @@ -296,6 +296,15 @@ local report = heard('Self-check') or '' check(':Mcpp selfCheck reports the backend, mcpp and the checks', report:find('mcpp-nvim ', 1, true) and report:find('mcpp-ide ', 1, true) and report:find(vim.fs.basename(mcpp), 1, true) ~= nil, report:sub(1, 120)) +-- The IDE report (design 2026-10-30 part 2 §3.2), to a file named on the command line. +said = {} +local report_path = vim.fn.tempname() .. '-mcpp-ide-report.json' +vim.cmd('Mcpp exportReport ' .. vim.fn.fnameescape(report_path)) +vim.wait(20000, function() return heard('IDE report') ~= nil end, 20) +local written = vim.fn.filereadable(report_path) == 1 and vim.json.decode(table.concat(vim.fn.readfile(report_path), '\n')) or {} +check(':Mcpp exportReport writes the report the backend collects', written.kind == 'mcpp-ide.report' and written.backend ~= nil, + vim.inspect(written):sub(1, 120)) + -- ── what only the mock plays ────────────────────────────────────────────────────────────────── if mock then before = session.state.last diff --git a/mide-plugins/vscode/data/config-registry.json b/mide-plugins/vscode/data/config-registry.json index eaf7d15..41e59d0 100644 --- a/mide-plugins/vscode/data/config-registry.json +++ b/mide-plugins/vscode/data/config-registry.json @@ -1176,6 +1176,25 @@ "since": "1.0.0", "title": "Effects denied in an untrusted workspace", "description": "What mcpp may not do while the workspace is not trusted (mcpp --deny): a run that needs one of them stops before it and says so. Trusting the workspace lifts the list." + }, + { + "key": "mcpp.backend.logLevel", + "type": "string", + "default": "info", + "enum": [ + "error", + "warning", + "info", + "debug" + ], + "scope": "window", + "group": "advanced", + "order": 100, + "tier": "advanced", + "applies": "immediate", + "since": "1.1.0", + "title": "Set the backend's log level", + "description": "What the mcpp-ide backend writes to the editor's log: errors only, warnings, what it does (the default), or every run of mcpp and mcppls with its arguments." } ] } diff --git a/mide-plugins/vscode/data/i18n/zh-cn.json b/mide-plugins/vscode/data/i18n/zh-cn.json index 9a0491c..a81d4e1 100644 --- a/mide-plugins/vscode/data/i18n/zh-cn.json +++ b/mide-plugins/vscode/data/i18n/zh-cn.json @@ -628,12 +628,19 @@ "No debugger extension is installed: install CodeLLDB, the C/C++ extension or lldb-dap.": "没有安装调试器扩展:请安装 CodeLLDB、C/C++ 扩展或 lldb-dap。", "No toolchain is installed for {0}.": "{0} 尚未安装工具链。", "Open Settings": "打开设置", - "Restart": "重新启动", "Select the build target": "选择构建目标", - "The mcpp-ide backend ended.": "mcpp-ide 后端已结束。", "the mcpp-ide backend is not running": "mcpp-ide 后端未运行", "The mcpp-ide backend was not found. Install it with `xlings install mcpp-ide`, or set mcpp.ide.backendPath.": "未找到 mcpp-ide 后端。请用 `xlings install mcpp-ide` 安装,或设置 mcpp.ide.backendPath。", "the project's default": "工程默认值", "The targets mcpp builds for": "mcpp 能构建的目标", - "this machine": "本机" + "this machine": "本机", + "The project's mcpp is not installed: {0}": "项目所需的 mcpp 未安装:{0}", + "The project's mcpp did not answer: {0}": "项目的 mcpp 没有回答:{0}", + "mcpp {0} at {1}, set by mcpp.path (the project's own version is not chosen)": "mcpp {0}({1}),由 mcpp.path 指定(不经项目的版本选择)", + "mcpp {0} (not asked in the project: the workspace is not trusted)": "mcpp {0}(未在项目内探测:工作区不受信任)", + "mcpp {0} at {1}, the project's": "mcpp {0}({1}),项目所用的版本", + "The IDE report is written to {0}": "IDE 报告已写入 {0}", + "Restart the backend": "重新启动后端", + "Export IDE Report": "导出 IDE 报告", + "The mcpp-ide backend ended four times in three minutes; build, run, test and the project pages go on without it.": "mcpp-ide 后端在三分钟内结束了四次;构建、运行、测试与项目页面在没有它的情况下继续工作。" } diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json index 5d7cb47..0d69c6f 100644 --- a/mide-plugins/vscode/package.json +++ b/mide-plugins/vscode/package.json @@ -187,6 +187,11 @@ "title": "%command.mcpp.selfCheck.title%", "category": "%category%" }, + { + "command": "mcpp.exportReport", + "title": "%command.mcpp.exportReport.title%", + "category": "%category%" + }, { "command": "mcpp.autoConfigureModules", "title": "%command.mcpp.autoConfigureModules.title%", @@ -1125,6 +1130,20 @@ "order": 100090, "description": "%mcpp.trust.denyEffects.title%", "markdownDescription": "%mcpp.trust.denyEffects.description%" + }, + "mcpp.backend.logLevel": { + "type": "string", + "default": "info", + "enum": [ + "error", + "warning", + "info", + "debug" + ], + "scope": "window", + "order": 100100, + "description": "%mcpp.backend.logLevel.title%", + "markdownDescription": "%mcpp.backend.logLevel.description%" } } }, diff --git a/mide-plugins/vscode/package.nls.json b/mide-plugins/vscode/package.nls.json index 3fed35c..2b304ad 100644 --- a/mide-plugins/vscode/package.nls.json +++ b/mide-plugins/vscode/package.nls.json @@ -264,5 +264,8 @@ "mcpp.toml.diagnostics.unknownSection.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", "mcpp.toml.diagnostics.unknownKey.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", "mcpp.toml.diagnostics.planeSeparation.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", - "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off." + "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", + "mcpp.backend.logLevel.title": "Set the backend's log level", + "mcpp.backend.logLevel.description": "What the mcpp-ide backend writes to the editor's log: errors only, warnings, what it does (the default), or every run of mcpp and mcppls with its arguments.", + "command.mcpp.exportReport.title": "Export IDE Report" } diff --git a/mide-plugins/vscode/package.nls.zh-cn.json b/mide-plugins/vscode/package.nls.zh-cn.json index fafcb9d..e22b4f7 100644 --- a/mide-plugins/vscode/package.nls.zh-cn.json +++ b/mide-plugins/vscode/package.nls.zh-cn.json @@ -264,5 +264,8 @@ "mcpp.toml.diagnostics.unknownSection.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", "mcpp.toml.diagnostics.unknownKey.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", "mcpp.toml.diagnostics.planeSeparation.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", - "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。" + "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", + "mcpp.backend.logLevel.title": "设置后端的日志级别", + "mcpp.backend.logLevel.description": "mcpp-ide 后端写入编辑器日志的内容:只写错误、写警告、写它所做的事(缺省),或写每一次运行 mcpp 与 mcppls 及其参数。", + "command.mcpp.exportReport.title": "导出 IDE 报告" } diff --git a/mide-plugins/vscode/src/cli/controller.ts b/mide-plugins/vscode/src/cli/controller.ts index dc99625..df0c044 100644 --- a/mide-plugins/vscode/src/cli/controller.ts +++ b/mide-plugins/vscode/src/cli/controller.ts @@ -1,3 +1,4 @@ +import type { ProjectMcpp } from "../ide/protocol"; import { existsSync } from "node:fs"; import { join } from "node:path"; import process from "node:process"; @@ -58,6 +59,11 @@ export interface McppCliControllerOptions { * the extension layer owns the bridge and passes it in. */ languageServerSummary?: () => string | undefined; + /** + * The project's mcpp as the backend asked it in the project (design 2026-10-30 part 2 §4.4, §6.1): + * the status item names its version, and its tooltip the program and whether `mcpp.path` chose it. + */ + projectMcpp?: () => ProjectMcpp | undefined; } interface ToolchainPickItem extends vscode.QuickPickItem { @@ -1115,14 +1121,24 @@ export class McppCliController { clearTimeout(this.statusRevert); this.statusRevert = undefined; } - const tooltip = t("Open the mcpp project and toolchain quick menu"); + const mcpp = this.options.projectMcpp?.(); + const mcppLine = mcpp === undefined ? undefined + : mcpp.state === "pinned-missing" ? t("The project's mcpp is not installed: {0}", mcpp.install ?? mcpp.message ?? "") + : mcpp.state === "failed" ? t("The project's mcpp did not answer: {0}", mcpp.message ?? "") + : mcpp.fromSetting ? t("mcpp {0} at {1}, set by mcpp.path (the project's own version is not chosen)", mcpp.version, mcpp.program) + : mcpp.state === "not-probed" ? t("mcpp {0} (not asked in the project: the workspace is not trusted)", mcpp.version) + : t("mcpp {0} at {1}, the project's", mcpp.version, mcpp.program); + const base = mcpp !== undefined && mcpp.version !== "" && (mcpp.state === "ok" || mcpp.state === "not-probed") + ? `${quickMenuStatusText} ${mcpp.version}` : quickMenuStatusText; + const tooltip = mcppLine === undefined ? t("Open the mcpp project and toolchain quick menu") + : `${mcppLine}\n${t("Open the mcpp project and toolchain quick menu")}`; const languageServer = languageServerStatusSuffix( read("mcpp.ui.statusBar.showLanguageServer"), this.options.languageServerSummary?.(), ); this.status.text = languageServer === undefined - ? quickMenuStatusText - : `${quickMenuStatusText} · ${t("C++ Modules: {0}", languageServer)}`; + ? base + : `${base} · ${t("C++ Modules: {0}", languageServer)}`; this.status.tooltip = languageServer === undefined ? tooltip : `${tooltip}\n${t("C++ Modules (provided by the mcpp language server extension): {0}", languageServer)}`; diff --git a/mide-plugins/vscode/src/commands/ids.ts b/mide-plugins/vscode/src/commands/ids.ts index d4431f3..0b8e400 100644 --- a/mide-plugins/vscode/src/commands/ids.ts +++ b/mide-plugins/vscode/src/commands/ids.ts @@ -93,6 +93,7 @@ export const TOOL_COMMANDS = { openSettings: "mcpp.openSettings", openLanguageServerSettings: "mcpp.openMcpplsSettings", selfCheck: "mcpp.selfCheck", + exportReport: "mcpp.exportReport", } as const; /** diff --git a/mide-plugins/vscode/src/commands/menu.ts b/mide-plugins/vscode/src/commands/menu.ts index 342690e..e3b3035 100644 --- a/mide-plugins/vscode/src/commands/menu.ts +++ b/mide-plugins/vscode/src/commands/menu.ts @@ -97,6 +97,7 @@ export const quickMenuItems: readonly QuickMenuItem[] = [ { labelKey: "Build and refresh the language service", command: CLI_COMMANDS.autoConfigureModules, icon: "refresh", iconColor: "blue", group: "settings" }, { labelKey: "Environment self-check", command: TOOL_COMMANDS.selfCheck, icon: "heart", iconColor: "green", group: "settings" }, + { labelKey: "Export IDE Report", command: TOOL_COMMANDS.exportReport, icon: "folder", iconColor: "neutral", group: "settings" }, { labelKey: "mcpp settings", command: TOOL_COMMANDS.openSettings, icon: "settings-gear", iconColor: "neutral", group: "settings" }, ]; diff --git a/mide-plugins/vscode/src/extension.ts b/mide-plugins/vscode/src/extension.ts index f8c2641..04122b6 100644 --- a/mide-plugins/vscode/src/extension.ts +++ b/mide-plugins/vscode/src/extension.ts @@ -1,3 +1,6 @@ +import * as path from "node:path"; +import * as os from "node:os"; +import type { ProjectMcpp } from "./ide/protocol"; import * as vscode from "vscode"; import { McppCliController } from "./cli/controller"; @@ -261,6 +264,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi } }; + const projectMcpp = new Map(); const cliController = new McppCliController({ output, extensionUri: extensionContext.extensionUri, @@ -271,6 +275,10 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi const view = readLanguageServerState(); return view.available ? view.state : undefined; }, + projectMcpp: () => { + const project = findCurrentProject(); + return project === undefined ? undefined : projectMcpp.get(project.root); + }, }); // ── the mcpp-ide backend ───────────────────────────────────────────────── @@ -283,6 +291,18 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi all: () => session.info?.projects ?? [], }; const jobs = new Jobs(session); + // The project's mcpp (§4.4): asked when the backend starts, when the current project changes, + // after the window regains focus, and when a job's envelope named another version. + const refreshProjectMcpp = async (): Promise => { + const project = findCurrentProject(); + if (project === undefined || !session.running) return; + try { + projectMcpp.set(project.root, await session.request("project/mcpp", { root: project.root })); + } catch { + projectMcpp.delete(project.root); + } + cliController.refreshStatus(); + }; // 0.9.0's one seam for running mcpp now runs it through the backend; the parsers are unchanged. useMcppExecutor(async (_executable, args, cwd, options = {}) => { try { @@ -415,6 +435,29 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi })), vscode.workspace.onDidGrantWorkspaceTrust(() => cliController.refreshStatus()), + + // The IDE report (§3.2): what a bug report needs, written where the person chooses. + vscode.commands.registerCommand(TOOL_COMMANDS.exportReport, runGuarded(async () => { + const project = findCurrentProject(); + let report: unknown; + try { + report = await session.request("report/collect", project ? { root: project.root } : {}); + } catch (error) { + report = { kind: "mcpp-ide.report", version: 1, backend: null, error: String(error), degraded: session.degraded }; + } + const stamp = new Date().toISOString().replace(/[:.]/g, "-"); + const target = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(path.join(project?.root ?? os.homedir(), `mcpp-ide-report-${stamp}.json`)), + filters: { JSON: ["json"] }, + }); + if (target === undefined) return; + await vscode.workspace.fs.writeFile(target, Buffer.from(`${JSON.stringify(report, null, 2)}\n`, "utf8")); + void vscode.window.showInformationMessage(t("The IDE report is written to {0}", target.fsPath)); + })), + session.onDidStart.event(() => void refreshProjectMcpp()), + session.onDidRefresh.event(() => { projectMcpp.clear(); void refreshProjectMcpp(); }), + session.on("project/mcppChanged", (n: { root: string }) => { projectMcpp.delete(n.root); void refreshProjectMcpp(); }), + vscode.window.onDidChangeActiveTextEditor(() => void refreshProjectMcpp()), ); void offerSettingRenames(extensionContext); diff --git a/mide-plugins/vscode/src/ide/protocol.ts b/mide-plugins/vscode/src/ide/protocol.ts index 167bfaf..b56e4ce 100644 --- a/mide-plugins/vscode/src/ide/protocol.ts +++ b/mide-plugins/vscode/src/ide/protocol.ts @@ -24,6 +24,19 @@ export interface LanguageServiceStatus { actions: { id: "install" | "update"; title: string }[]; } +/** `project/mcpp` (design 2026-10-30 part 2 §4.4): the project's mcpp, asked in the project. */ +export interface ProjectMcpp { + root: string; + state: "ok" | "pinned-missing" | "failed" | "not-probed"; + version: string; + program: string; + features: string[]; + featuresFrozen: boolean; + fromSetting: boolean; + message?: string; + install?: string; +} + export interface InitializeResult { server: { name: string; version: string }; protocol: number; diff --git a/mide-plugins/vscode/src/ide/session.ts b/mide-plugins/vscode/src/ide/session.ts index 0071094..8329218 100644 --- a/mide-plugins/vscode/src/ide/session.ts +++ b/mide-plugins/vscode/src/ide/session.ts @@ -18,15 +18,24 @@ export class Session implements vscode.Disposable { private located: Located | undefined; private initResult: InitializeResult | undefined; private commandTable: CommandEntry[] = []; - private restarts = 0; + /** When the backend ended, within the last three minutes (§3.1). */ + private exits: number[] = []; + private degradedNow = false; private starting: Promise | undefined; private readonly disposables: vscode.Disposable[] = []; private readonly listeners = new Map void>>(); readonly onDidStart = new vscode.EventEmitter(); + /** The backend ended four times in three minutes and is no longer restarted (§3.1, §8). */ + readonly onDidDegrade = new vscode.EventEmitter(); constructor(readonly context: vscode.ExtensionContext, readonly output: vscode.OutputChannel) { this.disposables.push( this.onDidStart, + this.onDidDegrade, + // The editor regained focus: a `xlings use` or a changed pin happened outside it (§4.2). + vscode.window.onDidChangeWindowState((state) => { + if (state.focused && this.backend?.running) void this.backend.request("project/refresh", {}).then(() => this.onDidRefresh.fire(), () => undefined); + }), vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("mcpp.ide.backendPath")) { void this.restart(); return; } if (e.affectsConfiguration("mcpp")) void this.pushSettings(); @@ -37,6 +46,14 @@ export class Session implements vscode.Disposable { ); } + /** Fired after the projects' mcpp were forgotten, so a view asks again. */ + readonly onDidRefresh = new vscode.EventEmitter(); + + /** Whether the backend was given up on after ending too often (§3.1). */ + get degraded(): boolean { + return this.degradedNow; + } + /** The backend's command and where it was found. */ get started(): Located | undefined { return this.located; @@ -122,18 +139,35 @@ export class Session implements vscode.Disposable { } } + /** + * THE BACKEND ENDED (design 2026-10-30 part 2 §3.1). It is started again after 1, 4 and 16 + * seconds; a fourth end within three minutes stops the restarts, and the extension goes on in its + * declared degraded mode (build, run, test and the project pages) with two actions: start the + * backend again, and export the IDE report. + */ private onExit(backend: Backend, code: number | null): void { if (backend !== this.backend) return; this.backend = undefined; this.output.appendLine(`mcpp-ide: the backend ended (${code ?? "signal"})`); - if (this.restarts < 1) { - ++this.restarts; - void this.start(); + const now = Date.now(); + this.exits = [...this.exits.filter((at) => now - at < 3 * 60_000), now]; + const delays = [1000, 4000, 16000]; + if (this.exits.length <= delays.length) { + const delay = delays[this.exits.length - 1]; + this.output.appendLine(`mcpp-ide: starting it again in ${delay / 1000} s`); + setTimeout(() => void this.start(), delay); return; } - const again = t("Restart"); - void vscode.window.showErrorMessage(`mcpp: ${t("The mcpp-ide backend ended.")}`, again).then((c) => { - if (c === again) { this.restarts = 0; void this.start(); } + this.degradedNow = true; + this.onDidDegrade.fire(); + const again = t("Restart the backend"); + const report = t("Export IDE Report"); + void vscode.window.showErrorMessage( + `mcpp: ${t("The mcpp-ide backend ended four times in three minutes; build, run, test and the project pages go on without it.")}`, + again, report, + ).then((c) => { + if (c === again) void this.restart(); + if (c === report) void vscode.commands.executeCommand("mcpp.exportReport"); }); } @@ -141,11 +175,15 @@ export class Session implements vscode.Disposable { const old = this.backend; this.backend = undefined; if (old) await old.stop(); - this.restarts = 0; + this.exits = []; + this.degradedNow = false; await this.start(); } async request(method: string, params: unknown = {}): Promise { + if (this.degradedNow && !this.backend?.running) { + throw new BackendError(method, { code: -32099, message: t("the mcpp-ide backend is not running") }); + } if (!this.backend?.running) { if (!(await this.start())) throw new BackendError(method, { code: -32099, message: t("the mcpp-ide backend is not running") }); } @@ -157,7 +195,13 @@ export class Session implements vscode.Disposable { await this.backend.request("settings/update", { settings: this.settings(), trusted: vscode.workspace.isTrusted, locale: vscode.env.language }); } + /** Whether a backend is running now (nothing is started to answer this). */ + get running(): boolean { + return this.backend?.running === true; + } + dispose(): void { + this.onDidRefresh.dispose(); this.disposables.forEach((d) => d.dispose()); void this.backend?.stop(); } diff --git a/mide-plugins/vscode/test/config/registry.test.ts b/mide-plugins/vscode/test/config/registry.test.ts index 66abb83..341c5d5 100644 --- a/mide-plugins/vscode/test/config/registry.test.ts +++ b/mide-plugins/vscode/test/config/registry.test.ts @@ -16,11 +16,11 @@ import { subKey, } from "../../src/config/registry"; -test("the registry declares 10 groups and 75 settings", () => { +test("the registry declares 10 groups and 76 settings", () => { assert.equal(GROUPS.length, 10); - // 0.9.0's 69, and the six the mcpp-ide backend reads (build target, profile and on-save, the - // debugger, the backend's path, the effects an untrusted workspace denies). - assert.equal(SETTINGS.length, 75); + // 0.9.0's 69, and the seven the mcpp-ide backend reads (build target, profile and on-save, the + // debugger, the backend's path, the effects an untrusted workspace denies, its log level). + assert.equal(SETTINGS.length, 76); }); test("the registry's own shape rules hold", () => { @@ -29,8 +29,8 @@ test("the registry's own shape rules hold", () => { test("36 settings are public and the rest are advanced", () => { assert.equal(settingsByTier("public").length, 36); - // 35 advanced + the 4 deprecated keys the Settings UI keeps out of the way. - assert.equal(settingsByTier("advanced").length, 39); + // 36 advanced + the 4 deprecated keys the Settings UI keeps out of the way. + assert.equal(settingsByTier("advanced").length, 40); // The four deprecated keys live in `advanced` so the Settings UI keeps them out of the way. assert.equal(settingsByTier("advanced").filter((entry) => entry.deprecated === true).length, 4); }); diff --git a/mide-plugins/vscode/test/config/wiring.test.ts b/mide-plugins/vscode/test/config/wiring.test.ts index 2725b0c..085584c 100644 --- a/mide-plugins/vscode/test/config/wiring.test.ts +++ b/mide-plugins/vscode/test/config/wiring.test.ts @@ -40,6 +40,11 @@ const READ_INDIRECTLY: ReadonlyArray<{ key: string; file: string; evidence: stri file: "src/ide/session.ts", evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", }, + { + key: "mcpp.backend.logLevel", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, { key: "mcpp.task.buildArgs", file: "src/cli/tasks.ts", diff --git a/protocol/protocol.schema.json b/protocol/protocol.schema.json index 7f198d5..f9c1de9 100644 --- a/protocol/protocol.schema.json +++ b/protocol/protocol.schema.json @@ -331,6 +331,46 @@ } } }, + "project/mcpp": { + "description": "The project's mcpp, asked in the project through the shim (design 2026-10-30 part 2 §4.2): its version, the program that ran, what it states it can do, and whether mcpp.path decided it. `state` is ok, pinned-missing (a pinned version not installed; `install` is the command), failed, or not-probed (an untrusted workspace: the global mcpp's facts).", + "params": { "type": "object", "properties": { "root": { "type": "string" } } }, + "result": { + "type": "object", + "required": ["root", "state", "version", "program", "features", "featuresFrozen", "fromSetting"], + "properties": { + "root": { "type": "string" }, + "state": { "enum": ["ok", "pinned-missing", "failed", "not-probed"] }, + "version": { "type": "string" }, + "program": { "type": "string" }, + "features": { "type": "array", "items": { "type": "string" } }, + "featuresFrozen": { "type": "boolean" }, + "fromSetting": { "type": "boolean" }, + "message": { "type": "string" }, + "install": { "type": "string" } + } + } + }, + "project/refresh": { + "description": "Forgets what a project's mcpp is (every project's, without `root`), so its next use asks again: the editor regained focus.", + "params": { "type": "object", "properties": { "root": { "type": "string" } } }, + "result": { "type": "object" } + }, + "capabilities/explain": { + "description": "Each feature the panels gate, whether it is available for the project, and if not which party lacks what and from which version (design 2026-10-30 part 2 §3.2).", + "params": { "type": "object", "properties": { "root": { "type": "string" } } }, + "result": { + "type": "object", + "required": ["root", "mcpp", "features"], + "properties": { + "features": { "type": "array", "items": { "type": "object", "required": ["id", "available"], "properties": { "reason": { "type": "object", "required": ["source", "capability", "message"] } } } } + } + } + }, + "report/collect": { + "description": "The IDE report: the backend, every project's mcpp, mcppls's status, the settings in effect, recent jobs and the backend's recent log. The editor writes it to a file the person chose.", + "params": { "type": "object", "properties": { "root": { "type": "string" } } }, + "result": { "type": "object", "required": ["kind", "version", "backend", "projects", "settings", "jobs", "log"] } + }, "shutdown": { "description": "Waits for the requests in flight; the next message is `exit`.", "params": { "type": "object" }, "result": { "type": "null" } }, "exit": { "description": "A notification: the backend ends (status 0 after shutdown, 1 otherwise).", "params": { "type": "object" }, "result": { "type": "null" } } }, @@ -375,7 +415,8 @@ "test/summary": { "params": { "type": "object", "required": ["jobId", "passed", "failed"] } }, "result/changed": { "description": "A project's result record changed: a build finished, whoever ran it.", "params": { "type": "object", "required": ["root", "result"] } }, "manifest/changed": { "params": { "type": "object", "required": ["root"] } }, - "manifest/diagnostics": { "params": { "type": "object", "required": ["root", "diagnostics"] } } + "manifest/diagnostics": { "params": { "type": "object", "required": ["root", "diagnostics"] } }, + "project/mcppChanged": { "description": "A job's envelope named another mcpp than the project's was asked as: it is asked again on its next use.", "params": { "type": "object", "required": ["root", "version"] } } }, "$defs": { "settingValues": { "type": "object", "additionalProperties": true }, @@ -410,7 +451,7 @@ "when": { "type": "string" }, "quickMenu": { "type": "boolean" }, "subcommand": { "type": "string", "description": "The name a command line types (`:Mcpp ` in Neovim): the row's own, else the id after `mcpp.`" }, - "action": { "type": "object", "required": ["kind"], "properties": { "kind": { "enum": ["job", "run", "debug", "cache", "panel", "cancel", "selectTarget", "installToolchain", "selectDefaultToolchain", "newProject", "quickMenu", "selfCheck", "rpc", "languageService", "forward", "editor", "problems"] } } } + "action": { "type": "object", "required": ["kind"], "properties": { "kind": { "enum": ["job", "run", "debug", "cache", "panel", "cancel", "selectTarget", "installToolchain", "selectDefaultToolchain", "newProject", "quickMenu", "selfCheck", "rpc", "languageService", "forward", "editor", "problems", "report"] } } } } }, "launchParams": { "type": "object", "required": ["root"], "properties": { "root": { "type": "string" }, "target": { "type": "string" }, "profile": { "type": "string" }, "bin": { "type": "string" }, "args": { "type": "array", "items": { "type": "string" } } } }, diff --git a/spec/commands.json b/spec/commands.json index 1926c16..fed388a 100644 --- a/spec/commands.json +++ b/spec/commands.json @@ -46,6 +46,8 @@ "omitted": { "nvim": "Neovim's settings are setup()'s `settings` table, which the backend validates; no panel edits them." } }, { "id": "mcpp.selfCheck", "group": "tools", "icon": { "jetbrains": "AllIcons.Actions.Checked" }, "action": { "kind": "selfCheck" }, "quickMenu": true }, + { "id": "mcpp.exportReport", "group": "tools", "icon": { "jetbrains": "AllIcons.ToolbarDecorator.Export" }, + "action": { "kind": "report" }, "quickMenu": true }, { "id": "mcpp.autoConfigureModules", "group": "languageService", "action": { "kind": "job", "job": "build", "then": "languageService" } }, { "id": "mcpp.refreshCompilationDatabase", "group": "languageService", "action": { "kind": "job", "job": "build" } }, diff --git a/spec/i18n/manifest.en.json b/spec/i18n/manifest.en.json index 3fed35c..2b304ad 100644 --- a/spec/i18n/manifest.en.json +++ b/spec/i18n/manifest.en.json @@ -264,5 +264,8 @@ "mcpp.toml.diagnostics.unknownSection.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", "mcpp.toml.diagnostics.unknownKey.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", "mcpp.toml.diagnostics.planeSeparation.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", - "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off." + "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", + "mcpp.backend.logLevel.title": "Set the backend's log level", + "mcpp.backend.logLevel.description": "What the mcpp-ide backend writes to the editor's log: errors only, warnings, what it does (the default), or every run of mcpp and mcppls with its arguments.", + "command.mcpp.exportReport.title": "Export IDE Report" } diff --git a/spec/i18n/manifest.zh-cn.json b/spec/i18n/manifest.zh-cn.json index fafcb9d..e22b4f7 100644 --- a/spec/i18n/manifest.zh-cn.json +++ b/spec/i18n/manifest.zh-cn.json @@ -264,5 +264,8 @@ "mcpp.toml.diagnostics.unknownSection.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", "mcpp.toml.diagnostics.unknownKey.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", "mcpp.toml.diagnostics.planeSeparation.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", - "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。" + "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", + "mcpp.backend.logLevel.title": "设置后端的日志级别", + "mcpp.backend.logLevel.description": "mcpp-ide 后端写入编辑器日志的内容:只写错误、写警告、写它所做的事(缺省),或写每一次运行 mcpp 与 mcppls 及其参数。", + "command.mcpp.exportReport.title": "导出 IDE 报告" } diff --git a/spec/i18n/runtime.zh-cn.json b/spec/i18n/runtime.zh-cn.json index 25e0ae0..d3f945c 100644 --- a/spec/i18n/runtime.zh-cn.json +++ b/spec/i18n/runtime.zh-cn.json @@ -764,5 +764,11 @@ "mcpp: Pack": "mcpp:打包", "mcpp: Pack (1/3): the package format": "mcpp:打包(1/3):包格式", "mcpp: Pack (2/3): what the machine that runs it provides": "mcpp:打包(2/3):运行它的机器提供什么", - "mcpp: Pack (3/3): the libraries inside the program or beside it": "mcpp:打包(3/3):库在程序里还是在程序旁边" + "mcpp: Pack (3/3): the libraries inside the program or beside it": "mcpp:打包(3/3):库在程序里还是在程序旁边", + "the project's mcpp {0} does not state {1}; mcpp {2} does": "项目的 mcpp {0} 未声明 {1};mcpp {2} 声明了它", + "the workspace is not trusted: the project's mcpp is not asked": "工作区不受信任:未询问项目的 mcpp", + "The mcpp-ide backend ended four times in three minutes; build, run, test and the project pages go on without it.": "mcpp-ide 后端在三分钟内结束了四次;构建、运行、测试与项目页面在没有它的情况下继续工作。", + "Restart the backend": "重新启动后端", + "Export IDE Report": "导出 IDE 报告", + "The IDE report is written to {0}": "IDE 报告已写入 {0}" } diff --git a/spec/settings.json b/spec/settings.json index eaf7d15..41e59d0 100644 --- a/spec/settings.json +++ b/spec/settings.json @@ -1176,6 +1176,25 @@ "since": "1.0.0", "title": "Effects denied in an untrusted workspace", "description": "What mcpp may not do while the workspace is not trusted (mcpp --deny): a run that needs one of them stops before it and says so. Trusting the workspace lifts the list." + }, + { + "key": "mcpp.backend.logLevel", + "type": "string", + "default": "info", + "enum": [ + "error", + "warning", + "info", + "debug" + ], + "scope": "window", + "group": "advanced", + "order": 100, + "tier": "advanced", + "applies": "immediate", + "since": "1.1.0", + "title": "Set the backend's log level", + "description": "What the mcpp-ide backend writes to the editor's log: errors only, warnings, what it does (the default), or every run of mcpp and mcppls with its arguments." } ] } diff --git a/src/bin/mockmcpp.cpp b/src/bin/mockmcpp.cpp index e3ab4ad..b4fd84f 100644 --- a/src/bin/mockmcpp.cpp +++ b/src/bin/mockmcpp.cpp @@ -9,7 +9,14 @@ // project's `.mock-mcpp-fail`) // MOCK_MCPP_SLOW_MS the build waits this long before it ends, so a test can // cancel it; SIGTERM then leaves the record `cancelled` -// MOCK_MCPP_LOG every argv is appended here, one line each +// MOCK_MCPP_LOG every argv is appended here, one line each, and the value of +// XLINGS_PROJECT_DIR the run received (`` when none) +// +// AND A VERSION MANAGER'S SHIM, per directory (design 2026-10-30 part 2 §4.2): in the directory a +// command runs in, `.mock-mcpp-version` names the version that answers (every output's version is +// that one), `.mock-mcpp-features` the features its envelopes state (comma-separated; without +// it the envelopes state none, as an mcpp before 2026.10.31.1), and `.mock-mcpp-missing` a pinned +// version that is not installed: the run ends as the shim ends, with its words. import std; import mcpplibs.os; @@ -61,12 +68,36 @@ std::string json_path(const std::filesystem::path& p) { return out; } +std::string here_file(std::string_view name) { + std::error_code ec; + auto text = slurp(std::filesystem::current_path(ec) / std::string(name)); + while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) text.pop_back(); + return text; +} + std::string fixture(std::string_view name) { const auto dir = fixtures_dir(); auto text = slurp(std::filesystem::path(dir) / name); std::error_code ec; text = replace_all(std::move(text), "@ROOT@", json_path(std::filesystem::current_path(ec))); text = replace_all(std::move(text), "@HOME@", json_path(env("HOME"))); + // The version this directory's shim chose, and the features that version states. + if (const auto version = here_file(".mock-mcpp-version"); !version.empty()) { + auto captured = slurp(std::filesystem::path(dir) / "version.txt"); + if (auto at = captured.find(' '); at != std::string::npos) captured = captured.substr(at + 1); + while (!captured.empty() && (captured.back() == '\n' || captured.back() == '\r')) captured.pop_back(); + if (!captured.empty()) text = replace_all(std::move(text), captured, version); + } + if (const auto features = here_file(".mock-mcpp-features"); !features.empty()) { + std::string list; + for (std::size_t at = 0; at <= features.size();) { + auto end = features.find(',', at); + if (end == std::string::npos) end = features.size(); + list += std::format("{}\"{}\"", list.empty() ? "" : ", ", features.substr(at, end - at)); + at = end + 1; + } + text = replace_all(std::move(text), "\"protocol\": {", std::format("\"features\": [{}], \"protocol\": {{", list)); + } return text; } @@ -123,7 +154,14 @@ int main(int argc, char** argv) { if (const auto log = env("MOCK_MCPP_LOG"); !log.empty()) { std::ofstream out(log, std::ios::app); for (const auto& a : args) out << a << ' '; - out << '\n'; + const char* pinned = std::getenv("XLINGS_PROJECT_DIR"); + out << "XLINGS_PROJECT_DIR=" << (pinned ? pinned : "") << '\n'; + } + // A pinned version that is not installed: the shim refuses, in its own words. + if (const auto missing = here_file(".mock-mcpp-missing"); !missing.empty()) { + err(std::format("[error] xlings: executable 'mcpp' not found\n[error] path: /x/xpkgs/xim-x-mcpp/{0}/bin\n" + "[error] hint: install with xlings install mcpp@{0}\n", missing)); + return 1; } auto has = [&](std::string_view a) { return std::ranges::find(args, a) != args.end(); }; auto first = [&](std::size_t i) { return i < args.size() ? args[i] : std::string{}; }; diff --git a/src/mcpp.cppm b/src/mcpp.cppm index 8a7c7f7..35f9fb4 100644 --- a/src/mcpp.cppm +++ b/src/mcpp.cppm @@ -33,9 +33,33 @@ struct Protocol { int envelopeMin = 0, envelopeMax = 0; std::map kinds; // kind -> version std::map> commandEffects; + std::vector features; // what a client gates on (mcpp docs/50 §4) bool contract = false; // the full control contract (>= kContractFloor) }; +// THE FEATURES OF AN mcpp THAT PREDATES `features` (design 2026-10-30 part 2 §4.3): a table frozen +// at the release that added the list, read for those releases only. An mcpp that states +// `features` is taken at its word. +std::vector frozen_features(std::string_view version); + +// What a project's mcpp is, asked in the project (design 2026-10-30 part 2 §4.2): `mcpp self env +// --format json` run in its root, through the program found on PATH (a version manager's shim +// chooses by the directory) or the one the settings name, with XLINGS_PROJECT_DIR removed. +struct ProjectMcpp { + // ok: the project's mcpp answered. pinned-missing: the project pins a version that is not + // installed, and the shim refused. failed: anything else. not-probed: an untrusted workspace, + // whose pinned program is not run; the global mcpp's facts stand in. + std::string state = "not-probed"; + std::string version; + std::string program; // the file that ran (`self env`'s `program`) + std::vector features; + bool featuresFrozen = false; // from the frozen table, not from mcpp + bool fromSetting = false; // mcpp.path named the program + std::string message; // why it is not ok, in the shim's or mcpp's words + std::string install; // the command that installs the pinned version + Json env = Json::object(); // `self env`'s data +}; + // `mcpp --version` and `mcpp --protocol-version`. // The environment overrides apply to these two runs as to every run of mcpp the backend makes. using Environment = std::vector>>; @@ -96,6 +120,9 @@ public: std::expected exec(const RunOptions& o, std::chrono::milliseconds timeout, std::size_t outputLimit) const; + // What the project at `root` runs (ProjectMcpp). + ProjectMcpp probe(const std::filesystem::path& root, const RunOptions& base) const; + private: std::filesystem::path program_; Protocol protocol_; @@ -164,6 +191,7 @@ std::expected handshake(const std::filesystem::path& prog o.args = {"--version"}; o.in = mcpplibs::os::process::stdio::null; o.env = env; + o.env.push_back({"XLINGS_PROJECT_DIR", std::nullopt}); // mcpp docs/50: no other project's pin auto v = mcpplibs::os::process::run(o); if (!v) return std::unexpected(v.error().message()); if (!v->status.success()) return std::unexpected(std::format("{} --version exited {}", program.string(), v->status.shell_code())); @@ -186,6 +214,8 @@ std::expected handshake(const std::filesystem::path& prog if (doc.contains("kinds") && doc["kinds"].is_object()) for (auto it = doc["kinds"].begin(); it != doc["kinds"].end(); ++it) if (it->is_number_integer()) p.kinds[it.key()] = it->get(); + if (doc.contains("features") && doc["features"].is_array()) + for (const auto& f : doc["features"]) if (f.is_string()) p.features.push_back(f.get()); if (doc.contains("commands") && doc["commands"].is_object()) for (auto it = doc["commands"].begin(); it != doc["commands"].end(); ++it) { std::vector effects; @@ -197,9 +227,16 @@ std::expected handshake(const std::filesystem::path& prog } p.contract = !p.version.empty() && compare_versions(p.version, kContractFloor) >= 0 && p.kinds.contains("mcpp.build"); + if (p.features.empty()) p.features = frozen_features(p.version); return p; } +std::vector frozen_features(std::string_view version) { + // 2026.10.30.1 introduced both meanings; 2026.10.31.1 added the list that names them. + if (!version.empty() && compare_versions(version, "2026.10.30.1") >= 0) return {"target-sets", "pkg-format-list"}; + return {}; +} + // A run of mcpp: its output on pipes, its input the null device (the backend's own streams carry // the protocol), and a unit of its own, so a cancelled build ends with everything it started. mcpplibs::os::process::spawn_options Client::spawn_options(const RunOptions& o) const { @@ -210,6 +247,10 @@ mcpplibs::os::process::spawn_options Client::spawn_options(const RunOptions& o) s.in = mcpplibs::os::process::stdio::null; s.own_unit = true; s.env.push_back({"MCPP_CLIENT", o.client}); + // THE PROJECT'S DIRECTORY DECIDES WHICH mcpp RUNS (mcpp docs/50, "How a tool runs mcpp for a + // project"): a shim reads XLINGS_PROJECT_DIR only where the directory has no pin, and an editor + // started from another project's terminal hands it to everything it starts. + s.env.push_back({"XLINGS_PROJECT_DIR", std::nullopt}); if (!o.deny.empty()) { std::string joined; for (const auto& d : o.deny) { if (!joined.empty()) joined += ','; joined += d; } @@ -280,6 +321,51 @@ std::expected Client::exec(const RunOptions& o, r->out_truncated || r->err_truncated}; } +ProjectMcpp Client::probe(const std::filesystem::path& root, const RunOptions& base) const { + ProjectMcpp out; + out.program = program_.generic_string(); + RunOptions o = base; + o.cwd = root; + o.args = {"self", "env", "--format", "json"}; + auto r = envelope(o); + if (!r) { + out.state = "failed"; + out.message = r.error(); + return out; + } + const Json& e = r->envelope; + if (e.is_object() && e.contains("mcpp") && e["mcpp"].is_object()) { + out.state = "ok"; + out.version = e["mcpp"].value("version", ""); + if (e["mcpp"].contains("features") && e["mcpp"]["features"].is_array()) { + for (const auto& f : e["mcpp"]["features"]) if (f.is_string()) out.features.push_back(f.get()); + } else { + out.features = frozen_features(out.version); + out.featuresFrozen = true; + } + if (e.contains("data") && e["data"].is_object()) { + out.env = e["data"]; + if (const auto program = out.env.value("program", ""); !program.empty()) out.program = program; + } + return out; + } + // No envelope: the shim refused (a pinned version that is not installed), or mcpp failed. The + // shim's own words are the message; its install hint, when it gave one, is the action. + out.message = r->narration.empty() ? r->output : r->narration; + while (!out.message.empty() && (out.message.back() == '\n' || out.message.back() == '\r')) out.message.pop_back(); + const auto at = out.message.find("xlings install mcpp@"); + if (at != std::string::npos) { + auto end = out.message.find_first_of(" \r\n'\"", at + 20); + out.install = out.message.substr(at, end == std::string::npos ? std::string::npos : end - at); + out.state = "pinned-missing"; + } else if (out.message.find("not installed") != std::string::npos || out.message.find("not found for 'mcpp'") != std::string::npos) { + out.state = "pinned-missing"; + } else { + out.state = "failed"; + } + return out; +} + Json diagnostics_of(const Json& envelope) { Json out = Json::array(); if (!envelope.is_object() || !envelope.contains("diagnostics") || !envelope["diagnostics"].is_array()) return out; diff --git a/src/server.cppm b/src/server.cppm index 7c22ede..8ded7ef 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -36,7 +36,7 @@ inline constexpr int kProtocol = 1; // The methods `install` registers, for the protocol schema's test: a method // here and not in protocol/protocol.schema.json (or the other way) fails it. -inline constexpr std::array kMethods = { +inline constexpr std::array kMethods = { "initialize", "settings/update", "settings/registry", "settings/preset", "i18n/bundle", "selfCheck", "commands/list", "menu/quick", "languageService/status", "projects/list", "projects/forFile", "project/summary", "project/new", "result/last", @@ -45,13 +45,14 @@ inline constexpr std::array kMethods = { "toolchains/list", "toolchains/install", "toolchains/setDefault", "targets/list", "cache/inventory", "cache/plan", "cache/apply", "graph/packages", "graph/modules", "pack/formats", "pack/start", "manifest/check", "toml/complete", "toml/hover", "toml/symbols", - "buildscript/complete", "mcpp/exec", "project/files", "languageService/exec", "shutdown", "exit"}; + "buildscript/complete", "mcpp/exec", "project/files", "languageService/exec", "project/mcpp", "project/refresh", + "capabilities/explain", "report/collect", "shutdown", "exit"}; // The notifications the backend sends. -inline constexpr std::array kNotifications = { +inline constexpr std::array kNotifications = { "job/started", "job/output", "job/plan", "job/progress", "job/diagnostic", "job/artifact", "job/record", "job/finished", "test/result", "test/summary", "result/changed", "manifest/changed", - "manifest/diagnostics"}; + "manifest/diagnostics", "project/mcppChanged"}; class Session { public: @@ -83,6 +84,21 @@ private: Result commands(const Json& p) const; Result quick_menu(const Json& p); Result test_list(const Json& p); + Result project_mcpp(const Json& p); + Result project_refresh(const Json& p); + Result capabilities_explain(const Json& p); + Result report_collect(const Json& p); + + // THE PROJECT'S mcpp (design 2026-10-30 part 2 §4.2): asked in the project, once, and asked again + // when its manifest changes, when the editor regains focus, or when a job's envelope names + // another version. Never more than two asked at once. + mcpp::ProjectMcpp probe_of(const std::filesystem::path& root); + Json probe_json(const std::filesystem::path& root, const mcpp::ProjectMcpp& probe) const; + bool has_feature(const std::filesystem::path& root, std::string_view feature); + // A line of the backend's log, at a level (0 error, 1 warning, 2 info, 3 debug): written to + // standard error, which every editor keeps in its log, when mcpp.backend.logLevel lets it + // through, and kept for the IDE report either way (§3.2). + void log(int level, std::string_view text); std::expected query(const std::filesystem::path& cwd, std::vector args) const; @@ -131,6 +147,10 @@ private: }; std::optional packOptions_; PackOptions pack_options(const std::filesystem::path& cwd); + std::map probes_; + std::counting_semaphore<2> probeSlots_{2}; + // The backend's log level (mcpp.backend.logLevel): what reaches standard error. + std::deque recentLog_; }; } // namespace mcppide::server @@ -169,15 +189,32 @@ Session::Session(rpc::Server& server) : rpc_(server) { if (env.is_object() && env.contains("data") && env["data"].is_object() && env["data"].contains("artifacts")) s["artifacts"] = env["data"]["artifacts"]; } + std::filesystem::path root; { std::lock_guard lock(mutex_); if (auto it = jobOf_.find(id); it != jobOf_.end()) { s["root"] = it->second.first.generic_string(); s["kind"] = it->second.second; + root = it->second.first; jobOf_.erase(it); } } rpc_.notify("job/finished", s); + // Every envelope names the mcpp that wrote it: another version than the one probed (a + // `xlings use`, a pin changed) is asked again, and the editor is told (§4.2). + const auto& env = summary.contains("envelope") ? summary["envelope"] : Json(nullptr); + if (!root.empty() && env.is_object() && env.contains("mcpp") && env["mcpp"].is_object()) { + const auto seen = env["mcpp"].value("version", ""); + bool changed = false; + { + std::lock_guard lock(mutex_); + if (auto it = probes_.find(root); it != probes_.end() && !seen.empty() && it->second.version != seen) { + probes_.erase(it); + changed = true; + } + } + if (changed) rpc_.notify("project/mcppChanged", Json{{"root", root.generic_string()}, {"version", seen}}); + } }, .started = [this](std::string_view id, const Json& started) { { @@ -267,13 +304,17 @@ void Session::locate_mcpp() { located_ = *located; client_ = mcpp::Client(located->program, *protocol); } + probes_.clear(); jobs_->set_client(client_); + if (located && protocol) + std::println(std::cerr, "[info] mcpp {} at {} ({})", protocol->version, located->program.generic_string(), located->source); } void Session::on_changed(const std::filesystem::path& root, std::string_view what, const Json& detail) { { std::lock_guard lock(mutex_); buildDatabases_.erase(root); + if (what != "result") probes_.erase(root); // a manifest may state another mcpp floor or pin } if (what == "result") { rpc_.notify("result/changed", Json{{"root", root.generic_string()}, {"result", detail}}); @@ -875,6 +916,162 @@ std::expected Session::language_service(const Json& p) { return out; } +mcpp::ProjectMcpp Session::probe_of(const std::filesystem::path& rootIn) { + const auto root = rootIn.lexically_normal(); + mcpp::Client client; + bool trusted, fromSetting; + { + std::lock_guard lock(mutex_); + if (auto it = probes_.find(root); it != probes_.end()) return it->second; + client = client_; + trusted = trusted_; + fromSetting = located_ && located_->source == "setting"; + } + mcpp::ProjectMcpp probe; + if (!client.ready()) { + probe.state = "failed"; + std::lock_guard lock(mutex_); + probe.message = mcppError_.empty() ? t("mcpp is not available") : mcppError_; + return probe; + } + if (!trusted) { + // A pinned program is not run in an untrusted workspace: the global mcpp's facts stand in, + // and are labelled as not asked in the project (§4.2). + probe.state = "not-probed"; + probe.version = client.protocol().version; + probe.program = client.program().generic_string(); + probe.features = client.protocol().features; + probe.message = t("the workspace is not trusted: the project's mcpp is not asked"); + } else { + mcpp::RunOptions o; + o.deny = deny(); + o.client = client_label(); + probeSlots_.acquire(); + probe = client.probe(root, o); + probeSlots_.release(); + } + probe.fromSetting = fromSetting; + log(probe.state == "ok" || probe.state == "not-probed" ? 2 : 1, + std::format("project {}: mcpp {} ({}){}{}", root.generic_string(), probe.version.empty() ? "?" : probe.version, + probe.state, probe.program.empty() ? "" : " at " + probe.program, + probe.message.empty() ? "" : ": " + probe.message)); + std::lock_guard lock(mutex_); + probes_[root] = probe; + return probe; +} + +bool Session::has_feature(const std::filesystem::path& root, std::string_view feature) { + const auto probe = probe_of(root); + return std::ranges::find(probe.features, feature) != probe.features.end(); +} + +void Session::log(int level, std::string_view text) { + static constexpr std::string_view kNames[] = {"error", "warning", "info", "debug"}; + const auto configured = setting("mcpp.backend.logLevel"); + int limit = 2; + if (configured.is_string()) + for (int i = 0; i < 4; ++i) if (configured.get() == kNames[i]) limit = i; + const auto line = std::format("[{}] {}", kNames[std::clamp(level, 0, 3)], text); + { + std::lock_guard lock(mutex_); + recentLog_.push_back(line); + while (recentLog_.size() > 200) recentLog_.pop_front(); + } + if (level <= limit) std::println(std::cerr, "{}", line); +} + +Json Session::probe_json(const std::filesystem::path& root, const mcpp::ProjectMcpp& probe) const { + Json out{{"root", root.generic_string()}, + {"state", probe.state}, + {"version", probe.version}, + {"program", probe.program}, + {"features", probe.features}, + {"featuresFrozen", probe.featuresFrozen}, + {"fromSetting", probe.fromSetting}}; + if (!probe.message.empty()) out["message"] = probe.message; + if (!probe.install.empty()) out["install"] = probe.install; + for (const auto* key : {"mcppHome", "defaultToolchain", "xlings", "registry"}) + if (probe.env.contains(key)) out[key] = probe.env[key]; + return out; +} + +// What the project's mcpp is (§4.4): the version and the program that ran in the project, whether +// mcpp.path decided it, what it states it can do, and the facts of `self env` the panels show. +std::expected Session::project_mcpp(const Json& p) { + const auto root = any_root(p).lexically_normal(); + return probe_json(root, probe_of(root)); +} + +// The editor regained focus, or asks again: every project is asked again on its next use. +std::expected Session::project_refresh(const Json& p) { + std::lock_guard lock(mutex_); + if (const auto root = root_of(p); !root.empty()) probes_.erase(root.lexically_normal()); + else probes_.clear(); + return Json::object(); +} + +// WHY A FEATURE IS NOT OFFERED (§3.2): each feature the panels gate, whether it is available for +// the project, and if not which party lacks what -- the project's mcpp, mcppls -- and from which +// version. The panels' tooltips and selfCheck read this one answer. +std::expected Session::capabilities_explain(const Json& p) { + const auto root = any_root(p).lexically_normal(); + const auto probe = probe_of(root); + struct Gate { const char* id; const char* feature; const char* since; }; + static constexpr Gate kGates[] = { + {"targets.sets", "target-sets", "2026.10.30.1"}, + {"pack.formatList", "pkg-format-list", "2026.10.30.1"}, + }; + Json features = Json::array(); + for (const auto& g : kGates) { + const bool on = std::ranges::find(probe.features, g.feature) != probe.features.end(); + Json f{{"id", g.id}, {"available", on}}; + if (!on) + f["reason"] = Json{{"source", "mcpp"}, + {"capability", g.feature}, + {"need", g.since}, + {"have", probe.version}, + {"message", probe.state == "ok" || probe.state == "not-probed" + ? t("the project's mcpp {0} does not state {1}; mcpp {2} does", + {probe.version, g.feature, g.since}) + : probe.message}}; + features.push_back(std::move(f)); + } + return Json{{"root", root.generic_string()}, {"mcpp", probe_json(root, probe)}, {"features", features}}; +} + +// THE IDE REPORT (§3.2): what a bug report needs, with no secret in it -- the backend and its +// protocol, every project's mcpp, mcppls's status, the settings that are in effect (paths shown, +// nothing else), the recent jobs and the backend's recent log. +std::expected Session::report_collect(const Json& p) { + Json projects = Json::array(); + std::vector roots; + { + std::lock_guard lock(mutex_); + for (const auto& [root, _] : probes_) roots.push_back(root); + } + if (const auto root = root_of(p); !root.empty() && std::ranges::find(roots, root.lexically_normal()) == roots.end()) + roots.push_back(root.lexically_normal()); + for (const auto& r : roots) projects.push_back(probe_json(r, probe_of(r))); + std::vector candidates; + Json settings, log = Json::array(); + { + std::lock_guard lock(mutex_); + candidates = mcpplsCandidates_; + settings = settings_; + for (const auto& line : recentLog_) log.push_back(line); + } + auto ls = language_service(Json{{"candidates", candidates}}); + return Json{{"kind", "mcpp-ide.report"}, + {"version", 1}, + {"backend", Json{{"version", kVersion}, {"protocol", kProtocol}, {"windows", !std::string(mcpplibs::os::conventions().exe_suffix).empty()}}}, + {"selfCheck", self_check()}, + {"projects", projects}, + {"languageService", ls ? *ls : Json(nullptr)}, + {"settings", settings}, + {"jobs", jobs_->list()}, + {"log", log}}; +} + std::expected Session::targets(const Json& p) { auto r = query(any_root(p), {"toolchain", "list", "--format", "json"}); if (!r) return std::unexpected(r.error()); @@ -884,8 +1081,10 @@ std::expected Session::targets(const Json& p) { bool sets; { std::lock_guard lock(mutex_); - sets = client_.ready() && mcpp::compare_versions(client_.protocol().version, "2026.10.30.1") >= 0; + sets = client_.ready(); } + // By what the project's mcpp states, not by its version (§4.3). + sets = sets && has_feature(any_root(p), "target-sets"); Json out = views::targets(r->envelope, chosen, sets); if (const auto root = root_of(p); !root.empty()) { std::lock_guard lock(mutex_); @@ -944,7 +1143,7 @@ std::expected Session::quick_menu(const Json& p) { const auto when = c.value("when", ""); std::string disabled; if (when.find("mcpp.inProject") != std::string::npos && root.empty()) disabled = t("No mcpp project in this workspace"); - else if (!ready && c["action"].value("kind", "") != "panel" && c["action"].value("kind", "") != "selfCheck") + else if (!ready && c["action"].value("kind", "") != "panel" && c["action"].value("kind", "") != "selfCheck" && c["action"].value("kind", "") != "report") disabled = t("mcpp is not available"); Json item{{"command", c["id"]}, {"title", c["title"]}, {"group", c["groupTitle"]}, {"enabled", disabled.empty()}}; if (!disabled.empty()) item["reason"] = disabled; @@ -1065,6 +1264,10 @@ void Session::install() { s.on("run/launch", [this](const Json& p, const rpc::Context& c) { return run_launch(p, c); }); s.on("debug/configuration", [this](const Json& p, const rpc::Context& c) { return debug_configuration(p, c); }); s.on("test/list", [this](const Json& p, const rpc::Context&) { return test_list(p); }); + s.on("project/mcpp", [this](const Json& p, const rpc::Context&) { return project_mcpp(p); }); + s.on("project/refresh", [this](const Json& p, const rpc::Context&) { return project_refresh(p); }); + s.on("capabilities/explain", [this](const Json& p, const rpc::Context&) { return capabilities_explain(p); }); + s.on("report/collect", [this](const Json& p, const rpc::Context&) { return report_collect(p); }); s.on("library/search", [this](const Json& p, const rpc::Context&) { return library_search(p); }); s.on("library/indexes", [this](const Json& p, const rpc::Context&) -> Result { auto r = query(any_root(p), {"index", "list", "--format", "json"}); diff --git a/tests/test_probe.cpp b/tests/test_probe.cpp new file mode 100644 index 0000000..789b222 --- /dev/null +++ b/tests/test_probe.cpp @@ -0,0 +1,150 @@ +// The project's mcpp (design 2026-10-30 part 2 §4, §13): each project asked in its own directory, +// through the program a version manager's shim stands for, with another project's +// XLINGS_PROJECT_DIR removed; features as mcpp states them, else from the frozen table; a pinned +// version that is not installed; an untrusted workspace; and a job whose envelope names another +// version, after which the project is asked again. The mock mcpp plays the shim per directory. +#include "support.hpp" +import std; +import nlohmann.json; +import mcppide.rpc; +import mcppide.server; +import mcppide.mcpp; + +using namespace mcppide; +using Json = nlohmann::json; + +namespace { + +struct Wire { + std::mutex lock; + std::vector messages; + rpc::Server server{[this](std::string_view frame) { + rpc::FrameReader r; + r.feed(frame); + std::lock_guard g(lock); + while (auto body = r.next()) messages.push_back(Json::parse(*body)); + return true; + }}; + int next = 1; + + Json call(std::string_view method, Json params) { + const int id = next++; + server.dispatch(Json{{"jsonrpc", "2.0"}, {"id", id}, {"method", method}, {"params", std::move(params)}}.dump()); + for (int i = 0; i < 3000; ++i) { + { + std::lock_guard g(lock); + for (const auto& m : messages) if (m.contains("id") && m["id"] == id) return m; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + return Json{{"error", {{"message", "no answer"}}}}; + } + bool saw(std::string_view method) { + std::lock_guard g(lock); + for (const auto& m : messages) if (m.value("method", "") == method) return true; + return false; + } +}; + +std::filesystem::path project(const std::filesystem::path& base, std::string_view name, + std::vector> files) { + const auto root = base / std::string(name); + std::filesystem::create_directories(root / "src"); + std::ofstream(root / "mcpp.toml") << std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n", name); + for (const auto& [file, text] : files) std::ofstream(root / file) << text << '\n'; + return root; +} + +} // namespace + +int main() { + const auto mock = testsupport::built("mcpp-ide-mock-mcpp"); + CHECK(!mock.empty()); + const auto base = std::filesystem::temp_directory_path() / std::format("mcppide-probe-{}", std::chrono::steady_clock::now().time_since_epoch().count()); + const auto old = project(base, "old", {{".mock-mcpp-version", "2026.10.29.1"}}); + const auto sets = project(base, "sets", {{".mock-mcpp-version", "2026.10.30.1"}}); + const auto stated = project(base, "stated", {{".mock-mcpp-version", "2026.10.31.1"}, + {".mock-mcpp-features", "target-sets,pkg-format-list,polyglot"}}); + const auto missing = project(base, "missing", {{".mock-mcpp-missing", "2026.8.17.1"}}); + + Wire wire; + server::Session session(wire.server); + session.install(); + auto result = [&](std::string_view method, Json params) { + auto answer = wire.call(method, std::move(params)); + if (!answer.contains("result")) std::println(std::cerr, "{}: {}", method, answer.dump()); + CHECK(answer.contains("result")); + return answer["result"]; + }; + result("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "vscode"}, {"trusted", true}, + {"settings", {{"mcpp.path", mock.generic_string()}}}, + {"workspaceFolders", Json::array({base.generic_string()})}}); + + // Before features: the frozen table, by the version the project's mcpp answers. + auto a = result("project/mcpp", Json{{"root", old.generic_string()}}); + CHECK(a["state"] == "ok" && a["version"] == "2026.10.29.1"); + CHECK(a["features"].empty() && a["featuresFrozen"] == true); + CHECK(a["fromSetting"] == true); + auto b = result("project/mcpp", Json{{"root", sets.generic_string()}}); + CHECK(b["version"] == "2026.10.30.1" && b["featuresFrozen"] == true); + CHECK(b["features"] == Json::array({"target-sets", "pkg-format-list"})); + // Stated: taken at mcpp's word, a name the table does not hold included. + auto c = result("project/mcpp", Json{{"root", stated.generic_string()}}); + CHECK(c["version"] == "2026.10.31.1" && c["featuresFrozen"] == false); + CHECK(c["features"] == Json::array({"target-sets", "pkg-format-list", "polyglot"})); + // A pinned version that is not installed: the shim's words and its install command. + auto d = result("project/mcpp", Json{{"root", missing.generic_string()}}); + CHECK(d["state"] == "pinned-missing"); + CHECK(d["install"] == "xlings install mcpp@2026.8.17.1"); + CHECK(d["message"].get().find("not found") != std::string::npos); + + // Why a feature is not offered: the party, the capability and the version that has it. + auto why = result("capabilities/explain", Json{{"root", old.generic_string()}}); + bool explained = false; + for (const auto& f : why["features"]) + if (f["id"] == "targets.sets") { + CHECK(f["available"] == false); + CHECK(f["reason"]["source"] == "mcpp" && f["reason"]["need"] == "2026.10.30.1"); + explained = true; + } + CHECK(explained); + + // No run carries another project's pin: every run of mcpp removes XLINGS_PROJECT_DIR, which an + // editor started from another project's terminal inherits (mcpp docs/50). The removal is the + // spawn's own instruction; a variable set in this process now would reach no child, since a + // program's environment is the one it was started with. + { + mcpp::Client client(mock, mcpp::Protocol{}); + const auto o = client.spawn_options(mcpp::RunOptions{}); + CHECK(std::ranges::any_of(o.env, [](const auto& kv) { return kv.first == "XLINGS_PROJECT_DIR" && !kv.second; })); + } + + // A job's envelope names another version: the project is asked again, and the editor told. + std::ofstream(old / ".mock-mcpp-version") << "2026.10.31.1\n"; + auto job = result("job/start", Json{{"kind", "build"}, {"root", old.generic_string()}}); + for (int i = 0; i < 2000 && !wire.saw("project/mcppChanged"); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK(wire.saw("project/mcppChanged")); + auto again = result("project/mcpp", Json{{"root", old.generic_string()}}); + CHECK(again["version"] == "2026.10.31.1"); + (void)job; + + // The report names every project asked, with no secret in it. + auto report = result("report/collect", Json::object()); + CHECK(report["kind"] == "mcpp-ide.report" && report["projects"].size() >= 4); + + // An untrusted workspace: the pinned program is not run. + Wire untrusted; + server::Session second(untrusted.server); + second.install(); + auto answer = untrusted.call("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "vscode"}, {"trusted", false}, + {"settings", {{"mcpp.path", mock.generic_string()}}}, + {"workspaceFolders", Json::array({base.generic_string()})}}); + CHECK(answer.contains("result")); + auto e = untrusted.call("project/mcpp", Json{{"root", missing.generic_string()}}); + CHECK(e.contains("result") && e["result"]["state"] == "not-probed"); + + std::error_code ignored; + std::filesystem::remove_all(base, ignored); + std::println("test_probe: ok"); + return 0; +} From a9c0b15d74186e7e65735bca42346012afcec4a0 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 20:49:51 +0900 Subject: [PATCH 090/119] =?UTF-8?q?checkpoint=202:=20what=20mcppls=20says?= =?UTF-8?q?=20about=20the=20workspace,=20from=20mcppls=20status,=20and=20i?= =?UTF-8?q?ts=20actions=20under=20each=20editor's=20id=20(mcpp=20design=20?= =?UTF-8?q?2026-10-30=20part=202=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend's languageService/status runs mcppls status (0.0.20 and later) or reads the table frozen at 0.0.18 and 0.0.19; a different answer is sent once (languageService/changed). The command table's forwards name mcppls's action ids, resolved per editor by commands/list: CLion runs the mcppls plugin's action, Neovim mcppls-nvim's command, VS Code the command and arguments mcppls names, with mcpp-ide's confirmation. VS Code no longer reads the mcppls extension's API object; a missing kit is a row that installs it; auto refresh after a build forwards nothing. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 12 + mcpp.toml | 5 + .../mcppcommunity/mcppclion/McppSession.kt | 19 +- .../mcppclion/actions/Dispatch.kt | 22 ++ .../mcppcommunity/mcppclion/BackendTest.kt | 8 +- mide-plugins/nvim/lua/mcpp/commands.lua | 19 +- mide-plugins/nvim/lua/mcpp/init.lua | 1 + mide-plugins/nvim/lua/mcpp/session.lua | 11 + mide-plugins/nvim/tests/smoke.lua | 10 +- mide-plugins/vscode/data/i18n/zh-cn.json | 10 +- mide-plugins/vscode/docs/settings.md | 16 +- mide-plugins/vscode/package.json | 5 + mide-plugins/vscode/package.nls.json | 1 + mide-plugins/vscode/package.nls.zh-cn.json | 1 + mide-plugins/vscode/src/commands/ids.ts | 1 + mide-plugins/vscode/src/extension.ts | 82 ++---- mide-plugins/vscode/src/ide/protocol.ts | 19 ++ mide-plugins/vscode/src/ide/session.ts | 50 +++- mide-plugins/vscode/src/mcppls/bridge.ts | 27 +- .../vscode/src/mcppls/capabilities.ts | 139 ++++------ mide-plugins/vscode/src/mcppls/contract.ts | 153 +++-------- .../vscode/src/mcppls/otherCppFeatures.ts | 58 +---- mide-plugins/vscode/src/mcppls/state.ts | 240 +++++------------- mide-plugins/vscode/src/mcppls/stateSource.ts | 56 ++-- .../vscode/src/views/languageServerView.ts | 60 +++-- mide-plugins/vscode/src/views/models.ts | 24 +- mide-plugins/vscode/test/commands/ids.test.ts | 1 + .../vscode/test/mcppls/bridge.test.ts | 50 +++- .../vscode/test/mcppls/capabilities.test.ts | 178 ++++++------- .../vscode/test/mcppls/contract.test.ts | 26 +- .../test/mcppls/otherCppFeatures.test.ts | 40 +-- mide-plugins/vscode/test/mcppls/state.test.ts | 177 ++++--------- .../vscode/test/mcppls/stateSource.test.ts | 42 ++- protocol/protocol.schema.json | 28 +- spec/commands.json | 43 ++-- spec/i18n/manifest.en.json | 1 + spec/i18n/manifest.zh-cn.json | 1 + src/bin/mockmcppls.cpp | 72 ++++++ src/languageservice.cppm | 111 ++++++++ src/server.cppm | 61 ++++- src/spec.cppm | 6 +- tests/test_mcppls_status.cpp | 133 ++++++++++ 42 files changed, 1087 insertions(+), 932 deletions(-) create mode 100644 src/bin/mockmcppls.cpp create mode 100644 tests/test_mcppls_status.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 1065d3c..432459f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,18 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 person names. `mcpp.backend.logLevel` decides what the backend writes to the editor's log. VS Code's status item names the project's mcpp version, and its tooltip the program and whether `mcpp.path` chose it. +- **What mcppls says about the workspace (§5).** The backend's `languageService/status` runs + `mcppls status --root --format json` (mcppls 0.0.20 and later, bounded at five seconds) and + returns its capabilities, the workspace's state, engine and kit, the servers serving it, and + mcppls's actions under each editor's own id; for mcppls 0.0.18 and 0.0.19 the actions come from a + table frozen at those releases. A different answer is sent once (`languageService/changed`). The + command table's forwards name mcppls's action ids, and `commands/list` gives each editor the id + mcppls states for it: CLion runs the mcppls plugin's action (and hides a row it does not have), + Neovim mcppls-nvim's command. VS Code reads the state and the commands from this answer instead of + the mcppls extension's API object and keeps only the confirmation each action needs; the state is + asked again after a job, on focus, when the project view becomes visible and, while it is visible, + at most every 30 seconds. A missing semantic kit is a row with **C++ Modules: Install the Semantic + Kit**. After a build nothing is forwarded by default (`auto`): mcppls reads mcpp's result record. - **mcpp-vscode 1.1.0 is mcpp-vscode 0.9.0 over the backend.** 0.9.0's extension is imported with its history (`mide-plugins/vscode`); every view, page, command, menu, setting and string a user sees is 0.9.0's. What 1.0 added stays, in 0.9.0's style: debugging (`mcpp.debug`, the `mcpp` diff --git a/mcpp.toml b/mcpp.toml index 7fb757d..e946e25 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -57,6 +57,11 @@ main = "src/main.cpp" kind = "bin" main = "src/bin/mockmcpp.cpp" +# Stands in for mcppls in the tests: its version and `mcppls status`. +[targets.mcpp-ide-mock-mcppls] +kind = "bin" +main = "src/bin/mockmcppls.cpp" + # The repository's tools: fixture capture, the editors' declarations from spec/, the i18n check. [targets.mcpp-ide-dev] kind = "bin" diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index e734947..16c3372 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -43,12 +43,17 @@ class McppSession(private val project: Project) : Disposable { if (key == "mcpp.ide.backendPath") restart() else pushSettings() }) on("manifest/changed") { p -> p.asJsonObject["root"]?.asString?.let { refreshTests(it) } } + // mcppls answered differently (another version, other actions): the commands are asked again, + // since mcppls's actions are offered under the ids it states (§5.3). + on("languageService/changed") { refreshCommands() } // The IDE regained focus: a `xlings use` or a changed pin happened outside it (§4.2). com.intellij.openapi.application.ApplicationManager.getApplication().messageBus.connect(this).subscribe( com.intellij.openapi.application.ApplicationActivationListener.TOPIC, object : com.intellij.openapi.application.ApplicationActivationListener { override fun applicationActivated(ideFrame: com.intellij.openapi.wm.IdeFrame) { - if (backend?.running == true) backend?.request("project/refresh", JsonObject()) + if (backend?.running != true) return + backend?.request("project/refresh", JsonObject()) + languageServiceStatus() } }) } @@ -155,6 +160,18 @@ class McppSession(private val project: Project) : Disposable { n.notify(project) } + /** What mcppls says about the project (§5.2); the backend tells `languageService/changed` when it differs. */ + fun languageServiceStatus(): CompletableFuture = + request("languageService/status", JsonObject().apply { + add("candidates", mcpplsCandidates()) + root?.let { addProperty("root", it) } + }) + + private fun refreshCommands() { + backend?.request("commands/list", JsonObject().apply { addProperty("editor", "jetbrains") }) + ?.thenAccept { table -> commands = table.asJsonObject.getAsJsonArray("commands").map { it.asJsonObject } } + } + fun restart() { val old = backend backend = null diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt index bd82a44..5b760aa 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt @@ -14,6 +14,8 @@ import com.intellij.ide.impl.ProjectUtil import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager @@ -42,6 +44,17 @@ class McppCommandAction : DumbAwareAction() { e.presentation.isEnabledAndVisible = project != null if (project == null) return val id = e.actionManager.getId(this) ?: return + // mcppls's actions are offered where mcppls states a JetBrains id for them and its plugin has + // the action (§5.3); the table is the backend's, asked when the session started. + val session = McppSession.get(project) + val row = session.commands.firstOrNull { it["id"].asString == id } + if (row != null && row["action"].asJsonObject["kind"]?.asString == "forward") { + val target = row["action"].asJsonObject["command"]?.asString + e.presentation.isEnabledAndVisible = target != null && e.actionManager.getAction(target) != null + return + } + // A command the backend's table does not hold for this IDE is not offered. + if (row == null && session.commands.isNotEmpty()) { e.presentation.isEnabledAndVisible = false; return } val needsProject = id in setOf("mcpp.build", "mcpp.run", "mcpp.debug", "mcpp.test", "mcpp.clean", "mcpp.pack", "mcpp.cleanProjectArtifacts") if (needsProject) e.presentation.isEnabled = McppSession.get(project).root != null } @@ -113,6 +126,7 @@ object Dispatch { ApplicationManager.getApplication().invokeLater { Messages.showInfoMessage(project, text, t("Environment Self-check")) } } "languageService" -> languageService(project) + "forward" -> forward(project, action) "report" -> exportReport(project) "rpc" -> job(project, action["method"].asString, JsonObject().apply { session.root?.let { addProperty("root", it) } }) else -> session.notify(t("This command is not available in this IDE."), NotificationType.INFORMATION) @@ -289,6 +303,14 @@ object Dispatch { } } + /** One of mcppls's actions, by the id of its CLion plugin's action that mcppls states. */ + private fun forward(project: Project, action: JsonObject) { + val session = McppSession.get(project) + val target = action["command"]?.asString?.let { ActionManager.getInstance().getAction(it) } + if (target == null) { session.notify(session.t("This command is not available in this IDE."), NotificationType.INFORMATION); return } + ActionManager.getInstance().tryToExecute(target, null, null, ActionPlaces.UNKNOWN, true) + } + fun languageServiceAction(project: Project, id: String) { // The mcppls CLion plugin is installed and updated where every plugin is: Settings | Plugins, // whose Marketplace tab gives this system's package, the server inside it. diff --git a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt index 416acdf..aa9f28d 100644 --- a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt +++ b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt @@ -1,6 +1,6 @@ // The wire and the backend, without an IDE: frames reassemble whatever the chunking; a session over // the mock mcpp finds the project, runs a build to `ok`, and lists the commands this editor offers -// (no forward to mcppls's VS Code commands among them). The session test runs when the build passes +// (a forward to one of mcppls's actions only under the JetBrains id mcppls states). The session test runs when the build passes // the backend and the mock mcpp (MCPP_IDE_BACKEND, MCPP_IDE_MOCK_MCPP). package io.github.mcppcommunity.mcppclion @@ -90,7 +90,8 @@ class BackendTest { val commands = b.request("commands/list", JsonObject().apply { addProperty("editor", "jetbrains") }).get(30, TimeUnit.SECONDS) .asJsonObject.getAsJsonArray("commands").map { it.asJsonObject } assertTrue(commands.any { it["id"].asString == "mcpp.build" }) - assertTrue(commands.none { it["action"].asJsonObject["kind"].asString == "forward" }) + assertTrue(commands.filter { it["action"].asJsonObject["kind"].asString == "forward" } + .all { it["action"].asJsonObject["command"]?.asString?.startsWith("community.mcpp.mcppls.") == true }) } root.deleteRecursively() } @@ -100,7 +101,8 @@ class BackendTest { val xml = javaClass.getResource("/META-INF/mcpp-actions.xml")?.readText() assumeTrue("the generated resources are on the test classpath", xml != null) assertTrue(xml!!.contains("""id="mcpp.build"""")) - assertTrue(!xml.contains("mcpp.languageServer.restart")) + // mcppls's actions are declared; each is shown where mcppls states a JetBrains id for it. + assertTrue(xml.contains("mcpp.languageServer.restart")) val bundle = javaClass.getResource("/messages/McppClionBundle.properties")!!.readText() assertTrue(bundle.contains("action.mcpp.build.text=Build")) } diff --git a/mide-plugins/nvim/lua/mcpp/commands.lua b/mide-plugins/nvim/lua/mcpp/commands.lua index c645315..3f47dc0 100644 --- a/mide-plugins/nvim/lua/mcpp/commands.lua +++ b/mide-plugins/nvim/lua/mcpp/commands.lua @@ -378,13 +378,22 @@ function M.language_service(s) end function actions.languageService() - local s, err = session.call('languageService/status', { candidates = session.mcppls_candidates() }) + local s, err = session.call('languageService/status', { candidates = session.mcppls_candidates(), root = M.root() }) if err then return report(err) end M.language_service(s) end +-- One of mcppls's actions, by the user command of mcppls-nvim that mcppls states (§5.3). +function actions.forward(row) + local command = row.action.command + if not command or vim.fn.exists(':' .. command) ~= 2 then + return vim.notify(t('This command is not available in this IDE.'), vim.log.levels.INFO) + end + vim.cmd(command) +end + function actions.rpc(row, _, root) views.job(row.action.method, { root = root }, row.subcommand) end @@ -395,13 +404,15 @@ end -- ── the table ───────────────────────────────────────────────────────────────────────────────── --- The rows Neovim offers now: not deprecated, not hidden from a palette, and the debugger only when --- nvim-dap is there. +-- The rows Neovim offers now: not deprecated, not hidden from a palette, the debugger only when +-- nvim-dap is there, and mcppls's actions only where mcppls-nvim defines the command. function M.offered() local out = {} for _, row in ipairs(session.state.commands) do local debug = row.action and row.action.kind == 'debug' - if not row.deprecated and row.palette ~= false and (not debug or dap()) then + local forward = row.action and row.action.kind == 'forward' + if not row.deprecated and row.palette ~= false and (not debug or dap()) + and (not forward or vim.fn.exists(':' .. (row.action.command or '')) == 2) then out[#out + 1] = row end end diff --git a/mide-plugins/nvim/lua/mcpp/init.lua b/mide-plugins/nvim/lua/mcpp/init.lua index c55081b..9edcfdb 100644 --- a/mide-plugins/nvim/lua/mcpp/init.lua +++ b/mide-plugins/nvim/lua/mcpp/init.lua @@ -129,6 +129,7 @@ function M.setup(opts) callback = function() if session.state.ready then session.request('project/refresh', {}, function() end) + session.request('languageService/status', { candidates = session.mcppls_candidates(), root = commands.root() }, function() end) end end, }) diff --git a/mide-plugins/nvim/lua/mcpp/session.lua b/mide-plugins/nvim/lua/mcpp/session.lua index 15ece58..23fff2a 100644 --- a/mide-plugins/nvim/lua/mcpp/session.lua +++ b/mide-plugins/nvim/lua/mcpp/session.lua @@ -384,6 +384,17 @@ local function initialize() state.strings = M.call('i18n/bundle', {}) or {} local listed = M.call('commands/list', { editor = 'nvim' }) state.commands = listed and listed.commands or {} + -- mcppls answered differently (another version, other actions): the commands are asked again, + -- since mcppls's actions are offered under the ids it states (§5.3). + if not state.listing then + state.listing = M.on('languageService/changed', function() + M.request('commands/list', { editor = 'nvim' }, function(err, again) + if not err and again and again.commands then + state.commands = again.commands + end + end) + end) + end local resolved = M.call('settings/update', settings_params()) if resolved then state.values = resolved.values or {} diff --git a/mide-plugins/nvim/tests/smoke.lua b/mide-plugins/nvim/tests/smoke.lua index 4ca1fa5..5b06ee8 100644 --- a/mide-plugins/nvim/tests/smoke.lua +++ b/mide-plugins/nvim/tests/smoke.lua @@ -145,8 +145,14 @@ for _, row in ipairs(session.state.commands) do end check('the command table arrives, each row with its subcommand', #session.state.commands > 20 and by.build ~= nil, #session.state.commands) check('cancel and problems are Neovim\'s, settings is not', by.cancel and by.cancel.id == 'mcpp.cancelJob' and by.problems ~= nil and by.openSettings == nil) -local forwards = vim.tbl_filter(function(r) return r.action.kind == 'forward' or r.action.kind == 'editor' end, session.state.commands) -check('no forward to mcppls\'s commands (mcppls-nvim has its own)', #forwards == 0, #forwards) +local editors = vim.tbl_filter(function(r) return r.action.kind == 'editor' end, session.state.commands) +check('no command of another editor', #editors == 0, #editors) +-- mcppls's actions only under the mcppls-nvim command mcppls states; offered only where it is defined. +local forwards = vim.tbl_filter(function(r) return r.action.kind == 'forward' end, session.state.commands) +check('a forward names mcppls-nvim\'s command', vim.tbl_isempty(vim.tbl_filter(function(r) + return not (r.action.command or ''):match('^Mcppls') end, forwards)), vim.inspect(forwards)) +check('a forward whose command is not defined is not offered', vim.tbl_isempty(vim.tbl_filter(function(r) + return r.action.kind == 'forward' and vim.fn.exists(':' .. r.action.command) ~= 2 end, require('mcpp.commands').offered()))) -- ── completion ──────────────────────────────────────────────────────────────────────────────── local all = vim.fn.getcompletion('Mcpp ', 'cmdline') diff --git a/mide-plugins/vscode/data/i18n/zh-cn.json b/mide-plugins/vscode/data/i18n/zh-cn.json index a81d4e1..61a7544 100644 --- a/mide-plugins/vscode/data/i18n/zh-cn.json +++ b/mide-plugins/vscode/data/i18n/zh-cn.json @@ -642,5 +642,13 @@ "The IDE report is written to {0}": "IDE 报告已写入 {0}", "Restart the backend": "重新启动后端", "Export IDE Report": "导出 IDE 报告", - "The mcpp-ide backend ended four times in three minutes; build, run, test and the project pages go on without it.": "mcpp-ide 后端在三分钟内结束了四次;构建、运行、测试与项目页面在没有它的情况下继续工作。" + "The mcpp-ide backend ended four times in three minutes; build, run, test and the project pages go on without it.": "mcpp-ide 后端在三分钟内结束了四次;构建、运行、测试与项目页面在没有它的情况下继续工作。", + "Starting": "启动中", + "Stopped": "已停止", + "Not serving this workspace": "未服务此工作区", + "The semantic kit is not installed": "语义套件未安装", + "Files are read with the openkal kit C++ Modules carries. Installing the target's kit gives its own standard library and headers.": "文件以 C++ Modules 自带的 openkal 套件解析。安装目标的套件后使用其自身的标准库与头文件。", + "Install the semantic kit": "安装语义套件", + "This mcppls does not install kits; files are read with the openkal kit it carries.": "此 mcppls 不安装套件;文件以其自带的 openkal 套件解析。", + "The C++ Modules server installs the target's semantic kit with xlings, in the background.": "C++ Modules 服务器在后台用 xlings 安装目标的语义套件。" } diff --git a/mide-plugins/vscode/docs/settings.md b/mide-plugins/vscode/docs/settings.md index 0107d95..f7543de 100644 --- a/mide-plugins/vscode/docs/settings.md +++ b/mide-plugins/vscode/docs/settings.md @@ -2,7 +2,7 @@ -75 settings, in 10 groups. All of them are read by the +76 settings, in 10 groups. All of them are read by the **mcpp** extension only. `mcppls.*` belongs to the C++ Modules extension and is never written by this one; the configuration panel says so on every screen. @@ -1104,6 +1104,20 @@ What mcpp may not do while the workspace is not trusted (mcpp --deny): a run tha | Applies | Takes effect immediately | | Since | 1.0.0 | +### `mcpp.backend.logLevel` — advanced + +What the mcpp-ide backend writes to the editor's log: errors only, warnings, what it does (the default), or every run of mcpp and mcppls with its arguments. + +mcpp-ide 后端写入编辑器日志的内容:只写错误、写警告、写它所做的事(缺省),或写每一次运行 mcpp 与 mcppls 及其参数。 + +| | | +|---|---| +| Type | `string`: `error` \| `warning` \| `info` \| `debug` | +| Default | `"info"` | +| Scope | per window | +| Applies | Takes effect immediately | +| Since | 1.1.0 | + ## Language `mcpp.ui.language` overrides the language of **runtime messages and this extension's diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json index 0d69c6f..7b59366 100644 --- a/mide-plugins/vscode/package.json +++ b/mide-plugins/vscode/package.json @@ -360,6 +360,11 @@ "title": "%command.mcpp.languageServer.installTools.title%", "category": "%category%" }, + { + "command": "mcpp.languageServer.installKit", + "title": "%command.mcpp.languageServer.installKit.title%", + "category": "%category%" + }, { "command": "mcpp.languageServer.reviewChanges", "title": "%command.mcpp.languageServer.reviewChanges.title%", diff --git a/mide-plugins/vscode/package.nls.json b/mide-plugins/vscode/package.nls.json index 2b304ad..bee4403 100644 --- a/mide-plugins/vscode/package.nls.json +++ b/mide-plugins/vscode/package.nls.json @@ -16,6 +16,7 @@ "command.mcpp.languageServer.collectReport.title": "C++ Modules: Collect a Diagnostic Report", "command.mcpp.languageServer.exportDiagnosticBundle.title": "C++ Modules: Export a Diagnostic Bundle", "command.mcpp.languageServer.installTools.title": "C++ Modules: Install Command Line Tools", + "command.mcpp.languageServer.installKit.title": "C++ Modules: Install the Semantic Kit", "command.mcpp.languageServer.manageConflicts.title": "C++ Modules: Turn Off Other C++ Language Services", "command.mcpp.languageServer.openLogFolder.title": "Open the C++ Modules log folder", "command.mcpp.languageServer.refreshState.title": "C++ Modules: Refresh This View", diff --git a/mide-plugins/vscode/package.nls.zh-cn.json b/mide-plugins/vscode/package.nls.zh-cn.json index e22b4f7..dc5c1b7 100644 --- a/mide-plugins/vscode/package.nls.zh-cn.json +++ b/mide-plugins/vscode/package.nls.zh-cn.json @@ -16,6 +16,7 @@ "command.mcpp.languageServer.collectReport.title": "C++ Modules: 收集诊断报告", "command.mcpp.languageServer.exportDiagnosticBundle.title": "C++ Modules: 导出诊断包", "command.mcpp.languageServer.installTools.title": "C++ Modules: 安装命令行工具", + "command.mcpp.languageServer.installKit.title": "C++ Modules: 安装语义套件", "command.mcpp.languageServer.manageConflicts.title": "C++ Modules: 关闭其他 C++ 语言服务", "command.mcpp.languageServer.openLogFolder.title": "打开 C++ Modules 日志目录", "command.mcpp.languageServer.refreshState.title": "C++ Modules: 刷新此视图", diff --git a/mide-plugins/vscode/src/commands/ids.ts b/mide-plugins/vscode/src/commands/ids.ts index 0b8e400..0ef1c3f 100644 --- a/mide-plugins/vscode/src/commands/ids.ts +++ b/mide-plugins/vscode/src/commands/ids.ts @@ -61,6 +61,7 @@ export const LANGUAGE_SERVER_COMMANDS = { restoreConflicts: "mcpp.languageServer.restoreConflicts", toggleInWorkspace: "mcpp.languageServer.toggleInWorkspace", installTools: "mcpp.languageServer.installTools", + installKit: "mcpp.languageServer.installKit", reviewChanges: "mcpp.languageServer.reviewChanges", } as const; diff --git a/mide-plugins/vscode/src/extension.ts b/mide-plugins/vscode/src/extension.ts index 04122b6..9e9ce7b 100644 --- a/mide-plugins/vscode/src/extension.ts +++ b/mide-plugins/vscode/src/extension.ts @@ -16,8 +16,7 @@ import { type LanguageServerBridge, type LanguageServerCommandResult, } from "./mcppls/bridge"; -import { compareVersions } from "./library/xpkg"; -import { CAPABILITIES, MCPPLS_EXTENSION_ID, VERIFIED_MCPPLS_MINIMUM, VERIFIED_MCPPLS_RANGE } from "./mcppls/contract"; +import { CAPABILITIES, MCPPLS_EXTENSION_ID } from "./mcppls/contract"; import { formatResult } from "./mcppls/messages"; import { describeState } from "./mcppls/state"; import { @@ -51,23 +50,6 @@ import { readCacheSnapshot } from "./cache/cacheView"; import { registerPages } from "./pages"; import { registerLanguageServerCommands } from "./views/languageServerView"; -/** - * The commands an extension declares in its own `package.json`, read without - * activating it. `undefined` means "no static information", which the capability - * probe treats as "assume it works until a call says otherwise". - */ -function declaredCommandsOf(id: string): readonly string[] | undefined { - const extension = vscode.extensions.getExtension(id); - const commands = (extension?.packageJSON as { contributes?: { commands?: Array<{ command?: string }> } }) - ?.contributes?.commands; - if (!Array.isArray(commands)) { - return undefined; - } - return commands - .map((entry) => entry.command) - .filter((command): command is string => typeof command === "string"); -} - /** When the language service was last asked to reload, for the self-check. */ let lastRefresh: { at: string; state: string; command?: string } | undefined; @@ -205,9 +187,13 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi applyLanguagePreference(); extensionContext.subscriptions.push(onConfigurationChanged(applyLanguagePreference)); + // The mcpp-ide backend (`mcpp-ide serve`, carried as one ok-image bundle and every system's + // runner) answers the views' data, mcppls's account of the workspace among them; it starts in the + // background below, and nothing waits for it. + const session = new Session(extensionContext, output); const bridge = createLanguageServerBridge({ extensionInstalled: (id) => vscode.extensions.getExtension(id) !== undefined, - declaredCommands: declaredCommandsOf, + serverActions: () => session.languageService?.serverActions, activateExtension: async (id) => { await vscode.extensions.getExtension(id)?.activate(); }, @@ -253,13 +239,19 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi lastRefresh = { at: new Date().toISOString(), state: "skipped (mcpp.languageService.refreshAfterBuild = off)" }; return; } + // `auto`: mcppls 0.0.18 and later read mcpp's result record after any build and refresh + // themselves, so nothing is forwarded (the backend reports an older mcppls as outdated). const result = - mode === "restart" ? await bridge.restartLanguageServer() : await bridge.refreshLanguageServerAfterBuild(); - lastRefresh = { at: new Date().toISOString(), state: result.state, command: result.command }; - resultText(output, result); - if (completion.state === "succeeded" && result.state === "completed") { + mode === "restart" ? await bridge.restartLanguageServer() + : mode === "reload" ? await bridge.refreshLanguageServerAfterBuild() + : undefined; + lastRefresh = result === undefined + ? { at: new Date().toISOString(), state: "mcppls reads mcpp's result record" } + : { at: new Date().toISOString(), state: result.state, command: result.command }; + if (result !== undefined) resultText(output, result); + if (completion.state === "succeeded" && (result === undefined || result.state === "completed")) { await vscode.window.showInformationMessage(taskCompletionText(completion)); - } else if (completion.state === "failed" && result.state === "completed") { + } else if (completion.state === "failed" && (result === undefined || result.state === "completed")) { await vscode.window.showWarningMessage(taskCompletionText(completion)); } }; @@ -272,7 +264,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi afterProjectTask, isTrusted: () => vscode.workspace.isTrusted, languageServerSummary: () => { - const view = readLanguageServerState(); + const view = readLanguageServerState(session.languageService); return view.available ? view.state : undefined; }, projectMcpp: () => { @@ -282,10 +274,8 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi }); // ── the mcpp-ide backend ───────────────────────────────────────────────── - // The backend (`mcpp-ide serve`, carried as one ok-image bundle and every system's runner) answers - // the views' data and the flows 0.9.0 did not have: debugging, the Testing view, the targets and - // target sets, packing. It starts in the background; nothing above waits for it. - const session = new Session(extensionContext, output); + // The session (created above) also answers the flows 0.9.0 did not have: debugging, the Testing + // view, the targets and target sets, packing. const ideProjects: IdeProjects = { current: () => findCurrentProject(), all: () => session.info?.projects ?? [], @@ -380,7 +370,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi // The C++ Modules block of the project view reads mcppls through the bridge; the views themselves // (the project tree, the libraries, the cache, the settings) are src/pages.ts's, which the // workbench mcpp-clion runs registers in the same way. - const languageService = registerLanguageServerCommands(extensionContext, { bridge, output }); + const languageService = registerLanguageServerCommands(extensionContext, { bridge, output, session }); registerPages(extensionContext, { output, mcppExecutable: (project) => cliController.mcppExecutable(project), @@ -431,7 +421,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi // Environment self-check: one copyable snapshot of everything a bug report needs. vscode.commands.registerCommand(TOOL_COMMANDS.selfCheck, runGuarded(async () => { - await showSelfCheck(output, cliController, bridge, extensionContext.extension.packageJSON.version); + await showSelfCheck(output, cliController, bridge, session, extensionContext.extension.packageJSON.version); })), vscode.workspace.onDidGrantWorkspaceTrust(() => cliController.refreshStatus()), @@ -461,10 +451,9 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi ); void offerSettingRenames(extensionContext); - void noteUnverifiedLanguageService(output); if (read("mcpp.diagnostics.selfCheckOnStartup")) { - void showSelfCheck(output, cliController, bridge, extensionContext.extension.packageJSON.version); + void showSelfCheck(output, cliController, bridge, session, extensionContext.extension.packageJSON.version); } extensionContext.subscriptions.push( @@ -501,28 +490,6 @@ async function offerSettingRenames(context: vscode.ExtensionContext): Promise { - const version = languageServerVersion(); - if (version === undefined) { - return; - } - // The same comparator the version matrix uses, against the contract's own - // minimum — the old hand-rolled comparison had dead `< 0` branches and a - // second, drifting copy of the floor (external review P1-3). - const below = compareVersions(version, VERIFIED_MCPPLS_MINIMUM) < 0; - if (below) { - output.appendLine( - t("{0} {1} is older than the verified range ({2}); the capability probe will hide what it cannot do.", MCPPLS_EXTENSION_ID, version, VERIFIED_MCPPLS_RANGE), - ); - } -} - /** * The self-check gathers, never guesses: anything it cannot read is reported as * unknown rather than left blank. It is the first thing to ask for in a bug @@ -532,6 +499,7 @@ async function showSelfCheck( output: vscode.OutputChannel, cliController: McppCliController, bridge: LanguageServerBridge, + session: Session, extensionVersion: unknown, ): Promise { const project = findCurrentProject(); @@ -544,7 +512,7 @@ async function showSelfCheck( timeoutMs: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined, }); const info = probeResult !== undefined && probeResult.exitCode === 0 ? parseProtocolInfo(probeResult.stdout) : undefined; - const state = readLanguageServerState(); + const state = readLanguageServerState(await session.refreshLanguageService()); const capabilities = CAPABILITIES.map((entry) => ({ key: entry.key, state: bridge.isGone(entry.key) ? "missing" : bridge.isUnconfirmed(entry.key) ? "unconfirmed" : "available", diff --git a/mide-plugins/vscode/src/ide/protocol.ts b/mide-plugins/vscode/src/ide/protocol.ts index b56e4ce..a57372e 100644 --- a/mide-plugins/vscode/src/ide/protocol.ts +++ b/mide-plugins/vscode/src/ide/protocol.ts @@ -14,6 +14,17 @@ export interface Check { message: string; } +/** One of mcppls's actions under each editor's own id (mcppls S3 section 8). */ +export interface ServerAction { + id: string; + title: string; + vscode?: string; + vscodeArguments?: unknown[]; + jetbrains?: string; + nvim?: string; + command?: string; +} + export interface LanguageServiceStatus { state: "ok" | "outdated" | "missing" | "unreadable"; program: string; @@ -22,6 +33,14 @@ export interface LanguageServiceStatus { required: string; message: string; actions: { id: "install" | "update"; title: string }[]; + /** What mcppls says about the workspace: `mcppls status` (0.0.20 and later) or the frozen table. */ + root?: string; + statusSource?: "status" | "frozen" | "none"; + statusError?: string; + capabilities?: string[]; + serverActions?: ServerAction[]; + workspace?: Record | null; + instances?: unknown[]; } /** `project/mcpp` (design 2026-10-30 part 2 §4.4): the project's mcpp, asked in the project. */ diff --git a/mide-plugins/vscode/src/ide/session.ts b/mide-plugins/vscode/src/ide/session.ts index 8329218..5a4bdc9 100644 --- a/mide-plugins/vscode/src/ide/session.ts +++ b/mide-plugins/vscode/src/ide/session.ts @@ -11,13 +11,15 @@ import { Backend, BackendError } from "./backend/client"; import { locateBackend, type Located } from "./backend/locate"; import { mcpplsCandidates } from "./backend/mcppls"; import { MCPPLS_EXTENSION } from "./mapping"; -import type { CommandEntry, InitializeResult } from "./protocol"; +import type { CommandEntry, InitializeResult, LanguageServiceStatus } from "./protocol"; export class Session implements vscode.Disposable { private backend: Backend | undefined; private located: Located | undefined; private initResult: InitializeResult | undefined; private commandTable: CommandEntry[] = []; + private languageServiceNow: LanguageServiceStatus | undefined; + private languageServiceAsking: Promise | undefined; /** When the backend ended, within the last three minutes (§3.1). */ private exits: number[] = []; private degradedNow = false; @@ -32,9 +34,12 @@ export class Session implements vscode.Disposable { this.disposables.push( this.onDidStart, this.onDidDegrade, - // The editor regained focus: a `xlings use` or a changed pin happened outside it (§4.2). + // The editor regained focus: a `xlings use` or a changed pin happened outside it (§4.2), or a + // server started or stopped (§5.2). vscode.window.onDidChangeWindowState((state) => { - if (state.focused && this.backend?.running) void this.backend.request("project/refresh", {}).then(() => this.onDidRefresh.fire(), () => undefined); + if (!state.focused || !this.backend?.running) return; + void this.backend.request("project/refresh", {}).then(() => this.onDidRefresh.fire(), () => undefined); + void this.refreshLanguageService(); }), vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("mcpp.ide.backendPath")) { void this.restart(); return; } @@ -42,8 +47,41 @@ export class Session implements vscode.Disposable { }), vscode.workspace.onDidGrantWorkspaceTrust(() => void this.pushSettings()), vscode.workspace.onDidChangeWorkspaceFolders(() => void this.restart()), - vscode.extensions.onDidChange(() => void this.request("languageService/status", { candidates: this.mcpplsCandidates() }).catch(() => undefined)), + vscode.extensions.onDidChange(() => void this.refreshLanguageService()), + this.onDidChangeLanguageService, ); + // mcppls answered differently, or a job ended (a build changes what the server serves). + this.on("languageService/changed", () => void this.refreshLanguageService()); + this.on("job/finished", () => void this.refreshLanguageService()); + } + + /** What mcppls says about the workspace (§5.2), as last asked; `undefined` before the first answer. */ + get languageService(): LanguageServiceStatus | undefined { + return this.languageServiceNow; + } + + /** Fired when `languageService` was asked again. */ + readonly onDidChangeLanguageService = new vscode.EventEmitter(); + + /** + * Asks the backend which mcppls answers and what it says about the workspace: one question at a + * time, and none while the backend is not running (nothing is started to answer it). + */ + refreshLanguageService(root?: string): Promise { + if (!this.backend?.running) return Promise.resolve(this.languageServiceNow); + if (this.languageServiceAsking) return this.languageServiceAsking; + const params: Record = { candidates: this.mcpplsCandidates() }; + const folder = root ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (folder !== undefined) params.root = folder; + this.languageServiceAsking = this.backend + .request("languageService/status", params) + .then((status) => { + this.languageServiceNow = status; + this.onDidChangeLanguageService.fire(status); + return status; + }, () => this.languageServiceNow) + .finally(() => { this.languageServiceAsking = undefined; }); + return this.languageServiceAsking; } /** Fired after the projects' mcpp were forgotten, so a view asks again. */ @@ -130,6 +168,10 @@ export class Session implements vscode.Disposable { languageService: { candidates: this.mcpplsCandidates() }, }); this.commandTable = (await backend.request<{ commands: CommandEntry[] }>("commands/list", { editor: "vscode" })).commands; + if (this.initResult.languageService) { + this.languageServiceNow = this.initResult.languageService; + this.onDidChangeLanguageService.fire(this.initResult.languageService); + } for (const c of this.initResult.checks) if (c.status === "error") this.output.appendLine(`mcpp: ${c.message}`); this.onDidStart.fire(this.initResult); return true; diff --git a/mide-plugins/vscode/src/mcppls/bridge.ts b/mide-plugins/vscode/src/mcppls/bridge.ts index 8ff8369..9811692 100644 --- a/mide-plugins/vscode/src/mcppls/bridge.ts +++ b/mide-plugins/vscode/src/mcppls/bridge.ts @@ -1,10 +1,11 @@ /** * The one place this extension talks to the C++ Modules extension. * - * It forwards commands and nothing else: no LSP client, no reading of - * `mcppls.*` settings, no writing of them, no parsing of its logs. The - * capability table (`./contract.ts`) says what may be forwarded and how much - * confirmation each action needs; `./capabilities.ts` decides whether it exists. + * It forwards mcppls's actions and nothing else: no LSP client, no reading of + * `mcppls.*` settings, no writing of them, no parsing of its logs. mcppls says + * which actions exist and which command each one is (the backend's + * `languageService/status`); the capability table (`./contract.ts`) says how + * much confirmation each needs; `./capabilities.ts` puts the two together. * * Results are **structured, not phrased**: a caller that has a user in front of * it turns them into words through `./messages.ts`. That keeps this module free @@ -29,7 +30,7 @@ export interface LanguageServerBridge { invoke(key: string, ...args: unknown[]): Promise; /** True when the user should not be offered this capability. */ isGone(key: string): boolean; - /** True when it exists but the static read could not confirm it. */ + /** True when mcppls has not answered yet. */ isUnconfirmed(key: string): boolean; /** How much confirmation the action needs. */ dangerOf(key: string): "none" | "confirm" | "destructive"; @@ -47,21 +48,23 @@ export interface LanguageServerBridge { manageConflicts(restore?: boolean): Promise; toggleInWorkspace(enable: boolean): Promise; installCommandLineTools(): Promise; + installKit(): Promise; reviewChanges(clear?: boolean): Promise; } /** Methods that map one-to-one onto a capability, so the bridge stays a table. */ const FORWARDED = { - restartLanguageServer: "restartServer", + restartLanguageServer: "restart", restartEngine: "restartEngine", resetWorkspaceCache: "resetCache", selectContext: "selectContext", showModuleGraph: "moduleGraph", - showLanguageServerLogs: "logs", - collectReport: "report", - exportDiagnosticBundle: "diagnosticBundle", + showLanguageServerLogs: "showLog", + collectReport: "collectReport", + exportDiagnosticBundle: "exportDiagnostics", runBuildToolInTerminal: "runBuildTool", installCommandLineTools: "installTools", + installKit: "installKit", } as const; export function createLanguageServerBridge(environment: CapabilityEnvironment): LanguageServerBridge { @@ -80,7 +83,7 @@ export function createLanguageServerBridge(environment: CapabilityEnvironment): if (refreshInFlight !== undefined) { return refreshInFlight; } - refreshInFlight = invoke("refresh").finally(() => { + refreshInFlight = invoke("reload").finally(() => { refreshInFlight = undefined; }); return refreshInFlight; @@ -100,7 +103,7 @@ export function createLanguageServerBridge(environment: CapabilityEnvironment): ...forwarded, // The two commands whose *argument* selects the direction. manageConflicts: (restore = false) => invoke(restore ? "restoreOtherCppFeatures" : "turnOffOtherCppFeatures"), - toggleInWorkspace: (enable) => invoke("toggleInWorkspace", enable), - reviewChanges: (clear = false) => invoke("review", clear), + toggleInWorkspace: (enable) => invoke(enable ? "turnOnInWorkspace" : "turnOffInWorkspace"), + reviewChanges: (clear = false) => invoke(clear ? "reviewClear" : "reviewRun"), }; } diff --git a/mide-plugins/vscode/src/mcppls/capabilities.ts b/mide-plugins/vscode/src/mcppls/capabilities.ts index 2a9f64f..6fd1c44 100644 --- a/mide-plugins/vscode/src/mcppls/capabilities.ts +++ b/mide-plugins/vscode/src/mcppls/capabilities.ts @@ -1,35 +1,30 @@ /** - * Capability probing and invocation for the C++ Modules extension. + * Which of mcppls's actions VS Code offers, and running them. * - * Pure: the only VS Code-shaped things it needs arrive through - * `CapabilityEnvironment`, which is exactly what the extension host supplies and - * what the tests fake. + * Pure: the only VS Code-shaped things it needs arrive through `CapabilityEnvironment`, which is + * exactly what the extension host supplies and what the tests fake. * - * Why probing is two-level: + * WHAT EXISTS IS mcppls's ANSWER. The backend asks mcppls (`mcppls status`, or the frozen table of + * 0.0.18 and 0.0.19) and hands over its actions, each under the VS Code command mcppls names and the + * arguments it is called with (design 2026-10-30 part 2 §5.3). An action mcppls names no VS Code + * command for is not offered; before the backend has answered, an action is *greyed out*, not + * hidden, so a slow answer cannot remove a feature. * - * - **Static** — `packageJSON.contributes.commands` says which ids mcppls - * declares. It costs nothing and never runs anything. A command the static - * read cannot see is *greyed out*, not hidden, so a wrong read cannot remove a - * feature. - * - **Runtime** — the first real use calls the command once and classifies the - * failure. `command '…' not found` means the capability is gone for this - * session; anything else is a failure of that one call and does not remove the - * feature. - * - * We deliberately do **not** probe by calling every command at activation: - * `selectContext`, `showModuleGraph` and `showLogs` are UI commands with side - * effects, and probing them would open pickers and panels. `commands.getCommands()` - * is not used either — whether it lists a contributed-but-unactivated command is - * not guaranteed across VS Code versions. + * The first real use calls the command and classifies a failure: `command '…' not found` means the + * action is gone for this session (an extension that has not registered it); anything else is a + * failure of that one call and does not remove the action. */ import { CAPABILITIES, MCPPLS_EXTENSION_ID, capability } from "./contract"; +import type { ServerAction } from "../ide/protocol"; + +const POLICY_KEYS = CAPABILITIES.map((entry) => entry.key); export interface CapabilityEnvironment { /** `vscode.extensions.getExtension(id) !== undefined`. */ extensionInstalled(id: string): boolean; - /** Declared command ids, from the extension's `package.json`. */ - declaredCommands?(id: string): readonly string[] | undefined; + /** mcppls's actions as the backend last reported them; `undefined` before it has answered. */ + serverActions(): readonly ServerAction[] | undefined; /** Activate the dependency before the first forward, when VS Code has not. */ activateExtension?(id: string): Thenable; executeCommand(command: string, ...args: unknown[]): Thenable; @@ -40,8 +35,10 @@ export type CapabilityState = "available" | "declared" | "undeclared" | "missing export interface CapabilityStatus { key: string; state: CapabilityState; - /** The command chosen for this capability, when there is one. */ + /** The VS Code command mcppls names for the action, when it names one. */ command?: string; + /** What mcppls says the command is called with. */ + arguments?: readonly unknown[]; } export type InvokeState = "completed" | "unavailable" | "missing" | "failed"; @@ -80,7 +77,7 @@ export class CapabilityRegistry { this.probe(); } - /** Re-read the static declaration; called when extensions are installed, enabled or updated. */ + /** Re-read mcppls's actions; called when the backend answered again or an extension changed. */ public invalidate(): void { this.statuses.clear(); this.probe(); @@ -90,7 +87,7 @@ export class CapabilityRegistry { return this.statuses.get(key) ?? { key, state: "unavailable" }; } - /** Capabilities whose first command is believed usable right now. */ + /** Capabilities whose command is believed usable right now. */ public availableKeys(): string[] { return [...this.statuses.values()] .filter((status) => status.state === "available" || status.state === "declared") @@ -103,38 +100,26 @@ export class CapabilityRegistry { return state === "unavailable" || state === "missing"; } - /** True when the capability exists but the static read could not confirm it: grey out, do not hide. */ + /** True when mcppls has not answered yet: grey out, do not hide. */ public isUnconfirmed(key: string): boolean { return this.status(key).state === "undeclared"; } private probe(): void { const installed = this.environment.extensionInstalled(MCPPLS_EXTENSION_ID); - const declared = installed ? this.environment.declaredCommands?.(MCPPLS_EXTENSION_ID) : undefined; - for (const entry of CAPABILITIES) { - if (entry.kind === "readState") { - // State comes from `extension.exports`, which cannot be inspected without - // activating; the reader decides and reports for itself. - this.statuses.set(entry.key, { key: entry.key, state: installed ? "declared" : "unavailable" }); - continue; - } - if (!installed) { - this.statuses.set(entry.key, { key: entry.key, state: "unavailable" }); - continue; - } - if (declared === undefined) { - // No static information: assume the first command works until a call says otherwise. - this.statuses.set(entry.key, { key: entry.key, state: "declared", command: entry.commands[0] }); - continue; - } - const command = entry.commands.find((candidate) => declared.includes(candidate)); + const actions = installed ? this.environment.serverActions() : undefined; + for (const action of actions ?? []) { + if (capability(action.id) === undefined) continue; // an action mcpp-ide has no policy for is not offered this.statuses.set( - entry.key, - command === undefined - ? { key: entry.key, state: "undeclared" } - : { key: entry.key, state: "declared", command }, + action.id, + action.vscode === undefined + ? { key: action.id, state: "unavailable" } + : { key: action.id, state: "declared", command: action.vscode, ...(action.vscodeArguments ? { arguments: action.vscodeArguments } : {}) }, ); } + if (actions !== undefined) return; + // Not answered yet (or not installed): every action is unconfirmed, or unavailable without mcppls. + for (const key of POLICY_KEYS) this.statuses.set(key, { key, state: installed ? "undeclared" : "unavailable" }); } /** @@ -142,67 +127,31 @@ export class CapabilityRegistry { * ran so the caller can name it in a log line. */ public async invoke(key: string, ...args: unknown[]): Promise { - const entry = capability(key); - if (entry === undefined) { + if (capability(key) === undefined) { return { state: "failed", capabilityKey: key, error: `unknown capability ${key}` }; } if (!this.environment.extensionInstalled(MCPPLS_EXTENSION_ID)) { return { state: "unavailable", capabilityKey: key }; } const known = this.status(key); - if (known.state === "missing") { - // Remembered from an earlier call: do not ask VS Code again. + if (known.state === "missing" || known.state === "unavailable" || known.command === undefined) { + // Not offered by this mcppls, remembered from an earlier call, or not answered yet: the + // installed C++ Modules does not offer it now, and VS Code is not asked. return { state: "missing", capabilityKey: key }; } - if (known.state === "unavailable") { - return { state: "unavailable", capabilityKey: key }; - } - - // Candidate chain: prefer the first command the probe could confirm. - const status = known; - const candidate = status.command ?? entry.commands[0]; - if (candidate === undefined) { - return { state: "failed", capabilityKey: key, error: `capability ${key} has no command` }; - } - + const command = known.command; + const callArgs = args.length > 0 ? args : [...(known.arguments ?? [])]; try { await this.environment.activateExtension?.(MCPPLS_EXTENSION_ID); - const value = await this.environment.executeCommand(candidate, ...args); - this.statuses.set(key, { key, state: "available", command: candidate }); - return { - state: "completed", - capabilityKey: key, - command: candidate, - ...(value === undefined ? {} : { value }), - }; + const value = await this.environment.executeCommand(command, ...callArgs); + this.statuses.set(key, { ...known, state: "available" }); + return { state: "completed", capabilityKey: key, command, ...(value === undefined ? {} : { value }) }; } catch (error) { - const failure = classifyCommandError(error); - if (failure === "missing") { - // This command does not exist in the installed version: try the next - // candidate once, then remember. - const next = entry.commands.find((other) => other !== candidate); - if (next !== undefined) { - try { - const value = await this.environment.executeCommand(next, ...args); - this.statuses.set(key, { key, state: "available", command: next }); - return { - state: "completed", - capabilityKey: key, - command: next, - ...(value === undefined ? {} : { value }), - }; - } catch (second) { - if (classifyCommandError(second) === "missing") { - this.statuses.set(key, { key, state: "missing" }); - return { state: "missing", capabilityKey: key, command: candidate }; - } - return { state: "failed", capabilityKey: key, command: next, error: messageOf(second) }; - } - } + if (classifyCommandError(error) === "missing") { this.statuses.set(key, { key, state: "missing" }); - return { state: "missing", capabilityKey: key, command: candidate }; + return { state: "missing", capabilityKey: key, command }; } - return { state: "failed", capabilityKey: key, command: candidate, error: messageOf(error) }; + return { state: "failed", capabilityKey: key, command, error: messageOf(error) }; } } } diff --git a/mide-plugins/vscode/src/mcppls/contract.ts b/mide-plugins/vscode/src/mcppls/contract.ts index 923b682..fd3ac8b 100644 --- a/mide-plugins/vscode/src/mcppls/contract.ts +++ b/mide-plugins/vscode/src/mcppls/contract.ts @@ -2,7 +2,7 @@ * The C++ Modules language service we depend on, as **data**. * * Everything this extension knows about `sunrisepeak.mcpp-language-server` - * (mcppls) lives here: its extension id, the commands we forward, and how badly + * (mcppls) lives here: its extension id, the actions we offer, and how badly * each one needs confirming. No `vscode` import, so the table is unit-testable * and `tools/` can read it. * @@ -19,34 +19,22 @@ export const MCPPLS_EXTENSION_ID = "sunrisepeak.mcpp-language-server"; -/** - * The version range this build was written against. Used for a **notice** only: - * a command that disappears is detected by calling it, not by reading a version, - * because a version number cannot predict a rename. - * - * The minimum is a fact in its own right (the comparison in `extension.ts` - * reads it through `compareVersions`), and the range string is its display - * form — one source of truth, two spellings (external review P1-3). - */ -export const VERIFIED_MCPPLS_MINIMUM = "0.0.4"; -export const VERIFIED_MCPPLS_RANGE = `>=${VERIFIED_MCPPLS_MINIMUM}`; - -export type CapabilityKind = "forward" | "readState"; - /** How much ceremony an action needs before it runs. */ export type Danger = "none" | "confirm" | "destructive"; +/** + * mcpp-ide's policy for one of mcppls's actions. The key is mcppls's own action id (`mcppls + * status`'s `actions[].id`, mcppls S3-8-7); the command it runs in VS Code, and what it is called + * with, are mcppls's answer, never a copy kept here (design 2026-10-30 part 2 §5.3). What stays here + * is what mcpp-ide decides: the title it shows, how much confirmation the action needs, and what to + * say when it is not there. + */ export interface Capability { key: string; - kind: CapabilityKind; /** English; the UI resolves it through `src/i18n/t.ts`. */ title: string; - /** Candidate command ids, most preferred first. Empty for `readState`. */ - commands: readonly string[]; - /** Always false: losing mcppls must never disable mcpp's own features. */ - required: boolean; danger: Danger; - /** Shown when the capability is gone; says what still works. */ + /** Shown when the action is not offered; says what still works. */ degradedHint: string; /** Extra sentence for a destructive action, spelling out what it does *not* touch. */ confirmHint?: string; @@ -54,173 +42,129 @@ export interface Capability { export const CAPABILITIES: readonly Capability[] = [ { - key: "refresh", - kind: "forward", + key: "reload", title: "Refresh the C++ Modules build description", - commands: ["mcppls.reloadBuildDescription", "mcppls.restartServer"], - required: false, danger: "none", degradedHint: "The build finished, but the language service could not be refreshed. Run \"C++ Modules: Restart Language Server\" from the Command Palette.", }, { key: "selectContext", - kind: "forward", title: "Select the C++ Modules analysis context", - commands: ["mcppls.selectContext"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer a context picker; analysis uses the default context.", }, { key: "moduleGraph", - kind: "forward", title: "Show the module graph", - commands: ["mcppls.showModuleGraph"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer a module graph.", }, { - key: "logs", - kind: "forward", + key: "showLog", title: "Show the C++ Modules log", - commands: ["mcppls.showLogs"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer a log command; look for its output channel in the Output view.", }, { - key: "readState", - kind: "readState", - title: "C++ Modules state", - commands: [], - required: false, - danger: "none", - degradedHint: "Installed C++ Modules does not expose its state; only the actions below are available.", - }, - { - key: "restartServer", - kind: "forward", + key: "restart", title: "Restart the C++ Modules language server", - commands: ["mcppls.restartServer"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer a restart command.", }, { key: "restartEngine", - kind: "forward", title: "Restart the C++ semantic engine", - commands: ["mcppls.restartClangd"], - required: false, danger: "confirm", degradedHint: "Installed C++ Modules does not offer an engine restart.", confirmHint: "The semantic engine restarts; module preparation starts over and may take a while.", }, { key: "resetCache", - kind: "forward", title: "Reset this workspace's C++ Modules cache", - commands: ["mcppls.resetWorkspaceCache"], - required: false, danger: "destructive", degradedHint: "Installed C++ Modules does not offer a cache reset.", confirmHint: "This discards the language server's model cache for this workspace and prepares it again, which can take minutes. mcpp's own build cache and the project's target/ directory are not touched.", }, { - key: "logsDirectory", - // Verified upstream: `mcppls.revealCacheDirectory(which)` takes `'logs'` for - // `paths.logDirectory` and anything else for the workspace cache root - // (`editors/vscode/src/commands.ts` -> `revealCacheDirectory`), and the log - // directory is `/logs` (`src/orchestrator/workspace.cpp`). - // The argument is passed by the caller, so the chain stays one command. - kind: "forward", + key: "openLogDirectory", title: "Open the C++ Modules log folder", - commands: ["mcppls.revealCacheDirectory"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer a reveal command; its log lives in the mcppls cache directory.", }, { - key: "report", - kind: "forward", + key: "collectReport", title: "Collect a C++ Modules diagnostic report", - commands: ["mcppls.collectReport"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer a report command.", }, { - key: "diagnosticBundle", - kind: "forward", + key: "exportDiagnostics", title: "Export a C++ Modules diagnostic bundle", - commands: ["mcppls.exportDiagnosticBundle"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer a diagnostic bundle.", }, { key: "runBuildTool", - kind: "forward", title: "Run the build tool in a terminal", - commands: ["mcppls.runBuildToolInTerminal"], - required: false, danger: "confirm", degradedHint: "Installed C++ Modules does not offer this action.", confirmHint: "The build tool runs in an integrated terminal, with its normal side effects.", }, - // Two capabilities, not one with a candidate chain: a chain is for a renamed - // command, and "turn off" falling back to "restore" would do the opposite. // `danger: "none"` because mcppls's own command asks where to act (this // workspace or user settings) and does nothing when that question is closed; // the settings it writes are the other extensions' own keys, never `mcppls.*`. { key: "turnOffOtherCppFeatures", - kind: "forward", title: "Turn off other C++ language services", - commands: ["mcppls.turnOffOtherCppFeatures"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer this; turn the other extension's language features off in its own settings.", }, { key: "restoreOtherCppFeatures", - kind: "forward", title: "Restore other C++ language services", - commands: ["mcppls.restoreOtherCppFeatures"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer this; turn the other extension's language features on in its own settings.", }, { - key: "toggleInWorkspace", - kind: "forward", + key: "turnOffInWorkspace", + title: "Enable or disable C++ Modules in this workspace", + danger: "confirm", + degradedHint: "Installed C++ Modules does not offer a per-workspace switch.", + confirmHint: "This writes mcppls.enable, a setting owned by the C++ Modules extension.", + }, + { + key: "turnOnInWorkspace", title: "Enable or disable C++ Modules in this workspace", - commands: ["mcppls.turnOffInWorkspace", "mcppls.turnOnInWorkspace"], - required: false, danger: "confirm", degradedHint: "Installed C++ Modules does not offer a per-workspace switch.", confirmHint: "This writes mcppls.enable, a setting owned by the C++ Modules extension.", }, { key: "installTools", - kind: "forward", title: "Install the C++ Modules command line tools", - commands: ["mcppls.installCommandLineTools"], - required: false, danger: "confirm", degradedHint: "Installed C++ Modules does not offer a command line tools installer.", confirmHint: "This may install system packages the language server needs.", }, { - key: "review", - kind: "forward", + key: "installKit", + title: "Install the semantic kit", + danger: "confirm", + degradedHint: "This mcppls does not install kits; files are read with the openkal kit it carries.", + confirmHint: "The C++ Modules server installs the target's semantic kit with xlings, in the background.", + }, + { + key: "reviewRun", + title: "Review workspace changes", + danger: "none", + degradedHint: "Installed C++ Modules does not offer its review commands here.", + }, + { + key: "reviewClear", title: "Review workspace changes", - commands: ["mcppls.review.run", "mcppls.review.clear"], - required: false, danger: "none", degradedHint: "Installed C++ Modules does not offer its review commands here.", }, @@ -232,16 +176,12 @@ export function capability(key: string): Capability | undefined { return BY_KEY.get(key); } -/** The commands mcppls must declare for a capability to be usable at all. */ -export function commandsOf(key: string): readonly string[] { - return BY_KEY.get(key)?.commands ?? []; -} - /** * The capability table's own invariants (must be empty). * * The "our ids never use the mcppls prefix" rule is checked where our ids live - * (`src/commands/ids.ts`); this covers the forward table itself. + * (`src/commands/ids.ts`); this covers the table itself: a key is mcppls's action id, never a + * command id of either extension. */ export function capabilityProblems(): string[] { const problems: string[] = []; @@ -251,19 +191,8 @@ export function capabilityProblems(): string[] { problems.push(`duplicate capability ${entry.key}`); } seen.add(entry.key); - if (entry.required) { - problems.push(`${entry.key} is marked required; losing the language service must not disable mcpp features`); - } - if (entry.kind === "forward" && entry.commands.length === 0) { - problems.push(`forward capability ${entry.key} has no command`); - } - if (entry.kind === "readState" && entry.commands.length > 0) { - problems.push(`readState capability ${entry.key} must not name a command`); - } - for (const command of entry.commands) { - if (!command.startsWith("mcppls.")) { - problems.push(`${entry.key} forwards ${command}, which is not an mcppls. command`); - } + if (entry.key.includes(".")) { + problems.push(`${entry.key} is a command id, not mcppls's action id`); } if (entry.danger !== "none" && entry.confirmHint === undefined) { problems.push(`${entry.key} needs confirmation but has no confirmHint`); diff --git a/mide-plugins/vscode/src/mcppls/otherCppFeatures.ts b/mide-plugins/vscode/src/mcppls/otherCppFeatures.ts index 827b2e6..f3434a8 100644 --- a/mide-plugins/vscode/src/mcppls/otherCppFeatures.ts +++ b/mide-plugins/vscode/src/mcppls/otherCppFeatures.ts @@ -1,17 +1,13 @@ /** * Which other C++ language services are serving next to C++ Modules, normalised. * - * Pure, like `./state.ts`: no `vscode`, nothing here throws. Two sources: + * Pure, like `./state.ts`: no `vscode`, nothing here throws. The read is local and read-only: + * mcppls's three candidates, from the installed extensions and their settings. The mcppls + * extension's API object is not read (design 2026-10-30 part 2 §5.2); whether there is something to + * restore is mcppls's own command's answer, so `canRestore` says only whether mcppls offers the + * command, and the caller sets it. * - * - **mcppls's own answer** (0.0.16+): `otherCppFeatures()` on the object its - * `activate()` returned. It uses the same candidate table and the same "is it - * active" decision as mcppls's own turn-off command, and it knows whether that - * command has something to restore. Preferred whenever it is there. - * - **A local, read-only fallback** for an older mcppls: the same three - * candidates, read from the installed extensions and their settings. It can - * never know what mcppls remembered, so `canRestore` is `false`. - * - * Either way this extension only *reads*. Turning a service off or restoring it + * This extension only *reads*. Turning a service off or restoring it * is mcppls's command (`mcppls.turnOffOtherCppFeatures`, which asks where), and * the settings it writes are the other extensions' own, never `mcppls.*`. */ @@ -25,10 +21,8 @@ export interface OtherCppService { export interface OtherCppFeatures { active: OtherCppService[]; - /** mcppls remembers settings it changed and can put them back. */ + /** mcppls offers to put back the settings it changed. */ canRestore: boolean; - /** Who answered: mcppls itself, or this extension's local read. */ - source: "mcppls" | "local"; } /** A candidate the local read knows; `section`/`key`/`off` is how its language features are turned off. */ @@ -67,41 +61,5 @@ export function localOtherCppFeatures( displayName: candidate.displayName, settable: candidate.setting !== undefined, })); - return { active, canRestore: false, source: "local" }; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function service(value: unknown): OtherCppService | undefined { - if (!isRecord(value) || typeof value.extensionId !== "string" || value.extensionId.length === 0) { - return undefined; - } - return { - extensionId: value.extensionId, - displayName: typeof value.displayName === "string" && value.displayName.length > 0 ? value.displayName : value.extensionId, - settable: value.settable !== false, - }; -} - -/** - * mcppls's answer, or `undefined` when the object has no such function, it - * throws, or what it returns has the wrong shape — the caller then reads locally. - */ -export function otherCppFeaturesFromExports(exportsValue: unknown): OtherCppFeatures | undefined { - if (!isRecord(exportsValue) || typeof exportsValue.otherCppFeatures !== "function") { - return undefined; - } - let answer: unknown; - try { - answer = (exportsValue.otherCppFeatures as () => unknown).call(exportsValue); - } catch { - return undefined; - } - if (!isRecord(answer) || !Array.isArray(answer.active)) { - return undefined; - } - const active = answer.active.map(service).filter((entry): entry is OtherCppService => entry !== undefined); - return { active, canRestore: answer.canRestore === true, source: "mcppls" }; + return { active, canRestore: false }; } diff --git a/mide-plugins/vscode/src/mcppls/state.ts b/mide-plugins/vscode/src/mcppls/state.ts index a931e62..c3548e9 100644 --- a/mide-plugins/vscode/src/mcppls/state.ts +++ b/mide-plugins/vscode/src/mcppls/state.ts @@ -1,58 +1,41 @@ /** - * What the C++ Modules extension reports about itself, normalised. + * What the C++ Modules language service reports about the workspace, normalised. * - * Pure: it takes the object mcppls's `activate()` returned and answers with a - * shape this extension can render, or with `available: false`. - * - * **This is a best-effort channel, not a contract.** mcppls's own name for that - * object is `TestApi`; the *contents* we read (`CxxModulesStatus`) are specified - * by S3, but whether the object is exposed at all is not promised. So every read - * is defensive: a missing function, a thrown error, an unknown `state` or a - * field of the wrong type all end in `available: false` and the view simply - * shows less. Nothing here may throw. - * - * The one thing worth calling out: an issue carries an optional `command` — S3's - * own fix for it. We surface that rather than guessing a remedy. + * Pure: it takes the backend's `languageService/status` answer, whose `workspace` is what + * `mcppls status` printed (mcppls S3 section 8, mcppls 0.0.20 and later), and answers with a shape + * the project view renders, or with `available: false` and why. The three IDEs show the same + * document (design 2026-10-30 part 2 §5.2); nothing here reads the mcppls extension's API object. + * Nothing here throws. */ +import type { LanguageServiceStatus } from "../ide/protocol"; + export interface McpplsIssue { code: string; message: string; - /** S3's own remedy, when it offers one. */ - command?: { command: string; arguments?: unknown[]; title?: string }; } -export interface McpplsEngineStatus { - name: string; - version: string; - role: string; - state: string; -} +export const MCPPLS_STATES = ["ready", "starting", "degraded", "stopped", "none"] as const; +export type McpplsState = (typeof MCPPLS_STATES)[number]; export interface McpplsStateView { available: boolean; - /** Why it is unavailable, for the log; never shown as an error. */ + /** Why it is unavailable, for the log and the tooltip; never shown as an error. */ reason?: string; version?: string; - active?: boolean; enabled?: boolean; + /** The best state among the servers serving the workspace; `none` when none ever did. */ state?: McpplsState; - project?: { root: string; source: string; level?: number; tier?: number }; - profile?: { kind: string; compiler?: string; stdlib: string; target: string; standard?: string }; - engine?: { name: string; version: string }; - engines?: McpplsEngineStatus[]; - progress?: { done: number; total: number }; + engine?: { name: string; version: string; restarts?: number }; + kit?: { name: string; state: string; install?: string; inUse?: string }; + mcpp?: { version: string; program?: string }; + /** How many servers (editors) serve the workspace now. */ + instances?: number; issues?: McpplsIssue[]; - notices?: McpplsIssue[]; - onlineRun?: { outcome: string; message: string; at: string }; } -export const MCPPLS_STATES = ["starting", "loading", "preparing", "ready", "degraded", "error"] as const; -export type McpplsState = (typeof MCPPLS_STATES)[number]; - export interface McpplsMeta { version?: string; - active?: boolean; enabled?: boolean; } @@ -64,155 +47,63 @@ function text(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } -function finiteNumber(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) ? value : undefined; -} - function knownState(value: unknown): McpplsState | undefined { - return typeof value === "string" && (MCPPLS_STATES as readonly string[]).includes(value) - ? (value as McpplsState) - : undefined; + return typeof value === "string" && (MCPPLS_STATES as readonly string[]).includes(value) ? (value as McpplsState) : undefined; } -function issue(value: unknown): McpplsIssue | undefined { - if (!isRecord(value)) { - return undefined; +/** The view of the backend's answer, `undefined` before the backend has answered. */ +export function readStateFromStatus(status: LanguageServiceStatus | undefined, meta: McpplsMeta = {}): McpplsStateView { + const base: McpplsStateView = { available: false, version: status?.version || meta.version, enabled: meta.enabled }; + if (status === undefined) { + return { ...base, reason: "the mcpp-ide backend has not answered yet" }; } - const code = text(value.code); - const message = text(value.message); - if (code === undefined || message === undefined) { - return undefined; + if (status.state !== "ok") { + return { ...base, reason: status.message }; } - const rawCommand = value.command; - const command = - isRecord(rawCommand) && text(rawCommand.command) !== undefined - ? { - command: text(rawCommand.command) as string, - arguments: Array.isArray(rawCommand.arguments) ? rawCommand.arguments : undefined, - title: text(rawCommand.title), - } - : undefined; - return { code, message, command }; -} - -function issues(value: unknown): McpplsIssue[] | undefined { - if (!Array.isArray(value)) { - return undefined; + if (status.statusSource !== "status" || !isRecord(status.workspace)) { + return { + ...base, + reason: + status.statusError !== undefined + ? `mcppls status: ${status.statusError}` + : `mcppls ${status.version} has no status command; 0.0.20 and later state the workspace's state`, + }; } - const parsed = value.map(issue).filter((entry): entry is McpplsIssue => entry !== undefined); - return parsed.length === 0 ? undefined : parsed; -} - -/** - * Normalise the object mcppls's `activate()` returned. - * - * `lastStatus()` is read through `Function.prototype.call` on the object itself - * so a getter or a prototype method behaves the same way. - */ -export function readStateFromExports(exports: unknown, meta: McpplsMeta = {}): McpplsStateView { - const base: McpplsStateView = { - available: false, - version: meta.version, - active: meta.active, - enabled: meta.enabled, - }; - if (!isRecord(exports)) { - return { ...base, reason: "mcppls exposes no API object" }; - } - const reader = (exports as Record).lastStatus; - if (typeof reader !== "function") { - return { ...base, reason: "mcppls's API object has no lastStatus()" }; - } - let raw: unknown; - try { - raw = (reader as () => unknown).call(exports); - } catch (error) { - return { ...base, reason: `lastStatus() threw: ${error instanceof Error ? error.message : String(error)}` }; - } - if (!isRecord(raw)) { - // `undefined` is normal before the first status arrives; say so, do not complain. - return { ...base, reason: raw === undefined ? "no status yet" : "lastStatus() returned an unexpected shape" }; - } - const state = knownState(raw.state); + const workspace = status.workspace as Record; + const state = knownState(workspace.state); if (state === undefined) { - return { ...base, reason: `unknown state ${JSON.stringify(raw.state)}` }; + return { ...base, reason: `unknown state ${JSON.stringify(workspace.state)}` }; } - - const view: McpplsStateView = { ...base, available: true, state }; - - const project = raw.project; - if (isRecord(project)) { - const root = text(project.root); - const source = text(project.source); - if (root !== undefined && source !== undefined) { - view.project = { - root, - source, - level: finiteNumber(project.level), - tier: finiteNumber(project.tier), - }; - } + const view: McpplsStateView = { ...base, available: true, state, instances: Array.isArray(status.instances) ? status.instances.length : 0 }; + if (isRecord(workspace.engine) && text(workspace.engine.name) !== undefined) { + const restarts = workspace.engine.restarts; + view.engine = { + name: text(workspace.engine.name) as string, + version: text(workspace.engine.version) ?? "", + ...(typeof restarts === "number" && Number.isFinite(restarts) ? { restarts } : {}), + }; } - - const profile = raw.profile; - if (isRecord(profile)) { - const kind = text(profile.kind); - const stdlib = text(profile.stdlib); - const target = text(profile.target); - if (kind !== undefined && stdlib !== undefined && target !== undefined) { - view.profile = { kind, stdlib, target, compiler: text(profile.compiler), standard: text(profile.standard) }; - } + if (isRecord(workspace.kit) && text(workspace.kit.state) !== undefined) { + view.kit = { + name: text(workspace.kit.name) ?? "", + state: text(workspace.kit.state) as string, + ...(text(workspace.kit.install) !== undefined ? { install: text(workspace.kit.install) } : {}), + ...(text(workspace.kit.inUse) !== undefined ? { inUse: text(workspace.kit.inUse) } : {}), + }; } - - const engine = raw.engine; - if (isRecord(engine)) { - const name = text(engine.name); - const version = text(engine.version); - if (name !== undefined && version !== undefined) { - view.engine = { name, version }; - } + if (isRecord(workspace.mcpp) && text(workspace.mcpp.version) !== undefined) { + view.mcpp = { version: text(workspace.mcpp.version) as string, program: text(workspace.mcpp.program) }; } - - if (Array.isArray(raw.engines)) { - const engines = raw.engines - .map((entry): McpplsEngineStatus | undefined => { - if (!isRecord(entry)) return undefined; - const name = text(entry.name); - const version = text(entry.version); - const role = text(entry.role); - const state = text(entry.state); - return name !== undefined && version !== undefined && role !== undefined && state !== undefined - ? { name, version, role, state } - : undefined; - }) - .filter((entry): entry is McpplsEngineStatus => entry !== undefined); - if (engines.length > 0) { - view.engines = engines; - } - } - - const progress = raw.progress; - if (isRecord(progress)) { - const done = finiteNumber(progress.done); - const total = finiteNumber(progress.total); - if (done !== undefined && total !== undefined) { - view.progress = { done, total }; - } + if (Array.isArray(workspace.problems)) { + const issues = workspace.problems + .map((entry): McpplsIssue | undefined => + isRecord(entry) && text(entry.code) !== undefined && text(entry.message) !== undefined + ? { code: text(entry.code) as string, message: text(entry.message) as string } + : undefined, + ) + .filter((entry): entry is McpplsIssue => entry !== undefined); + if (issues.length > 0) view.issues = issues; } - - view.issues = issues(raw.issues); - view.notices = issues(raw.notices); - - const onlineRun = raw.onlineRun; - if (isRecord(onlineRun)) { - const outcome = text(onlineRun.outcome); - const message = text(onlineRun.message); - const at = text(onlineRun.at); - if (outcome !== undefined && message !== undefined && at !== undefined) { - view.onlineRun = { outcome, message, at }; - } - } - return view; } @@ -221,9 +112,10 @@ export function describeState(view: McpplsStateView): string { if (!view.available) { return `unavailable (${view.reason ?? "unknown reason"})`; } - const parts = [view.state ?? "unknown"]; - if (view.project !== undefined) parts.push(`project ${view.project.source}`); - if (view.engine !== undefined) parts.push(`engine ${view.engine.name} ${view.engine.version}`); + const parts: string[] = [view.state ?? "unknown"]; + if (view.engine !== undefined) parts.push(`engine ${view.engine.name} ${view.engine.version}`.trimEnd()); + if (view.kit !== undefined) parts.push(`kit ${view.kit.name} (${view.kit.state})`); + if (view.instances !== undefined && view.instances > 1) parts.push(`${view.instances} servers`); if (view.issues !== undefined) parts.push(`${view.issues.length} issue(s)`); return parts.join(" · "); } diff --git a/mide-plugins/vscode/src/mcppls/stateSource.ts b/mide-plugins/vscode/src/mcppls/stateSource.ts index 7dd74b6..be13d40 100644 --- a/mide-plugins/vscode/src/mcppls/stateSource.ts +++ b/mide-plugins/vscode/src/mcppls/stateSource.ts @@ -1,23 +1,22 @@ /** * The `vscode`-facing half of the C++ Modules state read. * - * `state.ts` normalises what it is given; this file fetches it. It is the only - * place that touches `extension.exports`, and it is deliberately defensive: the - * object is mcppls's test API, not a contract, so every failure becomes - * `available: false` with a reason and never an exception. + * `state.ts` normalises what it is given; this file adds what VS Code knows (whether the extension + * is installed and enabled). The state itself is `mcppls status` as the backend reported it + * (`Session.languageService`, design 2026-10-30 part 2 §5.2); the mcppls extension's API object is + * not read. * - * `mcpp.languageService.readState` can switch the whole read off. When it is off - * the gate below returns before `extension.exports` is touched at all — not a - * partial read, no read — and the view falls back to the degraded content it - * always has for an installed-but-unreadable mcppls. + * `mcpp.languageService.readState` can switch the state off: the view then shows the degraded + * content it always has for an installed-but-unreadable mcppls. */ import * as vscode from "vscode"; import { read } from "../config/access"; import { MCPPLS_EXTENSION_ID } from "./contract"; -import { localOtherCppFeatures, otherCppFeaturesFromExports, type OtherCppFeatures } from "./otherCppFeatures"; -import { readStateFromExports, type McpplsStateView } from "./state"; +import type { LanguageServiceStatus } from "../ide/protocol"; +import { localOtherCppFeatures, type OtherCppFeatures } from "./otherCppFeatures"; +import { readStateFromStatus, type McpplsStateView } from "./state"; /** Why a state read did not happen. The view prints it verbatim. */ export function stateUnavailableReason(reason: "disabled" | "missing"): string { @@ -53,46 +52,23 @@ export function languageServerServes(): boolean { return languageServerInstalled() && languageServerEnabled(); } -export function readLanguageServerState(): McpplsStateView { - const extension = dependency(); - const meta = { - version: languageServerVersion(), - active: extension?.isActive, - enabled: languageServerEnabled(), - }; - // The gate: nothing below this line runs when the setting is off, so - // `extension.exports` is never read. `read()` falls back to the declared - // default (true) when the registry has no value for the key. +export function readLanguageServerState(status: LanguageServiceStatus | undefined): McpplsStateView { + const meta = { version: languageServerVersion(), enabled: languageServerEnabled() }; + // The gate: `read()` falls back to the declared default (true) when the registry has no value. if (read("mcpp.languageService.readState") === false) { return { available: false, reason: stateUnavailableReason("disabled"), ...meta }; } - if (extension === undefined) { + if (dependency() === undefined) { return { available: false, reason: stateUnavailableReason("missing"), ...meta }; } - try { - return readStateFromExports(extension.exports, meta); - } catch (error) { - return { - available: false, - reason: `reading mcppls state threw: ${error instanceof Error ? error.message : String(error)}`, - ...meta, - }; - } + return readStateFromStatus(status, meta); } /** - * The other C++ language services serving next to C++ Modules: mcppls's own - * answer when it gives one (and reading its exports is allowed), else a local - * read-only look at the same three candidates. + * The other C++ language services serving next to C++ Modules: a read-only look at the candidates' + * installation and their own settings. */ export function readOtherCppFeatures(): OtherCppFeatures { - const extension = dependency(); - if (extension !== undefined && extension.isActive && read("mcpp.languageService.readState") !== false) { - const answer = otherCppFeaturesFromExports(extension.exports); - if (answer !== undefined) { - return answer; - } - } return localOtherCppFeatures( (extensionId) => vscode.extensions.getExtension(extensionId) !== undefined, (section, key) => vscode.workspace.getConfiguration(section).get(key), diff --git a/mide-plugins/vscode/src/views/languageServerView.ts b/mide-plugins/vscode/src/views/languageServerView.ts index 1ac83f3..4e1cd3b 100644 --- a/mide-plugins/vscode/src/views/languageServerView.ts +++ b/mide-plugins/vscode/src/views/languageServerView.ts @@ -28,8 +28,9 @@ import * as vscode from "vscode"; import { LANGUAGE_SERVER_COMMANDS, LEGACY_LANGUAGE_SERVER_COMMANDS, TOOL_COMMANDS } from "../commands/ids"; import { read } from "../config/access"; import { t } from "../i18n/t"; +import type { Session } from "../ide/session"; import type { LanguageServerBridge } from "../mcppls/bridge"; -import { capability } from "../mcppls/contract"; +import { MCPPLS_EXTENSION_ID, capability } from "../mcppls/contract"; import { confirmationFor, formatResult, type FormattedResult } from "../mcppls/messages"; import { languageServerEnabled, @@ -47,9 +48,14 @@ import { degradedNoticeEnabled, resetCacheConfirmation } from "./viewPolicy"; /** The tick granularity; the settings are in seconds. */ const REFRESH_TICK_MS = 100; +/** The shortest interval at which `mcppls status` is asked while the view is visible (§5.2). */ +export const STATE_REFRESH_FLOOR_SECONDS = 30; + export interface LanguageServerViewDeps { bridge: LanguageServerBridge; output: vscode.OutputChannel; + /** The backend session: what mcppls says about the workspace (§5.2). */ + session: Session; } /** @@ -163,18 +169,21 @@ export function registerLanguageServerCommands( installed: languageServerInstalled(), enabled: languageServerEnabled(), version: languageServerVersion(), - state: readLanguageServerState(), - otherCppFeatures: languageServerInstalled() ? readOtherCppFeatures() : undefined, + state: readLanguageServerState(deps.session.languageService), + otherCppFeatures: languageServerInstalled() + ? { ...readOtherCppFeatures(), canRestore: !deps.bridge.isGone("restoreOtherCppFeatures") } + : undefined, }; }; // `mcpp.languageService.stateRefreshSeconds`, but only while the project view is - // visible and only while state is read at all (0 = off). + // visible, only while state is read at all (0 = off), and never more often than every 30 s: + // each tick runs `mcppls status` (§5.2). const timer = new PollTimer({ periodMs: 0, tick: (): void => { if (visible) { - refresh(); + void deps.session.refreshLanguageService(); } }, }); @@ -183,7 +192,7 @@ export function registerLanguageServerCommands( const disabled = read("mcpp.languageService.readState") === false; const seconds = read("mcpp.languageService.stateRefreshSeconds"); const usable = !disabled && Number.isFinite(seconds) && seconds > 0; - const decision = refreshTimerDecision(usable ? seconds : 0, visible); + const decision = refreshTimerDecision(usable ? Math.max(STATE_REFRESH_FLOOR_SECONDS, seconds) : 0, visible); timer.start(decision.active ? decision.seconds * 1000 : 0); }; applyTimer(); @@ -240,7 +249,7 @@ export function registerLanguageServerCommands( }; register(LANGUAGE_SERVER_COMMANDS.exportDiagnosticBundle, async () => { - const result = await runLanguageServerCommand(deps.bridge, deps.output, "diagnosticBundle"); + const result = await runLanguageServerCommand(deps.bridge, deps.output, "exportDiagnostics"); const bundle = bundleFrom(result?.value); if (bundle !== undefined) { await context.globalState.update(BUNDLE_PATH_KEY, bundle); @@ -264,28 +273,29 @@ export function registerLanguageServerCommands( await vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(remembered)); }); - register(LANGUAGE_SERVER_COMMANDS.openLogFolder, async () => { - // Upstream owns the path: `revealCacheDirectory('logs')` asks the server's own - // detail for `paths.logDirectory` and reveals it, falling back to the output - // channel when the server cannot answer. Nothing here reconstructs a path. - await runLanguageServerCommand(deps.bridge, deps.output, "logsDirectory", { args: ["logs"] }); - }); - - forward(LANGUAGE_SERVER_COMMANDS.restart, "restartServer"); + // mcppls owns the path and the arguments: its action names the command and what it is called + // with (`revealCacheDirectory('logs')` today). Nothing here reconstructs a path. + forward(LANGUAGE_SERVER_COMMANDS.openLogFolder, "openLogDirectory"); + forward(LANGUAGE_SERVER_COMMANDS.restart, "restart"); forward(LANGUAGE_SERVER_COMMANDS.restartEngine, "restartEngine"); forward(LANGUAGE_SERVER_COMMANDS.resetWorkspaceCache, "resetCache"); forward(LANGUAGE_SERVER_COMMANDS.selectContext, "selectContext"); forward(LANGUAGE_SERVER_COMMANDS.showModuleGraph, "moduleGraph"); - forward(LANGUAGE_SERVER_COMMANDS.showLogs, "logs"); - forward(LANGUAGE_SERVER_COMMANDS.collectReport, "report"); + forward(LANGUAGE_SERVER_COMMANDS.showLogs, "showLog"); + forward(LANGUAGE_SERVER_COMMANDS.collectReport, "collectReport"); // `exportDiagnosticBundle` is registered above rather than forwarded: its // answer is the zip's path, and the caller has to keep it. forward(LANGUAGE_SERVER_COMMANDS.runBuildToolInTerminal, "runBuildTool"); forward(LANGUAGE_SERVER_COMMANDS.installTools, "installTools"); + forward(LANGUAGE_SERVER_COMMANDS.installKit, "installKit"); forward(LANGUAGE_SERVER_COMMANDS.manageConflicts, "turnOffOtherCppFeatures"); forward(LANGUAGE_SERVER_COMMANDS.restoreConflicts, "restoreOtherCppFeatures"); - forward(LANGUAGE_SERVER_COMMANDS.reviewChanges, "review", () => [false]); - forward(LANGUAGE_SERVER_COMMANDS.toggleInWorkspace, "toggleInWorkspace", () => [!languageServerEnabled()]); + forward(LANGUAGE_SERVER_COMMANDS.reviewChanges, "reviewRun"); + register(LANGUAGE_SERVER_COMMANDS.toggleInWorkspace, async () => { + const enable = !languageServerEnabled(); + await runLanguageServerCommand(deps.bridge, deps.output, enable ? "turnOnInWorkspace" : "turnOffInWorkspace", { value: enable }); + refresh(); + }); // The legacy ids from 0.4.x keep working, pointing at the new actions. register(LEGACY_LANGUAGE_SERVER_COMMANDS.configureLanguageServer, async () => { @@ -295,13 +305,13 @@ export function registerLanguageServerCommands( await runLanguageServerCommand(deps.bridge, deps.output, "selectContext"); }); register(LEGACY_LANGUAGE_SERVER_COMMANDS.checkModuleSupport, async () => { - await runLanguageServerCommand(deps.bridge, deps.output, "restartServer"); + await runLanguageServerCommand(deps.bridge, deps.output, "restart"); }); register(LEGACY_LANGUAGE_SERVER_COMMANDS.showModuleGraph, async () => { await runLanguageServerCommand(deps.bridge, deps.output, "moduleGraph"); }); register(LEGACY_LANGUAGE_SERVER_COMMANDS.showLanguageServerLogs, async () => { - await runLanguageServerCommand(deps.bridge, deps.output, "logs"); + await runLanguageServerCommand(deps.bridge, deps.output, "showLog"); }); register(TOOL_COMMANDS.openLanguageServerSettings, async () => { @@ -309,6 +319,11 @@ export function registerLanguageServerCommands( }); context.subscriptions.push( + // The backend asked mcppls again: its actions and the workspace's state may differ. + deps.session.onDidChangeLanguageService.event(() => { + deps.bridge.capabilities.invalidate(); + refresh(); + }), vscode.extensions.onDidChange(() => refresh()), vscode.workspace.onDidChangeConfiguration((event) => { if ( @@ -328,7 +343,7 @@ export function registerLanguageServerCommands( // `mcpp.languageService.notifyOnDegraded` says the user does not want it. if (degradedNoticeEnabled(read("mcpp.languageService.notifyOnDegraded")) && !languageServerInstalled()) { deps.output.appendLine( - t("The C++ Modules extension ({0}) is not installed or is disabled.", capability("refresh")?.key ?? ""), + t("The C++ Modules extension ({0}) is not installed or is disabled.", MCPPLS_EXTENSION_ID), ); } @@ -343,6 +358,7 @@ export function registerLanguageServerCommands( applyTimer(); // Becoming visible is exactly when a stale block must be re-read. if (visible) { + void deps.session.refreshLanguageService(); refresh(); } }, diff --git a/mide-plugins/vscode/src/views/models.ts b/mide-plugins/vscode/src/views/models.ts index 82c1d74..c960f3e 100644 --- a/mide-plugins/vscode/src/views/models.ts +++ b/mide-plugins/vscode/src/views/models.ts @@ -452,6 +452,8 @@ export interface LanguageServiceBlock { reason?: string; state?: string; issues?: readonly LanguageServiceIssue[]; + /** The semantic kit of the workspace's target (`mcppls status`, §5.2). */ + kit?: { name: string; state: string }; }; /** Other C++ language services serving next to it (C/C++, clangd, ccls). */ otherCppFeatures?: { @@ -744,6 +746,17 @@ function languageServiceChildren(block: LanguageServiceBlock): TreeNode[] { if (block.otherCppFeatures !== undefined) { nodes.push(otherCppFeaturesNode(block.otherCppFeatures)); } + // A missing kit: files are read with the openkal kit, and mcppls installs the target's (§5.3). + if (state?.available === true && state.kit?.state === "missing" && state.kit.name !== "") { + nodes.push({ + id: "project.languageService.kit", + label: plain("The semantic kit is not installed"), + description: { key: "{0}", args: [state.kit.name] }, + icon: "cloud-download", + tooltip: plain("Files are read with the openkal kit C++ Modules carries. Installing the target's kit gives its own standard library and headers."), + command: { command: "mcpp.languageServer.installKit", title: plain("Install the semantic kit") }, + }); + } (state?.issues ?? []).forEach((issue, index) => { nodes.push({ @@ -819,6 +832,15 @@ function actionNode(action: ActionRow): TreeNode { }; } +/** The workspace's state as `mcppls status` states it. */ +const STATE_LABELS: Readonly> = { + ready: "Ready", + starting: "Starting", + degraded: "Degraded", + stopped: "Stopped", + none: "Not serving this workspace", +}; + function languageServiceStatus(state: LanguageServiceBlock["state"]): TreeNode { if (state === undefined || !state.available) { return { @@ -830,7 +852,7 @@ function languageServiceStatus(state: LanguageServiceBlock["state"]): TreeNode { } return { id: "project.languageService.status", - label: plain(state.state === "ready" ? "Ready" : state.state === "degraded" ? "Degraded" : state.state ?? "Unknown"), + label: plain(STATE_LABELS[state.state ?? ""] ?? "Unknown"), icon: state.state === "ready" ? "pass" : "warning", }; } diff --git a/mide-plugins/vscode/test/commands/ids.test.ts b/mide-plugins/vscode/test/commands/ids.test.ts index ed3713d..a0a5c6c 100644 --- a/mide-plugins/vscode/test/commands/ids.test.ts +++ b/mide-plugins/vscode/test/commands/ids.test.ts @@ -71,6 +71,7 @@ test("the C++ Modules group lists every id it registers", () => { "mcpp.languageServer.restoreConflicts", "mcpp.languageServer.toggleInWorkspace", "mcpp.languageServer.installTools", + "mcpp.languageServer.installKit", "mcpp.languageServer.reviewChanges", ]); }); diff --git a/mide-plugins/vscode/test/mcppls/bridge.test.ts b/mide-plugins/vscode/test/mcppls/bridge.test.ts index f732696..0a4bbd1 100644 --- a/mide-plugins/vscode/test/mcppls/bridge.test.ts +++ b/mide-plugins/vscode/test/mcppls/bridge.test.ts @@ -1,16 +1,38 @@ import assert from "node:assert/strict"; import test from "node:test"; +import type { ServerAction } from "../../src/ide/protocol"; import { createLanguageServerBridge, MCPPLS_EXTENSION_ID } from "../../src/mcppls/bridge"; import type { CapabilityEnvironment } from "../../src/mcppls/capabilities"; +/** mcppls's actions under their VS Code commands, as `mcppls status` names them (mcppls S3-8-7). */ +const ACTIONS: readonly ServerAction[] = [ + ["restart", "mcppls.restartServer"], + ["reload", "mcppls.reloadBuildDescription"], + ["restartEngine", "mcppls.restartClangd"], + ["resetCache", "mcppls.resetWorkspaceCache"], + ["showLog", "mcppls.showLogs"], + ["collectReport", "mcppls.collectReport"], + ["exportDiagnostics", "mcppls.exportDiagnosticBundle"], + ["selectContext", "mcppls.selectContext"], + ["moduleGraph", "mcppls.showModuleGraph"], + ["runBuildTool", "mcppls.runBuildToolInTerminal"], + ["installTools", "mcppls.installCommandLineTools"], + ["reviewRun", "mcppls.review.run"], + ["reviewClear", "mcppls.review.clear"], + ["turnOffInWorkspace", "mcppls.turnOffInWorkspace"], + ["turnOnInWorkspace", "mcppls.turnOnInWorkspace"], + ["turnOffOtherCppFeatures", "mcppls.turnOffOtherCppFeatures"], + ["restoreOtherCppFeatures", "mcppls.restoreOtherCppFeatures"], +].map(([id, vscode]) => ({ id, title: id, vscode })); + function harness(options: { installed?: boolean; behaviour?: (command: string) => void } = {}) { const installed = options.installed ?? true; const calls: Array<{ command: string; args: unknown[] }> = []; const activations: string[] = []; const environment: CapabilityEnvironment = { extensionInstalled: (id) => installed && id === MCPPLS_EXTENSION_ID, - declaredCommands: () => undefined, + serverActions: () => ACTIONS, activateExtension: async (id) => { activations.push(id); }, @@ -32,7 +54,7 @@ test("forwards one command per capability and activates the dependency first", a assert.deepEqual(await bridge.restartLanguageServer(), { state: "completed", - capabilityKey: "restartServer", + capabilityKey: "restart", command: "mcppls.restartServer", }); await bridge.selectContext(); @@ -61,12 +83,12 @@ test("forwards one command per capability and activates the dependency first", a assert.ok(activations.every((id) => id === MCPPLS_EXTENSION_ID)); }); -test("the refresh capability prefers the cheap reload", async () => { +test("the refresh after a build is mcppls's reload", async () => { const { bridge, calls } = harness(); const result = await bridge.refreshLanguageServerAfterBuild(); assert.deepEqual(result, { state: "completed", - capabilityKey: "refresh", + capabilityKey: "reload", command: "mcppls.reloadBuildDescription", }); assert.deepEqual(calls.map((call) => call.command), ["mcppls.reloadBuildDescription"]); @@ -76,9 +98,9 @@ test("an uninstalled dependency yields unavailable without calling anything", as const { bridge, calls } = harness({ installed: false }); assert.deepEqual(await bridge.restartLanguageServer(), { state: "unavailable", - capabilityKey: "restartServer", + capabilityKey: "restart", }); - assert.equal(bridge.isGone("restartServer"), true); + assert.equal(bridge.isGone("restart"), true); assert.deepEqual(calls, []); }); @@ -91,7 +113,7 @@ test("a failure is reported verbatim and does not remove the capability", async const result = await bridge.restartLanguageServer(); assert.equal(result.state, "failed"); assert.match(result.error ?? "", /server is not running/); - assert.equal(bridge.isGone("restartServer"), false); + assert.equal(bridge.isGone("restart"), false); }); test("a missing command is not called again", async () => { @@ -113,7 +135,7 @@ test("coalesces concurrent build refresh requests into one call", async () => { const calls: string[] = []; const bridge = createLanguageServerBridge({ extensionInstalled: () => true, - declaredCommands: () => undefined, + serverActions: () => ACTIONS, executeCommand: async (command: string): Promise => { calls.push(command); await new Promise((resolve) => { @@ -145,13 +167,17 @@ test("turning other C++ services off and restoring them are two commands, never assert.equal(bridge.dangerOf("restoreOtherCppFeatures"), "none"); }); -test("directional commands pass their argument through", async () => { +test("a direction chooses one of mcppls's two actions", async () => { const { bridge, calls } = harness(); await bridge.toggleInWorkspace(false); + await bridge.toggleInWorkspace(true); + await bridge.reviewChanges(); await bridge.reviewChanges(true); - assert.deepEqual(calls, [ - { command: "mcppls.turnOffInWorkspace", args: [false] }, - { command: "mcppls.review.run", args: [true] }, + assert.deepEqual(calls.map((call) => call.command), [ + "mcppls.turnOffInWorkspace", + "mcppls.turnOnInWorkspace", + "mcppls.review.run", + "mcppls.review.clear", ]); }); diff --git a/mide-plugins/vscode/test/mcppls/capabilities.test.ts b/mide-plugins/vscode/test/mcppls/capabilities.test.ts index 2057277..d1d448c 100644 --- a/mide-plugins/vscode/test/mcppls/capabilities.test.ts +++ b/mide-plugins/vscode/test/mcppls/capabilities.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; +import type { ServerAction } from "../../src/ide/protocol"; import { MCPPLS_EXTENSION_ID } from "../../src/mcppls/contract"; import { CapabilityRegistry, classifyCommandError, type CapabilityEnvironment } from "../../src/mcppls/capabilities"; @@ -8,22 +9,34 @@ interface Harness { registry: CapabilityRegistry; calls: Array<{ command: string; args: unknown[] }>; activations: string[]; + answer(actions: readonly ServerAction[] | undefined): void; } +/** mcppls's actions as `mcppls status` names them, a few of them. */ +const ACTIONS: readonly ServerAction[] = [ + { id: "restart", title: "Restart", vscode: "mcppls.restartServer", jetbrains: "community.mcpp.mcppls.RestartServer" }, + { id: "moduleGraph", title: "Graph", vscode: "mcppls.showModuleGraph" }, + { id: "openLogDirectory", title: "Logs", vscode: "mcppls.revealCacheDirectory", vscodeArguments: ["logs"] }, + { id: "showStatus", title: "Status", nvim: "McpplsStatus" }, + { id: "exportDiagnostics", title: "Bundle", vscode: "mcppls.exportDiagnosticBundle" }, + { id: "somethingNew", title: "New", vscode: "mcppls.somethingNew" }, +]; + function harness(options: { installed?: boolean; - declared?: readonly string[] | undefined; + actions?: readonly ServerAction[] | undefined; behaviour?: (command: string, attempt: number) => void; /** What the command answers with, for the few that answer at all. */ result?: (command: string) => unknown; } = {}): Harness { const installed = options.installed ?? true; + let actions = "actions" in options ? options.actions : ACTIONS; const calls: Array<{ command: string; args: unknown[] }> = []; const activations: string[] = []; const attempts = new Map(); const environment: CapabilityEnvironment = { extensionInstalled: (id) => installed && id === MCPPLS_EXTENSION_ID, - declaredCommands: () => options.declared, + serverActions: () => actions, activateExtension: async (id) => { activations.push(id); }, @@ -35,7 +48,16 @@ function harness(options: { return (options.result === undefined ? undefined : options.result(command)) as T; }, }; - return { registry: new CapabilityRegistry(environment), calls, activations }; + const registry = new CapabilityRegistry(environment); + return { + registry, + calls, + activations, + answer: (next) => { + actions = next; + registry.invalidate(); + }, + }; } const notFound = (command: string): never => { @@ -50,131 +72,91 @@ test("classifyCommandError separates a missing command from a real failure", () assert.equal(classifyCommandError("plain string"), "failed"); }); -test("with no static information a capability is assumed usable until a call says otherwise", () => { - const { registry, calls } = harness({ declared: undefined }); +test("an action runs the VS Code command mcppls names for it", async () => { + const { registry, calls } = harness(); assert.equal(registry.status("moduleGraph").state, "declared"); - assert.equal(registry.isGone("moduleGraph"), false); - return registry.invoke("moduleGraph").then((result) => { - assert.deepEqual(result, { state: "completed", capabilityKey: "moduleGraph", command: "mcppls.showModuleGraph" }); - assert.deepEqual(calls, [{ command: "mcppls.showModuleGraph", args: [] }]); - }); + const result = await registry.invoke("moduleGraph"); + assert.deepEqual(result, { state: "completed", capabilityKey: "moduleGraph", command: "mcppls.showModuleGraph" }); + assert.deepEqual(calls, [{ command: "mcppls.showModuleGraph", args: [] }]); }); -test("a statically undeclared command is only greyed out, never hidden", () => { - const { registry } = harness({ declared: ["mcppls.restartServer"] }); - assert.equal(registry.status("moduleGraph").state, "undeclared"); - assert.equal(registry.isUnconfirmed("moduleGraph"), true); - assert.equal(registry.isGone("moduleGraph"), false); +test("the arguments mcppls names are passed, unless the caller passes its own", async () => { + const { registry, calls } = harness(); + await registry.invoke("openLogDirectory"); + await registry.invoke("openLogDirectory", "elsewhere"); + assert.deepEqual(calls, [ + { command: "mcppls.revealCacheDirectory", args: ["logs"] }, + { command: "mcppls.revealCacheDirectory", args: ["elsewhere"] }, + ]); }); -test("the command's own answer is passed back untouched", async () => { - // `mcppls.exportDiagnosticBundle` resolves to the zip it wrote, and that path - // is the only reliable pointer at the file afterwards; most commands answer - // with nothing, so the field is optional and never interpreted here. - const { registry, calls } = harness({ - declared: ["mcppls.exportDiagnosticBundle", "mcppls.revealCacheDirectory"], - result: (command) => (command === "mcppls.exportDiagnosticBundle" ? "/cache/bundles/x.zip" : undefined), - }); +test("an action mcppls offers no VS Code command for, or does not name, is hidden and never called", async () => { + const { registry, calls } = harness(); + assert.equal(registry.isGone("selectContext"), true); + assert.equal((await registry.invoke("selectContext")).state, "missing"); + assert.equal(registry.isGone("showStatus"), true); + assert.deepEqual(calls, []); +}); - const answered = await registry.invoke("diagnosticBundle"); - assert.equal(answered.state, "completed"); - assert.equal(answered.value, "/cache/bundles/x.zip"); +test("an action mcpp-ide has no policy for is not offered", () => { + const { registry } = harness(); + assert.ok(!registry.availableKeys().includes("somethingNew")); +}); - // The reveal command takes its target as an argument; the table only names the - // command, so the caller's `'logs'` has to arrive as-is. - await registry.invoke("logsDirectory", "logs"); - assert.deepEqual(calls[calls.length - 1], { command: "mcppls.revealCacheDirectory", args: ["logs"] }); +test("before the backend answers, an action is greyed out, not hidden", async () => { + const { registry, calls, answer } = harness({ actions: undefined }); + assert.equal(registry.isUnconfirmed("restart"), true); + assert.equal(registry.isGone("restart"), false); + assert.equal((await registry.invoke("restart")).state, "missing"); + answer(ACTIONS); + assert.equal(registry.isUnconfirmed("restart"), false); + assert.equal((await registry.invoke("restart")).state, "completed"); + assert.deepEqual(calls, [{ command: "mcppls.restartServer", args: [] }]); +}); - const silent = await registry.invoke("logsDirectory"); - assert.equal(silent.state, "completed"); - assert.ok(!("value" in silent), "a command that answered nothing must not invent a value"); +test("the command's own answer is passed back untouched", async () => { + const { registry } = harness({ result: () => "/cache/bundles/b.zip" }); + const result = await registry.invoke("exportDiagnostics"); + assert.equal(result.value, "/cache/bundles/b.zip"); }); test("an uninstalled dependency makes every capability unavailable without calling anything", async () => { const { registry, calls } = harness({ installed: false }); - assert.equal(registry.status("refresh").state, "unavailable"); - assert.equal(registry.isGone("refresh"), true); - assert.deepEqual(await registry.invoke("refresh"), { state: "unavailable", capabilityKey: "refresh" }); + assert.equal(registry.isGone("restart"), true); + assert.deepEqual(await registry.invoke("restart"), { state: "unavailable", capabilityKey: "restart" }); assert.deepEqual(calls, []); }); -test("refresh falls back down its candidate chain when the preferred command is missing", async () => { - const { registry, calls } = harness({ - declared: undefined, - behaviour: (command, attempt) => { - if (command === "mcppls.reloadBuildDescription" && attempt === 1) { - notFound(command); - } - }, - }); - const result = await registry.invoke("refresh"); - assert.deepEqual(result, { state: "completed", capabilityKey: "refresh", command: "mcppls.restartServer" }); - assert.deepEqual(calls.map((call) => call.command), [ - "mcppls.reloadBuildDescription", - "mcppls.restartServer", - ]); - assert.equal(registry.status("refresh").state, "available"); -}); - -test("a capability whose whole chain is missing is remembered as gone", async () => { - const { registry, calls } = harness({ - declared: undefined, - behaviour: (command) => notFound(command), - }); - assert.deepEqual(await registry.invoke("refresh"), { - state: "missing", - capabilityKey: "refresh", - command: "mcppls.reloadBuildDescription", - }); - assert.equal(registry.status("refresh").state, "missing"); - assert.equal(registry.isGone("refresh"), true); - assert.equal(calls.length, 2, "the chain is tried once, not forever"); +test("a command VS Code does not know is remembered as gone", async () => { + const { registry, calls } = harness({ behaviour: notFound }); + assert.equal((await registry.invoke("restart")).state, "missing"); + assert.equal(registry.isGone("restart"), true); + assert.equal((await registry.invoke("restart")).state, "missing"); + assert.equal(calls.length, 1); }); test("a real failure keeps the capability and reports the error verbatim", async () => { const { registry } = harness({ - declared: undefined, - behaviour: (command) => { - throw new Error(`${command}: server is not running`); + behaviour: () => { + throw new Error("server is not running"); }, }); - const result = await registry.invoke("logs"); + const result = await registry.invoke("restart"); assert.equal(result.state, "failed"); - assert.match(result.error ?? "", /server is not running/); - assert.equal(registry.isGone("logs"), false); + assert.equal(result.error, "server is not running"); + assert.equal(registry.isGone("restart"), false); }); test("the dependency is activated before the first forward", async () => { - const { registry, activations } = harness({ declared: undefined }); - await registry.invoke("selectContext"); + const { registry, activations } = harness(); + await registry.invoke("restart"); assert.deepEqual(activations, [MCPPLS_EXTENSION_ID]); }); -test("invalidate re-reads the static declaration", () => { - let declared: readonly string[] = []; - const environment: CapabilityEnvironment = { - extensionInstalled: () => true, - declaredCommands: () => declared, - executeCommand: async (): Promise => undefined as T, - }; - const registry = new CapabilityRegistry(environment); - assert.equal(registry.status("moduleGraph").state, "undeclared"); - declared = ["mcppls.showModuleGraph"]; - registry.invalidate(); - assert.equal(registry.status("moduleGraph").state, "declared"); - assert.ok(registry.availableKeys().includes("moduleGraph")); -}); - -test("readState is never probed by calling a command", () => { - const { registry, calls } = harness({ declared: [] }); - assert.equal(registry.status("readState").state, "declared"); - assert.deepEqual(calls, []); -}); - test("an unknown capability fails without touching the dependency", async () => { - const { registry, calls } = harness(); + const { registry, calls, activations } = harness(); const result = await registry.invoke("nope"); assert.equal(result.state, "failed"); - assert.match(result.error ?? "", /unknown capability/); assert.deepEqual(calls, []); + assert.deepEqual(activations, []); }); diff --git a/mide-plugins/vscode/test/mcppls/contract.test.ts b/mide-plugins/vscode/test/mcppls/contract.test.ts index a66cfff..766f493 100644 --- a/mide-plugins/vscode/test/mcppls/contract.test.ts +++ b/mide-plugins/vscode/test/mcppls/contract.test.ts @@ -2,14 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { CLI_COMMANDS, DEPRECATED_COMMANDS } from "../../src/commands/ids"; -import { - CAPABILITIES, - MCPPLS_EXTENSION_ID, - VERIFIED_MCPPLS_RANGE, - capability, - capabilityProblems, - commandsOf, -} from "../../src/mcppls/contract"; +import { CAPABILITIES, MCPPLS_EXTENSION_ID, capability, capabilityProblems } from "../../src/mcppls/contract"; test("the capability table satisfies its own rules", () => { assert.deepEqual(capabilityProblems(), []); @@ -17,22 +10,12 @@ test("the capability table satisfies its own rules", () => { test("the dependency id is the published one", () => { assert.equal(MCPPLS_EXTENSION_ID, "sunrisepeak.mcpp-language-server"); - assert.equal(VERIFIED_MCPPLS_RANGE, ">=0.0.4"); }); -test("no capability is required: losing mcppls must not disable mcpp's own features", () => { - assert.ok(CAPABILITIES.every((entry) => entry.required === false)); -}); - -test("the refresh chain prefers the cheap reload and falls back to a restart", () => { - assert.deepEqual(commandsOf("refresh"), ["mcppls.reloadBuildDescription", "mcppls.restartServer"]); -}); - -test("every forwarded command is an mcppls command", () => { +test("the table keeps mcppls's action ids and no command id of either extension", () => { for (const entry of CAPABILITIES) { - for (const command of entry.commands) { - assert.ok(command.startsWith("mcppls."), `${entry.key} forwards ${command}`); - } + assert.ok(!entry.key.includes("."), `${entry.key} is a command id`); + assert.ok(!("commands" in entry), `${entry.key} keeps a command of mcppls's`); } }); @@ -53,5 +36,4 @@ test("capability keys are unique and unknown keys resolve to undefined", () => { const keys = CAPABILITIES.map((entry) => entry.key); assert.equal(new Set(keys).size, keys.length); assert.equal(capability("nope"), undefined); - assert.deepEqual(commandsOf("nope"), []); }); diff --git a/mide-plugins/vscode/test/mcppls/otherCppFeatures.test.ts b/mide-plugins/vscode/test/mcppls/otherCppFeatures.test.ts index 8f18ecd..b57eb83 100644 --- a/mide-plugins/vscode/test/mcppls/otherCppFeatures.test.ts +++ b/mide-plugins/vscode/test/mcppls/otherCppFeatures.test.ts @@ -1,11 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { - OTHER_CPP_SETTINGS, - localOtherCppFeatures, - otherCppFeaturesFromExports, -} from "../../src/mcppls/otherCppFeatures"; +import { OTHER_CPP_SETTINGS, localOtherCppFeatures } from "../../src/mcppls/otherCppFeatures"; const installed = (...ids: string[]) => (id: string) => ids.includes(id); const settings = (values: Record) => (section: string, key: string) => values[`${section}.${key}`]; @@ -22,7 +18,6 @@ test("the local read finds each candidate that is installed and not turned off", { extensionId: "ccls-project.ccls", displayName: "ccls", settable: false }, ], canRestore: false, - source: "local", }); }); @@ -34,36 +29,3 @@ test("the local read leaves out what is turned off by its own setting, or not in assert.deepEqual(found.active, []); assert.deepEqual(OTHER_CPP_SETTINGS, ["C_Cpp.intelliSenseEngine", "clangd.enable"]); }); - -test("mcppls's own answer is taken as it is, including whether it can restore", () => { - const exportsValue = { - otherCppFeatures: () => ({ - active: [{ extensionId: "llvm-vs-code-extensions.vscode-clangd", displayName: "clangd", settable: true }], - canRestore: true, - }), - }; - assert.deepEqual(otherCppFeaturesFromExports(exportsValue), { - active: [{ extensionId: "llvm-vs-code-extensions.vscode-clangd", displayName: "clangd", settable: true }], - canRestore: true, - source: "mcppls", - }); -}); - -test("an older mcppls, a throwing one, or a malformed answer means the local read is used", () => { - assert.equal(otherCppFeaturesFromExports(undefined), undefined); - assert.equal(otherCppFeaturesFromExports({ lastStatus: () => undefined }), undefined); - assert.equal( - otherCppFeaturesFromExports({ - otherCppFeatures: () => { - throw new Error("boom"); - }, - }), - undefined, - ); - assert.equal(otherCppFeaturesFromExports({ otherCppFeatures: () => ({ active: "clangd" }) }), undefined); - // Entries without an id are dropped rather than rendered as blanks. - assert.deepEqual( - otherCppFeaturesFromExports({ otherCppFeatures: () => ({ active: [{}, { extensionId: "x.y" }] }) }), - { active: [{ extensionId: "x.y", displayName: "x.y", settable: true }], canRestore: false, source: "mcppls" }, - ); -}); diff --git a/mide-plugins/vscode/test/mcppls/state.test.ts b/mide-plugins/vscode/test/mcppls/state.test.ts index b6e1eb0..1b45432 100644 --- a/mide-plugins/vscode/test/mcppls/state.test.ts +++ b/mide-plugins/vscode/test/mcppls/state.test.ts @@ -1,140 +1,69 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { describeState, readStateFromExports } from "../../src/mcppls/state"; +import type { LanguageServiceStatus } from "../../src/ide/protocol"; +import { describeState, readStateFromStatus } from "../../src/mcppls/state"; -const READY = { - state: "ready", - project: { root: "file:///w", source: "mcpp", level: 3, tier: 1 }, - profile: { kind: "build-toolchain", compiler: "clang 22.1.8", stdlib: "libc++ 22.1.8", target: "x86_64-linux-gnu", standard: "c++26" }, - engine: { name: "clangd", version: "23.1.0" }, - engines: [ - { name: "clangd", version: "23.1.0", role: "core", state: "ready" }, - { name: "mcppls", version: "0.0.9", role: "modules", state: "ready" }, - ], - progress: { done: 12, total: 12 }, - issues: [{ code: "unresolved-module", message: "sub not found" }], -}; - -const api = (status: unknown) => ({ lastStatus: () => status, statusBarText: () => "ready" }); +function status(overrides: Partial = {}): LanguageServiceStatus { + return { + state: "ok", + program: "/p/mcppls.okb", + source: "editor", + version: "0.0.20", + required: "0.0.18", + message: "mcppls 0.0.20", + actions: [], + statusSource: "status", + capabilities: ["status"], + serverActions: [], + workspace: { + root: "/w", + state: "ready", + engine: { name: "clangd", version: "23.1.0-mcppls.5", restarts: 1 }, + kit: { name: "mcppls-kit-x86_64-linux-gnu", state: "missing", install: "xlings install mcppls-kit-x86_64-linux-gnu", inUse: "openkal" }, + mcpp: { version: "2026.10.31.1", program: "/x/mcpp" }, + problems: [{ code: "kit-missing", message: "the kit is missing" }, { code: 3 }], + }, + instances: [{ pid: 1 }, { pid: 2 }], + ...overrides, + }; +} -test("reads the documented status shape", () => { - const view = readStateFromExports(api(READY), { version: "0.0.9", active: true, enabled: true }); +test("reads what mcppls status states about the workspace", () => { + const view = readStateFromStatus(status(), { enabled: true }); assert.equal(view.available, true); assert.equal(view.state, "ready"); - assert.equal(view.version, "0.0.9"); - assert.equal(view.active, true); + assert.equal(view.version, "0.0.20"); assert.equal(view.enabled, true); - assert.deepEqual(view.project, { root: "file:///w", source: "mcpp", level: 3, tier: 1 }); - assert.equal(view.profile?.stdlib, "libc++ 22.1.8"); - assert.equal(view.engine?.name, "clangd"); - assert.equal(view.engines?.length, 2); - assert.deepEqual(view.progress, { done: 12, total: 12 }); - assert.equal(view.issues?.[0]?.code, "unresolved-module"); -}); - -test("carries S3's own remedy for an issue", () => { - const view = readStateFromExports( - api({ - state: "degraded", - issues: [ - { - code: "producer-needs-download", - message: "the build tool needs a download", - command: { command: "mcppls.describeOnline", arguments: [], title: "Allow" }, - }, - ], - }), - ); - assert.equal(view.available, true); - assert.deepEqual(view.issues?.[0]?.command, { - command: "mcppls.describeOnline", - arguments: [], - title: "Allow", - }); -}); - -test("an issue without a well-formed command keeps its code and message", () => { - const view = readStateFromExports( - api({ state: "degraded", issues: [{ code: "x", message: "y", command: { arguments: [1] } }] }), - ); - assert.equal(view.issues?.[0]?.code, "x"); - assert.equal(view.issues?.[0]?.command, undefined); -}); - -test("no API object means available: false with a reason", () => { - for (const exports of [undefined, null, 42, "text", [], {}]) { - const view = readStateFromExports(exports); - assert.equal(view.available, false, JSON.stringify(exports)); - assert.equal(typeof view.reason, "string"); - } -}); - -test("an API object without lastStatus() is reported, not thrown", () => { - const view = readStateFromExports({ statusBarText: () => "ready" }); - assert.equal(view.available, false); - assert.match(view.reason ?? "", /lastStatus/); -}); - -test("a throwing lastStatus() is contained", () => { - const view = readStateFromExports({ - lastStatus: () => { - throw new Error("boom"); - }, - }); - assert.equal(view.available, false); - assert.match(view.reason ?? "", /boom/); -}); - -test("a status that has not arrived yet is not an error", () => { - const view = readStateFromExports(api(undefined)); - assert.equal(view.available, false); - assert.match(view.reason ?? "", /no status yet/); + assert.deepEqual(view.engine, { name: "clangd", version: "23.1.0-mcppls.5", restarts: 1 }); + assert.equal(view.kit?.state, "missing"); + assert.equal(view.kit?.inUse, "openkal"); + assert.equal(view.mcpp?.version, "2026.10.31.1"); + assert.equal(view.instances, 2); + assert.deepEqual(view.issues, [{ code: "kit-missing", message: "the kit is missing" }]); }); -test("an unknown state value is refused rather than rendered", () => { - const view = readStateFromExports(api({ state: "melted" })); - assert.equal(view.available, false); - assert.match(view.reason ?? "", /unknown state/); -}); - -test("every documented state is accepted", () => { - for (const state of ["starting", "loading", "preparing", "ready", "degraded", "error"]) { - const view = readStateFromExports(api({ state })); - assert.equal(view.available, true, state); - assert.equal(view.state, state); +test("each state of the workspace is kept, an unknown one is not", () => { + for (const state of ["ready", "starting", "degraded", "stopped", "none"]) { + assert.equal(readStateFromStatus(status({ workspace: { state } })).state, state); } + const melted = readStateFromStatus(status({ workspace: { state: "melted" } })); + assert.equal(melted.available, false); + assert.match(melted.reason ?? "", /unknown state/); }); -test("partially formed fields are dropped instead of producing half-objects", () => { - const view = readStateFromExports( - api({ - state: "ready", - project: { root: "file:///w" }, - profile: { kind: "semantic-kit" }, - engine: { name: "clangd" }, - progress: { done: "many", total: 3 }, - engines: [{ name: "clangd" }], - onlineRun: { outcome: "fetched" }, - }), - ); - assert.equal(view.available, true); - assert.equal(view.project, undefined); - assert.equal(view.profile, undefined); - assert.equal(view.engine, undefined); - assert.equal(view.progress, undefined); - assert.equal(view.engines, undefined); - assert.equal(view.onlineRun, undefined); -}); - -test("onlineRun is surfaced when complete", () => { - const view = readStateFromExports( - api({ state: "ready", onlineRun: { outcome: "fetched", message: "got it", at: "2026-10-02T12:40:11Z" } }), - ); - assert.deepEqual(view.onlineRun, { outcome: "fetched", message: "got it", at: "2026-10-02T12:40:11Z" }); +test("an mcppls before status, a status that failed, or no mcppls says why there is no state", () => { + const frozen = readStateFromStatus(status({ version: "0.0.19", statusSource: "frozen", workspace: null })); + assert.equal(frozen.available, false); + assert.match(frozen.reason ?? "", /0\.0\.19 has no status command/); + const failed = readStateFromStatus(status({ statusSource: "frozen", statusError: "did not answer within 5 s", workspace: null })); + assert.match(failed.reason ?? "", /did not answer/); + const missing = readStateFromStatus(status({ state: "missing", message: "mcppls was not found" })); + assert.equal(missing.reason, "mcppls was not found"); + assert.match(readStateFromStatus(undefined).reason ?? "", /not answered yet/); }); -test("describeState is a single readable line either way", () => { - assert.match(describeState(readStateFromExports(api(READY))), /ready · project mcpp · engine clangd/); - assert.match(describeState(readStateFromExports(undefined)), /^unavailable \(/); +test("one line for the self-check", () => { + assert.match(describeState(readStateFromStatus(status())), /^ready · engine clangd 23\.1\.0-mcppls\.5 · kit mcppls-kit-x86_64-linux-gnu \(missing\) · 2 servers · 1 issue\(s\)$/); + assert.match(describeState(readStateFromStatus(undefined)), /^unavailable \(/); }); diff --git a/mide-plugins/vscode/test/mcppls/stateSource.test.ts b/mide-plugins/vscode/test/mcppls/stateSource.test.ts index ba34eba..456695f 100644 --- a/mide-plugins/vscode/test/mcppls/stateSource.test.ts +++ b/mide-plugins/vscode/test/mcppls/stateSource.test.ts @@ -4,51 +4,39 @@ import path from "node:path"; import test from "node:test"; /** - * The settings that shape the C++ Modules view, asserted on the source itself. + * The C++ Modules state read, asserted on the source itself. * - * `src/mcppls/stateSource.ts` has no seam a pure test could drive: the decision - * it makes — read `extension.exports` or not — *is* the `vscode` boundary. So - * this gate is checked where it is written. It is deliberately about structure, - * not wording, so it keeps working when the sentence around the gate changes. + * `src/mcppls/stateSource.ts` is the `vscode` boundary, so its two rules are checked where they are + * written: the state is the backend's `mcppls status` (design 2026-10-30 part 2 §5.2), never the + * mcppls extension's API object; and `mcpp.languageService.readState` switches it off before + * anything is read. Structure, not wording. */ const SOURCE = path.join(process.cwd(), "src", "mcppls", "stateSource.ts"); -function source(): string { - return readFileSync(SOURCE, "utf8"); -} - /** The text without comments, so a doc comment cannot satisfy or break the scan. */ function code(): string { - return source() + return readFileSync(SOURCE, "utf8") .replace(/\/\*[\s\S]*?\*\//g, "") .replace(/\/\/[^\n]*/g, ""); } -test("mcpp.languageService.readState is read, and its gate precedes every use of extension.exports", () => { +test("the mcppls extension's API object is not read", () => { + assert.ok(!/\.exports\b/.test(code()), "stateSource.ts reads extension.exports"); +}); + +test("mcpp.languageService.readState is read, and its gate precedes the state", () => { const text = code(); const gate = text.indexOf('read("mcpp.languageService.readState")'); - assert.ok(gate >= 0, "stateSource.ts must read mcpp.languageService.readState to switch the read off"); - - const uses = [...text.matchAll(/\.exports\b/g)].map((match) => match.index ?? -1); - assert.ok(uses.length > 0, "the file must still read extension.exports, or the gate proves nothing"); - for (const index of uses) { - assert.ok( - index > gate, - "every use of extension.exports must come after the readState gate, or the setting cannot switch the read off", - ); - } - - // The gate returns "not available" with a reason rather than falling through - // to a partial read. + assert.ok(gate >= 0, "stateSource.ts must read mcpp.languageService.readState to switch the state off"); + const use = text.indexOf("readStateFromStatus(status"); + assert.ok(use > gate, "the state must be read after the readState gate"); const after = text.slice(gate, gate + 400); assert.match(after, /available:\s*false/); assert.match(after, /reason:/); }); -test("every failure path in stateSource.ts stays non-throwing", () => { +test("readLanguageServerState never throws", () => { const boundary = code().slice(code().indexOf("export function readLanguageServerState")); - assert.ok(boundary.includes("try {"), "the exports read must stay inside try/catch"); - assert.ok(boundary.includes("catch"), "the exports read must have a catch that returns a reason"); assert.ok(!/\bthrow\b/.test(boundary), "readLanguageServerState must never throw"); }); diff --git a/protocol/protocol.schema.json b/protocol/protocol.schema.json index f9c1de9..a20ae51 100644 --- a/protocol/protocol.schema.json +++ b/protocol/protocol.schema.json @@ -81,8 +81,8 @@ } }, "languageService/status": { - "description": "Which mcppls answers this editor's C++ files and whether it reads mcpp's result record (0.0.18 and later). Candidates are the servers the adapter knows of (the mcppls extension's or plugin's own); PATH, xlings and the plugin payload follow.", - "params": { "type": "object", "properties": { "candidates": { "type": "array", "items": { "type": "string" } } } }, + "description": "Which mcppls answers this editor's C++ files, whether it reads mcpp's result record (0.0.18 and later), and what it says about the workspace: `mcppls status` (0.0.20 and later) or the frozen table. Candidates are the servers the adapter knows of (the mcppls extension's or plugin's own); PATH, xlings and the plugin payload follow.", + "params": { "type": "object", "properties": { "candidates": { "type": "array", "items": { "type": "string" } }, "root": { "type": "string", "description": "The workspace to ask mcppls about (default: the first folder)" } } }, "result": { "$ref": "#/$defs/languageService" } }, "projects/list": { @@ -416,7 +416,8 @@ "result/changed": { "description": "A project's result record changed: a build finished, whoever ran it.", "params": { "type": "object", "required": ["root", "result"] } }, "manifest/changed": { "params": { "type": "object", "required": ["root"] } }, "manifest/diagnostics": { "params": { "type": "object", "required": ["root", "diagnostics"] } }, - "project/mcppChanged": { "description": "A job's envelope named another mcpp than the project's was asked as: it is asked again on its next use.", "params": { "type": "object", "required": ["root", "version"] } } + "project/mcppChanged": { "description": "A job's envelope named another mcpp than the project's was asked as: it is asked again on its next use.", "params": { "type": "object", "required": ["root", "version"] } }, + "languageService/changed": { "description": "mcppls answered differently from the last time it was asked: another program or version, other actions, or another state of the workspace. An editor that lists the commands asks for them again.", "params": { "type": "object", "required": ["root", "state", "workspaceState"] } } }, "$defs": { "settingValues": { "type": "object", "additionalProperties": true }, @@ -437,7 +438,26 @@ "version": { "type": "string" }, "required": { "type": "string" }, "message": { "type": "string" }, - "actions": { "type": "array", "items": { "type": "object", "required": ["id", "title"], "properties": { "id": { "enum": ["install", "update"] } } } } + "actions": { "type": "array", "items": { "type": "object", "required": ["id", "title"], "properties": { "id": { "enum": ["install", "update"] } } } }, + "root": { "type": "string", "description": "The workspace mcppls was asked about" }, + "statusSource": { "enum": ["status", "frozen", "none"], "description": "`mcppls status` (0.0.20 and later), the frozen table of 0.0.18 and 0.0.19, or no mcppls that reads the result record" }, + "statusError": { "type": "string", "description": "Why `mcppls status` could not be read; the actions are then the frozen table's" }, + "capabilities": { "type": "array", "items": { "type": "string" } }, + "serverActions": { + "type": "array", + "description": "mcppls's actions under each editor's own id (mcppls S3 section 8)", + "items": { + "type": "object", "required": ["id", "title"], + "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "vscode": { "type": "string" }, "vscodeArguments": { "type": "array" }, + "jetbrains": { "type": "string" }, "nvim": { "type": "string" }, "command": { "type": "string" } } + } + }, + "workspace": { + "type": ["object", "null"], + "properties": { "root": { "type": "string" }, "state": { "enum": ["ready", "starting", "degraded", "stopped", "none"] }, "since": { "type": "string" }, + "engine": { "type": "object" }, "kit": { "type": "object" }, "mcpp": { "type": "object" }, "problems": { "type": "array" } } + }, + "instances": { "type": "array", "items": { "type": "object" } } } }, "command": { diff --git a/spec/commands.json b/spec/commands.json index fed388a..850590c 100644 --- a/spec/commands.json +++ b/spec/commands.json @@ -81,28 +81,29 @@ { "id": "mcpp.languageServer.refreshState", "group": "languageService", "action": { "kind": "languageService" } }, { "id": "mcpp.openMcpplsSettings", "group": "languageService", "palette": false, "editors": [ "vscode" ], "action": { "kind": "editor", "vscode": "workbench.action.openSettings", "args": [ "@ext:sunrisepeak.mcpp-language-server" ] } }, - { "id": "mcpp.languageServer.restart", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.restartServer" ] } }, - { "id": "mcpp.languageServer.restartEngine", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.restartClangd" ] } }, - { "id": "mcpp.languageServer.resetWorkspaceCache", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.resetWorkspaceCache" ] } }, - { "id": "mcpp.languageServer.selectContext", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.selectContext" ] } }, - { "id": "mcpp.languageServer.showModuleGraph", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.showModuleGraph" ] } }, - { "id": "mcpp.languageServer.showLogs", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.showLogs" ] } }, - { "id": "mcpp.languageServer.collectReport", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.collectReport" ] } }, - { "id": "mcpp.languageServer.exportDiagnosticBundle", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.exportDiagnosticBundle" ] } }, - { "id": "mcpp.languageServer.revealBundle", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.revealCacheDirectory" ] } }, - { "id": "mcpp.languageServer.openLogFolder", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.revealCacheDirectory" ] } }, - { "id": "mcpp.languageServer.runBuildToolInTerminal", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.runBuildToolInTerminal" ] } }, - { "id": "mcpp.languageServer.manageConflicts", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.turnOffOtherCppFeatures" ] } }, + { "id": "mcpp.languageServer.restart", "group": "languageService", "action": {"kind": "forward", "mcppls": "restart"} }, + { "id": "mcpp.languageServer.restartEngine", "group": "languageService", "action": {"kind": "forward", "mcppls": "restartEngine"} }, + { "id": "mcpp.languageServer.resetWorkspaceCache", "group": "languageService", "action": {"kind": "forward", "mcppls": "resetCache"} }, + { "id": "mcpp.languageServer.selectContext", "group": "languageService", "action": {"kind": "forward", "mcppls": "selectContext"} }, + { "id": "mcpp.languageServer.showModuleGraph", "group": "languageService", "action": {"kind": "forward", "mcppls": "moduleGraph"} }, + { "id": "mcpp.languageServer.showLogs", "group": "languageService", "action": {"kind": "forward", "mcppls": "showLog"} }, + { "id": "mcpp.languageServer.collectReport", "group": "languageService", "action": {"kind": "forward", "mcppls": "collectReport"} }, + { "id": "mcpp.languageServer.exportDiagnosticBundle", "group": "languageService", "action": {"kind": "forward", "mcppls": "exportDiagnostics"} }, + { "id": "mcpp.languageServer.revealBundle", "group": "languageService", "action": {"kind": "forward", "mcppls": "openCacheDirectory"} }, + { "id": "mcpp.languageServer.openLogFolder", "group": "languageService", "action": {"kind": "forward", "mcppls": "openLogDirectory"} }, + { "id": "mcpp.languageServer.runBuildToolInTerminal", "group": "languageService", "action": {"kind": "forward", "mcppls": "runBuildTool"} }, + { "id": "mcpp.languageServer.manageConflicts", "group": "languageService", "action": {"kind": "forward", "mcppls": "turnOffOtherCppFeatures"} }, { "id": "mcpp.languageServer.restoreConflicts", "group": "languageService", "menus": [ { "menu": "view/item/context", "when": "view == mcpp.project && viewItem =~ /^mcppOtherCppFeatures.*Restorable$/" } ], - "action": { "kind": "forward", "commands": [ "mcppls.restoreOtherCppFeatures" ] } }, - { "id": "mcpp.languageServer.toggleInWorkspace", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.turnOffInWorkspace", "mcppls.turnOnInWorkspace" ] } }, - { "id": "mcpp.languageServer.installTools", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.installCommandLineTools" ] } }, - { "id": "mcpp.languageServer.reviewChanges", "group": "languageService", "action": { "kind": "forward", "commands": [ "mcppls.review.run" ] } }, - { "id": "mcpp.configureLanguageServer", "group": "languageService", "deprecated": true, "action": { "kind": "forward", "commands": [ "mcppls.selectContext" ] } }, - { "id": "mcpp.configureClangd", "group": "languageService", "deprecated": true, "action": { "kind": "forward", "commands": [ "mcppls.selectContext" ] } }, - { "id": "mcpp.checkModuleSupport", "group": "languageService", "deprecated": true, "action": { "kind": "forward", "commands": [ "mcppls.restartServer" ] } }, - { "id": "mcpp.showModuleGraph", "group": "languageService", "deprecated": true, "action": { "kind": "forward", "commands": [ "mcppls.showModuleGraph" ] } }, - { "id": "mcpp.showLanguageServerLogs", "group": "languageService", "deprecated": true, "action": { "kind": "forward", "commands": [ "mcppls.showLogs" ] } } + "action": {"kind": "forward", "mcppls": "restoreOtherCppFeatures"} }, + { "id": "mcpp.languageServer.toggleInWorkspace", "group": "languageService", "action": {"kind": "forward", "mcppls": "turnOffInWorkspace"} }, + { "id": "mcpp.languageServer.installTools", "group": "languageService", "action": {"kind": "forward", "mcppls": "installTools"} }, + { "id": "mcpp.languageServer.installKit", "group": "languageService", "action": {"kind": "forward", "mcppls": "installKit"} }, + { "id": "mcpp.languageServer.reviewChanges", "group": "languageService", "action": {"kind": "forward", "mcppls": "reviewRun"} }, + { "id": "mcpp.configureLanguageServer", "group": "languageService", "deprecated": true, "action": {"kind": "forward", "mcppls": "selectContext"} }, + { "id": "mcpp.configureClangd", "group": "languageService", "deprecated": true, "action": {"kind": "forward", "mcppls": "selectContext"} }, + { "id": "mcpp.checkModuleSupport", "group": "languageService", "deprecated": true, "action": {"kind": "forward", "mcppls": "restart"} }, + { "id": "mcpp.showModuleGraph", "group": "languageService", "deprecated": true, "action": {"kind": "forward", "mcppls": "moduleGraph"} }, + { "id": "mcpp.showLanguageServerLogs", "group": "languageService", "deprecated": true, "action": {"kind": "forward", "mcppls": "showLog"} } ] } diff --git a/spec/i18n/manifest.en.json b/spec/i18n/manifest.en.json index 2b304ad..bee4403 100644 --- a/spec/i18n/manifest.en.json +++ b/spec/i18n/manifest.en.json @@ -16,6 +16,7 @@ "command.mcpp.languageServer.collectReport.title": "C++ Modules: Collect a Diagnostic Report", "command.mcpp.languageServer.exportDiagnosticBundle.title": "C++ Modules: Export a Diagnostic Bundle", "command.mcpp.languageServer.installTools.title": "C++ Modules: Install Command Line Tools", + "command.mcpp.languageServer.installKit.title": "C++ Modules: Install the Semantic Kit", "command.mcpp.languageServer.manageConflicts.title": "C++ Modules: Turn Off Other C++ Language Services", "command.mcpp.languageServer.openLogFolder.title": "Open the C++ Modules log folder", "command.mcpp.languageServer.refreshState.title": "C++ Modules: Refresh This View", diff --git a/spec/i18n/manifest.zh-cn.json b/spec/i18n/manifest.zh-cn.json index e22b4f7..dc5c1b7 100644 --- a/spec/i18n/manifest.zh-cn.json +++ b/spec/i18n/manifest.zh-cn.json @@ -16,6 +16,7 @@ "command.mcpp.languageServer.collectReport.title": "C++ Modules: 收集诊断报告", "command.mcpp.languageServer.exportDiagnosticBundle.title": "C++ Modules: 导出诊断包", "command.mcpp.languageServer.installTools.title": "C++ Modules: 安装命令行工具", + "command.mcpp.languageServer.installKit.title": "C++ Modules: 安装语义套件", "command.mcpp.languageServer.manageConflicts.title": "C++ Modules: 关闭其他 C++ 语言服务", "command.mcpp.languageServer.openLogFolder.title": "打开 C++ Modules 日志目录", "command.mcpp.languageServer.refreshState.title": "C++ Modules: 刷新此视图", diff --git a/src/bin/mockmcppls.cpp b/src/bin/mockmcppls.cpp new file mode 100644 index 0000000..a7ee4d8 --- /dev/null +++ b/src/bin/mockmcppls.cpp @@ -0,0 +1,72 @@ +// A stand-in for mcppls in the tests: its version and `mcppls status` (mcppls S3 section 8). +// +// It is placed in a payload the test makes (`/bin/mcppls`, named by payload.json), and reads +// beside it: +// +// /mock-version the version `--version` prints (default 0.0.20) +// +// and in the root `status` is asked about: +// +// /.mock-mcppls-state the workspace's state (default `none`) +// +// `status` states three actions: `restart` for every editor, `restartEngine` for VS Code alone, and +// `openLogDirectory` with VS Code arguments. +import std; +import mcpplibs.os; + +namespace { + +std::string slurp(const std::filesystem::path& p) { + std::ifstream in(p, std::ios::binary); + std::string text(std::istreambuf_iterator(in), {}); + while (!text.empty() && (text.back() == '\n' || text.back() == '\r' || text.back() == ' ')) text.pop_back(); + return text; +} + +void out(std::string_view text) { + (void)mcpplibs::os::stdio::write_output(text); + (void)mcpplibs::os::stdio::flush_output(); +} + +std::string json_string(std::string_view s) { + std::string q = "\""; + for (char c : s) { + if (c == '"' || c == '\\') q.push_back('\\'); + q.push_back(c); + } + return q + "\""; +} + +} // namespace + +int main(int argc, char** argv) { + const std::vector args(argv + 1, argv + argc); + const auto payload = std::filesystem::absolute(argv[0]).parent_path().parent_path(); + std::string version = slurp(payload / "mock-version"); + if (version.empty()) version = "0.0.20"; + if (!args.empty() && args[0] == "--version") { + out(std::format("mcppls {}\n", version)); + return 0; + } + if (!args.empty() && args[0] == "status") { + std::string root; + for (std::size_t i = 1; i + 1 < args.size(); ++i) + if (args[i] == "--root") root = args[i + 1]; + std::string state = slurp(std::filesystem::path(root) / ".mock-mcppls-state"); + if (state.empty()) state = "none"; + out(std::format(R"({{"schema": 1, "server": {{"version": {}, "platform": "test", "payload": {}}}, +"capabilities": ["status"], +"workspace": {{"root": {}, "state": {}}}, +"instances": [], +"actions": [ + {{"id": "restart", "title": "Restart the language server", "vscode": "mcppls.restartServer", "jetbrains": "community.mcpp.mcppls.RestartServer", "nvim": "McpplsRestart"}}, + {{"id": "restartEngine", "title": "Restart the engine", "vscode": "mcppls.restartClangd"}}, + {{"id": "openLogDirectory", "title": "Open the log directory", "vscode": "mcppls.revealCacheDirectory", "vscodeArguments": ["logs"]}} +]}} +)", + json_string(version), json_string(payload.generic_string()), json_string(root), json_string(state))); + return 0; + } + out("usage: mcppls [--version | status --root --format json]\n"); + return 2; +} diff --git a/src/languageservice.cppm b/src/languageservice.cppm index 73a51c3..633cbeb 100644 --- a/src/languageservice.cppm +++ b/src/languageservice.cppm @@ -8,6 +8,12 @@ // in a terminal or by an agent -- so a build through mcpp-ide needs nothing forwarded to it. An older // mcppls is reported with what to do, never degraded around in silence. // +// WHAT mcppls SAYS ABOUT A WORKSPACE IS ITS STATUS. mcppls 0.0.20 and later answer `mcppls status --root +// --format json` (mcppls S3 section 8) without starting an engine: its capabilities, the +// workspace's state, its engine and kit, and mcppls's actions under each editor's own id. The editors +// show that one document. mcppls 0.0.18 and 0.0.19 have no `status`; for them the actions are the ones +// those releases offered (the frozen table) and the workspace is not known. +// // A SERVER IS A COMMAND. mcppls's payload (the VSIX's and the CLion package's `payload/`, the one Zed's // install puts in the user data directory) carries the server as one okb of both architectures that the // platform's runner starts: payload.json's `server` names `runner` and `path` (`bin/okrun bin/mcppls.okb`), @@ -26,6 +32,8 @@ using Json = nlohmann::json; // The first mcppls that reads the result record. inline constexpr std::string_view kRequired = "0.0.18"; +// The first mcppls that answers `mcppls status`. +inline constexpr std::string_view kStatusSince = "0.0.20"; enum class State { ok, outdated, missing, unreadable }; std::string_view state_name(State s); @@ -61,6 +69,25 @@ std::vector candidates(const std::vector& fromEditor); // Finds mcppls among the candidates and reads its version. Status probe(const std::vector& fromEditor); + +// mcppls's account of a workspace: `mcppls status`'s document, or the frozen table for an mcppls +// before it. +struct Report { + std::string source = "none"; // "status" | "frozen" | "none" + Json capabilities = Json::array(); + Json actions = Json::array(); // [{id, title, vscode?, vscodeArguments?, jetbrains?, nvim?, command?}] + Json workspace; // null when mcppls was not asked + Json instances = Json::array(); + std::string detail; // why `status` could not be read +}; + +// The actions mcppls 0.0.18 and 0.0.19 offered, under the ids those releases gave them. +const Json& frozen_actions(); +// The report for the mcppls `s` found, about `root`: `status` when it has the command, else the frozen +// table. Bounded: a status that does not answer within five seconds is reported, not waited for. +Report report(const Status& s, const std::filesystem::path& root); +// The report `mcppls status --format json` printed `output` for (the tests). +Report parse_status(std::string_view output); // The status for an mcppls at `program` whose `--version` printed `output` (the tests). Status classify(const std::filesystem::path& program, std::string source, std::string_view output); @@ -187,4 +214,88 @@ Status probe(const std::vector& fromEditor) { return s; } +const Json& frozen_actions() { + static const Json table = [] { + auto row = [](std::string_view id, std::string_view title, Json editors) { + Json r{{"id", id}, {"title", title}}; + r.update(editors); + return r; + }; + return Json::array({ + row("restart", "Restart the language server", {{"vscode", "mcppls.restartServer"}, {"nvim", "McpplsRestart"}}), + row("reload", "Read the build description again", {{"vscode", "mcppls.reloadBuildDescription"}, {"nvim", "McpplsReload"}}), + row("restartEngine", "Restart the engine", {{"vscode", "mcppls.restartClangd"}}), + row("resetCache", "Reset the workspace cache", {{"vscode", "mcppls.resetWorkspaceCache"}, {"nvim", "McpplsResetCache"}}), + row("showStatus", "Show the status", {{"nvim", "McpplsStatus"}}), + row("showLog", "Show the log", {{"vscode", "mcppls.showLogs"}}), + row("openLogDirectory", "Open the log directory", {{"vscode", "mcppls.revealCacheDirectory"}, {"vscodeArguments", Json::array({"logs"})}}), + row("openCacheDirectory", "Open the workspace cache directory", {{"vscode", "mcppls.revealCacheDirectory"}}), + row("collectReport", "Collect a report", {{"vscode", "mcppls.collectReport"}}), + row("exportDiagnostics", "Export a diagnostic bundle", {{"vscode", "mcppls.exportDiagnosticBundle"}}), + row("selectContext", "Select the analysis context", {{"vscode", "mcppls.selectContext"}}), + row("moduleGraph", "Show the module graph", {{"vscode", "mcppls.showModuleGraph"}}), + row("runBuildTool", "Run the build tool in a terminal", {{"vscode", "mcppls.runBuildToolInTerminal"}}), + row("installTools", "Install the command line tools", {{"vscode", "mcppls.installCommandLineTools"}}), + row("reviewRun", "Review the workspace's changes", {{"vscode", "mcppls.review.run"}}), + row("reviewClear", "Clear the review", {{"vscode", "mcppls.review.clear"}}), + row("turnOffInWorkspace", "Turn off in this workspace", {{"vscode", "mcppls.turnOffInWorkspace"}}), + row("turnOnInWorkspace", "Turn on in this workspace", {{"vscode", "mcppls.turnOnInWorkspace"}}), + // 0.0.19's CLion plugin; under 0.0.18 the action is not there, and the IDE finds no action by the id. + row("turnOffOtherCppFeatures", "Turn off other C++ features", + {{"vscode", "mcppls.turnOffOtherCppFeatures"}, {"jetbrains", "community.mcpp.mcppls.UseMcpplsOnly"}}), + row("restoreOtherCppFeatures", "Restore other C++ features", + {{"vscode", "mcppls.restoreOtherCppFeatures"}, {"jetbrains", "community.mcpp.mcppls.RestoreClionEngine"}}), + }); + }(); + return table; +} + +Report parse_status(std::string_view output) { + Report r; + const Json doc = Json::parse(output, nullptr, false); + if (!doc.is_object() || doc.value("schema", 0) < 1) { + r.source = "frozen"; + r.actions = frozen_actions(); + r.detail = "`mcppls status` printed no status document"; + return r; + } + r.source = "status"; + if (doc.contains("capabilities") && doc["capabilities"].is_array()) + for (const auto& c : doc["capabilities"]) if (c.is_string()) r.capabilities.push_back(c); + if (doc.contains("actions") && doc["actions"].is_array()) + for (const auto& a : doc["actions"]) if (a.is_object() && a.contains("id") && a["id"].is_string()) r.actions.push_back(a); + if (doc.contains("workspace") && doc["workspace"].is_object()) r.workspace = doc["workspace"]; + if (doc.contains("instances") && doc["instances"].is_array()) r.instances = doc["instances"]; + return r; +} + +Report report(const Status& s, const std::filesystem::path& root) { + if (s.state != State::ok || s.command.empty()) return Report{}; + if (mcpp::compare_versions(s.version, kStatusSince) < 0) { + Report r; + r.source = "frozen"; + r.actions = frozen_actions(); + return r; + } + mcpplibs::os::process::spawn_options o; + o.program = s.command.front(); + for (std::size_t i = 1; i < s.command.size(); ++i) o.args.push_back(s.command[i].string()); + for (const char* a : {"status", "--format", "json", "--root"}) o.args.push_back(a); + o.args.push_back(root.string()); + o.in = mcpplibs::os::process::stdio::null; + mcpplibs::os::process::bounds limits; + limits.hard = std::chrono::milliseconds(5000); + limits.out_limit = std::size_t{1} << 20; + limits.err_limit = std::size_t{64} << 10; + auto run = mcpplibs::os::process::run(o, limits); + if (!run) { + Report r = parse_status(""); + r.detail = run.error().message(); + return r; + } + Report r = parse_status(run->out); + if (run->timed_out) r.detail = "`mcppls status` did not answer within 5 s"; + return r; +} + } // namespace mcppide::languageservice diff --git a/src/server.cppm b/src/server.cppm index 8ded7ef..5e07e5d 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -49,10 +49,10 @@ inline constexpr std::array kMethods = { "capabilities/explain", "report/collect", "shutdown", "exit"}; // The notifications the backend sends. -inline constexpr std::array kNotifications = { +inline constexpr std::array kNotifications = { "job/started", "job/output", "job/plan", "job/progress", "job/diagnostic", "job/artifact", "job/record", "job/finished", "test/result", "test/summary", "result/changed", "manifest/changed", - "manifest/diagnostics", "project/mcppChanged"}; + "manifest/diagnostics", "project/mcppChanged", "languageService/changed"}; class Session { public: @@ -81,7 +81,8 @@ private: Result manifest_check(const Json& p); Result language_service(const Json& p); Result targets(const Json& p); - Result commands(const Json& p) const; + Result commands(const Json& p); + Json server_actions(); Result quick_menu(const Json& p); Result test_list(const Json& p); Result project_mcpp(const Json& p); @@ -126,6 +127,10 @@ private: std::string clientName_ = "mcpp-ide"; std::string editor_ = "vscode"; std::vector mcpplsCandidates_; + // mcppls's actions as its last report named them (null until one was asked), and what the editors + // were last told about it, so that a change is sent once. + Json mcpplsActions_; + std::string mcpplsSeen_; std::vector folders_; std::unique_ptr jobs_; std::unique_ptr watcher_; @@ -887,6 +892,8 @@ std::expected Session::language_service(const Json& p) { else mcpplsCandidates_ = candidates; } const auto s = languageservice::probe(candidates); + const auto root = any_root(p); + const auto report = languageservice::report(s, root); const std::string required(languageservice::kRequired); Json command = Json::array(); for (const auto& part : s.command) command.push_back(part.generic_string()); @@ -913,9 +920,41 @@ std::expected Session::language_service(const Json& p) { break; } out["actions"] = actions; + // What mcppls says about the workspace (§5.2): its capabilities, the workspace's state and + // mcppls's actions under each editor's id, from `mcppls status` or the frozen table. + out["root"] = root.generic_string(); + out["statusSource"] = report.source; + out["capabilities"] = report.capabilities; + out["serverActions"] = report.actions; + out["workspace"] = report.workspace; + out["instances"] = report.instances; + if (!report.detail.empty()) out["statusError"] = report.detail; + // A change the editors did not see yet: another mcppls, other actions, another state. + const auto seen = Json{s.version, s.program.generic_string(), report.actions, + report.workspace.is_object() ? report.workspace.value("state", "") : std::string{}}.dump(); + bool changed = false; + { + std::lock_guard lock(mutex_); + changed = !mcpplsSeen_.empty() && mcpplsSeen_ != seen; + mcpplsSeen_ = seen; + mcpplsActions_ = report.actions; + } + if (changed) rpc_.notify("languageService/changed", Json{{"root", out["root"]}, {"state", out["state"]}, + {"workspaceState", report.workspace.is_object() ? report.workspace.value("state", "none") : "none"}}); return out; } +// mcppls's actions, asked for once when no report has named them yet. +Json Session::server_actions() { + { + std::lock_guard lock(mutex_); + if (!mcpplsActions_.is_null()) return mcpplsActions_; + } + (void)language_service(Json::object()); + std::lock_guard lock(mutex_); + return mcpplsActions_.is_null() ? Json::array() : mcpplsActions_; +} + mcpp::ProjectMcpp Session::probe_of(const std::filesystem::path& rootIn) { const auto root = rootIn.lexically_normal(); mcpp::Client client; @@ -1096,7 +1135,7 @@ std::expected Session::targets(const Json& p) { // The command table for one editor, in the user's language: what the adapter registers at run time // beside the declarations generated from the same table (spec/commands.json), or, in Neovim, which // declares nothing, all it offers. Each row names its subcommand, the name a command line types. -std::expected Session::commands(const Json& p) const { +std::expected Session::commands(const Json& p) { std::string editor, locale; { std::lock_guard lock(mutex_); @@ -1107,12 +1146,26 @@ std::expected Session::commands(const Json& p) const { locale = locale_; } const auto& table = spec::commands(); + // A forward runs mcppls's action under this editor's id, which mcppls states (§5.3); an action it + // gives this editor no id for is not offered. + Json actions = Json::array(); + const bool forwards = std::ranges::any_of(table["commands"], [&](const Json& c) { + return spec::offered_in(c, editor) && c.contains("action") && c["action"].value("kind", "") == "forward"; + }); + if (forwards) actions = server_actions(); Json groups = Json::object(); for (const auto& g : table["groups"]) groups[g.value("id", "")] = spec::manifest_string(g.value("title", ""), locale); Json out = Json::array(); for (const auto& c : table["commands"]) { if (!spec::offered_in(c, editor)) continue; Json row = c; + if (c.contains("action") && c["action"].value("kind", "") == "forward") { + const auto wanted = c["action"].value("mcppls", ""); + const auto it = std::ranges::find_if(actions, [&](const Json& a) { return a.value("id", "") == wanted && a.contains(editor); }); + if (it == actions.end()) continue; + row["action"]["command"] = (*it)[editor]; + if (editor == "vscode" && it->contains("vscodeArguments")) row["action"]["arguments"] = (*it)["vscodeArguments"]; + } const auto id = c.value("id", ""); row["title"] = spec::manifest_string(std::format("command.{}.title", id), locale); row["subcommand"] = spec::subcommand_of(c); diff --git a/src/spec.cppm b/src/spec.cppm index 2de63c7..e58ec5b 100644 --- a/src/spec.cppm +++ b/src/spec.cppm @@ -34,7 +34,8 @@ const Json& buildscript_api(); const Json& commands(); // Whether an editor (the protocol's `editor`: vscode, jetbrains, nvim, cli) offers a command: the // row's `editors` when it has them (`omitted` says why an editor is left out), else every editor; -// a forward to mcppls's own commands or an editor's own command only where those exist (VS Code). +// a forward to one of mcppls's actions in every editor (the backend offers it where mcppls names an +// id for that editor), an editor's own command only in VS Code. bool offered_in(const Json& command, std::string_view editor); // The name a command line types for a command (`:Mcpp ` in Neovim): the row's `subcommand`, // else its id after `mcpp.`. @@ -117,7 +118,8 @@ bool offered_in(const Json& c, std::string_view editor) { if (c.contains("editors") && c["editors"].is_array()) return std::ranges::any_of(c["editors"], [&](const Json& e) { return e.is_string() && e.get() == editor; }); const auto kind = c.contains("action") && c["action"].is_object() ? c["action"].value("kind", "") : std::string{}; - if (kind == "forward" || kind == "editor") return editor == "vscode"; + if (kind == "forward") return editor != "cli"; + if (kind == "editor") return editor == "vscode"; return true; } diff --git a/tests/test_mcppls_status.cpp b/tests/test_mcppls_status.cpp new file mode 100644 index 0000000..2fc3522 --- /dev/null +++ b/tests/test_mcppls_status.cpp @@ -0,0 +1,133 @@ +// What mcppls says about a workspace (design 2026-10-30 part 2 §5): `mcppls status` from 0.0.20 on, +// the frozen table for 0.0.18 and 0.0.19; mcppls's actions offered in each editor under the id +// mcppls gives that editor, and none where it gives none; a change of state told once. The mock +// mcppls sits in a payload made here, as an editor's own payload would. +#include "support.hpp" +import std; +import nlohmann.json; +import mcppide.rpc; +import mcppide.server; +import mcppide.languageservice; + +using namespace mcppide; +using Json = nlohmann::json; +namespace ls = mcppide::languageservice; + +namespace { + +struct Wire { + std::mutex lock; + std::vector messages; + rpc::Server server{[this](std::string_view frame) { + rpc::FrameReader r; + r.feed(frame); + std::lock_guard g(lock); + while (auto body = r.next()) messages.push_back(Json::parse(*body)); + return true; + }}; + int next = 1; + + Json call(std::string_view method, Json params) { + const int id = next++; + server.dispatch(Json{{"jsonrpc", "2.0"}, {"id", id}, {"method", method}, {"params", std::move(params)}}.dump()); + for (int i = 0; i < 3000; ++i) { + { + std::lock_guard g(lock); + for (const auto& m : messages) if (m.contains("id") && m["id"] == id) return m; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + return Json{{"error", {{"message", "no answer"}}}}; + } + int count(std::string_view method) { + std::lock_guard g(lock); + return static_cast(std::ranges::count_if(messages, [&](const Json& m) { return m.value("method", "") == method; })); + } +}; + +Json command(const Json& list, std::string_view id) { + for (const auto& c : list["commands"]) if (c.value("id", "") == id) return c; + return Json{}; +} + +} // namespace + +int main() { + // The document, read: what `status` printed, and the frozen table when it printed nothing usable. + { + const auto r = ls::parse_status(R"({"schema": 1, "capabilities": ["status", 7], "workspace": {"state": "ready"}, + "actions": [{"id": "restart", "vscode": "mcppls.restartServer"}, {"title": "no id"}]})"); + CHECK(r.source == "status"); + CHECK(r.capabilities == Json::array({"status"})); + CHECK(r.actions.size() == 1 && r.workspace["state"] == "ready"); + const auto broken = ls::parse_status("mcppls: unknown command status"); + CHECK(broken.source == "frozen" && !broken.detail.empty()); + CHECK(broken.actions == ls::frozen_actions()); + } + + const auto mock = testsupport::built("mcpp-ide-mock-mcppls"); + CHECK(!mock.empty()); + const auto base = std::filesystem::temp_directory_path() / std::format("mcppide-mcppls-{}", std::chrono::steady_clock::now().time_since_epoch().count()); + const auto payload = base / "payload"; + const auto root = base / "project"; + std::filesystem::create_directories(payload / "bin"); + std::filesystem::create_directories(root); + std::filesystem::copy_file(mock, payload / "bin" / mock.filename()); + std::ofstream(payload / "payload.json") << std::format(R"({{"server": {{"path": "bin/{}"}}}})", mock.filename().string()); + std::ofstream(root / "mcpp.toml") << "[package]\nname = \"p\"\nversion = \"0.1.0\"\n"; + + Wire wire; + server::Session session(wire.server); + session.install(); + auto result = [&](std::string_view method, Json params) { + auto answer = wire.call(method, std::move(params)); + if (!answer.contains("result")) std::println(std::cerr, "{}: {}", method, answer.dump()); + CHECK(answer.contains("result")); + return answer["result"]; + }; + const Json candidates = Json::array({payload.generic_string()}); + result("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "jetbrains"}, {"trusted", true}, + {"languageService", {{"candidates", candidates}}}, + {"workspaceFolders", Json::array({root.generic_string()})}}); + + // 0.0.19: no `status`; the actions are the frozen table's, and the workspace is not known. + std::ofstream(payload / "mock-version") << "0.0.19\n"; + auto old = result("languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); + CHECK(old["state"] == "ok" && old["version"] == "0.0.19"); + CHECK(old["statusSource"] == "frozen" && old["workspace"].is_null()); + CHECK(old["serverActions"] == ls::frozen_actions()); + + // 0.0.20: the status document. + std::ofstream(payload / "mock-version") << "0.0.20\n"; + std::ofstream(root / ".mock-mcppls-state") << "starting\n"; + auto now = result("languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); + CHECK(now["statusSource"] == "status"); + CHECK(now["workspace"]["state"] == "starting"); + CHECK(now["capabilities"] == Json::array({"status"})); + CHECK(now["serverActions"].size() == 3); + + // The commands each editor is offered: mcppls's action under that editor's id, or not at all. + auto jetbrains = result("commands/list", Json{{"editor", "jetbrains"}}); + CHECK(command(jetbrains, "mcpp.languageServer.restart")["action"]["command"] == "community.mcpp.mcppls.RestartServer"); + CHECK(command(jetbrains, "mcpp.languageServer.restartEngine").is_null()); + auto nvim = result("commands/list", Json{{"editor", "nvim"}}); + CHECK(command(nvim, "mcpp.languageServer.restart")["action"]["command"] == "McpplsRestart"); + auto vscode = result("commands/list", Json{{"editor", "vscode"}}); + CHECK(command(vscode, "mcpp.languageServer.openLogFolder")["action"]["arguments"] == Json::array({"logs"})); + CHECK(command(vscode, "mcpp.languageServer.restartEngine")["action"]["command"] == "mcppls.restartClangd"); + auto cli = result("commands/list", Json{{"editor", "cli"}}); + CHECK(std::ranges::none_of(cli["commands"], [](const Json& c) { return c["action"].value("kind", "") == "forward"; })); + + // A change of state is told once; asking again with nothing changed tells nothing. + const int before = wire.count("languageService/changed"); + std::ofstream(root / ".mock-mcppls-state") << "ready\n"; + auto ready = result("languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); + CHECK(ready["workspace"]["state"] == "ready"); + (void)result("languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); + CHECK(wire.count("languageService/changed") == before + 1); + + std::error_code ignored; + std::filesystem::remove_all(base, ignored); + std::println("test_mcppls_status: ok"); + return 0; +} From 78ea18b8d66a2e1fc166e0b6b28e9d7d6f32641b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 21:04:21 +0900 Subject: [PATCH 091/119] the Neovim report's "cannot write" in Chinese Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- spec/i18n/runtime.zh-cn.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/i18n/runtime.zh-cn.json b/spec/i18n/runtime.zh-cn.json index d3f945c..29cb814 100644 --- a/spec/i18n/runtime.zh-cn.json +++ b/spec/i18n/runtime.zh-cn.json @@ -770,5 +770,6 @@ "The mcpp-ide backend ended four times in three minutes; build, run, test and the project pages go on without it.": "mcpp-ide 后端在三分钟内结束了四次;构建、运行、测试与项目页面在没有它的情况下继续工作。", "Restart the backend": "重新启动后端", "Export IDE Report": "导出 IDE 报告", - "The IDE report is written to {0}": "IDE 报告已写入 {0}" + "The IDE report is written to {0}": "IDE 报告已写入 {0}", + "cannot write {0}": "无法写入 {0}" } From e0d949a90c92cab2cf6cb7e31dbc0398b081f2c7 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 21:17:00 +0900 Subject: [PATCH 092/119] =?UTF-8?q?checkpoints=203=20and=204:=20entries=20?= =?UTF-8?q?beside=20main=20and=20a=20test=20framework's=20cases=20(mcpp=20?= =?UTF-8?q?design=202026-10-30=20part=202=20=C2=A76,=20=C2=A77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit source/entries says what a C++ file is to its project, from the text: a program's main, a test's, or neither. VS Code: Run and Debug above main (mcpp.codeLens.enable), Build and Debug in the editor title, Run, Debug, Build and Test in the editor's context menu, placed by the command table. CLion: the gutter's ▶ from the backend's answer on the PSI element, the hammer runs mcpp build, a status bar item, keymaps with VS Code's chords. spec/test-frameworks.json: GoogleTest, Catch2, doctest. test/cases reads a program's cases from its source and, after a build, from its listing; test/runCases runs some with the framework's filter and reads its report (test/caseResult). The Testing view and CLion's test tree show cases. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 18 + build.mcpp | 3 +- .../mcppcommunity/mcppclion/McppSession.kt | 77 ++- .../mcppcommunity/mcppclion/McppStatusBar.kt | 95 ++++ .../mcppclion/run/McppProducers.kt | 82 +++- .../mcppclion/run/McppProjectTaskRunner.kt | 72 +++ .../mcppclion/run/McppRunConfiguration.kt | 14 + .../mcppcommunity/mcppclion/run/McppStates.kt | 38 +- .../src/main/resources/META-INF/plugin.xml | 6 + .../mcppcommunity/mcppclion/BackendTest.kt | 8 + mide-plugins/vscode/data/config-registry.json | 26 ++ mide-plugins/vscode/data/i18n/zh-cn.json | 5 +- mide-plugins/vscode/docs/settings.md | 30 +- mide-plugins/vscode/package.json | 58 +++ mide-plugins/vscode/package.nls.json | 7 +- mide-plugins/vscode/package.nls.zh-cn.json | 7 +- mide-plugins/vscode/src/cli/controller.ts | 1 + mide-plugins/vscode/src/commands/ids.ts | 2 + mide-plugins/vscode/src/extension.ts | 5 +- mide-plugins/vscode/src/ide/codeLens.ts | 88 ++++ mide-plugins/vscode/src/ide/debug.ts | 3 + mide-plugins/vscode/src/ide/jobs.ts | 5 +- mide-plugins/vscode/src/ide/mapping.ts | 34 ++ mide-plugins/vscode/src/ide/protocol.ts | 32 ++ mide-plugins/vscode/src/ide/testing.ts | 145 +++++- mide-plugins/vscode/test/artifacts.test.ts | 11 +- .../vscode/test/config/registry.test.ts | 17 +- .../vscode/test/config/wiring.test.ts | 5 + mide-plugins/vscode/test/ide/mapping.test.ts | 24 +- protocol/protocol.schema.json | 25 + spec/commands.json | 11 +- spec/i18n/manifest.en.json | 7 +- spec/i18n/manifest.zh-cn.json | 7 +- spec/settings.json | 26 ++ spec/test-frameworks.json | 49 ++ src/bin/dev.cpp | 60 ++- src/jobs.cppm | 20 +- src/scan.cppm | 231 +++++++++ src/server.cppm | 223 ++++++++- src/spec.cppm | 7 + src/testing.cppm | 440 ++++++++++++++++++ tests/test_testing.cpp | 183 ++++++++ 42 files changed, 2112 insertions(+), 95 deletions(-) create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStatusBar.kt create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProjectTaskRunner.kt create mode 100644 mide-plugins/vscode/src/ide/codeLens.ts create mode 100644 spec/test-frameworks.json create mode 100644 src/scan.cppm create mode 100644 src/testing.cppm create mode 100644 tests/test_testing.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 432459f..ab5e4f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,24 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 asked again after a job, on focus, when the project view becomes visible and, while it is visible, at most every 30 seconds. A missing semantic kit is a row with **C++ Modules: Install the Semantic Kit**. After a build nothing is forwarded by default (`auto`): mcppls reads mcpp's result record. +- **Entries (§6).** The backend's `source/entries` says what a C++ file is to its project -- a + program's `main`, a test's (`tests/**`, named as mcpp names it), or neither -- with the line of + `main` read from the text, outside comments and literals. VS Code shows Run and Debug above `main` + (`mcpp.codeLens.enable`), at the position C++ Modules gives and otherwise at the line read, said to + be approximate; Build and Debug join Run and Test in the editor's title bar, and Run, Debug, Build + and Test are in the editor's context menu. The command table states these places (`placements`), + and the generator writes VS Code's menus and CLion's editor popup from it. CLion's ▶ stands on the + PSI element `main` on the line the backend read, the IDE's Build (the hammer) of an mcpp project is + `mcpp build`, a status bar item shows the project's mcpp and the running job with the quick menu, + and two keymaps (`mcpp`, `mcpp (macOS)`) carry VS Code's chords. +- **Test cases (§7).** `spec/test-frameworks.json` describes GoogleTest, Catch2 and doctest: a test + program belongs to one when its plan holds the framework's package and its source calls the + framework's macros (`mcpp.test.macros` adds a project's own). `test/cases` reads the cases from the + source and, after a build in a trusted workspace, from the program's own listing; `test/runCases` + runs some of them with the framework's filter and reads its report, each outcome sent as + `test/caseResult`. VS Code's Testing view has the cases under their program, located at their + macros, and the program at its `main`; CLion has a ▶ on each case and runs cases in its test tree. + Debugging a test's program is offered where the project's mcpp describes its launch (`test-launch`). - **mcpp-vscode 1.1.0 is mcpp-vscode 0.9.0 over the backend.** 0.9.0's extension is imported with its history (`mide-plugins/vscode`); every view, page, command, menu, setting and string a user sees is 0.9.0's. What 1.0 added stays, in 0.9.0's style: debugging (`mcpp.debug`, the `mcpp` diff --git a/build.mcpp b/build.mcpp index 8c6cebd..37a1522 100644 --- a/build.mcpp +++ b/build.mcpp @@ -45,7 +45,7 @@ int main() { const std::filesystem::path root = env_or_empty("MCPP_MANIFEST_DIR"); const std::filesystem::path out = env_or_empty("MCPP_OUT_DIR"); const std::filesystem::path spec = root / "spec"; - const std::array, 8> files{{ + const std::array, 9> files{{ {"kSettingsJson", "settings.json"}, {"kPresetsJson", "presets.json"}, {"kCommandsJson", "commands.json"}, @@ -54,6 +54,7 @@ int main() { {"kManifestZhCnJson", "i18n/manifest.zh-cn.json"}, {"kTomlSchemaJson", "toml-schema.json"}, {"kBuildScriptApiJson", "buildscript-api.json"}, + {"kTestFrameworksJson", "test-frameworks.json"}, }}; std::string module = "// Generated by mcpp-ide's build.mcpp from spec/ (not part of the repository).\n" diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index 16c3372..797e90d 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -42,7 +42,24 @@ class McppSession(private val project: Project) : Disposable { project.messageBus.connect(this).subscribe(McppSettings.CHANGED, McppSettings.Listener { key -> if (key == "mcpp.ide.backendPath") restart() else pushSettings() }) - on("manifest/changed") { p -> p.asJsonObject["root"]?.asString?.let { refreshTests(it) } } + on("manifest/changed") { forgetSources() } + // A build ended: a test program can list the cases its macros generate (§7.3). + on("job/finished") { p -> + val o = p.asJsonObject + if (o["status"]?.asString == "ok" && o["kind"]?.asString in setOf("build", "test")) { + cases.keys.toList().forEach { key -> key.split('\u0000').takeIf { it.size == 2 }?.let { refreshCases(it[0], it[1], list = true) } } + } + } + // A C++ source saved: what was read from it (main, a framework's cases) is read again. + project.messageBus.connect(this).subscribe(com.intellij.openapi.vfs.VirtualFileManager.VFS_CHANGES, + object : com.intellij.openapi.vfs.newvfs.BulkFileListener { + override fun after(events: MutableList) { + for (e in events) { + val path = e.path + if (path.endsWith(".cpp") || path.endsWith(".cc") || path.endsWith(".cxx") || path.endsWith(".cppm")) forgetSource(path) + } + } + }) // mcppls answered differently (another version, other actions): the commands are asked again, // since mcppls's actions are offered under the ids it states (§5.3). on("languageService/changed") { refreshCommands() } @@ -188,25 +205,57 @@ class McppSession(private val project: Project) : Disposable { return start().thenCompose { backend?.request(method, params) ?: CompletableFuture.failedFuture(IllegalStateException(t("the mcpp-ide backend is not running"))) } } - // The tests of each project by the file holding their `main` (mcpp test --list), for the gutter - // and the run configurations from context, which must not wait for mcpp. - private val testMains = java.util.concurrent.ConcurrentHashMap>() + // What each C++ file is to its project (source/entries) and each test program's cases + // (test/cases), for the gutter, which must not wait for the backend: asked in the background + // when unknown, and the editor's markers recomputed when the answer arrives. + private val sources = java.util.concurrent.ConcurrentHashMap() + private val cases = java.util.concurrent.ConcurrentHashMap() + + fun entriesFor(path: String): JsonObject? { + val known = sources[path] + if (known == null) refreshEntries(path) + return known + } + + fun refreshEntries(path: String) { + request("source/entries", JsonObject().apply { addProperty("file", path) }).thenAccept { r -> + sources[path] = r.asJsonObject + restartMarkers() + } + } - /** The test whose `main` is `path`, from what is known; asks again in the background when nothing is. */ - fun testForFile(root: String, path: String): String? { - val known = testMains[root] - if (known == null) refreshTests(root) - return known?.get(File(path).canonicalPath) + fun casesFor(root: String, test: String): JsonObject? { + val known = cases["$root\u0000$test"] + if (known == null) refreshCases(root, test, list = false) + return known } - fun refreshTests(root: String) { - request("test/list", JsonObject().apply { addProperty("root", root) }).thenAccept { r -> - testMains[root] = r.asJsonObject.getAsJsonArray("tests").map { it.asJsonObject } - .filter { it["main"]?.asString?.isNotEmpty() == true } - .associate { File(it["main"].asString).canonicalPath to it["test"].asString } + fun refreshCases(root: String, test: String, list: Boolean) { + request("test/cases", JsonObject().apply { + addProperty("root", root) + addProperty("test", test) + addProperty("list", list && TrustedProjects.isProjectTrusted(project)) + }).thenAccept { r -> + cases["$root\u0000$test"] = r.asJsonObject + restartMarkers() } } + /** A source changed: what was read from it is read again on its next use. */ + fun forgetSource(path: String) { + sources.remove(path) + cases.keys.removeIf { it.split('\u0000').getOrNull(1)?.let { test -> path.replace('\\', '/').endsWith("/tests/$test.cpp") } == true } + } + + private fun forgetSources() { + sources.clear() + cases.clear() + } + + private fun restartMarkers() = com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater { + if (!project.isDisposed) com.intellij.codeInsight.daemon.DaemonCodeAnalyzer.getInstance(project).restart() + } + fun pushSettings() { val b = backend ?: return b.request("settings/update", JsonObject().apply { diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStatusBar.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStatusBar.kt new file mode 100644 index 0000000..5628955 --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStatusBar.kt @@ -0,0 +1,95 @@ +// The mcpp item of the status bar (mcpp design 2026-10-30 part 2 §6.1): what VS Code's status item +// shows -- the project's mcpp version, and the job that runs -- with the same quick menu on a click. +package io.github.mcppcommunity.mcppclion + +import com.google.gson.JsonObject +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep +import com.intellij.openapi.wm.StatusBar +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.StatusBarWidgetFactory +import com.intellij.util.Consumer +import io.github.mcppcommunity.mcppclion.actions.Dispatch +import java.awt.Component +import java.awt.event.MouseEvent +import java.io.File + +class McppStatusBarFactory : StatusBarWidgetFactory { + override fun getId(): String = ID + override fun getDisplayName(): String = "mcpp" + override fun isAvailable(project: Project): Boolean = project.basePath?.let { File(it, "mcpp.toml").isFile } == true + override fun createWidget(project: Project): StatusBarWidget = McppStatusBarWidget(project) + + companion object { + const val ID = "io.github.mcppcommunity.mcppclion.status" + } +} + +class McppStatusBarWidget(private val project: Project) : StatusBarWidget, StatusBarWidget.TextPresentation { + private var bar: StatusBar? = null + @Volatile private var version: String = "" + @Volatile private var program: String = "" + @Volatile private var running: String = "" + private val subscriptions = mutableListOf<() -> Unit>() + + override fun ID(): String = McppStatusBarFactory.ID + override fun getPresentation(): StatusBarWidget.WidgetPresentation = this + + override fun install(statusBar: StatusBar) { + bar = statusBar + val session = McppSession.get(project) + subscriptions += session.on("job/started") { p -> running = p.asJsonObject["kind"]?.asString ?: "job"; update() } + subscriptions += session.on("job/finished") { running = ""; update() } + subscriptions += session.on("project/mcppChanged") { ask() } + ask() + } + + /** The project's mcpp, as the project asks for it (project/mcpp). */ + private fun ask() { + val session = McppSession.get(project) + val root = session.root ?: return + session.request("project/mcpp", JsonObject().apply { addProperty("root", root) }).thenAccept { r -> + val o = r.asJsonObject + version = if (o["state"]?.asString == "ok") o["version"]?.asString ?: "" else o["state"]?.asString ?: "" + program = o["program"]?.asString ?: "" + update() + } + } + + private fun update() = ApplicationManager.getApplication().invokeLater { bar?.updateWidget(ID()) } + + override fun getText(): String = buildString { + append("mcpp") + if (version.isNotEmpty()) append(" ").append(version) + if (running.isNotEmpty()) append(" · ").append(running).append("…") + } + + override fun getTooltipText(): String = McppSession.get(project).t("mcpp {0} at {1}", version, program) + + override fun getAlignment(): Float = Component.LEFT_ALIGNMENT + + override fun getClickConsumer(): Consumer = Consumer { event -> + val session = McppSession.get(project) + session.request("menu/quick", JsonObject().apply { session.root?.let { addProperty("root", it) } }).thenAccept { r -> + val rows = r.asJsonObject.getAsJsonArray("items")?.map { it.asJsonObject }?.filter { it["enabled"]?.asBoolean != false } ?: emptyList() + ApplicationManager.getApplication().invokeLater { + JBPopupFactory.getInstance().createListPopup(object : BaseListPopupStep("mcpp", rows) { + override fun getTextFor(row: JsonObject): String = row["title"]?.asString ?: row["command"].asString + override fun onChosen(row: JsonObject, finalChoice: Boolean): PopupStep<*>? { + Dispatch.run(project, row["command"].asString, JsonObject()) + return FINAL_CHOICE + } + }).showUnderneathOf(event.component) + } + } + } + + override fun dispose() { + subscriptions.forEach { it() } + subscriptions.clear() + bar = null + } +} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProducers.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProducers.kt index 25e3f30..48a50fd 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProducers.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProducers.kt @@ -1,8 +1,12 @@ -// Run configurations from context, and the ▶ beside `main`: a C++ file of an mcpp project whose -// `main` is a test's (mcpp test --list names the file) runs that test; any other `main` runs the -// project's program. Both come from the same producer, so Run and Debug are offered alike. +// Run configurations from context, and the ▶ in the gutter (mcpp design 2026-10-30 part 2 §6.3, +// §7.4). What a file is -- a program's `main`, a test's, or neither -- is the backend's answer +// (source/entries); where `main` is, the language service's: the ▶ stands on the PSI element `main` +// on the line the backend read. A test program of a framework has a ▶ on each case's macro +// (test/cases), which runs that case. Every ▶ comes from the same producer, so Run and Debug are +// offered alike. package io.github.mcppcommunity.mcppclion.run +import com.google.gson.JsonObject import com.intellij.execution.actions.ConfigurationContext import com.intellij.execution.actions.LazyRunConfigurationProducer import com.intellij.execution.configurations.ConfigurationFactory @@ -16,8 +20,6 @@ import com.intellij.psi.PsiElement import io.github.mcppcommunity.mcppclion.McppSession import java.io.File -private val MAIN = Regex("""^\s*(?:export\s+)?(?:int|auto)\s+main\s*\(""") - /** The mcpp project a file belongs to: the nearest directory above it with an mcpp.toml. */ internal fun projectOf(file: VirtualFile?): String? { var dir = file?.parent @@ -28,8 +30,25 @@ internal fun projectOf(file: VirtualFile?): String? { return null } -/** The test a file is the `main` of (mcpp test --list), as far as the session knows; null otherwise. */ -internal fun testOf(session: McppSession, root: String, file: VirtualFile): String? = session.testForFile(root, file.path) +/** The 0-based line of an element, from its document. */ +private fun lineOf(element: PsiElement): Int? { + val document = element.containingFile?.viewProvider?.document ?: return null + return document.getLineNumber(element.textOffset) +} + +/** What the backend says the file is, as far as the session knows (asked in the background otherwise). */ +internal fun entriesOf(session: McppSession, file: VirtualFile): JsonObject? = + session.entriesFor(file.path)?.takeIf { it["kind"]?.asString in setOf("program", "test") } + +/** The case written at `line` of a test's source, by its full name, with its framework. */ +internal fun caseAt(session: McppSession, entries: JsonObject, line: Int): Pair? { + val root = entries["root"]?.asString ?: return null + val test = entries["test"]?.asString ?: return null + val cases = session.casesFor(root, test) ?: return null + val framework = cases["framework"]?.takeIf { !it.isJsonNull }?.asString ?: return null + val hit = cases.getAsJsonArray("cases")?.firstOrNull { it.asJsonObject["line"]?.asInt == line } ?: return null + return hit.asJsonObject["name"].asString to framework +} class McppConfigurationProducer : LazyRunConfigurationProducer() { override fun getConfigurationFactory(): ConfigurationFactory = @@ -37,15 +56,23 @@ class McppConfigurationProducer : LazyRunConfigurationProducer): Boolean { val file = context.location?.virtualFile ?: return false - val root = projectOf(file) ?: return false - val text = context.psiLocation?.containingFile?.text ?: return false - if (!text.lineSequence().any { MAIN.containsMatchIn(it) }) return false + val session = McppSession.get(c.project) + val entries = entriesOf(session, file) ?: return false + val root = entries["root"]?.asString ?: projectOf(file) ?: return false c.options.root = root - val test = testOf(McppSession.get(c.project), root, file) - if (test != null) { + if (entries["kind"].asString == "test") { + val test = entries["test"].asString c.options.command = "test" c.options.filter = test - c.name = "mcpp test $test" + val line = context.psiLocation?.let { lineOf(it) } + val case = line?.let { caseAt(session, entries, it) } + if (case != null) { + c.options.framework = case.second + c.options.cases = case.first + c.name = "mcpp test $test · ${case.first}" + } else { + c.name = "mcpp test $test" + } } else { c.options.command = "run" c.name = "mcpp run ${File(root).name}" @@ -55,23 +82,30 @@ class McppConfigurationProducer : LazyRunConfigurationProducer { + val promise = AsyncPromise() + val session = McppSession.get(project) + val root = session.root ?: project.basePath + if (root == null) { + promise.setResult(TaskRunnerResults.FAILURE) + return promise + } + // The job's end can arrive before the reply that names its id: ends are kept until it is known. + val lock = Any() + var jobId: String? = null + val early = mutableListOf() + var unsubscribe: (() -> Unit)? = null + fun finish(o: JsonObject) { + unsubscribe?.invoke() + val status = o["status"]?.asString + if (status != "ok") session.notify(session.t("mcpp {0}: {1}", "build", status ?: "failed"), NotificationType.ERROR) + promise.setResult(when (status) { + "ok" -> TaskRunnerResults.SUCCESS + "cancelled", "superseded" -> TaskRunnerResults.ABORTED + else -> TaskRunnerResults.FAILURE + }) + } + unsubscribe = session.on("job/finished") { p -> + val o = p.asJsonObject + val mine = synchronized(lock) { + if (jobId == null) { early += o; false } else o["jobId"]?.asString == jobId + } + if (mine) finish(o) + } + session.request("job/start", JsonObject().apply { + addProperty("kind", "build") + addProperty("root", root) + addProperty("color", false) + }).whenComplete { r, e -> + if (e != null) { + unsubscribe?.invoke() + session.notify(session.t("mcpp: {0}", (e.cause ?: e).message ?: ""), NotificationType.ERROR) + promise.setResult(TaskRunnerResults.FAILURE) + return@whenComplete + } + val id = r.asJsonObject["jobId"].asString + val ended = synchronized(lock) { jobId = id; early.firstOrNull { it["jobId"]?.asString == id }.also { early.clear() } } + if (ended != null) finish(ended) + } + return promise + } +} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppRunConfiguration.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppRunConfiguration.kt index b7f4947..cbbd516 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppRunConfiguration.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppRunConfiguration.kt @@ -45,6 +45,8 @@ class McppRunOptions : RunConfigurationOptions() { private val filterProperty = string("").provideDelegate(this, "filter") private val binProperty = string("").provideDelegate(this, "bin") private val rootProperty = string("").provideDelegate(this, "root") + private val frameworkProperty = string("").provideDelegate(this, "framework") + private val casesProperty = string("").provideDelegate(this, "cases") var command: String get() = commandProperty.getValue(this) ?: "run"; set(v) = commandProperty.setValue(this, v) var target: String get() = targetProperty.getValue(this) ?: ""; set(v) = targetProperty.setValue(this, v) @@ -53,6 +55,10 @@ class McppRunOptions : RunConfigurationOptions() { var filter: String get() = filterProperty.getValue(this) ?: ""; set(v) = filterProperty.setValue(this, v) var bin: String get() = binProperty.getValue(this) ?: ""; set(v) = binProperty.setValue(this, v) var root: String get() = rootProperty.getValue(this) ?: ""; set(v) = rootProperty.setValue(this, v) + /** The test framework whose cases `cases` names (design 2026-10-30 part 2 §7); empty: whole programs. */ + var framework: String get() = frameworkProperty.getValue(this) ?: ""; set(v) = frameworkProperty.setValue(this, v) + /** The cases to run, one per line, by the framework's full names; empty: every case. */ + var cases: String get() = casesProperty.getValue(this) ?: ""; set(v) = casesProperty.setValue(this, v) } class McppRunConfiguration(project: Project, factory: ConfigurationFactory, name: String) : @@ -81,6 +87,8 @@ class McppRunConfiguration(project: Project, factory: ConfigurationFactory, name private val args = JBTextField() private val filter = JBTextField() private val bin = JBTextField() + private val framework = JBTextField() + private val cases = com.intellij.ui.components.JBTextArea(3, 40) override fun resetEditorFrom(c: McppRunConfiguration) { command.selectedItem = c.options.command @@ -89,6 +97,8 @@ class McppRunConfiguration(project: Project, factory: ConfigurationFactory, name args.text = c.options.args filter.text = c.options.filter bin.text = c.options.bin + framework.text = c.options.framework + cases.text = c.options.cases } override fun applyEditorTo(c: McppRunConfiguration) { @@ -98,6 +108,8 @@ class McppRunConfiguration(project: Project, factory: ConfigurationFactory, name c.options.args = args.text c.options.filter = filter.text.trim() c.options.bin = bin.text.trim() + c.options.framework = framework.text.trim() + c.options.cases = cases.text.lines().map { it.trimEnd() }.filter { it.isNotEmpty() }.joinToString("\n") } override fun createEditor(): JComponent = FormBuilder.createFormBuilder() @@ -106,6 +118,8 @@ class McppRunConfiguration(project: Project, factory: ConfigurationFactory, name .addLabeledComponent("Profile (empty: the mcpp.build.profile setting):", profile) .addLabeledComponent("Executable (run):", bin) .addLabeledComponent("Test filter (test):", filter) + .addLabeledComponent("Test framework (test cases):", framework) + .addLabeledComponent("Cases, one per line (test cases):", cases) .addLabeledComponent("Arguments:", args) .panel } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppStates.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppStates.kt index b896a02..f3958cf 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppStates.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppStates.kt @@ -49,7 +49,7 @@ open class JobProcessHandler(protected val session: McppSession, private val par override fun startNotify() { super.startNotify() - for (m in listOf("job/output", "job/finished", "test/result", "test/summary")) + for (m in listOf("job/output", "job/finished", "test/result", "test/summary", "test/caseResult")) subscriptions += session.on(m) { p -> route(m, p) } session.request(method, params).whenComplete { r, e -> if (e != null) { @@ -118,11 +118,37 @@ class McppJobState(private val c: McppRunConfiguration, env: ExecutionEnvironmen * A test job whose results fill the IDE's test tree: each `test/result` becomes the TeamCity * service messages the test runner reads, located at the file that holds the test's `main`. */ -class TestJobProcessHandler(session: McppSession, params: JsonObject, private val mains: Map) : JobProcessHandler(session, params) { +class TestJobProcessHandler(session: McppSession, params: JsonObject, private val mains: Map, method: String = "job/start") : + JobProcessHandler(session, params, method) { private fun escape(s: String) = s.replace("|", "||").replace("'", "|'").replace("\n", "|n").replace("\r", "|r").replace("[", "|[").replace("]", "|]") + /** The program whose cases are reported, once its first case is (test/runCases). */ + private var suite: String? = null override fun handle(method: String, p: JsonObject) { when (method) { + // A case of a framework (§7.4): the program is a suite, each case a test in it. + "test/caseResult" -> { + val program = p["test"].asString + val out = StringBuilder() + if (suite == null) { + suite = program + val hint = mains[program]?.let { " locationHint='file://${escape(it)}'" } ?: "" + out.append("##teamcity[testSuiteStarted name='${escape(program)}'$hint]\n") + } + val name = escape(p["case"].asString) + out.append("##teamcity[testStarted name='$name']\n") + when (p["status"]?.asString) { + "passed" -> {} + "skipped" -> out.append("##teamcity[testIgnored name='$name']\n") + else -> out.append("##teamcity[testFailed name='$name' message='${escape(p["message"]?.asString ?: "")}']\n") + } + out.append("##teamcity[testFinished name='$name' duration='${p["durationMs"]?.asLong ?: 0}']\n") + notifyTextAvailable(out.toString(), ProcessOutputTypes.STDOUT) + } + "job/finished" -> { + suite?.let { notifyTextAvailable("##teamcity[testSuiteFinished name='${escape(it)}']\n", ProcessOutputTypes.STDOUT) } + super.handle(method, p) + } "test/result" -> { val name = escape(p["test"].asString) val hint = mains[p["test"].asString]?.let { " locationHint='file://${escape(it)}'" } ?: "" @@ -149,6 +175,14 @@ class McppTestState(private val c: McppRunConfiguration, env: ExecutionEnvironme session.request("test/list", JsonObject().apply { addProperty("root", c.root) }).get() .asJsonObject.getAsJsonArray("tests").associate { it.asJsonObject["test"].asString to it.asJsonObject["main"].asString } }.getOrDefault(emptyMap()) + // A framework's cases (test/runCases): the program receives the framework's filter. + if (c.options.framework.isNotBlank() && c.options.filter.isNotBlank()) + return TestJobProcessHandler(session, jobParams(c, "test").apply { + remove("kind") + addProperty("test", c.options.filter) + addProperty("framework", c.options.framework) + add("cases", JsonArray().apply { c.options.cases.lines().filter { it.isNotBlank() }.forEach { add(it) } }) + }, mains, "test/runCases") return TestJobProcessHandler(session, jobParams(c, "test").apply { if (c.options.filter.isNotBlank()) addProperty("filter", c.options.filter) if (c.programArgs.isNotEmpty()) add("args", JsonArray().apply { add("--"); c.programArgs.forEach { add(it) } }) diff --git a/mide-plugins/clion/src/main/resources/META-INF/plugin.xml b/mide-plugins/clion/src/main/resources/META-INF/plugin.xml index b697e43..998c94a 100644 --- a/mide-plugins/clion/src/main/resources/META-INF/plugin.xml +++ b/mide-plugins/clion/src/main/resources/META-INF/plugin.xml @@ -46,5 +46,11 @@ + + + + + + diff --git a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt index aa9f28d..28f6d24 100644 --- a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt +++ b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt @@ -103,7 +103,15 @@ class BackendTest { assertTrue(xml!!.contains("""id="mcpp.build"""")) // mcppls's actions are declared; each is shown where mcppls states a JetBrains id for it. assertTrue(xml.contains("mcpp.languageServer.restart")) + // Build, Run, Debug and Test in the editor's context menu (part 2 §6.2). + assertTrue(xml.contains("""""")) + assertTrue(xml.contains("""""")) val bundle = javaClass.getResource("/messages/McppClionBundle.properties")!!.readText() assertTrue(bundle.contains("action.mcpp.build.text=Build")) + // VS Code's chords as keymaps of their own. + val keymap = javaClass.getResource("/keymaps/mcpp.xml")!!.readText() + assertTrue(keymap.contains("""""")) + assertTrue(keymap.contains("""first-keystroke="ctrl alt B"""")) + assertTrue(javaClass.getResource("/keymaps/mcpp-macos.xml")!!.readText().contains("""first-keystroke="meta alt B"""")) } } diff --git a/mide-plugins/vscode/data/config-registry.json b/mide-plugins/vscode/data/config-registry.json index 41e59d0..cd46686 100644 --- a/mide-plugins/vscode/data/config-registry.json +++ b/mide-plugins/vscode/data/config-registry.json @@ -293,6 +293,32 @@ "title": "Choose the debugger", "description": "The debug adapter a debug session uses. auto: CodeLLDB when it is installed, then the C/C++ extension's gdb or lldb, then lldb-dap." }, + { + "key": "mcpp.codeLens.enable", + "type": "boolean", + "default": true, + "scope": "resource", + "group": "tasks", + "order": 140, + "tier": "public", + "applies": "immediate", + "since": "1.1.0", + "title": "Show Run and Debug above main", + "description": "A CodeLens above the main of a program or a test: Run and Debug. Where C++ Modules serves the file its position is the language service's; otherwise the line is read from the text." + }, + { + "key": "mcpp.test.macros", + "type": "array", + "default": [], + "scope": "resource", + "group": "tasks", + "order": 150, + "tier": "advanced", + "applies": "immediate", + "since": "1.1.0", + "title": "Add a test framework's macros", + "description": "Macros that write a test framework's cases, each as framework:MACRO (gtest:MY_TEST). A test program's cases are listed only when its source calls one of its framework's macros." + }, { "key": "mcpp.languageService.menuItems", "type": "boolean", diff --git a/mide-plugins/vscode/data/i18n/zh-cn.json b/mide-plugins/vscode/data/i18n/zh-cn.json index 61a7544..fd89852 100644 --- a/mide-plugins/vscode/data/i18n/zh-cn.json +++ b/mide-plugins/vscode/data/i18n/zh-cn.json @@ -650,5 +650,8 @@ "Files are read with the openkal kit C++ Modules carries. Installing the target's kit gives its own standard library and headers.": "文件以 C++ Modules 自带的 openkal 套件解析。安装目标的套件后使用其自身的标准库与头文件。", "Install the semantic kit": "安装语义套件", "This mcppls does not install kits; files are read with the openkal kit it carries.": "此 mcppls 不安装套件;文件以其自带的 openkal 套件解析。", - "The C++ Modules server installs the target's semantic kit with xlings, in the background.": "C++ Modules 服务器在后台用 xlings 安装目标的语义套件。" + "The C++ Modules server installs the target's semantic kit with xlings, in the background.": "C++ Modules 服务器在后台用 xlings 安装目标的语义套件。", + "The line of main was read from the text; C++ Modules gives its position when it serves this file.": "main 的行号从文本读出;C++ Modules 服务此文件时由它给出位置。", + "The test program did not run: {0}": "测试程序没有运行:{0}", + "Debug": "调试" } diff --git a/mide-plugins/vscode/docs/settings.md b/mide-plugins/vscode/docs/settings.md index f7543de..9291c46 100644 --- a/mide-plugins/vscode/docs/settings.md +++ b/mide-plugins/vscode/docs/settings.md @@ -2,7 +2,7 @@ -76 settings, in 10 groups. All of them are read by the +78 settings, in 10 groups. All of them are read by the **mcpp** extension only. `mcppls.*` belongs to the C++ Modules extension and is never written by this one; the configuration panel says so on every screen. @@ -240,6 +240,34 @@ The debug adapter a debug session uses. auto: CodeLLDB when it is installed, the | Applies | Takes effect immediately | | Since | 1.0.0 | +### `mcpp.codeLens.enable` + +A CodeLens above the main of a program or a test: Run and Debug. Where C++ Modules serves the file its position is the language service's; otherwise the line is read from the text. + +在程序或测试的 main 上方显示 CodeLens:运行、调试。C++ Modules 服务该文件时位置取自语言服务,否则从文本读出行号。 + +| | | +|---|---| +| Type | `boolean` | +| Default | `true` | +| Scope | per folder | +| Applies | Takes effect immediately | +| Since | 1.1.0 | + +### `mcpp.test.macros` — advanced + +Macros that write a test framework's cases, each as framework:MACRO (gtest:MY_TEST). A test program's cases are listed only when its source calls one of its framework's macros. + +书写测试框架用例的宏,每项写作 框架:宏名(gtest:MY_TEST)。测试程序的源文件调用了其框架的宏时才列举它的用例。 + +| | | +|---|---| +| Type | `array` | +| Default | `[]` | +| Scope | per folder | +| Applies | Takes effect immediately | +| Since | 1.1.0 | + ## C++ Modules language service ### `mcpp.languageService.menuItems` — advanced diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json index 7b59366..078f99a 100644 --- a/mide-plugins/vscode/package.json +++ b/mide-plugins/vscode/package.json @@ -45,12 +45,44 @@ "main": "./dist/src/extension.js", "contributes": { "menus": { + "editor/context": [ + { + "command": "mcpp.build", + "group": "mcpp@3", + "when": "mcpp.inProject && editorLangId == cpp" + }, + { + "command": "mcpp.run", + "group": "mcpp@1", + "when": "mcpp.inProject && editorLangId == cpp" + }, + { + "command": "mcpp.debug", + "group": "mcpp@2", + "when": "mcpp.inProject && editorLangId == cpp" + }, + { + "command": "mcpp.test", + "group": "mcpp@4", + "when": "mcpp.inProject && editorLangId == cpp" + } + ], "editor/title": [ + { + "command": "mcpp.editor.build", + "group": "navigation@0", + "when": "mcpp.inProject && mcpp.editorTitleButtons" + }, { "command": "mcpp.run", "group": "navigation@1", "when": "mcpp.inProject && mcpp.editorTitleButtons" }, + { + "command": "mcpp.debug", + "group": "navigation@3", + "when": "mcpp.inProject && mcpp.editorTitleButtons" + }, { "command": "mcpp.test", "group": "navigation@2", @@ -81,6 +113,10 @@ } ], "commandPalette": [ + { + "command": "mcpp.editor.build", + "when": "false" + }, { "command": "mcpp.debug", "when": "mcpp.inProject" @@ -105,6 +141,12 @@ "title": "%command.mcpp.build.title%", "category": "%category%" }, + { + "command": "mcpp.editor.build", + "title": "%command.mcpp.editor.build.title%", + "category": "%category%", + "icon": "$(tools)" + }, { "command": "mcpp.run", "title": "%command.mcpp.run.title%", @@ -559,6 +601,22 @@ "description": "%mcpp.debug.engine.title%", "markdownDescription": "%mcpp.debug.engine.description%" }, + "mcpp.codeLens.enable": { + "type": "boolean", + "default": true, + "scope": "resource", + "order": 20140, + "description": "%mcpp.codeLens.enable.title%", + "markdownDescription": "%mcpp.codeLens.enable.description%" + }, + "mcpp.test.macros": { + "type": "array", + "default": [], + "scope": "resource", + "order": 20150, + "description": "%mcpp.test.macros.title%", + "markdownDescription": "%mcpp.test.macros.description%" + }, "mcpp.languageService.menuItems": { "type": "boolean", "default": true, diff --git a/mide-plugins/vscode/package.nls.json b/mide-plugins/vscode/package.nls.json index bee4403..7503047 100644 --- a/mide-plugins/vscode/package.nls.json +++ b/mide-plugins/vscode/package.nls.json @@ -4,6 +4,7 @@ "colors.mcpp.cacheStaleForeground": "Colour of artifacts that can be cleaned.", "command.mcpp.autoConfigureModules.title": "Build and Refresh the Language Service", "command.mcpp.build.title": "Build", + "command.mcpp.editor.build.title": "Build", "command.mcpp.checkModuleSupport.title": "[Deprecated] C++ Modules: Restart the Language Server", "command.mcpp.clean.title": "Clean target", "command.mcpp.cleanLegacyCache.title": "Remove the Pre-v1 Cache", @@ -268,5 +269,9 @@ "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", "mcpp.backend.logLevel.title": "Set the backend's log level", "mcpp.backend.logLevel.description": "What the mcpp-ide backend writes to the editor's log: errors only, warnings, what it does (the default), or every run of mcpp and mcppls with its arguments.", - "command.mcpp.exportReport.title": "Export IDE Report" + "command.mcpp.exportReport.title": "Export IDE Report", + "mcpp.codeLens.enable.title": "Show Run and Debug above main", + "mcpp.codeLens.enable.description": "A CodeLens above the main of a program or a test: Run and Debug. Where C++ Modules serves the file its position is the language service's; otherwise the line is read from the text.", + "mcpp.test.macros.title": "Add a test framework's macros", + "mcpp.test.macros.description": "Macros that write a test framework's cases, each as framework:MACRO (gtest:MY_TEST). A test program's cases are listed only when its source calls one of its framework's macros." } diff --git a/mide-plugins/vscode/package.nls.zh-cn.json b/mide-plugins/vscode/package.nls.zh-cn.json index dc5c1b7..7ede0f2 100644 --- a/mide-plugins/vscode/package.nls.zh-cn.json +++ b/mide-plugins/vscode/package.nls.zh-cn.json @@ -4,6 +4,7 @@ "colors.mcpp.cacheStaleForeground": "可清理产物的颜色。", "command.mcpp.autoConfigureModules.title": "一键构建并刷新模块语言服务", "command.mcpp.build.title": "构建", + "command.mcpp.editor.build.title": "构建", "command.mcpp.checkModuleSupport.title": "[已弃用] C++ Modules: 重启语言服务", "command.mcpp.clean.title": "清理 target", "command.mcpp.cleanLegacyCache.title": "清理遗留缓存", @@ -268,5 +269,9 @@ "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", "mcpp.backend.logLevel.title": "设置后端的日志级别", "mcpp.backend.logLevel.description": "mcpp-ide 后端写入编辑器日志的内容:只写错误、写警告、写它所做的事(缺省),或写每一次运行 mcpp 与 mcppls 及其参数。", - "command.mcpp.exportReport.title": "导出 IDE 报告" + "command.mcpp.exportReport.title": "导出 IDE 报告", + "mcpp.codeLens.enable.title": "在 main 上方显示运行与调试", + "mcpp.codeLens.enable.description": "在程序或测试的 main 上方显示 CodeLens:运行、调试。C++ Modules 服务该文件时位置取自语言服务,否则从文本读出行号。", + "mcpp.test.macros.title": "追加测试框架的宏", + "mcpp.test.macros.description": "书写测试框架用例的宏,每项写作 框架:宏名(gtest:MY_TEST)。测试程序的源文件调用了其框架的宏时才列举它的用例。" } diff --git a/mide-plugins/vscode/src/cli/controller.ts b/mide-plugins/vscode/src/cli/controller.ts index df0c044..ef3e991 100644 --- a/mide-plugins/vscode/src/cli/controller.ts +++ b/mide-plugins/vscode/src/cli/controller.ts @@ -299,6 +299,7 @@ export class McppCliController { vscode.commands.registerCommand(CLI_COMMANDS.showMenu, this.guarded(() => this.showMenu())), vscode.commands.registerCommand(CLI_COMMANDS.newProject, this.guarded(() => this.newProject())), vscode.commands.registerCommand(CLI_COMMANDS.build, this.guarded(() => this.runProjectTask("build"))), + vscode.commands.registerCommand(CLI_COMMANDS.editorBuild, this.guarded(() => this.runProjectTask("build"))), vscode.commands.registerCommand(CLI_COMMANDS.run, this.guarded(() => this.runProjectTask("run"))), vscode.commands.registerCommand(CLI_COMMANDS.test, this.guarded(() => this.runProjectTask("test"))), vscode.commands.registerCommand(CLI_COMMANDS.clean, this.guarded(() => this.runProjectTask("clean"))), diff --git a/mide-plugins/vscode/src/commands/ids.ts b/mide-plugins/vscode/src/commands/ids.ts index 0ef1c3f..523d9f8 100644 --- a/mide-plugins/vscode/src/commands/ids.ts +++ b/mide-plugins/vscode/src/commands/ids.ts @@ -31,6 +31,8 @@ export const CLI_COMMANDS = { selectTarget: "mcpp.selectTarget", showTargets: "mcpp.showTargets", showCachePanel: "mcpp.showCachePanel", + /** Build in the editor's title bar, with an icon (mcpp.build has none in 0.9.0's manifest). */ + editorBuild: "mcpp.editor.build", } as const; /** The language-service entries that existed in 0.4.x (kept as aliases, see below). */ diff --git a/mide-plugins/vscode/src/extension.ts b/mide-plugins/vscode/src/extension.ts index 9e9ce7b..2f2facc 100644 --- a/mide-plugins/vscode/src/extension.ts +++ b/mide-plugins/vscode/src/extension.ts @@ -42,6 +42,7 @@ import { Jobs } from "./ide/jobs"; import type { IdeProjects } from "./ide/projects"; import { Session } from "./ide/session"; import { Testing } from "./ide/testing"; +import { McppCodeLens } from "./ide/codeLens"; import { changedSettings, onDidChange as onConfigurationChanged, read } from "./config/access"; import { applyRenames, pendingRenames, renamePrompt } from "./config/migrate"; import { languagePreference, setLanguagePreference, t, type LanguagePreference } from "./i18n/t"; @@ -321,11 +322,13 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi } }); extensionContext.subscriptions.push({ dispose: () => { useMcppExecutor(undefined); useMcpplsExecutor(undefined); } }); + const testing = new Testing(session, jobs, ideProjects); extensionContext.subscriptions.push( session, jobs, new Debugging(session, ideProjects), - new Testing(session, jobs, ideProjects), + testing, + new McppCodeLens(session, testing, (root) => projectMcpp.get(root)), jobs.onDidChangeRunning.event((n) => void vscode.commands.executeCommand("setContext", "mcpp.jobRunning", n > 0)), vscode.tasks.onDidStartTask(() => void updateJobRunning()), vscode.tasks.onDidEndTask(() => void updateJobRunning()), diff --git a/mide-plugins/vscode/src/ide/codeLens.ts b/mide-plugins/vscode/src/ide/codeLens.ts new file mode 100644 index 0000000..ecc303c --- /dev/null +++ b/mide-plugins/vscode/src/ide/codeLens.ts @@ -0,0 +1,88 @@ +/** + * Run and Debug above `main` (design 2026-10-30 part 2 §6.1, §6.3): the backend says what the file + * is to its project (`source/entries`: a program's `main`, a test's, or neither); the position is + * the language service's, from the document symbols of the server answering the file (C++ Modules), + * and only without one the line the backend read from the text, which the lens's tooltip calls + * approximate. `mcpp.codeLens.enable` turns the lenses off. + * + * A program's Run and Debug are mcpp.run and mcpp.debug. A test's Run is its program in the Testing + * view; its Debug is offered where the project's mcpp describes a test's launch (`test-launch`). + */ +import * as vscode from "vscode"; + +import { read } from "../config/access"; +import { t } from "../i18n/t"; +import { mainLineOf } from "./mapping"; +import type { ProjectMcpp, SourceEntries } from "./protocol"; +import type { Session } from "./session"; +import type { Testing } from "./testing"; + +export const CODELENS_COMMANDS = { + runTest: "mcpp.codeLens.runTest", + debugTest: "mcpp.codeLens.debugTest", +} as const; + +export class McppCodeLens implements vscode.CodeLensProvider, vscode.Disposable { + private readonly changed = new vscode.EventEmitter(); + readonly onDidChangeCodeLenses = this.changed.event; + private readonly disposables: vscode.Disposable[] = [this.changed]; + + constructor( + private readonly session: Session, + private readonly testing: Testing, + private readonly projectMcpp: (root: string) => ProjectMcpp | undefined, + ) { + this.disposables.push( + vscode.languages.registerCodeLensProvider([{ language: "cpp", scheme: "file" }], this), + session.onDidStart.event(() => this.changed.fire()), + vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("mcpp.codeLens")) this.changed.fire(); }), + vscode.commands.registerCommand(CODELENS_COMMANDS.runTest, async (root: string, test: string) => { + if (!this.testing.item(root, test)) await this.testing.discover(); + const item = this.testing.item(root, test); + if (item) await this.testing.runItem(item); + }), + vscode.commands.registerCommand(CODELENS_COMMANDS.debugTest, async (root: string, test: string) => { + const folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(root)); + await vscode.debug.startDebugging(folder, { type: "mcpp", request: "launch", name: `mcpp: debug ${test}`, root, test }); + }), + ); + } + + async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { + if (read("mcpp.codeLens.enable") === false || !this.session.running) return []; + let entries: SourceEntries; + try { + entries = await this.session.request("source/entries", { file: document.uri.fsPath, text: document.getText() }); + } catch { + return []; + } + if (token.isCancellationRequested || entries.kind === "none" || entries.root === undefined) return []; + let line: number | undefined; + try { + const symbols = await vscode.commands.executeCommand("vscode.executeDocumentSymbolProvider", document.uri); + line = mainLineOf(symbols); + } catch { + line = undefined; + } + const approximate = line === undefined; + line ??= entries.main?.line; + if (line === undefined) return []; + const range = new vscode.Range(line, 0, line, 0); + const tooltip = approximate ? t("The line of main was read from the text; C++ Modules gives its position when it serves this file.") : undefined; + const root = entries.root; + if (entries.kind === "program") + return [ + new vscode.CodeLens(range, { title: t("Run"), command: "mcpp.run", tooltip }), + new vscode.CodeLens(range, { title: t("Debug"), command: "mcpp.debug", tooltip }), + ]; + const test = entries.test ?? ""; + const lenses = [new vscode.CodeLens(range, { title: t("Run"), command: CODELENS_COMMANDS.runTest, arguments: [root, test], tooltip })]; + if (this.projectMcpp(root)?.features.includes("test-launch")) + lenses.push(new vscode.CodeLens(range, { title: t("Debug"), command: CODELENS_COMMANDS.debugTest, arguments: [root, test], tooltip })); + return lenses; + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/mide-plugins/vscode/src/ide/debug.ts b/mide-plugins/vscode/src/ide/debug.ts index e6c0cc3..ee99761 100644 --- a/mide-plugins/vscode/src/ide/debug.ts +++ b/mide-plugins/vscode/src/ide/debug.ts @@ -43,6 +43,9 @@ export class Debugging implements vscode.DebugConfigurationProvider, vscode.Disp { location: vscode.ProgressLocation.Window, title: t("mcpp: building for the debugger…") }, () => this.session.request<{ engine: string; configuration: vscode.DebugConfiguration & { mcppNote?: string } }>("debug/configuration", { root: project.root, + // A test's program (mcpp test --print-launch), where the project's mcpp describes one. + ...(config.test ? { test: config.test } : {}), + ...(config.framework ? { framework: config.framework, cases: Array.isArray(config.cases) ? config.cases : [] } : {}), ...(config.target !== undefined ? { target: config.target } : {}), ...(config.profile ? { profile: config.profile } : {}), ...(config.engine ? { engine: config.engine } : {}), diff --git a/mide-plugins/vscode/src/ide/jobs.ts b/mide-plugins/vscode/src/ide/jobs.ts index f90923d..bd92ccc 100644 --- a/mide-plugins/vscode/src/ide/jobs.ts +++ b/mide-plugins/vscode/src/ide/jobs.ts @@ -6,7 +6,7 @@ import * as vscode from "vscode"; import type { JobFinished } from "./protocol"; import type { Session } from "./session"; -const JOB_EVENTS = ["job/started", "job/output", "job/plan", "job/progress", "job/diagnostic", "job/artifact", "job/record", "job/finished", "test/result", "test/summary"]; +const JOB_EVENTS = ["job/started", "job/output", "job/plan", "job/progress", "job/diagnostic", "job/artifact", "job/record", "job/finished", "test/result", "test/summary", "test/caseResult"]; export interface JobEvents { output?(text: string): void; @@ -15,6 +15,8 @@ export interface JobEvents { progress?(p: { finished: number; total: number }): void; testResult?(p: any): void; testSummary?(p: any): void; + /** A case's outcome (test/runCases), sent before the job's end. */ + caseResult?(p: any): void; finished?(p: JobFinished): void; } @@ -80,6 +82,7 @@ export class Jobs implements vscode.Disposable { case "job/progress": e.progress?.(params); break; case "test/result": e.testResult?.(params); break; case "test/summary": e.testSummary?.(params); break; + case "test/caseResult": e.caseResult?.(params); break; case "job/finished": e.finished?.(params); owner.handle.finish(params); diff --git a/mide-plugins/vscode/src/ide/mapping.ts b/mide-plugins/vscode/src/ide/mapping.ts index 3ed2b08..054d0ec 100644 --- a/mide-plugins/vscode/src/ide/mapping.ts +++ b/mide-plugins/vscode/src/ide/mapping.ts @@ -24,7 +24,41 @@ export function testId(root: string, member: string, test: string): string { return `${root}::${member}::${test}`; } +/** What separates a case's name from its program's id: a name is the framework's, and may hold "::". */ +const CASE = "\u001f"; + +/** A case of a test program (design 2026-10-30 part 2 §7): its program's id, then its full name. */ +export function caseId(root: string, member: string, test: string, name: string): string { + return `${testId(root, member, test)}${CASE}${name}`; +} + +export function parseCaseId(id: string): { root: string; member: string; test: string; name: string } | undefined { + const at = id.indexOf(CASE); + if (at < 0) return undefined; + const program = parseTestId(id.slice(0, at)); + return program === undefined ? undefined : { ...program, name: id.slice(at + 1) }; +} + +/** A case result's name as its source names it: GoogleTest's instance prefix and parameter dropped. */ +export function baseCaseName(framework: string | null, name: string): string { + if (framework !== "gtest") return name; + const dot = name.indexOf("."); + if (dot < 0) return name; + const suiteParts = name.slice(0, dot).split("/").filter((part) => part !== "" && !/^\d+$/.test(part)); + const test = name.slice(dot + 1).split("/")[0]; + return `${suiteParts[suiteParts.length - 1] ?? ""}.${test}`; +} + +/** VS Code's SymbolKind.Function (LSP numbers it 12). */ +const FUNCTION_SYMBOL = 11; + +/** The line of `main` among a document's symbols: a function named main at the top level. */ +export function mainLineOf(symbols: readonly { name: string; kind: number; range: { start: { line: number } } }[] | undefined): number | undefined { + return symbols?.find((s) => s.kind === FUNCTION_SYMBOL && (s.name === "main" || s.name.startsWith("main(")))?.range.start.line; +} + export function parseTestId(id: string): { root: string; member: string; test: string } | undefined { + if (id.includes(CASE)) return undefined; const parts = id.split("::"); if (parts.length < 3) return undefined; const test = parts.pop()!; diff --git a/mide-plugins/vscode/src/ide/protocol.ts b/mide-plugins/vscode/src/ide/protocol.ts index a57372e..c6ab7f9 100644 --- a/mide-plugins/vscode/src/ide/protocol.ts +++ b/mide-plugins/vscode/src/ide/protocol.ts @@ -170,6 +170,38 @@ export interface JobFinished { artifacts?: { kind: string; name: string; path: string }[]; } +/** `source/entries` (design 2026-10-30 part 2 §6.3): what a C++ file is to its project. */ +export interface SourceEntries { + file: string; + root?: string; + kind: "program" | "test" | "none"; + test?: string; + framework?: string; + main?: { line: number; approximate?: boolean }; +} + +/** `test/cases` (§7.3): a test program's cases, from its source and, once listed, from the program. */ +export interface TestCases { + root: string; + test: string; + file: string; + framework: string | null; + listed?: boolean; + main?: { line: number; approximate?: boolean }; + cases: { name: string; line?: number; source: "static" | "listed" }[]; +} + +/** `test/caseResult`: one case's outcome, read from the framework's report. */ +export interface CaseResult { + jobId: string; + root: string; + test: string; + case: string; + status: "passed" | "failed" | "skipped"; + durationMs: number; + message: string; +} + export interface TestResult { jobId: string; root: string; diff --git a/mide-plugins/vscode/src/ide/testing.ts b/mide-plugins/vscode/src/ide/testing.ts index dc67cc4..b0fdcc5 100644 --- a/mide-plugins/vscode/src/ide/testing.ts +++ b/mide-plugins/vscode/src/ide/testing.ts @@ -1,34 +1,76 @@ /** - * mcpp's tests in VS Code's Testing view. The tests of each project come from `test/list` (mcpp - * test --list: no build), each item located at the file that holds its `main`; a run is one test - * job, filtered to the test asked for, and each `test/result` marks its item. + * mcpp's tests in VS Code's Testing view (design 2026-10-30 part 2 §7). The tests of each project + * come from `test/list` (mcpp test --list: no build), each a program located at its `main`; a + * program of a test framework has its cases as children, located at their macros (`test/cases`, + * read from the source), and after a build the program lists them (only where its source calls its + * framework's macros, and only in a trusted workspace), which adds the cases macros generate. + * + * A run of whole programs is one test job, filtered to the program asked for; a run of cases is + * `test/runCases`, whose program receives the framework's filter, and each `test/caseResult` marks + * its case. */ import * as path from "node:path"; import * as vscode from "vscode"; import { t } from "../i18n/t"; import type { Jobs } from "./jobs"; -import { parseTestId, testId } from "./mapping"; +import { baseCaseName, caseId, parseCaseId, parseTestId, testId } from "./mapping"; import type { IdeProjects } from "./projects"; import type { Session } from "./session"; -import type { TestResult } from "./protocol"; +import type { CaseResult, JobFinished, TestCases, TestResult } from "./protocol"; + +interface Program { + root: string; + member: string; + test: string; + framework: string | null; +} export class Testing implements vscode.Disposable { private readonly controller = vscode.tests.createTestController("mcpp", "mcpp"); private readonly disposables: vscode.Disposable[] = [this.controller]; + private readonly programs = new Map(); constructor(private readonly session: Session, private readonly jobs: Jobs, private readonly projects: IdeProjects) { this.controller.resolveHandler = async (item) => { if (!item) await this.discover(); }; - this.controller.refreshHandler = async () => { await this.discover(); }; + this.controller.refreshHandler = async () => { await this.discover(true); }; this.controller.createRunProfile(t("Run"), vscode.TestRunProfileKind.Run, (request, token) => this.run(request, token), true); this.disposables.push( session.on("manifest/changed", () => void this.discover()), session.on("result/changed", () => void this.discover()), session.onDidStart.event(() => void this.discover()), + // A build ended: the programs can list the cases their macros generate. + session.on("job/finished", (p: JobFinished) => { + if (p.status === "ok" && (p.kind === "build" || p.kind === "test") && vscode.workspace.isTrusted) void this.discover(true); + }), + vscode.workspace.onDidSaveTextDocument((doc) => { + if (doc.languageId === "cpp" && /[\\/]tests[\\/]/.test(doc.uri.fsPath)) void this.discover(); + }), ); } - async discover(): Promise { + /** The program a test's `main` is, by its project and name (the CodeLens runs it). */ + item(root: string, test: string): vscode.TestItem | undefined { + const group = this.controller.items.get(root); + let found: vscode.TestItem | undefined; + group?.children.forEach((child) => { + const id = parseTestId(child.id); + if (id && id.test === test) found = child; + }); + return found; + } + + /** Runs one program or case, as the Testing view's ▶ does. */ + async runItem(item: vscode.TestItem): Promise { + const source = new vscode.CancellationTokenSource(); + try { + await this.run(new vscode.TestRunRequest([item]), source.token); + } finally { + source.dispose(); + } + } + + async discover(list = false): Promise { for (const project of this.projects.all()) { let tests: { test: string; member: string; main: string }[] = []; try { @@ -41,30 +83,95 @@ export class Testing implements vscode.Disposable { group = this.controller.createTestItem(project.root, project.name ?? path.basename(project.root), vscode.Uri.file(project.root)); this.controller.items.add(group); } - const items = tests.map((t) => { - const item = this.controller.createTestItem(testId(project.root, t.member, t.test), t.member ? `${t.member} · ${t.test}` : t.test, - t.main ? vscode.Uri.file(t.main) : undefined); - if (t.main) item.range = new vscode.Range(0, 0, 0, 0); - return item; - }); + const items: vscode.TestItem[] = []; + for (const test of tests) { + const id = testId(project.root, test.member, test.test); + const item = this.controller.createTestItem(id, test.member ? `${test.member} · ${test.test}` : test.test, + test.main ? vscode.Uri.file(test.main) : undefined); + const program: Program = { root: project.root, member: test.member, test: test.test, framework: null }; + let cases: TestCases | undefined; + try { + cases = await this.session.request("test/cases", { root: project.root, test: test.test, list }); + } catch { + cases = undefined; + } + // The program at its `main` (F11); a framework's program at its first case when it has none. + const mainLine = cases?.main?.line ?? cases?.cases.find((c) => c.line !== undefined)?.line ?? 0; + if (test.main) item.range = new vscode.Range(mainLine, 0, mainLine, 0); + if (cases?.framework) { + program.framework = cases.framework; + item.description = cases.framework; + item.children.replace(cases.cases.map((c) => { + const child = this.controller.createTestItem(caseId(project.root, test.member, test.test, c.name), c.name, + test.main ? vscode.Uri.file(test.main) : undefined); + if (test.main && c.line !== undefined) child.range = new vscode.Range(c.line, 0, c.line, 0); + return child; + })); + } + this.programs.set(id, program); + items.push(item); + } group.children.replace(items); } } private async run(request: vscode.TestRunRequest, token: vscode.CancellationToken): Promise { const run = this.controller.createTestRun(request); - // One job per project; a single test asked for becomes the job's filter. - const byRoot = new Map(); + // One job per program whose cases were asked for; one job per project for whole programs. + const wholePrograms = new Map(); + const casesOf = new Map(); const collect = (item: vscode.TestItem) => { + if (request.exclude?.includes(item)) return; + const c = parseCaseId(item.id); + if (c) { + const owner = testId(c.root, c.member, c.test); + casesOf.set(owner, [...(casesOf.get(owner) ?? []), item]); + return; + } const id = parseTestId(item.id); if (!id) { item.children.forEach(collect); return; } - if (request.exclude?.includes(item)) return; - if (!byRoot.has(id.root)) byRoot.set(id.root, []); - byRoot.get(id.root)!.push(item); + wholePrograms.set(id.root, [...(wholePrograms.get(id.root) ?? []), item]); }; if (request.include) request.include.forEach(collect); else this.controller.items.forEach(collect); - for (const [root, items] of byRoot) { + + for (const [owner, items] of casesOf) { + if (token.isCancellationRequested) break; + const program = this.programs.get(owner); + if (!program?.framework) continue; + items.forEach((i) => run.enqueued(i)); + // Every case of the program asked for: the filter is left out and the program runs them all. + let all = 0; + this.controller.items.get(program.root)?.children.get(owner)?.children.forEach(() => { all++; }); + const names = items.length === all ? [] : items.map((i) => parseCaseId(i.id)!.name); + const byName = new Map(items.map((i) => [parseCaseId(i.id)!.name, i])); + const seen = new Set(); + const handle = await this.jobs.start( + { root: program.root, test: program.test, framework: program.framework, cases: names, color: true }, + { + output: (text) => run.appendOutput(text.replace(/\r?\n/g, "\r\n")), + caseResult: (r: CaseResult) => { + const item = byName.get(r.case) ?? byName.get(baseCaseName(program.framework, r.case)); + if (!item) return; + seen.add(item); + if (r.status === "passed") run.passed(item, r.durationMs); + else if (r.status === "skipped") run.skipped(item); + else run.failed(item, new vscode.TestMessage(r.message || r.status), r.durationMs); + }, + }, + "test/runCases", + ); + const cancel = token.onCancellationRequested(() => void handle.cancel()); + const done = await handle.done; + cancel.dispose(); + for (const i of items) + if (!seen.has(i)) { + if (done.status === "ok" || done.status === "failed") run.skipped(i); + else run.errored(i, new vscode.TestMessage(t("The test program did not run: {0}", done.status))); + } + } + + for (const [root, items] of wholePrograms) { if (token.isCancellationRequested) break; items.forEach((i) => run.enqueued(i)); const index = new Map(items.map((i) => [parseTestId(i.id)!.test, i])); diff --git a/mide-plugins/vscode/test/artifacts.test.ts b/mide-plugins/vscode/test/artifacts.test.ts index 9fd7777..d1e2bb3 100644 --- a/mide-plugins/vscode/test/artifacts.test.ts +++ b/mide-plugins/vscode/test/artifacts.test.ts @@ -21,7 +21,7 @@ interface PackageManifest { capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string; restrictedConfigurations?: string[] } }; contributes?: { commands?: Array<{ command: string; title?: string; category?: string; icon?: string }>; - menus?: { "editor/title"?: Array<{ command: string; group?: string; when?: string }> }; + menus?: { "editor/title"?: Array<{ command: string; group?: string; when?: string }>; "editor/context"?: Array<{ command: string; group?: string; when?: string }> }; configuration?: { title?: string; properties?: Record }; configurationDefaults?: Record; languages?: Array<{ id: string; aliases?: string[]; filenames?: string[]; configuration?: string }>; @@ -340,15 +340,22 @@ function sourceFiles(directory = path.join(root, "src")): Array<[string, string] test("shows editor title buttons only inside mcpp projects", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; // Both conditions matter: a project must be open, and the user must not have - // turned the buttons off with `mcpp.task.editorTitleButtons`. + // turned the buttons off with `mcpp.task.editorTitleButtons`. 0.9.0's Run and Test as they were, + // then Build and Debug (design 2026-10-30 part 2 §6.1). assert.deepEqual(manifest.contributes?.menus?.["editor/title"], [ + { command: "mcpp.editor.build", group: "navigation@0", when: "mcpp.inProject && mcpp.editorTitleButtons" }, { command: "mcpp.run", group: "navigation@1", when: "mcpp.inProject && mcpp.editorTitleButtons" }, + { command: "mcpp.debug", group: "navigation@3", when: "mcpp.inProject && mcpp.editorTitleButtons" }, { command: "mcpp.test", group: "navigation@2", when: "mcpp.inProject && mcpp.editorTitleButtons" }, ]); + // Run, Debug, Build and Test in the editor's context menu. + assert.deepEqual((manifest.contributes?.menus?.["editor/context"] ?? []).map((entry) => entry.command), + ["mcpp.build", "mcpp.run", "mcpp.debug", "mcpp.test"]); const commands = manifest.contributes?.commands ?? []; assert.equal(commands.find((command) => command.command === "mcpp.run")?.icon, "$(play)"); assert.equal(commands.find((command) => command.command === "mcpp.test")?.icon, "$(beaker)"); + assert.equal(commands.find((command) => command.command === "mcpp.editor.build")?.icon, "$(tools)"); assert.ok(!commands.some((command) => command.command === "mcpp.inProject")); }); diff --git a/mide-plugins/vscode/test/config/registry.test.ts b/mide-plugins/vscode/test/config/registry.test.ts index 341c5d5..d0de3f0 100644 --- a/mide-plugins/vscode/test/config/registry.test.ts +++ b/mide-plugins/vscode/test/config/registry.test.ts @@ -16,21 +16,22 @@ import { subKey, } from "../../src/config/registry"; -test("the registry declares 10 groups and 76 settings", () => { +test("the registry declares 10 groups and 78 settings", () => { assert.equal(GROUPS.length, 10); - // 0.9.0's 69, and the seven the mcpp-ide backend reads (build target, profile and on-save, the - // debugger, the backend's path, the effects an untrusted workspace denies, its log level). - assert.equal(SETTINGS.length, 76); + // 0.9.0's 69, the seven the mcpp-ide backend reads (build target, profile and on-save, the + // debugger, the backend's path, the effects an untrusted workspace denies, its log level), the + // CodeLens above main and the macros of a test framework. + assert.equal(SETTINGS.length, 78); }); test("the registry's own shape rules hold", () => { assert.deepEqual(registryShapeProblems(), []); }); -test("36 settings are public and the rest are advanced", () => { - assert.equal(settingsByTier("public").length, 36); - // 36 advanced + the 4 deprecated keys the Settings UI keeps out of the way. - assert.equal(settingsByTier("advanced").length, 40); +test("37 settings are public and the rest are advanced", () => { + assert.equal(settingsByTier("public").length, 37); + // 37 advanced + the 4 deprecated keys the Settings UI keeps out of the way. + assert.equal(settingsByTier("advanced").length, 41); // The four deprecated keys live in `advanced` so the Settings UI keeps them out of the way. assert.equal(settingsByTier("advanced").filter((entry) => entry.deprecated === true).length, 4); }); diff --git a/mide-plugins/vscode/test/config/wiring.test.ts b/mide-plugins/vscode/test/config/wiring.test.ts index 085584c..e5ca7f1 100644 --- a/mide-plugins/vscode/test/config/wiring.test.ts +++ b/mide-plugins/vscode/test/config/wiring.test.ts @@ -45,6 +45,11 @@ const READ_INDIRECTLY: ReadonlyArray<{ key: string; file: string; evidence: stri file: "src/ide/session.ts", evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", }, + { + key: "mcpp.test.macros", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, { key: "mcpp.task.buildArgs", file: "src/cli/tasks.ts", diff --git a/mide-plugins/vscode/test/ide/mapping.test.ts b/mide-plugins/vscode/test/ide/mapping.test.ts index 35acd70..026426d 100644 --- a/mide-plugins/vscode/test/ide/mapping.test.ts +++ b/mide-plugins/vscode/test/ide/mapping.test.ts @@ -2,7 +2,7 @@ import * as assert from "node:assert/strict"; import { test } from "node:test"; -import { availableDebuggers, format, parseTestId, severityOf, testId, zeroBased } from "../../src/ide/mapping"; +import { availableDebuggers, baseCaseName, caseId, format, mainLineOf, parseCaseId, parseTestId, severityOf, testId, zeroBased } from "../../src/ide/mapping"; test("positions are 1-based in mcpp and 0-based in VS Code", () => { assert.deepEqual(zeroBased({ line: 1, column: 21 }), { line: 0, character: 20 }); @@ -25,3 +25,25 @@ test("the debuggers present, in order of preference", () => { assert.deepEqual(availableDebuggers((id) => id === "ms-vscode.cpptools" || id === "vadimcn.vscode-lldb"), ["lldb", "cppdbg"]); assert.equal(format("{0} and {1}", ["a", 2]), "a and 2"); }); + +test("a case's id holds its program's and its name, which may hold ::", () => { + const id = caseId("/w/p", "", "unit/math", "ns::Suite.Name"); + assert.deepEqual(parseCaseId(id), { root: "/w/p", member: "", test: "unit/math", name: "ns::Suite.Name" }); + assert.equal(parseTestId(id), undefined); + assert.equal(parseCaseId(testId("/w/p", "", "unit/math")), undefined); +}); + +test("a GoogleTest result is matched to its source's case without instance and parameter", () => { + assert.equal(baseCaseName("gtest", "Inst/Param.Works/1"), "Param.Works"); + assert.equal(baseCaseName("gtest", "Typed/0.Holds"), "Typed.Holds"); + assert.equal(baseCaseName("gtest", "Math.Adds"), "Math.Adds"); + assert.equal(baseCaseName("catch2", "a/b.c"), "a/b.c"); +}); + +test("main is the language service's top-level function of that name", () => { + const at = (line: number) => ({ start: { line } }); + assert.equal(mainLineOf([{ name: "helper", kind: 11, range: at(1) }, { name: "main", kind: 11, range: at(7) }]), 7); + assert.equal(mainLineOf([{ name: "main(int, char **)", kind: 11, range: at(3) }]), 3); + assert.equal(mainLineOf([{ name: "main", kind: 12, range: at(3) }]), undefined); + assert.equal(mainLineOf(undefined), undefined); +}); diff --git a/protocol/protocol.schema.json b/protocol/protocol.schema.json index a20ae51..15c8399 100644 --- a/protocol/protocol.schema.json +++ b/protocol/protocol.schema.json @@ -85,6 +85,30 @@ "params": { "type": "object", "properties": { "candidates": { "type": "array", "items": { "type": "string" } }, "root": { "type": "string", "description": "The workspace to ask mcppls about (default: the first folder)" } } }, "result": { "$ref": "#/$defs/languageService" } }, + "source/entries": { + "description": "What a C++ file is to its project (part 2 §6.3): a test's main (tests/**, named as mcpp names it), a program's main, or neither, with the line of main read from the text (approximate) and, for a test, its framework. Nothing is built or run.", + "params": { "type": "object", "required": ["file"], "properties": { "file": { "type": "string" }, "text": { "type": "string", "description": "The editor's unsaved text" }, "boundary": { "type": "string" } } }, + "result": { + "type": "object", "required": ["file", "kind"], + "properties": { "file": { "type": "string" }, "root": { "type": "string" }, "kind": { "enum": ["program", "test", "none"] }, "test": { "type": "string" }, + "framework": { "type": "string" }, "main": { "type": "object", "required": ["line"], "properties": { "line": { "type": "integer" }, "approximate": { "type": "boolean" } } } } + } + }, + "test/cases": { + "description": "A test program's cases (part 2 §7.3): as its source names them (a framework's macros), and with `list` -- trusted, and only for a source that calls its framework's macros -- as the program lists them. Without a framework, no cases: the program is the unit.", + "params": { "type": "object", "properties": { "root": { "type": "string" }, "test": { "type": "string" }, "file": { "type": "string" }, "list": { "type": "boolean" }, "target": { "type": "string" } } }, + "result": { + "type": "object", "required": ["root", "test", "framework", "cases"], + "properties": { "root": { "type": "string" }, "test": { "type": "string" }, "file": { "type": "string" }, "framework": { "type": ["string", "null"] }, "listed": { "type": "boolean" }, + "main": { "type": "object" }, + "cases": { "type": "array", "items": { "type": "object", "required": ["name", "source"], "properties": { "name": { "type": "string" }, "line": { "type": "integer" }, "source": { "enum": ["static", "listed"] } } } } } + } + }, + "test/runCases": { + "description": "Runs some of a test program's cases (all of them when `cases` is empty): a test job whose program receives the framework's filter and report arguments after `--`. Each case's outcome is sent as test/caseResult when the job ends.", + "params": { "type": "object", "required": ["root", "test", "framework"], "properties": { "root": { "type": "string" }, "test": { "type": "string" }, "framework": { "type": "string" }, "cases": { "type": "array", "items": { "type": "string" } }, "profile": { "type": "string" }, "target": { "type": "string" }, "color": { "type": "boolean" } } }, + "result": { "type": "object", "required": ["jobId"] } + }, "projects/list": { "params": { "type": "object", "properties": { "folders": { "type": "array", "items": { "type": "string" } }, "depth": { "type": "integer" } } }, "result": { "type": "array", "items": { "$ref": "#/$defs/project" } } @@ -417,6 +441,7 @@ "manifest/changed": { "params": { "type": "object", "required": ["root"] } }, "manifest/diagnostics": { "params": { "type": "object", "required": ["root", "diagnostics"] } }, "project/mcppChanged": { "description": "A job's envelope named another mcpp than the project's was asked as: it is asked again on its next use.", "params": { "type": "object", "required": ["root", "version"] } }, + "test/caseResult": { "description": "One case's outcome, read from the framework's report when a test/runCases job ended.", "params": { "type": "object", "required": ["jobId", "root", "test", "case", "status"], "properties": { "status": { "enum": ["passed", "failed", "skipped"] }, "durationMs": { "type": "integer" }, "message": { "type": "string" } } } }, "languageService/changed": { "description": "mcppls answered differently from the last time it was asked: another program or version, other actions, or another state of the workspace. An editor that lists the commands asks for them again.", "params": { "type": "object", "required": ["root", "state", "workspaceState"] } } }, "$defs": { diff --git a/spec/commands.json b/spec/commands.json index 850590c..5cf4994 100644 --- a/spec/commands.json +++ b/spec/commands.json @@ -11,16 +11,25 @@ "commands": [ { "id": "mcpp.build", "group": "project", "icon": { "jetbrains": "AllIcons.Actions.Compile" }, "when": "mcpp.inProject", "keybinding": { "key": "ctrl+alt+b", "mac": "cmd+alt+b" }, + "placements": { "context": 3 }, "action": { "kind": "job", "job": "build" }, "quickMenu": true }, + { "id": "mcpp.editor.build", "group": "project", "palette": false, "editors": ["vscode"], + "omitted": { "jetbrains": "mcpp.build's own action carries its icon; this row exists for VS Code's editor title, which shows a command's icon, and mcpp.build has none in 0.9.0's manifest.", + "nvim": "mcpp.build is :Mcpp build; this row exists for VS Code's editor title." }, + "icon": { "vscode": "$(tools)" }, "when": "mcpp.inProject", + "placements": { "title": 0 }, + "action": { "kind": "job", "job": "build" } }, { "id": "mcpp.run", "group": "project", "icon": { "vscode": "$(play)", "jetbrains": "AllIcons.Actions.Execute" }, "when": "mcpp.inProject", "keybinding": { "key": "ctrl+alt+r", "mac": "cmd+alt+r" }, "menus": [ { "menu": "editor/title", "group": "navigation@1", "when": "mcpp.inProject && mcpp.editorTitleButtons" } ], + "placements": { "context": 1, "gutter": 1 }, "action": { "kind": "run" }, "quickMenu": true }, { "id": "mcpp.debug", "paletteWhen": "mcpp.inProject", "group": "project", "icon": { "vscode": "$(debug-alt)", "jetbrains": "AllIcons.Actions.StartDebugger" }, - "when": "mcpp.inProject", "action": { "kind": "debug" }, "quickMenu": true }, + "when": "mcpp.inProject", "placements": { "title": 3, "context": 2, "gutter": 2 }, "action": { "kind": "debug" }, "quickMenu": true }, { "id": "mcpp.test", "group": "project", "icon": { "vscode": "$(beaker)", "jetbrains": "AllIcons.RunConfigurations.TestState.Run" }, "when": "mcpp.inProject", "keybinding": { "key": "ctrl+alt+t", "mac": "cmd+alt+t" }, "menus": [ { "menu": "editor/title", "group": "navigation@2", "when": "mcpp.inProject && mcpp.editorTitleButtons" } ], + "placements": { "context": 4 }, "action": { "kind": "job", "job": "test" }, "quickMenu": true }, { "id": "mcpp.clean", "group": "project", "icon": { "jetbrains": "AllIcons.Actions.GC" }, "when": "mcpp.inProject", "action": { "kind": "cache", "plan": "project" }, "quickMenu": true }, diff --git a/spec/i18n/manifest.en.json b/spec/i18n/manifest.en.json index bee4403..7503047 100644 --- a/spec/i18n/manifest.en.json +++ b/spec/i18n/manifest.en.json @@ -4,6 +4,7 @@ "colors.mcpp.cacheStaleForeground": "Colour of artifacts that can be cleaned.", "command.mcpp.autoConfigureModules.title": "Build and Refresh the Language Service", "command.mcpp.build.title": "Build", + "command.mcpp.editor.build.title": "Build", "command.mcpp.checkModuleSupport.title": "[Deprecated] C++ Modules: Restart the Language Server", "command.mcpp.clean.title": "Clean target", "command.mcpp.cleanLegacyCache.title": "Remove the Pre-v1 Cache", @@ -268,5 +269,9 @@ "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml's problems come from mcpp itself now, located where mcpp reports them; this check no longer exists. mcpp.toml.diagnostics.enabled turns them all off.", "mcpp.backend.logLevel.title": "Set the backend's log level", "mcpp.backend.logLevel.description": "What the mcpp-ide backend writes to the editor's log: errors only, warnings, what it does (the default), or every run of mcpp and mcppls with its arguments.", - "command.mcpp.exportReport.title": "Export IDE Report" + "command.mcpp.exportReport.title": "Export IDE Report", + "mcpp.codeLens.enable.title": "Show Run and Debug above main", + "mcpp.codeLens.enable.description": "A CodeLens above the main of a program or a test: Run and Debug. Where C++ Modules serves the file its position is the language service's; otherwise the line is read from the text.", + "mcpp.test.macros.title": "Add a test framework's macros", + "mcpp.test.macros.description": "Macros that write a test framework's cases, each as framework:MACRO (gtest:MY_TEST). A test program's cases are listed only when its source calls one of its framework's macros." } diff --git a/spec/i18n/manifest.zh-cn.json b/spec/i18n/manifest.zh-cn.json index dc5c1b7..7ede0f2 100644 --- a/spec/i18n/manifest.zh-cn.json +++ b/spec/i18n/manifest.zh-cn.json @@ -4,6 +4,7 @@ "colors.mcpp.cacheStaleForeground": "可清理产物的颜色。", "command.mcpp.autoConfigureModules.title": "一键构建并刷新模块语言服务", "command.mcpp.build.title": "构建", + "command.mcpp.editor.build.title": "构建", "command.mcpp.checkModuleSupport.title": "[已弃用] C++ Modules: 重启语言服务", "command.mcpp.clean.title": "清理 target", "command.mcpp.cleanLegacyCache.title": "清理遗留缓存", @@ -268,5 +269,9 @@ "mcpp.toml.diagnostics.legacyKeys.deprecationMessage": "mcpp.toml 的问题现在由 mcpp 自己报告并定位;这项检查已不存在。mcpp.toml.diagnostics.enabled 可整体关闭。", "mcpp.backend.logLevel.title": "设置后端的日志级别", "mcpp.backend.logLevel.description": "mcpp-ide 后端写入编辑器日志的内容:只写错误、写警告、写它所做的事(缺省),或写每一次运行 mcpp 与 mcppls 及其参数。", - "command.mcpp.exportReport.title": "导出 IDE 报告" + "command.mcpp.exportReport.title": "导出 IDE 报告", + "mcpp.codeLens.enable.title": "在 main 上方显示运行与调试", + "mcpp.codeLens.enable.description": "在程序或测试的 main 上方显示 CodeLens:运行、调试。C++ Modules 服务该文件时位置取自语言服务,否则从文本读出行号。", + "mcpp.test.macros.title": "追加测试框架的宏", + "mcpp.test.macros.description": "书写测试框架用例的宏,每项写作 框架:宏名(gtest:MY_TEST)。测试程序的源文件调用了其框架的宏时才列举它的用例。" } diff --git a/spec/settings.json b/spec/settings.json index 41e59d0..cd46686 100644 --- a/spec/settings.json +++ b/spec/settings.json @@ -293,6 +293,32 @@ "title": "Choose the debugger", "description": "The debug adapter a debug session uses. auto: CodeLLDB when it is installed, then the C/C++ extension's gdb or lldb, then lldb-dap." }, + { + "key": "mcpp.codeLens.enable", + "type": "boolean", + "default": true, + "scope": "resource", + "group": "tasks", + "order": 140, + "tier": "public", + "applies": "immediate", + "since": "1.1.0", + "title": "Show Run and Debug above main", + "description": "A CodeLens above the main of a program or a test: Run and Debug. Where C++ Modules serves the file its position is the language service's; otherwise the line is read from the text." + }, + { + "key": "mcpp.test.macros", + "type": "array", + "default": [], + "scope": "resource", + "group": "tasks", + "order": 150, + "tier": "advanced", + "applies": "immediate", + "since": "1.1.0", + "title": "Add a test framework's macros", + "description": "Macros that write a test framework's cases, each as framework:MACRO (gtest:MY_TEST). A test program's cases are listed only when its source calls one of its framework's macros." + }, { "key": "mcpp.languageService.menuItems", "type": "boolean", diff --git a/spec/test-frameworks.json b/spec/test-frameworks.json new file mode 100644 index 0000000..21f4b63 --- /dev/null +++ b/spec/test-frameworks.json @@ -0,0 +1,49 @@ +{ + "description": "The test frameworks whose cases the editors show (mcpp design 2026-10-30 part 2 §7.2). A test program belongs to a framework when its plan holds one of the framework's packages and its source calls one of the framework's macros. `name` says how a case's full name is made from the macro's arguments; `list` lists the cases (run only for a program whose source calls a macro); `filter` selects cases, `{}` being the cases joined by `join`, each with the characters of `escape` preceded by a backslash (a GoogleTest name is two identifiers, which hold none of its filter's special characters); `report` writes the results to the file `{}`, read as `reportFormat`. boost-ext.ut is not here: its runner takes no filter from the command line, so its tests run as programs.", + "frameworks": [ + { + "id": "gtest", + "title": "GoogleTest", + "packages": ["compat.gtest", "gtest", "gmock"], + "macros": ["TEST", "TEST_F", "TEST_P", "TYPED_TEST", "TYPED_TEST_P"], + "name": "suite.case", + "list": ["--gtest_list_tests"], + "listFormat": "gtest-list", + "filter": ["--gtest_filter={}"], + "join": ":", + "escape": "", + "report": ["--gtest_output=json:{}"], + "reportFormat": "gtest-json" + }, + { + "id": "catch2", + "title": "Catch2", + "packages": ["compat.catch2", "catch2"], + "macros": ["TEST_CASE", "TEST_CASE_METHOD", "SCENARIO", "TEMPLATE_TEST_CASE"], + "name": "literal", + "scenarioPrefix": "Scenario: ", + "list": ["--list-tests", "--reporter", "json"], + "listFormat": "catch2-json", + "filter": ["{}"], + "join": ",", + "escape": ",[]*\\", + "report": ["--reporter", "json::out={}"], + "reportFormat": "catch2-json" + }, + { + "id": "doctest", + "title": "doctest", + "packages": ["compat.doctest", "doctest"], + "macros": ["TEST_CASE", "TEST_CASE_FIXTURE", "SCENARIO"], + "name": "literal", + "scenarioPrefix": " Scenario: ", + "list": ["--list-test-cases", "--no-intro", "--no-version"], + "listFormat": "lines", + "filter": ["--test-case={}"], + "join": ",", + "escape": ",", + "report": ["--reporters=junit", "--out={}"], + "reportFormat": "junit" + } + ] +} diff --git a/src/bin/dev.cpp b/src/bin/dev.cpp index d8b61a1..de1fc37 100644 --- a/src/bin/dev.cpp +++ b/src/bin/dev.cpp @@ -227,6 +227,18 @@ void vscode_contributes(const Spec& spec, Json& contributes) { if (m.contains("when")) entry["when"] = m["when"]; menus[m.value("menu", "")].push_back(entry); } + // Where else the command is offered (design 2026-10-30 part 2 §6.2): the editor's title + // bar beside 0.9.0's Run and Test, and the editor's context menu, each in its order. An + // entry 0.9.0 wrote is kept as written above; these are added after it. + if (c.contains("placements") && c["placements"].is_object()) { + const auto& at = c["placements"]; + if (at.contains("title")) + menus["editor/title"].push_back(Json{{"command", id}, {"group", std::format("navigation@{}", at["title"].get())}, + {"when", "mcpp.inProject && mcpp.editorTitleButtons"}}); + if (at.contains("context")) + menus["editor/context"].push_back(Json{{"command", id}, {"group", std::format("mcpp@{}", at["context"].get())}, + {"when", "mcpp.inProject && editorLangId == cpp"}}); + } // A command is in the palette everywhere unless its row says otherwise; `when` gates its // keybinding and the backend's menus, not the palette. if (!c.value("palette", true)) palette.push_back(Json{{"command", id}, {"when", "false"}}); @@ -339,9 +351,55 @@ int gen_jetbrains(std::span args) { } xml += " \n"; } - xml += " \n \n\n"; + xml += " \n"; + // The editor's context menu (§6.2): the rows placed there, in their order, as references to the + // actions above. + std::vector> popup; + for (const auto& c : spec.commands["commands"]) + if (offered_in(c, "jetbrains") && c.contains("placements") && c["placements"].contains("context")) + popup.push_back({c["placements"]["context"].get(), c.value("id", "")}); + std::ranges::sort(popup); + if (!popup.empty()) { + xml += " \n \n"; + for (const auto& [order, id] : popup) xml += std::format(" \n", xml_escape(id)); + xml += " \n \n"; + } + xml += " \n\n"; spit(out / "META-INF" / "mcpp-actions.xml", xml); + // The editors' chords (§6.1) as JetBrains keymaps, one for the default keymap and one for + // macOS's: CLion's own keymaps keep their bindings (Ctrl+Alt+B is Go to Implementation there), + // and a person who wants the chords chooses this keymap in Settings | Keymap. + auto keystroke = [](std::string chord) { + std::string out; + std::size_t at = 0; + while (at <= chord.size()) { + const auto plus = chord.find('+', at); + auto part = chord.substr(at, plus == std::string::npos ? std::string::npos : plus - at); + if (part == "cmd") part = "meta"; + else if (part.size() == 1) part = std::string(1, static_cast(std::toupper(static_cast(part[0])))); + if (!out.empty()) out += ' '; + out += part; + if (plus == std::string::npos) break; + at = plus + 1; + } + return out; + }; + for (const auto& [file, name, parent, key] : std::array, 2>{{ + {"mcpp.xml", "mcpp", "$default", "key"}, {"mcpp-macos.xml", "mcpp (macOS)", "Mac OS X 10.5+", "mac"}}}) { + std::string keymap = std::format("\n" + "\n", name, parent); + for (const auto& c : spec.commands["commands"]) { + if (!offered_in(c, "jetbrains") || !c.contains("keybinding")) continue; + const auto chord = c["keybinding"].value(key, c["keybinding"].value("key", "")); + if (chord.empty()) continue; + keymap += std::format(" \n \n \n", + xml_escape(c.value("id", "")), xml_escape(keystroke(chord))); + } + keymap += "\n"; + spit(out / "keymaps" / file, keymap); + } + // The bundles: the actions' texts, the groups', and every editor string under its own key. auto bundle = [&](const Json& strings) { std::string text = "# Generated by mcpp-ide-dev gen jetbrains from spec/i18n.\n"; diff --git a/src/jobs.cppm b/src/jobs.cppm index 2e46b09..69211fb 100644 --- a/src/jobs.cppm +++ b/src/jobs.cppm @@ -43,6 +43,9 @@ struct Request { std::string client = "mcpp-ide"; // Variables set (or, with no value, removed) for this run, over the backend's environment. std::vector>> env; + // Called once when the job has ended, however it ended, before the editors are told: what the + // run wrote is read here (a test framework's report). + std::function after; }; struct Sink { @@ -82,6 +85,8 @@ private: }; void run_lane(std::string key); void execute(const std::shared_ptr& job); + // The request's own hook, then the editors. + void finish(const Job& job, const Json& summary); mutable std::mutex mutex_; std::condition_variable_any wake_; @@ -152,8 +157,7 @@ std::string Jobs::submit(Request request) { std::erase_if(lane.queue, [&](const std::shared_ptr& waiting) { if (waiting->request.kind != job->request.kind) return false; waiting->cancelled = true; - if (sink_.finished) - sink_.finished(waiting->id, Json{{"status", "superseded"}, {"exitCode", nullptr}}); + finish(*waiting, Json{{"status", "superseded"}, {"exitCode", nullptr}}); return true; }); if (job->request.supersede && lane.running && lane.running->request.kind == job->request.kind && @@ -180,7 +184,7 @@ bool Jobs::cancel(std::string_view jobId) { } for (auto it = lane.queue.begin(); it != lane.queue.end(); ++it) if ((*it)->id == jobId) { - if (sink_.finished) sink_.finished((*it)->id, Json{{"status", "cancelled"}, {"exitCode", nullptr}}); + finish(**it, Json{{"status", "cancelled"}, {"exitCode", nullptr}}); lane.queue.erase(it); return true; } @@ -261,8 +265,7 @@ void Jobs::execute(const std::shared_ptr& job) { if (!spawned) { const auto why = spawned.error().message(); if (sink_.narration) sink_.narration(job->id, why + "\n"); - if (sink_.finished) sink_.finished(job->id, Json{{"status", "failed"}, {"exitCode", nullptr}, - {"error", why}}); + finish(*job, Json{{"status", "failed"}, {"exitCode", nullptr}, {"error", why}}); return; } auto process = std::make_shared(std::move(*spawned)); @@ -328,7 +331,12 @@ void Jobs::execute(const std::shared_ptr& job) { } Json summary{{"status", status}, {"exitCode", code}, {"durationMs", ms.count()}}; if (!envelope.is_null()) summary["envelope"] = envelope; - if (sink_.finished) sink_.finished(job->id, summary); + finish(*job, summary); +} + +void Jobs::finish(const Job& job, const Json& summary) { + if (job.request.after) job.request.after(job.id, summary); + if (sink_.finished) sink_.finished(job.id, summary); } } // namespace mcppide::jobs diff --git a/src/scan.cppm b/src/scan.cppm new file mode 100644 index 0000000..69955d7 --- /dev/null +++ b/src/scan.cppm @@ -0,0 +1,231 @@ +// What a C++ source says about where it starts and what it tests, read as text (mcpp design +// 2026-10-30 part 2 §6.3, §7.3): the line of `main`, and the calls of a test framework's macros. +// +// NOTHING IS BUILT AND NOTHING IS RUN. A file is scanned when it is opened, before any build and +// without mcppls, so the editors can place ▶ at once. Comments, string and character literals (raw +// strings included) and preprocessor lines are skipped, so a `main(` in a comment or a `TEST(` in a +// string is not one. A position found this way is approximate where a language service has a +// better one: the editors take the language service's position of `main` when it gives one. +// +// A MACRO CALL COUNTS AT THE START OF A DECLARATION: after `;`, `{`, `}`, a preprocessor line or +// the start of the file. That is where a framework's test macros are written (at namespace scope, +// or in a test body for a section), and an expression that merely contains the name is not one. +export module mcppide.scan; + +import std; + +export namespace mcppide::scan { + +// One call of a macro: its name, where it is (0-based line and column of the name), and its +// arguments as written, split at the top-level commas, each trimmed. +struct Call { + std::string name; + int line = 0; + int column = 0; + std::vector arguments; +}; + +struct Result { + std::optional mainLine; // 0-based + std::vector calls; +}; + +// Scans `text` for `main` and for calls of the macros named in `macros`. +Result scan(std::string_view text, const std::vector& macros); + +// A string literal's contents, as the program sees them for the plain escapes (\" \\ \n \t); the +// argument itself when it is not one literal. Adjacent literals are joined. +std::string literal_value(std::string_view argument); + +} // namespace mcppide::scan + +namespace mcppide::scan { + +namespace { + +bool ident_start(char c) { return std::isalpha(static_cast(c)) || c == '_'; } +bool ident_char(char c) { return std::isalnum(static_cast(c)) || c == '_'; } + +std::string trim(std::string_view s) { + while (!s.empty() && std::isspace(static_cast(s.front()))) s.remove_prefix(1); + while (!s.empty() && std::isspace(static_cast(s.back()))) s.remove_suffix(1); + return std::string(s); +} + +// A cursor over the text that knows its line and steps over what is not code. +struct Cursor { + std::string_view text; + std::size_t at = 0; + int line = 0; + std::size_t lineStart = 0; + + bool done() const { return at >= text.size(); } + char peek(std::size_t ahead = 0) const { return at + ahead < text.size() ? text[at + ahead] : '\0'; } + void step() { + if (text[at] == '\n') { ++line; lineStart = at + 1; } + ++at; + } + int column() const { return static_cast(at - lineStart); } + + void skip_line_comment() { while (!done() && peek() != '\n') step(); } + void skip_block_comment() { + step(); step(); + while (!done() && !(peek() == '*' && peek(1) == '/')) step(); + if (!done()) { step(); step(); } + } + void skip_quoted(char quote) { + step(); + while (!done() && peek() != quote && peek() != '\n') { + if (peek() == '\\' && at + 1 < text.size()) step(); + step(); + } + if (!done() && peek() == quote) step(); + } + // R"delim( ... )delim" -- the cursor is on the R. + void skip_raw() { + step(); step(); // R" + std::string delimiter; + while (!done() && peek() != '(' && delimiter.size() < 16) { delimiter.push_back(peek()); step(); } + const std::string end = ")" + delimiter + "\""; + while (!done() && text.substr(at, end.size()) != end) step(); + for (std::size_t i = 0; i < end.size() && !done(); ++i) step(); + } + // A preprocessor line, with its continuations. + void skip_directive() { + while (!done() && peek() != '\n') { + if (peek() == '\\' && peek(1) == '\n') step(); + else if (peek() == '/' && peek(1) == '*') { skip_block_comment(); continue; } + step(); + } + } + // Whether the cursor is on the start of a raw string literal (R, u8R, uR, UR, LR). + bool on_raw() const { + std::size_t i = at; + if (text.substr(i, 2) == "u8") i += 2; + else if (peek() == 'u' || peek() == 'U' || peek() == 'L') i += 1; + return i + 1 < text.size() && text[i] == 'R' && text[i + 1] == '"'; + } +}; + +// The arguments of a call whose `(` the cursor is on, split at top-level commas; the cursor ends +// after the `)`. +std::vector arguments(Cursor& c) { + std::vector out; + std::string current; + int depth = 0; + c.step(); // ( + while (!c.done()) { + const char ch = c.peek(); + if (ch == '/' && c.peek(1) == '/') { c.skip_line_comment(); continue; } + if (ch == '/' && c.peek(1) == '*') { c.skip_block_comment(); continue; } + if (ch == '"' || ch == '\'') { + const auto from = c.at; + c.skip_quoted(ch); + current.append(c.text.substr(from, c.at - from)); + continue; + } + if (ch == '(' || ch == '[' || ch == '{' || ch == '<') { if (ch != '<') ++depth; } + if (ch == ')' || ch == ']' || ch == '}') { + if (depth == 0) { + c.step(); + out.push_back(trim(current)); + return out; + } + --depth; + } + if (ch == ',' && depth == 0) { + out.push_back(trim(current)); + current.clear(); + c.step(); + continue; + } + current.push_back(ch); + c.step(); + } + out.push_back(trim(current)); + return out; +} + +} // namespace + +Result scan(std::string_view text, const std::vector& macros) { + Result r; + Cursor c{text}; + int depth = 0; // braces + bool atDeclaration = true; // the next identifier starts a declaration + bool lineHasCode = false; // a `#` is a directive only first on its line + std::string previous; // the previous identifier, for `int main(` + while (!c.done()) { + const char ch = c.peek(); + if (ch == '\n') { lineHasCode = false; c.step(); continue; } + if (std::isspace(static_cast(ch))) { c.step(); continue; } + if (ch == '/' && c.peek(1) == '/') { c.skip_line_comment(); continue; } + if (ch == '/' && c.peek(1) == '*') { c.skip_block_comment(); continue; } + if (ch == '#' && !lineHasCode) { c.skip_directive(); atDeclaration = true; previous.clear(); continue; } + lineHasCode = true; + if (c.on_raw()) { c.skip_raw(); atDeclaration = false; previous.clear(); continue; } + if (ch == '"' || ch == '\'') { c.skip_quoted(ch); atDeclaration = false; previous.clear(); continue; } + if (ident_start(ch)) { + const auto from = c.at; + const int line = c.line, column = c.column(); + while (!c.done() && ident_char(c.peek())) c.step(); + const std::string word(text.substr(from, c.at - from)); + // What follows the name, past blanks and comments on the way. + Cursor look = c; + while (!look.done()) { + if (std::isspace(static_cast(look.peek()))) { look.step(); continue; } + if (look.peek() == '/' && look.peek(1) == '*') { look.skip_block_comment(); continue; } + if (look.peek() == '/' && look.peek(1) == '/') { look.skip_line_comment(); continue; } + break; + } + const bool call = look.peek() == '('; + if (call && atDeclaration && std::ranges::find(macros, word) != macros.end()) { + c = look; + r.calls.push_back(Call{word, line, column, arguments(c)}); + atDeclaration = false; + previous.clear(); + continue; + } + if (call && word == "main" && depth == 0 && !r.mainLine && (previous == "int" || previous == "auto")) + r.mainLine = line; + previous = word; + atDeclaration = false; + continue; + } + if (ch == '{') { ++depth; atDeclaration = true; } + else if (ch == '}') { if (depth > 0) --depth; atDeclaration = true; } + else if (ch == ';') atDeclaration = true; + else atDeclaration = false; + // `std::main`, `x.main`: not the program's. + if (ch != ':' ) previous.clear(); + c.step(); + } + return r; +} + +std::string literal_value(std::string_view argument) { + std::string out; + std::size_t i = 0; + bool any = false; + while (i < argument.size()) { + while (i < argument.size() && std::isspace(static_cast(argument[i]))) ++i; + if (i >= argument.size()) break; + if (argument[i] != '"') return std::string(argument); + ++i; + while (i < argument.size() && argument[i] != '"') { + if (argument[i] == '\\' && i + 1 < argument.size()) { + const char e = argument[i + 1]; + out.push_back(e == 'n' ? '\n' : e == 't' ? '\t' : e); + i += 2; + continue; + } + out.push_back(argument[i++]); + } + if (i >= argument.size()) return std::string(argument); + ++i; + any = true; + } + return any ? out : std::string(argument); +} + +} // namespace mcppide::scan diff --git a/src/server.cppm b/src/server.cppm index 5e07e5d..d038713 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -25,6 +25,8 @@ import mcppide.i18n; import mcppide.manifest; import mcppide.spec; import mcppide.languageservice; +import mcppide.scan; +import mcppide.testing; import mcpplibs.os; export namespace mcppide::server { @@ -36,7 +38,7 @@ inline constexpr int kProtocol = 1; // The methods `install` registers, for the protocol schema's test: a method // here and not in protocol/protocol.schema.json (or the other way) fails it. -inline constexpr std::array kMethods = { +inline constexpr std::array kMethods = { "initialize", "settings/update", "settings/registry", "settings/preset", "i18n/bundle", "selfCheck", "commands/list", "menu/quick", "languageService/status", "projects/list", "projects/forFile", "project/summary", "project/new", "result/last", @@ -46,13 +48,13 @@ inline constexpr std::array kMethods = { "cache/inventory", "cache/plan", "cache/apply", "graph/packages", "graph/modules", "pack/formats", "pack/start", "manifest/check", "toml/complete", "toml/hover", "toml/symbols", "buildscript/complete", "mcpp/exec", "project/files", "languageService/exec", "project/mcpp", "project/refresh", - "capabilities/explain", "report/collect", "shutdown", "exit"}; + "capabilities/explain", "report/collect", "source/entries", "test/cases", "test/runCases", "shutdown", "exit"}; // The notifications the backend sends. -inline constexpr std::array kNotifications = { +inline constexpr std::array kNotifications = { "job/started", "job/output", "job/plan", "job/progress", "job/diagnostic", "job/artifact", "job/record", "job/finished", "test/result", "test/summary", "result/changed", "manifest/changed", - "manifest/diagnostics", "project/mcppChanged", "languageService/changed"}; + "manifest/diagnostics", "project/mcppChanged", "languageService/changed", "test/caseResult"}; class Session { public: @@ -67,7 +69,8 @@ private: using Result = std::expected; Result initialize(const Json& p); Result update_settings(const Json& p); - Result job_start(const Json& p); + // `after` runs when the job has ended, before the editors are told (jobs::Request::after). + Result job_start(const Json& p, std::function after = {}); Result run_launch(const Json& p, const rpc::Context& ctx); Result debug_configuration(const Json& p, const rpc::Context& ctx); Result library_search(const Json& p); @@ -85,6 +88,13 @@ private: Json server_actions(); Result quick_menu(const Json& p); Result test_list(const Json& p); + Result source_entries(const Json& p); + Result test_cases(const Json& p); + Result test_run_cases(const Json& p); + // `mcpp.test.macros` as testing reads it: {"gtest": ["MY_TEST"]}. + Json test_macros() const; + // The packages of a project's plan, from its build database when one was read. + std::vector plan_packages(const std::filesystem::path& root, bool read); Result project_mcpp(const Json& p); Result project_refresh(const Json& p); Result capabilities_explain(const Json& p); @@ -569,7 +579,7 @@ std::expected Session::update_settings(const Json& p) { return resolved; } -std::expected Session::job_start(const Json& p) { +std::expected Session::job_start(const Json& p, std::function after) { const auto kindName = p.value("kind", ""); auto kind = jobs::kind_from(kindName); if (!kind) return std::unexpected(invalid(std::format("unknown job kind {}", kindName))); @@ -645,6 +655,7 @@ std::expected Session::job_start(const Json& p) { } for (const auto& a : extra) r.args.push_back(a); if (r.args.empty()) return std::unexpected(invalid("a command job names its arguments")); + r.after = std::move(after); const auto id = jobs_->submit(std::move(r)); watcher_->watch(root); return Json{{"jobId", id}}; @@ -653,11 +664,23 @@ std::expected Session::job_start(const Json& p) { std::expected Session::run_launch(const Json& p, const rpc::Context& ctx) { const auto root = root_of(p); if (root.empty()) return std::unexpected(invalid("run/launch names its project's root")); - std::vector args{"run", "--print-launch"}; + // A test's program is described by `mcpp test --print-launch` (mcpp's `test-launch`), + // with its framework's filter after `--` when cases are asked for. + const auto test = p.value("test", ""); + std::vector args{test.empty() ? "run" : "test", "--print-launch"}; const auto profile = p.value("profile", setting("mcpp.build.profile").is_string() ? setting("mcpp.build.profile").get() : "dev"); if (profile == "release") args.push_back("--release"); for (const auto& a : target_args(p)) args.push_back(a); - if (auto bin = p.value("bin", ""); !bin.empty()) args.push_back(bin); + if (!test.empty()) { + args.push_back(test); + if (const auto* f = testing::framework(p.value("framework", ""))) + if (const auto filter = testing::filter_args(*f, strings_of(p.value("cases", Json::array()))); !filter.empty()) { + args.push_back("--"); + for (const auto& a : filter) args.push_back(a); + } + } else if (auto bin = p.value("bin", ""); !bin.empty()) { + args.push_back(bin); + } auto r = query(root, std::move(args)); if (!r) return std::unexpected(r.error()); if (ctx.cancelled()) return std::unexpected(rpc::Error{rpc::REQUEST_CANCELLED, "cancelled"}); @@ -1238,6 +1261,187 @@ std::expected Session::test_list(const Json& p) { return Json{{"root", root.generic_string()}, {"tests", tests}}; } +Json Session::test_macros() const { + Json out = Json::object(); + for (const auto& entry : strings_of(setting("mcpp.test.macros"))) { + const auto colon = entry.find(':'); + if (colon == std::string::npos || colon == 0 || colon + 1 == entry.size()) continue; + out[entry.substr(0, colon)].push_back(entry.substr(colon + 1)); + } + return out; +} + +std::vector Session::plan_packages(const std::filesystem::path& root, bool read) { + Json db; + { + std::lock_guard lock(mutex_); + if (auto it = buildDatabases_.find(root); it != buildDatabases_.end()) db = it->second; + } + if (db.is_null() && read) + if (auto r = build_database(root)) db = *r; + std::vector out; + if (!db.is_object()) return out; + const auto& data = db.contains("data") ? db["data"] : db; + const auto& database = data.contains("database") ? data["database"] : data; + for (const auto& set : database.value("sets", Json::array())) { + for (const auto* key : {"family-name", "name"}) + if (set.contains(key) && set[key].is_string() && std::ranges::find(out, set[key].get()) == out.end()) + out.push_back(set[key].get()); + } + return out; +} + +namespace { + +std::string read_text(const std::filesystem::path& file) { + std::ifstream in(file, std::ios::binary); + return std::string(std::istreambuf_iterator(in), {}); +} + +// A test's name by mcpp's convention: the path below tests/ without its extension (docs/08). +std::optional test_name_of(const std::filesystem::path& root, const std::filesystem::path& file) { + const auto ext = file.extension().string(); + if (ext != ".cpp" && ext != ".cc" && ext != ".cxx") return std::nullopt; + const auto rel = file.lexically_normal().lexically_relative(root.lexically_normal()); + if (rel.empty() || *rel.begin() != "tests") return std::nullopt; + std::filesystem::path below; + bool first = true; + for (const auto& part : rel) { + if (first) { first = false; continue; } + below /= part; + } + below.replace_extension(); + return below.generic_string(); +} + +// The source of a test by its name: tests/.cpp (or .cc, .cxx). +std::filesystem::path test_source(const std::filesystem::path& root, std::string_view name) { + for (const auto* ext : {".cpp", ".cc", ".cxx"}) { + auto file = root / "tests" / (std::string(name) + ext); + std::error_code ec; + if (std::filesystem::is_regular_file(file, ec)) return file; + } + return {}; +} + +} // namespace + +// What a C++ file is to its project (§6.3): a test's or a program's `main`, or neither, with the +// line of `main` read from the text (approximate: an editor takes its language service's position +// when it has one). Nothing is built and nothing run. +std::expected Session::source_entries(const Json& p) { + const std::filesystem::path file(p.value("file", "")); + if (file.empty()) return std::unexpected(invalid("source/entries names a file")); + auto proj = projects::project_for(file.generic_string(), p.value("boundary", "")); + if (!proj) return Json{{"file", file.generic_string()}, {"kind", "none"}}; + const auto text = p.contains("text") && p["text"].is_string() ? p["text"].get() : read_text(file); + const auto extra = test_macros(); + const auto found = scan::scan(text, testing::macros(extra)); + Json out{{"file", file.generic_string()}, {"root", proj->root.generic_string()}, {"kind", "none"}}; + if (auto name = test_name_of(proj->root, file)) { + out["kind"] = "test"; + out["test"] = *name; + if (const auto* f = testing::framework_of(plan_packages(proj->root, false), found, extra)) out["framework"] = f->id; + } else if (found.mainLine) { + out["kind"] = "program"; + } + if (found.mainLine && out["kind"] != "none") out["main"] = Json{{"line", *found.mainLine}, {"approximate", true}}; + return out; +} + +// A test program's cases (§7.3): as its source names them, and -- when `list` is asked for, the +// workspace is trusted and the source calls its framework's macros -- as the program lists them. +std::expected Session::test_cases(const Json& p) { + std::filesystem::path root = root_of(p); + std::string test = p.value("test", ""); + std::filesystem::path file(p.value("file", "")); + if (!file.empty()) { + if (root.empty()) if (auto proj = projects::project_for(file.generic_string(), "")) root = proj->root; + if (test.empty()) if (auto name = test_name_of(root, file)) test = *name; + } + if (root.empty() || test.empty()) return std::unexpected(invalid("test/cases names a project's test, or a test's file")); + if (file.empty()) file = test_source(root, test); + Json out{{"root", root.generic_string()}, {"test", test}, {"file", file.generic_string()}, {"framework", nullptr}, {"cases", Json::array()}}; + if (file.empty()) return out; + const auto extra = test_macros(); + const auto found = scan::scan(read_text(file), testing::macros(extra)); + if (found.mainLine) out["main"] = Json{{"line", *found.mainLine}, {"approximate", true}}; + bool trusted; + { + std::lock_guard lock(mutex_); + trusted = trusted_; + } + const auto* f = testing::framework_of(plan_packages(root, trusted), found, extra); + if (!f) return out; + out["framework"] = f->id; + auto cases = testing::static_cases(*f, found, extra); + if (p.value("list", false) && trusted) { + mcpp::Client client; + { + std::lock_guard lock(mutex_); + client = client_; + } + if (client.ready()) { + mcpp::RunOptions o; + o.cwd = root; + o.args = {"test", "--message-format", "json"}; + for (const auto& a : target_args(p)) o.args.push_back(a); + o.args.push_back(test); + o.args.push_back("--"); + for (const auto& a : f->list) o.args.push_back(a); + o.deny = deny(); + o.client = client_label(); + if (auto r = client.records(o)) + for (const auto& rec : r->records) + if (rec.is_object() && rec.value("test", "") == test && rec.contains("run_output")) { + cases = testing::merge(cases, testing::listed(*f, rec.value("run_output", "")), *f); + out["listed"] = true; + } + } + } + for (const auto& c : cases) { + Json row{{"name", c.name}, {"source", c.source}}; + if (c.line >= 0) row["line"] = c.line; + out["cases"].push_back(row); + } + return out; +} + +// Runs some of a test program's cases (§7.3): `mcpp test -- `, the report +// written to a directory of this run's own, read when the job ends -- each case's outcome sent as +// `test/caseResult` -- and removed. +std::expected Session::test_run_cases(const Json& p) { + const auto root = root_of(p); + const auto test = p.value("test", ""); + const auto* f = testing::framework(p.value("framework", "")); + if (root.empty() || test.empty() || !f) return std::unexpected(invalid("test/runCases names a project, a test and its framework")); + const auto cases = strings_of(p.value("cases", Json::array())); + const auto dir = std::filesystem::temp_directory_path() / + std::format("mcpp-ide-cases-{}-{}", std::random_device{}(), std::chrono::steady_clock::now().time_since_epoch().count()); + std::error_code ec; + std::filesystem::create_directories(dir, ec); + if (ec) return std::unexpected(rpc::Error{rpc::REQUEST_FAILED, ec.message()}); + const auto report = dir / "report"; + Json args = Json::array({"--"}); + for (const auto& a : testing::run_args(*f, cases, report)) args.push_back(a); + Json job{{"kind", "test"}, {"root", root.generic_string()}, {"filter", test}, {"args", args}, {"color", p.value("color", true)}}; + for (const auto* key : {"profile", "target"}) if (p.contains(key)) job[key] = p[key]; + const std::string framework = f->id; + return job_start(job, [this, root, test, framework, dir, report](std::string_view jobId, const Json&) { + const auto* fw = testing::framework(framework); + std::ifstream in(report, std::ios::binary); + const std::string text(std::istreambuf_iterator(in), {}); + in.close(); + if (fw) + for (const auto& o : testing::read_report(*fw, text)) + rpc_.notify("test/caseResult", Json{{"jobId", jobId}, {"root", root.generic_string()}, {"test", test}, + {"case", o.name}, {"status", o.status}, {"durationMs", o.durationMs}, + {"message", o.message}}); + std::error_code ignored; + std::filesystem::remove_all(dir, ignored); + }); +} + Session::PackOptions Session::pack_options(const std::filesystem::path& cwd) { mcpp::Client client; { @@ -1317,6 +1521,9 @@ void Session::install() { s.on("run/launch", [this](const Json& p, const rpc::Context& c) { return run_launch(p, c); }); s.on("debug/configuration", [this](const Json& p, const rpc::Context& c) { return debug_configuration(p, c); }); s.on("test/list", [this](const Json& p, const rpc::Context&) { return test_list(p); }); + s.on("source/entries", [this](const Json& p, const rpc::Context&) { return source_entries(p); }); + s.on("test/cases", [this](const Json& p, const rpc::Context&) { return test_cases(p); }); + s.on("test/runCases", [this](const Json& p, const rpc::Context&) { return test_run_cases(p); }); s.on("project/mcpp", [this](const Json& p, const rpc::Context&) { return project_mcpp(p); }); s.on("project/refresh", [this](const Json& p, const rpc::Context&) { return project_refresh(p); }); s.on("capabilities/explain", [this](const Json& p, const rpc::Context&) { return capabilities_explain(p); }); diff --git a/src/spec.cppm b/src/spec.cppm index e58ec5b..3f6bd03 100644 --- a/src/spec.cppm +++ b/src/spec.cppm @@ -32,6 +32,8 @@ const Json& toml_schema(); const Json& buildscript_api(); // spec/commands.json: the command table the editors offer (groups, commands). const Json& commands(); +// spec/test-frameworks.json: the frameworks whose cases the editors show. +const Json& test_frameworks(); // Whether an editor (the protocol's `editor`: vscode, jetbrains, nvim, cli) offers a command: the // row's `editors` when it has them (`omitted` says why an editor is left out), else every editor; // a forward to one of mcppls's actions in every editor (the backend offers it where mcppls names an @@ -114,6 +116,11 @@ const Json& commands() { return c; } +const Json& test_frameworks() { + static const Json t = Json::parse(generated::kTestFrameworksJson); + return t; +} + bool offered_in(const Json& c, std::string_view editor) { if (c.contains("editors") && c["editors"].is_array()) return std::ranges::any_of(c["editors"], [&](const Json& e) { return e.is_string() && e.get() == editor; }); diff --git a/src/testing.cppm b/src/testing.cppm new file mode 100644 index 0000000..302c369 --- /dev/null +++ b/src/testing.cppm @@ -0,0 +1,440 @@ +// A test program's cases (mcpp design 2026-10-30 part 2 §7): which framework a program uses, its +// cases as its source names them and as the program lists them, the arguments that run some of +// them and write a report, and the report read back. +// +// mcpp's own unit is the program (`mcpp test `, judged by its exit code; F17). A case is the +// framework's, so everything here is the framework's convention, from spec/test-frameworks.json, +// and reaches the program after `--`, as written. +// +// A PROGRAM BELONGS TO A FRAMEWORK WHEN BOTH HOLD: its plan holds one of the framework's packages, +// and its source calls one of the framework's macros. A program that depends on a framework and +// writes its own `main` must not be asked to list its cases: GoogleTest's `--gtest_list_tests` +// would run such a program's tests there and then. +export module mcppide.testing; + +import std; +import nlohmann.json; +import mcppide.scan; +import mcppide.spec; + +export namespace mcppide::testing { + +using Json = nlohmann::json; + +struct Framework { + std::string id, title; + std::vector packages, macros; + std::string name; // "suite.case" | "literal" + std::string scenarioPrefix; // what SCENARIO puts before its name + std::vector list; + std::string listFormat; // "gtest-list" | "catch2-json" | "lines" + std::vector filter, report; + std::string join, escape, reportFormat; +}; + +// spec/test-frameworks.json, in its order. +const std::vector& frameworks(); +const Framework* framework(std::string_view id); + +// The macro names to look for: every framework's, and those `mcpp.test.macros` adds +// ({"gtest": ["MY_TEST"]}). +std::vector macros(const Json& extra); + +// The framework of a program whose plan holds `packages` and whose source scanned as `found`; none +// when no framework has both. +const Framework* framework_of(const std::vector& packages, const scan::Result& found, const Json& extra); + +struct Case { + std::string name; // the framework's full name: Suite.Name, or the test's name + int line = -1; // 0-based, in the program's source; -1 when only the program listed it + std::string source; // "static" (its source) | "listed" (the program listed it) +}; + +// The cases the source names, in order. +std::vector static_cases(const Framework& f, const scan::Result& found, const Json& extra); + +// The names the program's list command printed. +std::vector listed(const Framework& f, std::string_view output); + +// The name a listed case has in its source: GoogleTest's instantiated and typed names lose the +// instance prefix and the parameter (`Inst/Suite.Name/0` and `Suite/0.Name` are `Suite.Name`). +std::string base_name(const Framework& f, std::string_view name); + +// The cases after a listing: a listed name whose base is a static case's gives that case its +// listed name (the first occurrence; a second name of one base is a case of its own at the same +// line); a name with no static case is added without a line. +std::vector merge(const std::vector& statics, const std::vector& names, const Framework& f); + +// What follows `--` to run `cases` (all of them when empty) and write the report to `report`. +std::vector run_args(const Framework& f, const std::vector& cases, const std::filesystem::path& report); +// The filter alone: what a debugged program receives to run `cases`. +std::vector filter_args(const Framework& f, const std::vector& cases); + +struct Outcome { + std::string name; + std::string status; // passed | failed | skipped + std::int64_t durationMs = 0; + std::string message; +}; + +// A report the framework wrote. +std::vector read_report(const Framework& f, std::string_view text); + +} // namespace mcppide::testing + +namespace mcppide::testing { + +namespace { + +std::vector strings(const Json& v) { + std::vector out; + if (v.is_array()) for (const auto& x : v) if (x.is_string()) out.push_back(x.get()); + return out; +} + +std::string trim(std::string_view s) { + while (!s.empty() && std::isspace(static_cast(s.front()))) s.remove_prefix(1); + while (!s.empty() && std::isspace(static_cast(s.back()))) s.remove_suffix(1); + return std::string(s); +} + +std::vector lines(std::string_view text) { + std::vector out; + std::size_t at = 0; + while (at <= text.size()) { + const auto end = text.find('\n', at); + auto line = text.substr(at, end == std::string_view::npos ? std::string_view::npos : end - at); + if (!line.empty() && line.back() == '\r') line.remove_suffix(1); + out.emplace_back(line); + if (end == std::string_view::npos) break; + at = end + 1; + } + return out; +} + +// The macros of `f`, with what `extra` adds for it. +std::vector macros_of(const Framework& f, const Json& extra) { + auto out = f.macros; + if (extra.is_object() && extra.contains(f.id)) + for (const auto& m : strings(extra[f.id])) out.push_back(m); + return out; +} + +std::string name_of(const Framework& f, const scan::Call& call) { + if (f.name == "suite.case") + return call.arguments.size() >= 2 ? call.arguments[0] + "." + call.arguments[1] : std::string{}; + for (const auto& a : call.arguments) + if (!a.empty() && a.front() == '"') { + const auto value = scan::literal_value(a); + return call.name == "SCENARIO" ? f.scenarioPrefix + value : value; + } + return {}; +} + +std::int64_t milliseconds(std::string_view seconds) { + double v = 0; + std::from_chars(seconds.data(), seconds.data() + seconds.size(), v); + return static_cast(v * 1000.0 + 0.5); +} + +// The value of an XML attribute in `tag` (the text between `<` and `>`), with the five entities. +std::string attribute(std::string_view tag, std::string_view name) { + const auto key = std::string(" ") + std::string(name) + "=\""; + const auto at = tag.find(key); + if (at == std::string_view::npos) return {}; + const auto from = at + key.size(); + const auto end = tag.find('"', from); + std::string raw(tag.substr(from, end == std::string_view::npos ? std::string_view::npos : end - from)); + std::string out; + for (std::size_t i = 0; i < raw.size(); ++i) { + if (raw[i] != '&') { out.push_back(raw[i]); continue; } + const auto semi = raw.find(';', i); + if (semi == std::string::npos) { out.push_back('&'); continue; } + const auto entity = std::string_view(raw).substr(i + 1, semi - i - 1); + out += entity == "lt" ? "<" : entity == "gt" ? ">" : entity == "quot" ? "\"" : entity == "apos" ? "'" : entity == "amp" ? "&" : ""; + i = semi; + } + return out; +} + +std::vector gtest_report(const Json& doc) { + std::vector out; + for (const auto& suite : doc.value("testsuites", Json::array())) { + const auto suiteName = suite.value("name", ""); + for (const auto& t : suite.value("testsuite", Json::array())) { + Outcome o; + o.name = suiteName + "." + t.value("name", ""); + o.durationMs = milliseconds(t.value("time", "0s")); + const auto failures = t.value("failures", Json::array()); + if (t.value("result", "") == "SKIPPED" || t.value("status", "") == "NOTRUN") o.status = "skipped"; + else o.status = failures.empty() ? "passed" : "failed"; + for (const auto& f : failures) { + if (!o.message.empty()) o.message += "\n"; + o.message += f.value("failure", ""); + } + out.push_back(std::move(o)); + } + } + return out; +} + +std::vector catch2_report(const Json& doc) { + std::vector out; + const auto run = doc.value("test-run", Json::object()); + for (const auto& tc : run.value("test-cases", Json::array())) { + Outcome o; + o.name = tc.value("test-info", Json::object()).value("name", ""); + const auto totals = tc.value("totals", Json::object()); + const auto assertions = totals.value("assertions", Json::object()); + const auto failed = assertions.value("failed", 0); + const auto skipped = totals.value("test-cases", Json::object()).value("skipped", 0) + assertions.value("skipped", 0); + o.status = failed > 0 ? "failed" : skipped > 0 ? "skipped" : "passed"; + // The failed assertions' expressions, as the reporter wrote them. + std::function walk = [&](const Json& node) { + if (node.is_object()) { + if (node.value("kind", "") == "assertion" && node.value("status", true) == false) { + if (!o.message.empty()) o.message += "\n"; + const auto loc = node.value("source-location", Json::object()); + o.message += std::format("{}:{}: {}", loc.value("filename", ""), loc.value("line", 0), node.value("expression", node.value("message", ""))); + } + for (const auto& v : node) walk(v); + } else if (node.is_array()) { + for (const auto& v : node) walk(v); + } + }; + walk(tc.value("runs", Json::array())); + if (o.status == "failed" && o.message.empty()) o.message = std::format("{} assertion(s) failed", failed); + out.push_back(std::move(o)); + } + return out; +} + +std::vector junit_report(std::string_view text) { + std::vector out; + std::size_t at = 0; + while ((at = text.find("', at); + if (close == std::string_view::npos) break; + const auto tag = text.substr(at, close - at); + Outcome o; + o.name = attribute(tag, "name"); + o.durationMs = milliseconds(attribute(tag, "time")); + o.status = attribute(tag, "status") == "skipped" ? "skipped" : "passed"; + const bool empty = close > 0 && text[close - 1] == '/'; + if (!empty) { + const auto end = text.find("", close); + const auto body = text.substr(close + 1, end == std::string_view::npos ? std::string_view::npos : end - close - 1); + if (const auto f = body.find("', f); + o.message = attribute(body.substr(f, fclose - f), "message"); + } else if (body.find("& frameworks() { + static const std::vector all = [] { + std::vector out; + for (const auto& f : spec::test_frameworks().value("frameworks", Json::array())) { + Framework v; + v.id = f.value("id", ""); + v.title = f.value("title", v.id); + v.packages = strings(f.value("packages", Json::array())); + v.macros = strings(f.value("macros", Json::array())); + v.name = f.value("name", "literal"); + v.scenarioPrefix = f.value("scenarioPrefix", ""); + v.list = strings(f.value("list", Json::array())); + v.listFormat = f.value("listFormat", "lines"); + v.filter = strings(f.value("filter", Json::array())); + v.report = strings(f.value("report", Json::array())); + v.join = f.value("join", ","); + v.escape = f.value("escape", ""); + v.reportFormat = f.value("reportFormat", ""); + out.push_back(std::move(v)); + } + return out; + }(); + return all; +} + +const Framework* framework(std::string_view id) { + for (const auto& f : frameworks()) if (f.id == id) return &f; + return nullptr; +} + +std::vector macros(const Json& extra) { + std::vector out; + for (const auto& f : frameworks()) + for (const auto& m : macros_of(f, extra)) + if (std::ranges::find(out, m) == out.end()) out.push_back(m); + return out; +} + +const Framework* framework_of(const std::vector& packages, const scan::Result& found, const Json& extra) { + for (const auto& f : frameworks()) { + const bool linked = std::ranges::any_of(f.packages, [&](const std::string& p) { + return std::ranges::any_of(packages, [&](const std::string& in) { return in == p || in.starts_with(p + ":"); }); + }); + if (!linked) continue; + const auto names = macros_of(f, extra); + if (std::ranges::any_of(found.calls, [&](const scan::Call& c) { return std::ranges::find(names, c.name) != names.end(); })) + return &f; + } + return nullptr; +} + +std::vector static_cases(const Framework& f, const scan::Result& found, const Json& extra) { + std::vector out; + const auto names = macros_of(f, extra); + for (const auto& call : found.calls) { + if (std::ranges::find(names, call.name) == names.end()) continue; + auto name = name_of(f, call); + if (!name.empty()) out.push_back(Case{std::move(name), call.line, "static"}); + } + return out; +} + +std::vector listed(const Framework& f, std::string_view output) { + std::vector out; + if (f.listFormat == "gtest-list") { + std::string suite; + for (const auto& raw : lines(output)) { + const std::string_view line(raw); + const auto comment = line.find(" #"); + const auto text = trim(comment == std::string_view::npos ? line : line.substr(0, comment)); + if (text.empty()) continue; + if (!line.empty() && line.front() != ' ') { + if (text.back() == '.') suite = text; + continue; + } + if (!suite.empty()) out.push_back(suite + text); + } + } else if (f.listFormat == "catch2-json") { + const auto start = output.find('{'); + const Json doc = Json::parse(start == std::string_view::npos ? std::string_view{} : output.substr(start), nullptr, false); + if (doc.is_object()) + for (const auto& t : doc.value("listings", Json::object()).value("tests", Json::array())) + if (t.contains("name") && t["name"].is_string()) out.push_back(t["name"].get()); + } else { + for (const auto& raw : lines(output)) { + const auto text = trim(raw); + if (text.empty() || text.starts_with("[doctest]") || text.starts_with("===")) continue; + out.push_back(raw.starts_with(" Scenario: ") ? raw : text); + } + } + return out; +} + +std::string base_name(const Framework& f, std::string_view name) { + if (f.name != "suite.case") return std::string(name); + const auto dot = name.find('.'); + if (dot == std::string_view::npos) return std::string(name); + auto suite = name.substr(0, dot); + auto test = name.substr(dot + 1); + // TEST_P: Inst/Suite.Name/0; TYPED_TEST: Suite/0.Name; TYPED_TEST_P: Inst/Suite/0.Name. + if (const auto slash = test.find('/'); slash != std::string_view::npos) test = test.substr(0, slash); + const auto parts = [](std::string_view s) { + std::vector v; + std::size_t at = 0; + while (true) { + const auto next = s.find('/', at); + v.push_back(s.substr(at, next == std::string_view::npos ? std::string_view::npos : next - at)); + if (next == std::string_view::npos) break; + at = next + 1; + } + return v; + }(suite); + // The suite is the last part that is not a number. + for (auto it = parts.rbegin(); it != parts.rend(); ++it) + if (!it->empty() && !std::ranges::all_of(*it, [](char c) { return std::isdigit(static_cast(c)); })) { + suite = *it; + break; + } + return std::format("{}.{}", suite, test); +} + +std::vector merge(const std::vector& statics, const std::vector& names, const Framework& f) { + std::vector out = statics; + std::vector taken(out.size(), false); + for (const auto& name : names) { + const auto base = base_name(f, name); + bool placed = false; + for (std::size_t i = 0; i < statics.size() && !placed; ++i) { + if (statics[i].name != base) continue; + if (!taken[i]) { + out[i].name = name; + out[i].source = "listed"; + taken[i] = true; + } else { + out.push_back(Case{name, statics[i].line, "listed"}); + } + placed = true; + } + if (!placed) out.push_back(Case{name, -1, "listed"}); + } + return out; +} + +std::vector run_args(const Framework& f, const std::vector& cases, const std::filesystem::path& report) { + auto out = filter_args(f, cases); + for (const auto& a : f.report) { + std::string arg = a; + if (const auto at = arg.find("{}"); at != std::string::npos) arg.replace(at, 2, report.string()); + out.push_back(std::move(arg)); + } + return out; +} + +std::vector filter_args(const Framework& f, const std::vector& cases) { + std::vector out; + if (!cases.empty()) { + // A GoogleTest case named as its source names it stands for its instances too: + // Inst/Suite.Name/N (TEST_P) and Suite/N.Name (TYPED_TEST). + std::vector patterns; + for (const auto& c : cases) { + patterns.push_back(c); + if (f.name == "suite.case" && c.find('/') == std::string::npos) + if (const auto dot = c.find('.'); dot != std::string::npos) { + patterns.push_back("*/" + c + "/*"); + patterns.push_back(c.substr(0, dot) + "/*." + c.substr(dot + 1)); + } + } + std::string joined; + for (const auto& c : patterns) { + if (!joined.empty()) joined += f.join; + for (char ch : c) { + if (f.escape.find(ch) != std::string::npos) joined.push_back('\\'); + joined.push_back(ch); + } + } + for (const auto& a : f.filter) { + std::string arg = a; + if (const auto at = arg.find("{}"); at != std::string::npos) arg.replace(at, 2, joined); + out.push_back(std::move(arg)); + } + } + return out; +} + +std::vector read_report(const Framework& f, std::string_view text) { + if (f.reportFormat == "junit") return junit_report(text); + const Json doc = Json::parse(text, nullptr, false); + if (!doc.is_object()) return {}; + if (f.reportFormat == "gtest-json") return gtest_report(doc); + if (f.reportFormat == "catch2-json") return catch2_report(doc); + return {}; +} + +} // namespace mcppide::testing diff --git a/tests/test_testing.cpp b/tests/test_testing.cpp new file mode 100644 index 0000000..2211c89 --- /dev/null +++ b/tests/test_testing.cpp @@ -0,0 +1,183 @@ +// A source read as text (design 2026-10-30 part 2 §6.3, §7.3): `main` and a framework's macros, +// never inside a comment, a string or a raw string; a program's framework needs both its package +// and its macros; cases named as each framework names them, listed names attached to them, the +// filter escaped by the framework's rules, and the three report formats read back. And the +// backend's source/entries and test/cases over a project on disk. +#include "support.hpp" +import std; +import nlohmann.json; +import mcppide.scan; +import mcppide.testing; +import mcppide.rpc; +import mcppide.server; + +using namespace mcppide; +using Json = nlohmann::json; + +namespace { + +const testing::Framework& fw(std::string_view id) { + const auto* f = testing::framework(id); + CHECK(f != nullptr); + return *f; +} + +} // namespace + +int main() { + // main: the first `int main(` or `auto main(` at file scope, outside comments and literals. + { + const auto r = scan::scan(R"(// int main() in a comment +/* int main() */ +const char* s = "int main("; +const char* raw = R"x(int main() +)x"; +#define X int main( +namespace n { int main(); } +int +main(int argc, char** argv) { return 0; } +)", {}); + CHECK(r.mainLine && *r.mainLine == 8); + CHECK(!scan::scan("void f() { obj.main(); std::main(); }", {}).mainLine); + CHECK(scan::scan("export auto main() -> int { return 0; }", {}).mainLine == 0); + } + // Macro calls at the start of a declaration, with their arguments. + const auto macros = testing::macros(Json::object()); + { + const auto r = scan::scan(R"(#include +TEST(Math, Adds) { EXPECT_EQ(1 + 1, 2); } +// TEST(Not, This) +TEST_P(Param, Works) {} +void helper() { auto x = TEST(1, 2); } +TEST(Math, + Spans) {} +)", macros); + CHECK(r.calls.size() == 3); + CHECK(r.calls[0].name == "TEST" && r.calls[0].line == 1 && r.calls[0].arguments == std::vector({"Math", "Adds"})); + CHECK(r.calls[1].name == "TEST_P" && r.calls[1].line == 3); + CHECK(r.calls[2].line == 5 && r.calls[2].arguments[1] == "Spans"); + CHECK(scan::literal_value(R"("a \"b\"" " c")") == "a \"b\" c"); + } + + // A program's framework: its package in the plan and its macros in its source. + { + const auto gtestSource = scan::scan("TEST(A, B) {}\nTEST_F(F, C) {}\n", macros); + CHECK(testing::framework_of({"app", "compat.gtest"}, gtestSource, Json::object()) == &fw("gtest")); + CHECK(testing::framework_of({"app"}, gtestSource, Json::object()) == nullptr); + const auto ownMain = scan::scan("int main() { return 0; }\n", macros); + CHECK(testing::framework_of({"compat.gtest"}, ownMain, Json::object()) == nullptr); + // A macro of the project's own, named by mcpp.test.macros. + const Json extra{{"gtest", Json::array({"MY_TEST"})}}; + const auto own = scan::scan("MY_TEST(S, N) {}\n", testing::macros(extra)); + CHECK(testing::framework_of({"compat.gtest"}, own, extra) == &fw("gtest")); + const auto cases = testing::static_cases(fw("gtest"), own, extra); + CHECK(cases.size() == 1 && cases[0].name == "S.N" && cases[0].line == 0); + // Catch2 and doctest share TEST_CASE; the package decides. + const auto both = scan::scan("TEST_CASE(\"adds\", \"[math]\") {}\nSCENARIO(\"grows\") {}\n", macros); + CHECK(testing::framework_of({"compat.catch2"}, both, Json::object()) == &fw("catch2")); + CHECK(testing::framework_of({"compat.doctest"}, both, Json::object()) == &fw("doctest")); + const auto catchCases = testing::static_cases(fw("catch2"), both, Json::object()); + CHECK(catchCases.size() == 2 && catchCases[0].name == "adds" && catchCases[1].name == "Scenario: grows"); + const auto doctestCases = testing::static_cases(fw("doctest"), both, Json::object()); + CHECK(doctestCases[1].name == " Scenario: grows"); + } + + // Listings, base names and the merge. + { + const auto names = testing::listed(fw("gtest"), "Math.\n Adds\n Spans\nInst/Param.\n Works/0 # GetParam() = 1\n Works/1 # GetParam() = 2\nTyped/0. # TypeParam = int\n Holds\n"); + CHECK(names == std::vector({"Math.Adds", "Math.Spans", "Inst/Param.Works/0", "Inst/Param.Works/1", "Typed/0.Holds"})); + CHECK(testing::base_name(fw("gtest"), "Inst/Param.Works/1") == "Param.Works"); + CHECK(testing::base_name(fw("gtest"), "Typed/0.Holds") == "Typed.Holds"); + const std::vector statics{{"Math.Adds", 1, "static"}, {"Param.Works", 3, "static"}, {"Math.Spans", 5, "static"}}; + const auto merged = testing::merge(statics, names, fw("gtest")); + CHECK(merged[0].name == "Math.Adds" && merged[0].source == "listed"); + CHECK(merged[1].name == "Inst/Param.Works/0" && merged[1].line == 3); + CHECK(std::ranges::any_of(merged, [](const auto& c) { return c.name == "Inst/Param.Works/1" && c.line == 3; })); + CHECK(std::ranges::any_of(merged, [](const auto& c) { return c.name == "Typed/0.Holds" && c.line == -1; })); + const auto listedCatch = testing::listed(fw("catch2"), R"({"version": 1, "listings": {"tests": [{"name": "adds"}, {"name": "a, b"}]}})"); + CHECK(listedCatch == std::vector({"adds", "a, b"})); + const auto listedDoctest = testing::listed(fw("doctest"), "[doctest] doctest version is \"2.4.12\"\n===\nadds\n Scenario: grows\n===\n[doctest] unskipped test cases passing the current filters: 2\n"); + CHECK(listedDoctest == std::vector({"adds", " Scenario: grows"})); + } + + // The arguments after `--`: the filter escaped by the framework's rules, and the report. + { + const auto g = testing::run_args(fw("gtest"), {"Math.Adds", "Inst/Param.Works/0"}, "/r/out"); + CHECK(g == std::vector({"--gtest_filter=Math.Adds:*/Math.Adds/*:Math/*.Adds:Inst/Param.Works/0", "--gtest_output=json:/r/out"})); + const auto c = testing::run_args(fw("catch2"), {"a, b", "x[1]"}, "/r/out"); + CHECK(c == std::vector({"a\\, b,x\\[1\\]", "--reporter", "json::out=/r/out"})); + const auto d = testing::run_args(fw("doctest"), {}, "/r/out"); + CHECK(d == std::vector({"--reporters=junit", "--out=/r/out"})); + } + + // Reports. + { + const auto g = testing::read_report(fw("gtest"), R"({"testsuites": [{"name": "Math", "testsuite": [ + {"name": "Adds", "status": "RUN", "result": "COMPLETED", "time": "0.002s"}, + {"name": "Spans", "status": "RUN", "result": "COMPLETED", "time": "0s", "failures": [{"failure": "a.cpp:3\nExpected 2"}]}, + {"name": "Later", "status": "RUN", "result": "SKIPPED", "time": "0s"}]}]})"); + CHECK(g.size() == 3 && g[0].name == "Math.Adds" && g[0].status == "passed" && g[0].durationMs == 2); + CHECK(g[1].status == "failed" && g[1].message.find("Expected 2") != std::string::npos); + CHECK(g[2].status == "skipped"); + const auto c = testing::read_report(fw("catch2"), R"({"version": 1, "test-run": {"test-cases": [ + {"test-info": {"name": "adds"}, "runs": [], "totals": {"assertions": {"passed": 1, "failed": 0}}}, + {"test-info": {"name": "grows"}, "runs": [{"path": [{"kind": "assertion", "status": false, "source-location": {"filename": "g.cpp", "line": 7}}]}], + "totals": {"assertions": {"passed": 0, "failed": 1}}}]}})"); + CHECK(c.size() == 2 && c[0].status == "passed" && c[1].status == "failed" && c[1].message.starts_with("g.cpp:7")); + const auto d = testing::read_report(fw("doctest"), R"( + + + + +)"); + CHECK(d.size() == 3 && d[0].status == "passed" && d[0].durationMs == 1); + CHECK(d[1].name == "a & b" && d[1].status == "failed" && d[1].message.find("NOT correct") != std::string::npos); + CHECK(d[2].status == "skipped"); + } + + // source/entries and test/cases over a project on disk (no mcpp: the plan's packages are not + // known, so no framework is claimed, and the static entries still answer). + { + const auto base = std::filesystem::temp_directory_path() / std::format("mcppide-testing-{}", std::random_device{}()); + std::filesystem::create_directories(base / "src"); + std::filesystem::create_directories(base / "tests" / "unit"); + std::ofstream(base / "mcpp.toml") << "[package]\nname = \"p\"\nversion = \"0.1.0\"\n"; + std::ofstream(base / "src" / "main.cpp") << "import std;\n\nint main() { return 0; }\n"; + std::ofstream(base / "src" / "lib.cpp") << "int f() { return 1; }\n"; + std::ofstream(base / "tests" / "unit" / "math.cpp") << "#include \nTEST(Math, Adds) {}\n"; + rpc::Server server{[](std::string_view) { return true; }}; + server::Session session(server); + session.install(); + std::vector answers; + rpc::Server wire{[&](std::string_view frame) { + rpc::FrameReader r; + r.feed(frame); + while (auto body = r.next()) answers.push_back(Json::parse(*body)); + return true; + }}; + server::Session s2(wire); + s2.install(); + auto call = [&](std::string_view method, Json params) { + answers.clear(); + wire.dispatch(Json{{"jsonrpc", "2.0"}, {"id", 1}, {"method", method}, {"params", std::move(params)}}.dump()); + for (int i = 0; i < 2000 && answers.empty(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK(!answers.empty() && answers.back().contains("result")); + return answers.back()["result"]; + }; + (void)call("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "vscode"}, {"trusted", false}, + {"settings", {{"mcpp.path", "/no/such/mcpp"}}}, + {"workspaceFolders", Json::array({base.generic_string()})}}); + auto program = call("source/entries", Json{{"file", (base / "src" / "main.cpp").generic_string()}}); + CHECK(program["kind"] == "program" && program["main"]["line"] == 2 && program["main"]["approximate"] == true); + CHECK(call("source/entries", Json{{"file", (base / "src" / "lib.cpp").generic_string()}})["kind"] == "none"); + auto test = call("source/entries", Json{{"file", (base / "tests" / "unit" / "math.cpp").generic_string()}}); + CHECK(test["kind"] == "test" && test["test"] == "unit/math" && !test.contains("framework")); + auto cases = call("test/cases", Json{{"root", base.generic_string()}, {"test", "unit/math"}}); + CHECK(cases["framework"].is_null() && cases["cases"].empty()); + std::error_code ignored; + std::filesystem::remove_all(base, ignored); + } + + std::println("test_testing: ok"); + return 0; +} From 0d0f1c3abd0ca80a5661aec5de32cc558dbc6d80 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 21:25:41 +0900 Subject: [PATCH 093/119] =?UTF-8?q?CLion:=20a=20case's=20=E2=96=B6=20shows?= =?UTF-8?q?=20its=20last=20outcome,=20the=20test=20tree=20locating=20each?= =?UTF-8?q?=20case=20at=20its=20macro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../mcppcommunity/mcppclion/run/McppProducers.kt | 9 +++++++-- .../mcppcommunity/mcppclion/run/McppStates.kt | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProducers.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProducers.kt index 48a50fd..aab7ff7 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProducers.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProducers.kt @@ -104,8 +104,13 @@ class McppRunLineMarker : RunLineMarkerContributor() { if (text == "main" && entries.getAsJsonObject("main")?.get("line")?.asInt == line) return Info(AllIcons.RunConfigurations.TestState.Run, ExecutorAction.getActions(0)) { "mcpp" } // A case: the macro's name on the line the backend read the case from. - if (entries["kind"].asString == "test" && text.isNotEmpty() && text.all { it.isUpperCase() || it == '_' } && caseAt(session, entries, line) != null) - return Info(AllIcons.RunConfigurations.TestState.Run, ExecutorAction.getActions(0)) { "mcpp" } + if (entries["kind"].asString == "test" && text.isNotEmpty() && text.all { it.isUpperCase() || it == '_' }) { + val case = caseAt(session, entries, line) ?: return null + // The last outcome of the case, as the test tree recorded it (its locationHint). + val url = "file://${file.path}:${line + 1}" + val icon = getTestStateIcon(url, element.project, false) ?: AllIcons.RunConfigurations.TestState.Run + return Info(icon, ExecutorAction.getActions(0)) { "mcpp: ${case.first}" } + } return null } } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppStates.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppStates.kt index f3958cf..68c8f07 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppStates.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppStates.kt @@ -136,7 +136,10 @@ class TestJobProcessHandler(session: McppSession, params: JsonObject, private va out.append("##teamcity[testSuiteStarted name='${escape(program)}'$hint]\n") } val name = escape(p["case"].asString) - out.append("##teamcity[testStarted name='$name']\n") + // The case's place in its source, for the editor's ▶ to show the last outcome (TestStateStorage). + val hint = caseUrl(session, p["root"]?.asString ?: "", program, p["case"].asString, mains[program]) + ?.let { " locationHint='${escape(it)}'" } ?: "" + out.append("##teamcity[testStarted name='$name'$hint]\n") when (p["status"]?.asString) { "passed" -> {} "skipped" -> out.append("##teamcity[testIgnored name='$name']\n") @@ -274,3 +277,13 @@ class LaunchingProcessHandler(private val c: McppRunConfiguration) : ProcessHand class McppProgramState(private val c: McppRunConfiguration, env: ExecutionEnvironment) : CommandLineState(env) { override fun startProcess(): ProcessHandler = LaunchingProcessHandler(c) } + +/** The URL of a case: its program's source and the line of its macro (`file://:`, 1-based). */ +internal fun caseUrl(session: McppSession, root: String, test: String, case: String, main: String?): String? { + val file = main?.takeIf { it.isNotEmpty() } ?: return null + val cases = session.casesFor(root, test) ?: return null + val line = cases.getAsJsonArray("cases")?.firstOrNull { it.asJsonObject["name"]?.asString == case } + ?.asJsonObject?.get("line")?.asInt ?: return null + return "file://$file:${line + 1}" +} + From 6edea12a98dc6a89dc61b63f584fa9e16fc256aa Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 21:35:24 +0900 Subject: [PATCH 094/119] a test's name from its file as the file system spells both (macOS's linked /var, Windows' drive letters); capabilities/explain names tests.debug (test-launch) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- src/server.cppm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/server.cppm b/src/server.cppm index d038713..7bcd26c 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -1082,6 +1082,7 @@ std::expected Session::capabilities_explain(const Json& p) { static constexpr Gate kGates[] = { {"targets.sets", "target-sets", "2026.10.30.1"}, {"pack.formatList", "pkg-format-list", "2026.10.30.1"}, + {"tests.debug", "test-launch", "2026.11.1.1"}, }; Json features = Json::array(); for (const auto& g : kGates) { @@ -1302,7 +1303,14 @@ std::string read_text(const std::filesystem::path& file) { std::optional test_name_of(const std::filesystem::path& root, const std::filesystem::path& file) { const auto ext = file.extension().string(); if (ext != ".cpp" && ext != ".cc" && ext != ".cxx") return std::nullopt; - const auto rel = file.lexically_normal().lexically_relative(root.lexically_normal()); + // Both as the file system spells them: a temporary directory behind a link (macOS's /var), a + // drive letter in either case (Windows). + std::error_code ec; + auto canonical = [&](const std::filesystem::path& p) { + auto c = std::filesystem::weakly_canonical(p, ec); + return ec ? p.lexically_normal() : c; + }; + const auto rel = canonical(file).lexically_relative(canonical(root)); if (rel.empty() || *rel.begin() != "tests") return std::nullopt; std::filesystem::path below; bool first = true; From dfe074a5ad5079db0076c03aaa7d47f902670503 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 22:06:43 +0900 Subject: [PATCH 095/119] the Extension Host suite's mcppls stubs carry a server the backend asks for their actions (0.0.20's status, the frozen table for 0.0.18 and 0.0.19); a build forwards a reload only under refreshAfterBuild = reload; CLion's startup no longer names a removed method Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../mcppcommunity/mcppclion/McppStartup.kt | 1 - mide-plugins/vscode/test/e2e/mcpplsStub.ts | 58 +++++++++++++++++++ mide-plugins/vscode/test/e2e/runTest.ts | 15 ++++- .../vscode/test/e2e/suite/extension.e2e.ts | 43 +++++++++----- 4 files changed, 99 insertions(+), 18 deletions(-) diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStartup.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStartup.kt index a6b9802..2020cda 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStartup.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStartup.kt @@ -26,7 +26,6 @@ class McppStartup : ProjectActivity { } n.notify(project) } - session.root?.let { session.refreshTests(it) } } val stale = File(base, "compile_commands.json") if (stale.isFile) { diff --git a/mide-plugins/vscode/test/e2e/mcpplsStub.ts b/mide-plugins/vscode/test/e2e/mcpplsStub.ts index b5defea..a802871 100644 --- a/mide-plugins/vscode/test/e2e/mcpplsStub.ts +++ b/mide-plugins/vscode/test/e2e/mcpplsStub.ts @@ -6,6 +6,12 @@ * one that renamed its command, one that exposes no state API, and one whose * state API throws. The stub is generated rather than checked in five times so * the command list has a single source here. + * + * Each variant carries a server in its payload, as the real extension does: what the backend asks + * (`mcppls --version`, and `mcppls status` from 0.0.20) to learn mcppls's actions and their VS Code + * commands (design 2026-10-30 part 2 §5.3). The full one is 0.0.20 and states its actions; the + * partial one is 0.0.18, the oldest supported, read through the backend's frozen table; the others + * are 0.0.19, read the same way. */ export type StubVariant = "full" | "partial" | "renamed" | "noapi" | "throwing"; @@ -108,3 +114,55 @@ ${apiReturn} ${exportsBody} `; } + +/** The server's version: `status` exists from 0.0.20. */ +export function stubServerVersion(variant: StubVariant): string { + return variant === "full" ? "0.0.20" : variant === "partial" ? "0.0.18" : "0.0.19"; +} + +/** mcppls 0.0.20's actions under VS Code, as `mcppls status` states them (mcppls S3-8-7). */ +const STATUS_ACTIONS: ReadonlyArray<[string, string, unknown[]?]> = [ + ["restart", "mcppls.restartServer"], + ["reload", "mcppls.reloadBuildDescription"], + ["restartEngine", "mcppls.restartClangd"], + ["resetCache", "mcppls.resetWorkspaceCache"], + ["showLog", "mcppls.showLogs"], + ["openLogDirectory", "mcppls.revealCacheDirectory", ["logs"]], + ["openCacheDirectory", "mcppls.revealCacheDirectory"], + ["collectReport", "mcppls.collectReport"], + ["exportDiagnostics", "mcppls.exportDiagnosticBundle"], + ["selectContext", "mcppls.selectContext"], + ["moduleGraph", "mcppls.showModuleGraph"], + ["runBuildTool", "mcppls.runBuildToolInTerminal"], + ["installTools", "mcppls.installCommandLineTools"], + ["reviewRun", "mcppls.review.run"], + ["reviewClear", "mcppls.review.clear"], + ["turnOffInWorkspace", "mcppls.turnOffInWorkspace"], + ["turnOnInWorkspace", "mcppls.turnOnInWorkspace"], + ["turnOffOtherCppFeatures", "mcppls.turnOffOtherCppFeatures"], + ["restoreOtherCppFeatures", "mcppls.restoreOtherCppFeatures"], +]; + +/** The server's program, a node script: `--version`, and `status --format json --root `. */ +export function stubServerJs(variant: StubVariant): string { + const version = stubServerVersion(variant); + const actions = STATUS_ACTIONS.map(([id, vscode, args]) => ({ id, title: id, vscode, ...(args ? { vscodeArguments: args } : {}) })); + return `const args = process.argv.slice(2); +if (args[0] === "--version") { + process.stdout.write("mcppls ${version}\\n"); +} else if (args[0] === "status" && ${JSON.stringify(variant === "full")}) { + const root = args[args.indexOf("--root") + 1]; + process.stdout.write(JSON.stringify({ + schema: 1, + server: { version: "${version}", platform: "test" }, + capabilities: ["status"], + workspace: { root, state: "ready" }, + instances: [], + actions: ${JSON.stringify(actions)}, + }) + "\\n"); +} else { + process.stderr.write("mcppls: unknown command\\n"); + process.exit(2); +} +`; +} diff --git a/mide-plugins/vscode/test/e2e/runTest.ts b/mide-plugins/vscode/test/e2e/runTest.ts index 7480c54..cef4a64 100644 --- a/mide-plugins/vscode/test/e2e/runTest.ts +++ b/mide-plugins/vscode/test/e2e/runTest.ts @@ -4,7 +4,7 @@ import { join, resolve } from "node:path"; import { runTests } from "@vscode/test-electron"; -import { STUB_VARIANTS, stubExtensionJs, stubPackageJson, type StubVariant } from "./mcpplsStub"; +import { STUB_VARIANTS, stubExtensionJs, stubPackageJson, stubServerJs, type StubVariant } from "./mcpplsStub"; /** * One Extension Host run per mcppls variant. Each run gets its own extensions @@ -39,6 +39,19 @@ async function runVariant(repositoryRoot: string, fixtureRoot: string, variant: } writeFileSync(join(mcpplsStub, "package.json"), stubPackageJson(variant)); writeFileSync(join(mcpplsStub, "extension.js"), stubExtensionJs(variant)); + // The stub's server, in its payload as the real extension carries it: the backend asks it for + // mcppls's actions. A node script, behind a .cmd shim on Windows as the fake mcpp is. + const payloadBin = join(mcpplsStub, "payload", "bin"); + mkdirSync(payloadBin, { recursive: true }); + writeFileSync(join(payloadBin, "mcppls.js"), stubServerJs(variant)); + const server = process.platform === "win32" ? "mcppls.cmd" : "mcppls"; + if (process.platform === "win32") { + writeFileSync(join(payloadBin, server), `@node "%~dp0mcppls.js" %*\r\n`); + } else { + writeFileSync(join(payloadBin, server), `#!/usr/bin/env node\n${stubServerJs(variant)}`); + chmodSync(join(payloadBin, server), 0o755); + } + writeFileSync(join(mcpplsStub, "payload", "payload.json"), JSON.stringify({ server: { path: `bin/${server}` } })); copyFileSync(join(fixtureRoot, "project/mcpp.toml"), join(workspaceDir, "mcpp.toml")); copyFileSync(join(fixtureRoot, "project/main.cpp"), join(workspaceDir, "main.cpp")); copyFileSync(join(fixtureRoot, "project/build.mcpp"), join(workspaceDir, "build.mcpp")); diff --git a/mide-plugins/vscode/test/e2e/suite/extension.e2e.ts b/mide-plugins/vscode/test/e2e/suite/extension.e2e.ts index d801876..5b63e51 100644 --- a/mide-plugins/vscode/test/e2e/suite/extension.e2e.ts +++ b/mide-plugins/vscode/test/e2e/suite/extension.e2e.ts @@ -51,7 +51,7 @@ suite(`mcpp extension smoke (mcppls stub: ${STUB})`, () => { assert.ok(contributes.colors, "theme colours are contributed"); }); - test("a build runs mcpp and then asks the language service to refresh", async () => { + test("a build runs mcpp; the language service is asked to reload only when the setting says so", async () => { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, "the fixture workspace should be open"); const fakeMcpp = process.env.MCPP_E2E_FAKE_MCPP; @@ -60,25 +60,36 @@ suite(`mcpp extension smoke (mcppls stub: ${STUB})`, () => { assert.ok(fakeMcpp, "MCPP_E2E_FAKE_MCPP"); assert.ok(mcppLog, "MCPP_E2E_LOG"); assert.ok(mcpplsLog, "MCPP_E2E_MCPPLS_LOG"); - await vscode.workspace - .getConfiguration("mcpp", workspaceFolder.uri) - .update("path", fakeMcpp, vscode.ConfigurationTarget.Workspace); + const settings = vscode.workspace.getConfiguration("mcpp", workspaceFolder.uri); + await settings.update("path", fakeMcpp, vscode.ConfigurationTarget.Workspace); + const builds = (): number => (existsSync(mcppLog) ? lines(mcppLog).filter((line) => line === "build").length : 0); + const waitForBuilds = async (count: number): Promise => { + const deadline = Date.now() + 20_000; + while (builds() < count && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100)); + assert.ok(builds() >= count, `mcpp build ran ${builds()} time(s), expected ${count}`); + }; + + // `auto`: mcppls reads mcpp's result record and refreshes itself, so nothing is forwarded + // (design 2026-10-30 part 2 §5.4). + void vscode.commands.executeCommand("mcpp.build"); + await waitForBuilds(1); + await new Promise((r) => setTimeout(r, 1500)); + const afterAuto = existsSync(mcpplsLog) ? lines(mcpplsLog) : []; + assert.ok(!afterAuto.includes("mcppls.reloadBuildDescription"), `auto forwarded: ${afterAuto.join(",")}`); + // `reload`: the action mcppls names for VS Code, when its extension registered the command. + await settings.update("languageService.refreshAfterBuild", "reload", vscode.ConfigurationTarget.Workspace); void vscode.commands.executeCommand("mcpp.build"); - assert.ok((await waitForFile(mcppLog, 20_000, "build")).trim().split(/\r?\n/).includes("build"), "a build must run even when a read-only self env probe also ran"); - - // Which command the refresh chain lands on is exactly what the variant decides. - const recorded = await waitForFile(mcpplsLog, 20_000); - if (STUB === "partial") { - assert.ok(recorded.includes("mcppls.restartServer"), `expected a restart, saw: ${recorded.trim()}`); - } else if (STUB === "renamed") { - assert.equal(recorded.trim(), "", "a stub without the command must not be called"); + await waitForBuilds(2); + if (STUB === "full" || STUB === "noapi" || STUB === "throwing") { + const recorded = await waitForFile(mcpplsLog, 20_000, "mcppls.reloadBuildDescription"); + assert.ok(recorded.includes("mcppls.reloadBuildDescription"), `expected the reload, saw: ${recorded.trim()}`); } else { - assert.ok( - recorded.includes("mcppls.reloadBuildDescription"), - `expected the cheap reload to be preferred, saw: ${recorded.trim()}`, - ); + await new Promise((r) => setTimeout(r, 1500)); + const recorded = existsSync(mcpplsLog) ? lines(mcpplsLog) : []; + assert.deepEqual(recorded.filter((line) => line.startsWith("mcppls.")), [], `${STUB} registered no reload`); } + await settings.update("languageService.refreshAfterBuild", undefined, vscode.ConfigurationTarget.Workspace); }); test("forwarded commands reach mcppls, and a missing one does not throw", async () => { From a383fb10ec0e98d9e938d6f83f7690571ea30207 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 22:37:25 +0900 Subject: [PATCH 096/119] =?UTF-8?q?checkpoint=205a:=20the=20rules=20layer?= =?UTF-8?q?=20of=20mcpp.toml=20and=20build.mcpp=20in=20the=20backend=20(mc?= =?UTF-8?q?pp=20design=202026-10-30=20part=202=20=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tolerant TOML scanner, completion, hover, the three jumps and the seven manifest rules, and build.mcpp's SPEC-007 checks, completion, snippets and hover, ported from mcpp-vscode 0.9's TypeScript. tests/rules/golden holds that implementation's answers over tests/rules/corpus at every position of every line; test_rules compares the backend's with them. One mcpp.toml snapshot and one build-program snapshot (spec/), the latter refreshed from 2026.10.29.1. The reference gained what the backend's reader already did: a table read through the schema (workspace mirrors, a platform's tables under a selector, named entries), a selector-only table offered with its selector, the schema's own arrays of tables allowed, and an unclosed multi-line string at the end no longer stops the parse. CLion 2026.1's abstract ProjectTaskRunner.canRun(task). Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .gitattributes | 1 + mcpp.lock | 13 +- .../mcppclion/run/McppProjectTaskRunner.kt | 4 + mide-plugins/vscode/data/i18n/zh-cn.json | 6 +- mide-plugins/vscode/src/toml/completion.ts | 16 + mide-plugins/vscode/src/toml/diagnostics.ts | 9 +- mide-plugins/vscode/src/toml/parser.ts | 59 +- mide-plugins/vscode/src/toml/schema.ts | 2 +- spec/buildscript-api.json | 33 +- spec/i18n/runtime.zh-cn.json | 6 +- src/buildscript.cppm | 796 +++++++++ src/manifest.cppm | 1462 +++++++++++++---- src/server.cppm | 4 - src/text.cppm | 156 ++ src/toml.cppm | 712 ++++++++ tests/rules/corpus/brackets.toml | 6 + tests/rules/corpus/broken.toml | 36 + tests/rules/corpus/comments.toml | 3 + tests/rules/corpus/complete.toml | 80 + tests/rules/corpus/crlf.toml | 6 + tests/rules/corpus/editing.mcpp | 4 + tests/rules/corpus/editing.toml | 24 + tests/rules/corpus/empty.toml | 0 tests/rules/corpus/plain.mcpp | 14 + tests/rules/corpus/rules.mcpp | 59 + tests/rules/corpus/unclosed.toml | 6 + tests/rules/golden/brackets.toml.json | 1 + tests/rules/golden/broken.toml.json | 1 + tests/rules/golden/catalogue.json | 1 + tests/rules/golden/comments.toml.json | 1 + tests/rules/golden/complete.toml.json | 1 + tests/rules/golden/crlf.toml.json | 1 + tests/rules/golden/editing.mcpp.json | 1 + tests/rules/golden/editing.toml.json | 1 + tests/rules/golden/empty.toml.json | 1 + tests/rules/golden/plain.mcpp.json | 1 + tests/rules/golden/rules.mcpp.json | 1 + tests/rules/golden/unclosed.toml.json | 1 + tests/test_manifest.cpp | 97 -- tests/test_rules.cpp | 195 +++ 40 files changed, 3356 insertions(+), 465 deletions(-) create mode 100644 src/buildscript.cppm create mode 100644 src/text.cppm create mode 100644 src/toml.cppm create mode 100644 tests/rules/corpus/brackets.toml create mode 100644 tests/rules/corpus/broken.toml create mode 100644 tests/rules/corpus/comments.toml create mode 100644 tests/rules/corpus/complete.toml create mode 100644 tests/rules/corpus/crlf.toml create mode 100644 tests/rules/corpus/editing.mcpp create mode 100644 tests/rules/corpus/editing.toml create mode 100644 tests/rules/corpus/empty.toml create mode 100644 tests/rules/corpus/plain.mcpp create mode 100644 tests/rules/corpus/rules.mcpp create mode 100644 tests/rules/corpus/unclosed.toml create mode 100644 tests/rules/golden/brackets.toml.json create mode 100644 tests/rules/golden/broken.toml.json create mode 100644 tests/rules/golden/catalogue.json create mode 100644 tests/rules/golden/comments.toml.json create mode 100644 tests/rules/golden/complete.toml.json create mode 100644 tests/rules/golden/crlf.toml.json create mode 100644 tests/rules/golden/editing.mcpp.json create mode 100644 tests/rules/golden/editing.toml.json create mode 100644 tests/rules/golden/empty.toml.json create mode 100644 tests/rules/golden/plain.mcpp.json create mode 100644 tests/rules/golden/rules.mcpp.json create mode 100644 tests/rules/golden/unclosed.toml.json delete mode 100644 tests/test_manifest.cpp create mode 100644 tests/test_rules.cpp diff --git a/.gitattributes b/.gitattributes index 8909cc8..045dd15 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ * text=auto eol=lf protocol/fixtures/** -text *.png binary +tests/rules/corpus/** -text diff --git a/mcpp.lock b/mcpp.lock index 2c6d32f..7c6a938 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -30,9 +30,9 @@ targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-linux"] namespace = "mcpplibs" -version = "0.18.2" -source = "index+mcpplibs@0.18.2" -hash = "fnv1a:2947b54ddbcf8813" +version = "0.18.3" +source = "index+mcpplibs@0.18.3" +hash = "fnv1a:2947b44ddbcf8660" [package."openkal-llvm-runtime"] namespace = "mcpplibs" @@ -68,10 +68,9 @@ targets = ["aarch64-openkal", "x86_64-openkal"] [package."mcpplibs.os"] namespace = "mcpplibs" -version = "0.2.1" -source = "index+mcpplibs@0.2.1" -hash = "fnv1a:d0be97a50033077e" -targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] +version = "0.2.3" +source = "index+mcpplibs@0.2.3" +hash = "fnv1a:d0be95a500330418" [package."nlohmann.json"] namespace = "nlohmann" diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProjectTaskRunner.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProjectTaskRunner.kt index a3ce38e..345f439 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProjectTaskRunner.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppProjectTaskRunner.kt @@ -22,6 +22,10 @@ class McppProjectTaskRunner : ProjectTaskRunner() { override fun canRun(project: Project, task: ProjectTask): Boolean = task is ModuleBuildTask && project.basePath?.let { File(it, "mcpp.toml").isFile } == true + // Abstract in 2026.1, which asks it of a task alone: the module's project answers. + @Suppress("OVERRIDE_DEPRECATION") + override fun canRun(task: ProjectTask): Boolean = (task as? ModuleBuildTask)?.module?.project?.let { canRun(it, task) } ?: false + override fun run(project: Project, context: ProjectTaskContext, vararg tasks: ProjectTask): Promise { val promise = AsyncPromise() val session = McppSession.get(project) diff --git a/mide-plugins/vscode/data/i18n/zh-cn.json b/mide-plugins/vscode/data/i18n/zh-cn.json index fd89852..da98d3c 100644 --- a/mide-plugins/vscode/data/i18n/zh-cn.json +++ b/mide-plugins/vscode/data/i18n/zh-cn.json @@ -653,5 +653,9 @@ "The C++ Modules server installs the target's semantic kit with xlings, in the background.": "C++ Modules 服务器在后台用 xlings 安装目标的语义套件。", "The line of main was read from the text; C++ Modules gives its position when it serves this file.": "main 的行号从文本读出;C++ Modules 服务此文件时由它给出位置。", "The test program did not run: {0}": "测试程序没有运行:{0}", - "Debug": "调试" + "Debug": "调试", + "A feature as a table": "以表形式写出的特性", + "Build settings for the targets a selector matches": "选择器匹配的目标的构建设置", + "Dependencies for the targets a selector matches": "选择器匹配的目标的依赖", + "Dependencies of one index namespace": "一个索引命名空间的依赖" } diff --git a/mide-plugins/vscode/src/toml/completion.ts b/mide-plugins/vscode/src/toml/completion.ts index 876d2e6..6525aa3 100644 --- a/mide-plugins/vscode/src/toml/completion.ts +++ b/mide-plugins/vscode/src/toml/completion.ts @@ -71,6 +71,10 @@ export const SECTION_HEADERS: readonly SectionHeaderSpec[] = [ { group: "xlings", label: "[xlings]", header: "[xlings]", detail: () => t("Build environment (supplied by xlings)") }, { group: "xlings.workspace", label: "[xlings.workspace]", header: "[xlings.workspace]", detail: () => t("Pin tool versions") }, { group: "target", label: "[target.]", header: "[target.${1:x86_64-linux-gnu}]", detail: () => t("Configuration per target triple") }, + { group: "dependencies.", label: "[dependencies.]", header: "[dependencies.${1:mcpplibs}]", detail: () => t("Dependencies of one index namespace") }, + { group: "features.", label: "[features.]", header: "[features.${1:name}]", detail: () => t("A feature as a table") }, + { group: "target.cfg.build", label: "[target.'cfg(...)'.build]", header: "[target.'cfg(${1:os = \"linux\"})'.build]", detail: () => t("Build settings for the targets a selector matches") }, + { group: "target.cfg.dependencies", label: "[target.'cfg(...)'.dependencies]", header: "[target.'cfg(${1:os = \"linux\"})'.dependencies]", detail: () => t("Dependencies for the targets a selector matches") }, { group: "pack", label: "[pack]", header: "[pack]", detail: () => t("mcpp pack packaging configuration") }, { group: "pack.bundle-project", label: "[pack.bundle-project]", header: "[pack.bundle-project]", detail: () => t("Fine-tuning of the vendored filtering policy") }, { group: "indices", label: "[indices]", header: "[indices]", detail: () => t("Project-level index redirection") }, @@ -197,6 +201,16 @@ function headerEntries(): HeaderEntry[] { // `[workspace.dependencies]`). Merge them, preferring the curated wording, so // schema-driven detail never costs a writable table its suggestion. const entries: HeaderEntry[] = SCHEMA.sections.map((section) => { + // A table written only under a selector (`[target..abi]`) is offered with the + // selector to fill in. + if ((section as { selector?: boolean }).selector === true) { + const tail = section.header.slice(section.header.indexOf("") + "".length); + return { + label: `[target.'cfg(...)'${tail}`, + header: `[target.'cfg(\${1:os = "linux"})'${tail}`, + detail: `plane: ${section.plane}`, + }; + } const spec = HEADER_SPEC_BY_GROUP.get(section.name); const entry: HeaderEntry = { label: spec?.label ?? section.header, @@ -254,6 +268,8 @@ function keyPlaceholder(key: TomlKey): string { return "0"; case "array": return "[]"; + case "table": + return "{ }"; case "enum": return JSON.stringify(key.values?.[0] ?? ""); default: diff --git a/mide-plugins/vscode/src/toml/diagnostics.ts b/mide-plugins/vscode/src/toml/diagnostics.ts index 1f0095e..eec85ad 100644 --- a/mide-plugins/vscode/src/toml/diagnostics.ts +++ b/mide-plugins/vscode/src/toml/diagnostics.ts @@ -18,7 +18,7 @@ // 6 legacy-key legacy 段/键 // 7 array-table 数组表 [[...]](mcpp 只接受少数几处,见下) -import { sectionByName, type TomlKey, type TomlSection } from "./schema"; +import { SCHEMA, sectionByName, type TomlKey, type TomlSection } from "./schema"; export type Severity = "error" | "warning" | "info" | "off"; @@ -398,9 +398,14 @@ const ALLOWED_ARRAY_TABLES: ReadonlyArray> = [ ["xlings", "deps"], ]; +/** The schema's own arrays of tables (`[[platform.link.inputs]]`, `[[runtime.artifacts]]`). */ +const SCHEMA_ARRAY_TABLES: ReadonlyArray> = SCHEMA.sections + .filter((section) => section.header.startsWith("[[")) + .map((section) => splitDots(section.name)); + function isAllowedArrayTable(name: string): boolean { const parts = splitDots(name); - return ALLOWED_ARRAY_TABLES.some( + return [...ALLOWED_ARRAY_TABLES, ...SCHEMA_ARRAY_TABLES].some( (pattern) => pattern.length === parts.length && pattern.every((part, index) => part === "*" || part === parts[index]), ); diff --git a/mide-plugins/vscode/src/toml/parser.ts b/mide-plugins/vscode/src/toml/parser.ts index 9587c0e..4535992 100644 --- a/mide-plugins/vscode/src/toml/parser.ts +++ b/mide-plugins/vscode/src/toml/parser.ts @@ -8,6 +8,8 @@ // 点分键、字符串(含三引号多行串)、整数、布尔、数组与内联表(可嵌套、 // 可跨行)、行注释。CRLF 行尾在进入解析前剥除,列号按剥除后的文本计算。 +import { SCHEMA } from "./schema"; + /** 0 基行列范围,end 为开区间(与 VS Code Range 同构)。 */ export interface TomlRange { startLine: number; @@ -151,30 +153,64 @@ const PARAMETERIZED_BASES: ReadonlySet = new Set([ "dev-dependencies", ]); +/** The schema's section names (the rules layer's reading of a table, design 2026-10-30 part 2 §8). */ +const SCHEMA_SECTIONS: ReadonlySet = new Set(SCHEMA.sections.map((section) => section.name)); + +/** A platform's tables that a target selector overrides (`[target..link]` is `[platform.link]`). */ +const PLATFORM_OVERRIDES: ReadonlySet = new Set(["carrier", "layers", "link", "runners", "tools"]); + +/** Tables whose sub-tables are named entries: `[targets.]`, `[platform.build.]`. */ +const NAMED_ENTRY_BASES: ReadonlySet = new Set([ + ...PARAMETERIZED_BASES, + "build-dependencies", + "features", + "platform", +]); + function resolveGroup(segments: readonly string[]): string | undefined { const joined = segments.join("."); - if (KNOWN_SECTIONS.has(joined)) { + if (KNOWN_SECTIONS.has(joined) || SCHEMA_SECTIONS.has(joined)) { return joined; } const head = segments[0]; if (head === "target") { // [target.] 或 [target..<子表>];sel 是三元组或去引号后的 - // cfg 表达式,本身不含未引号点,因此子表部分从第三段开始。 + // cfg 表达式,本身不含未引号点,因此子表部分从第三段开始。A platform's table under a + // selector is the platform's. if (segments.length <= 2) { return "target"; } - return resolveGroup(segments.slice(2)); + const rest = segments.slice(2); + return resolveGroup(PLATFORM_OVERRIDES.has(rest[0]) ? ["platform", ...rest] : rest); + } + if (head === "workspace" && segments[1] === "dependencies" && segments.length >= 3) { + return "workspace.dependencies"; + } + if (head === "workspace" && segments.length >= 2) { + // `[workspace.]` is the table it mirrors. + return resolveGroup(segments.slice(1)); + } + if (segments.length >= 2 && segments[segments.length - 1] === "requires_abi") { + return "requires_abi"; + } + if (head === "platform" && segments[1] === "hosted" && segments[2] === "layers") { + return "platform.layers"; } if (head === "runtime" && segments.length === 2) { // [runtime.""]:带点 capability 名的显式 provider 子表。 return "runtime.capability"; } - if (PARAMETERIZED_BASES.has(head) && segments.length >= 2) { + if (NAMED_ENTRY_BASES.has(head) && segments.length >= 2) { + // The longest leading run that names a section: `[platform.build.'cfg(...)']` is + // `platform.build`, `[targets.app]` is `targets`. + for (let length = segments.length - 1; length >= 1; length -= 1) { + const name = segments.slice(0, length).join("."); + if (SCHEMA_SECTIONS.has(name) || KNOWN_SECTIONS.has(name)) { + return name; + } + } return head; } - if (head === "workspace" && segments[1] === "dependencies" && segments.length >= 3) { - return "workspace.dependencies"; - } return undefined; } @@ -319,6 +355,15 @@ class Scanner { } private sliceText(a: Pos, b: Pos): string { + // A scan that ran to the end of the document (an unclosed multi-line string) ends one line past + // the last; its text ends where the last line does. + if (b.line >= this.lines.length) { + const last = this.lines.length - 1; + b = { line: last, col: this.lines[last].length }; + if (a.line > last) { + return ""; + } + } if (a.line === b.line) { return this.lines[a.line].slice(a.col, b.col); } diff --git a/mide-plugins/vscode/src/toml/schema.ts b/mide-plugins/vscode/src/toml/schema.ts index a27b93d..8c6e60a 100644 --- a/mide-plugins/vscode/src/toml/schema.ts +++ b/mide-plugins/vscode/src/toml/schema.ts @@ -11,7 +11,7 @@ import raw from "../../data/toml-schema.json"; export interface TomlKey { key: string; - type: "string" | "boolean" | "number" | "array" | "enum"; + type: "string" | "boolean" | "number" | "array" | "enum" | "table"; values?: string[]; default?: unknown; since?: string; diff --git a/spec/buildscript-api.json b/spec/buildscript-api.json index a2a5c6f..54083d0 100644 --- a/spec/buildscript-api.json +++ b/spec/buildscript-api.json @@ -1,6 +1,6 @@ { - "sourceVersion": "2026.10.27.1", - "sourceCommit": "e0b1133dd2912f539ee68a06724cb524e3531f46", + "sourceVersion": "2026.10.29.1", + "sourceCommit": "db3b8159480ce94df2ba7ec20c7f9e0654f97764", "protocolVersion": 17, "cacheEpoch": 3, "roles": [ @@ -155,8 +155,10 @@ "transform": "Verbatim", "mustExist": false, "since": 4, - "rules": [], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192" + "rules": [ + "R9.10" + ], + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192" }, { "wire": "runner-named", @@ -320,7 +322,7 @@ "R2.1", "R9.4" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212" }, { "wire": "diagnostic", @@ -360,7 +362,8 @@ "R5.1", "R5.3", "R6.4", - "R9.2" + "R9.2", + "R9.10" ], "docsUrl": null }, @@ -378,7 +381,7 @@ "rules": [ "R2.1" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" }, { "wire": "floor", @@ -394,7 +397,7 @@ "rules": [ "R2.1" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" }, { "wire": "pack-format", @@ -407,7 +410,9 @@ "transform": "Verbatim", "mustExist": false, "since": 9, - "rules": [], + "rules": [ + "R9.10" + ], "docsUrl": null }, { @@ -455,7 +460,7 @@ "R9.2", "R9.5" ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/684a306bced037014da08b038412f57c203d0bc9/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11" + "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11" }, { "wire": "runtime-search-dir", @@ -532,7 +537,9 @@ "transform": "Verbatim", "mustExist": false, "since": 16, - "rules": [], + "rules": [ + "R9.2" + ], "docsUrl": "https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended" }, { @@ -589,7 +596,9 @@ "transform": "Verbatim", "mustExist": false, "since": 17, - "rules": [], + "rules": [ + "R9.2" + ], "docsUrl": "https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended" }, { diff --git a/spec/i18n/runtime.zh-cn.json b/spec/i18n/runtime.zh-cn.json index 29cb814..eea5c27 100644 --- a/spec/i18n/runtime.zh-cn.json +++ b/spec/i18n/runtime.zh-cn.json @@ -771,5 +771,9 @@ "Restart the backend": "重新启动后端", "Export IDE Report": "导出 IDE 报告", "The IDE report is written to {0}": "IDE 报告已写入 {0}", - "cannot write {0}": "无法写入 {0}" + "cannot write {0}": "无法写入 {0}", + "A feature as a table": "以表形式写出的特性", + "Build settings for the targets a selector matches": "选择器匹配的目标的构建设置", + "Dependencies for the targets a selector matches": "选择器匹配的目标的依赖", + "Dependencies of one index namespace": "一个索引命名空间的依赖" } diff --git a/src/buildscript.cppm b/src/buildscript.cppm new file mode 100644 index 0000000..38550dc --- /dev/null +++ b/src/buildscript.cppm @@ -0,0 +1,796 @@ +// The rules layer for build.mcpp: the SPEC-007 rule checks, completion of `mcpp::` names and of the +// modules a build program may import, the API's snippets, and hover -- from the snapshot of mcpp's +// build-program directives (spec/buildscript-api.json) and nothing else. No compiler runs and no +// workspace is read, so it serves an untrusted workspace and cannot disagree with the build over a +// stale database. +// +// While C++ Modules (mcppls) serves the file it compiles the file against the modules it really +// imports, and what it answers better is left to it: the `unknownSymbol` rule, the module names +// after `import` and their hover. The rule checks and the snippets stay: no compiler says those. +// The layer never says "module not found": a build program's imports are resolved by mcpp's module +// graph (docs/specs/build-plugins.md §9), which this layer only recognises, never validates. +// +// It is line-oriented, not a parser: no preprocessor, no types, no evaluation of a condition. The +// one cross-line heuristic is the action block, from `mcpp::action` to `.submit(`, up to 40 lines. +// Every rule errs towards silence. Diagnostic positions are 1-based (`line:column`); a column counts +// UTF-16 units. A port of mcpp-vscode 0.9's src/buildscript/{analysis,api,modules,snippets,providers}.ts +// (design 2026-10-30 part 2 §8); tests/rules holds that implementation's answers. +export module mcppide.buildscript; + +import std; +import nlohmann.json; +import mcppide.spec; +import mcppide.text; + +export namespace mcppide::buildscript { + +using Json = nlohmann::json; + +// The five action roles (`mcpp::roles::*`), in the engine's order. +inline constexpr std::array kActionRoles = {"source", "check", "object", "artifact", "prepare"}; + +// A directive of the snapshot by its wire name (`link-search`), or null. +const Json* directive(std::string_view wire); + +struct KnownModule { + std::string name, description, docsUrl; +}; +// std, std.compat, mcpp, mcpp.core and the reserved `mcpp..*` patterns, in the order a +// list shows them. +const std::vector& known_modules(); +// The module, a concrete name matching a pattern answered with that name; none for any other. +std::optional known_module(std::string_view name); + +// An `import` site: the module name's range (0-based line, UTF-16 columns). +struct ImportSite { + std::string module; + int line = 0, start = 0, end = 0; +}; +std::vector scan_imports(const std::vector& lines); + +struct Diagnostic { + std::string code, message, severity; + int line = 0, start = 0, end = 0; // 1-based +}; +// Every diagnostic the text earns, at `severity`. `unknownSymbols`: rule 1, off while mcppls serves. +std::vector analyse(const std::vector& lines, std::string_view severity, bool unknownSymbols = true); + +struct Snippet { + std::string filterText, label, detail; + std::optional documentation; + std::string insertText; +}; +std::vector snippets(); + +// A completion item: kind module | function | snippet. +struct Item { + std::string label, kind, detail; + std::optional documentation, filterText, insertText; +}; +struct Switches { + bool snippets = true; // mcpp.buildScript.snippets + bool knownModules = true; // mcpp.buildScript.imports.knownModules + bool served = false; // mcppls serves the file +}; +// What completes at the cursor: none outside `import ` and `mcpp::`. +std::optional> completion(std::string_view line, int character, const Switches& s = {}); +// The hover at a cursor (markdown), none where there is nothing to say. +std::optional hover(const std::vector& lines, int line, int character, bool served = false); + +// The snapshot's provenance. +Json api_source(); + +} // namespace mcppide::buildscript + +namespace mcppide::buildscript { + +namespace { + +const Json& api() { return spec::buildscript_api(); } + +std::string str(const Json& j, std::string_view key) { + auto it = j.find(key); + return it != j.end() && it->is_string() ? it->get() : std::string(); +} + +std::string underscored(std::string s) { + std::ranges::replace(s, '-', '_'); + return s; +} + +std::string joined(const std::vector& v, std::string_view sep) { + std::string out; + for (const auto& s : v) out += (out.empty() ? "" : std::string(sep)) + s; + return out; +} + +std::string roles_list(std::string_view sep = ", ") { + std::string out; + for (auto r : kActionRoles) out += (out.empty() ? "" : std::string(sep)) + std::string(r); + return out; +} + +// The typed-API names docs/30 lists: a name here is not flagged (a wrong one costs a missed hint). +constexpr std::string_view kTypedApiNames[] = { + "abi_tool", "accel", "action", "cflag", "cxx_runtime", "cxx_stdlib", "cxxflag", "decision", "define", "dep_bin", + "dep_dir", "dep_linkage", "deploy", "device_sources", "fact", "floor", "generated", "graph_file", "has_feature", + "host", "include_dir", "include_dir_after", "link_flag", "link_lib", "link_script", "link_search", "manifest_dir", + "min_platform_version", "msvc_crt_linkage", "msvc_instance_dir", "ninja_program", "out_dir", + "pack_debug_symbols_dir", "pack_format", "pack_stage_dir", "pack_strip", "package_authors", "package_description", + "package_license", "package_repo", "package_version", "phase", "pkg_config_libdir", "plugins", "profile", + "provision", "provides_pack_format", "report", "rerun_if_changed", "rerun_if_changed_glob", "rerun_if_env_changed", + "roles", "runner", "runtime_search_dir", "source", "stage_dir", "sysroot_dir", "target", "target_arch", + "target_env", "target_os", "tool", "tool_env", "toolchain", "toolchain_binutils_dir", "toolchain_dir", + "toolchain_sysroot", "tools", "toolset_identity", "warning", "windows_entry", "windows_subsystem", "xpkg_dir", + "xpkg_pending", "xpkg_program", "xpkg_request", "xpkg_source", +}; + +const std::set>& known_symbols() { + static const auto names = [] { + std::set> out(std::begin(kTypedApiNames), std::end(kTypedApiNames)); + for (auto r : kActionRoles) out.insert(std::string(r)); + for (const auto& d : api()["directives"]) { + out.insert(str(d, "wire")); + out.insert(underscored(str(d, "wire"))); + } + return out; + }(); + return names; +} + +bool is_role(std::string_view s) { return std::ranges::find(kActionRoles, s) != kActionRoles.end(); } + +constexpr std::string_view kMcppDocs = "https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md"; + +// A C++ module name: /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/ +bool module_name(std::string_view s) { + static const std::regex re(R"(^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$)"); + return std::regex_match(s.begin(), s.end(), re); +} + +int u16(std::string_view line, std::size_t byte) { return text::utf16_column(line, byte); } + +// ── masking ── + +struct ScanState { + bool blockComment = false; + std::optional rawDelim; +}; + +struct StringLiteral { + std::string value; // the characters it denotes + std::size_t start = 0; // the opening quote + std::size_t end = 0; // one past the closing quote + std::string raw; // as written +}; + +struct MaskedLine { + std::string code; // comments and literals blanked, offsets kept + std::vector strings; +}; + +std::string unescape(std::string_view raw) { + std::string out; + for (std::size_t i = 0; i < raw.size(); ++i) { + if (raw[i] != '\\') { out += raw[i]; continue; } + if (i + 1 >= raw.size()) { out += '\\'; ++i; continue; } + const char next = raw[i + 1]; + out += next == 'n' ? '\n' : next == 't' ? '\t' : next == 'r' ? '\r' : next; + ++i; + } + return out; +} + +// `R"delim(` at the start of `s`: the delimiter. +std::optional> raw_open(std::string_view s) { + static const std::regex re(R"(^R"([^(\s]{0,16})\()"); + std::match_results m; + if (!std::regex_search(s.begin(), s.end(), m, re)) return std::nullopt; + return std::pair{m[1].str(), static_cast(m[0].length())}; +} + +bool ident_char(char c) { return std::isalnum(static_cast(c)) || c == '_'; } + +// The analysis's masking: the line with every comment and literal character a space, and the +// literals. `/*` itself stays (only what follows it is blanked), as the analysis has always read it. +MaskedLine mask_for_analysis(std::string_view line, ScanState& state) { + std::string out(line); + std::vector strings; + auto blank = [&](std::size_t from, std::size_t to) { + for (std::size_t i = from; i < std::min(to, line.size()); ++i) out[i] = ' '; + }; + std::size_t i = 0; + if (state.rawDelim) { + const auto terminator = ")" + *state.rawDelim + "\""; + const auto end = line.find(terminator); + if (end == std::string_view::npos) return {std::string(line.size(), ' '), strings}; + blank(0, end + terminator.size()); + i = end + terminator.size(); + state.rawDelim.reset(); + } + while (i < line.size()) { + if (state.blockComment) { + const auto end = line.find("*/", i); + if (end == std::string_view::npos) { + blank(i, line.size()); + break; + } + blank(i, end + 2); + i = end + 2; + state.blockComment = false; + continue; + } + const auto pair = line.substr(i, 2); + if (pair == "//") { + blank(i, line.size()); + break; + } + if (pair == "/*") { + state.blockComment = true; + i += 2; + continue; + } + if (line[i] == 'R' && i + 1 < line.size() && line[i + 1] == '"') { + if (auto open = raw_open(line.substr(i))) { + const auto terminator = ")" + open->first + "\""; + const auto end = line.find(terminator, i + open->second); + if (end == std::string_view::npos) { + state.rawDelim = open->first; + blank(i, line.size()); + break; + } + blank(i, end + terminator.size()); + i = end + terminator.size(); + continue; + } + } + // `'` after an identifier character is a digit separator (1'000). + if (line[i] == '\'' && i > 0 && ident_char(line[i - 1])) { + ++i; + continue; + } + if (line[i] == '"' || line[i] == '\'') { + const char quote = line[i]; + std::size_t j = i + 1; + while (j < line.size()) { + if (line[j] == '\\') { j += 2; continue; } + if (line[j] == quote) { ++j; break; } + ++j; + } + const auto stop = std::min(j, line.size()); + const auto contentEnd = std::max(i + 1, stop - 1); + strings.push_back({unescape(line.substr(i + 1, contentEnd - (i + 1))), i, stop, std::string(line.substr(i, stop - i))}); + blank(i, stop); + i = stop; + continue; + } + ++i; + } + return {out, strings}; +} + +// The import scanner's masking: comments and literals, `/*` included, each blanked character one +// space per UTF-16 unit and a digit separator dropped -- the text whose offsets the scanner has +// always reported. +std::string mask_for_imports(std::string_view line, ScanState& state) { + std::string out; + auto blank = [&](std::size_t from, std::size_t to) { + to = std::min(to, line.size()); + if (from < to) out += std::string(static_cast(text::utf16_length(line.substr(from, to - from))), ' '); + }; + std::size_t i = 0; + if (state.rawDelim) { + const auto terminator = ")" + *state.rawDelim + "\""; + const auto end = line.find(terminator, i); + if (end == std::string_view::npos) { + blank(0, line.size()); + return out; + } + blank(0, end + terminator.size()); + i = end + terminator.size(); + state.rawDelim.reset(); + } + while (i < line.size()) { + if (state.blockComment) { + const auto end = line.find("*/", i); + if (end == std::string_view::npos) { + blank(i, line.size()); + return out; + } + blank(i, end + 2); + i = end + 2; + state.blockComment = false; + continue; + } + const auto pair = line.substr(i, 2); + if (pair == "//") { + blank(i, line.size()); + return out; + } + if (pair == "/*") { + state.blockComment = true; + out += " "; + i += 2; + continue; + } + if (line[i] == 'R' && i + 1 < line.size() && line[i + 1] == '"') { + if (auto open = raw_open(line.substr(i))) { + const auto terminator = ")" + open->first + "\""; + const auto end = line.find(terminator, i + open->second); + if (end == std::string_view::npos) { + state.rawDelim = open->first; + blank(i, line.size()); + return out; + } + const auto stop = end + terminator.size(); + blank(i, stop); + i = stop; + continue; + } + } + if (line[i] == '\'' && i > 0 && ident_char(line[i - 1])) { + ++i; + continue; + } + if (line[i] == '"' || line[i] == '\'') { + const char quote = line[i]; + std::size_t j = i + 1; + while (j < line.size()) { + if (line[j] == '\\') { j += 2; continue; } + if (line[j] == quote) { ++j; break; } + ++j; + } + const auto stop = std::min(j, line.size()); + blank(i, stop); + i = stop; + continue; + } + out += line[i]; + ++i; + } + return out; +} + +using Match = std::match_results; + +std::optional search(const std::string& s, const std::regex& re) { + Match m; + if (std::regex_search(s, m, re)) return m; + return std::nullopt; +} + +std::string_view trim(std::string_view s) { return text::trim(s); } + +} // namespace + +const Json* directive(std::string_view wire) { + for (const auto& d : api()["directives"]) + if (str(d, "wire") == wire) return &d; + return nullptr; +} + +const std::vector& known_modules() { + static const std::vector all = [] { + const std::string std_docs = std::string(kMcppDocs) + "#import-std-mcpp-2026821"; + const std::string core_docs = std::string(kMcppDocs) + "#the-interfaces-name-mcppcore-protocol-14"; + const std::string plugin_docs = std::string(kMcppDocs) + "#the-names-of-a-plugins-modules"; + return std::vector{ + {"std", + "The C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × " + "dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+).", + std_docs}, + {"std.compat", + "The C++ standard library plus the C library names in the global namespace; `import std;` and `import std.compat;` " + "may be used together.", + std_docs}, + {"mcpp", + "The mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent " + "equivalent spelling of `mcpp.core` (SPEC-007 R9.1).", + core_docs}, + {"mcpp.core", + "The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning " + "change is a new symbol (SPEC-007 R9.2).", + core_docs}, + {"mcpp.plugins.*", + "The official general-purpose library (L2), provided by the `plugins-core` feature and built only on the L1 engine " + "interface, e.g. `mcpp.plugins.tool` (SPEC-007 §9).", + plugin_docs}, + {"mcpp.deps.*", + "Official dependency-adapter plugins (L3), e.g. a package manager or external build system brought into the graph " + "(SPEC-007 §0, §9).", + plugin_docs}, + {"mcpp.rules.*", "Official rule packages (L3): code generation and device-language toolchains (SPEC-007 §0, §9).", + plugin_docs}, + {"mcpp.dist.*", + "Official distribution members (L3): they turn a link artifact into something installable, e.g. a WiX or APK " + "payload (SPEC-007 §0, §9).", + plugin_docs}, + {"mcpp.tools.*", "Official tool namespaces (L3), e.g. `mcpp.tools.island` helpers described in docs/31 (SPEC-007 §9).", + plugin_docs}, + }; + }(); + return all; +} + +std::optional known_module(std::string_view name) { + for (const auto& m : known_modules()) + if (!m.name.ends_with(".*") && m.name == name) return m; + for (const auto& m : known_modules()) { + if (!m.name.ends_with(".*")) continue; + const auto prefix = std::string_view(m.name).substr(0, m.name.size() - 1); // the trailing dot kept + if (name.starts_with(prefix) && module_name(name.substr(prefix.size()))) + return KnownModule{std::string(name), m.description, m.docsUrl}; + } + return std::nullopt; +} + +std::vector scan_imports(const std::vector& lines) { + // `import X;`, `export import X;`: a member call (`obj.import(...)`) or an identifier ending in + // `import` is not one; header units, header imports and partitions are not module names. + static const std::regex re(R"((?:^|[\s;{}])import\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*(?=;))"); + std::vector sites; + ScanState state; + for (std::size_t l = 0; l < lines.size(); ++l) { + const auto code = mask_for_imports(lines[l], state); + for (auto it = std::sregex_iterator(code.begin(), code.end(), re); it != std::sregex_iterator(); ++it) { + const auto& m = *it; + const auto name = m[1].str(); + const auto whole = m[0].str(); + const auto start = static_cast(m.position(0)) + whole.rfind(name); + sites.push_back({name, static_cast(l), u16(code, start), u16(code, start + name.size())}); + } + } + return sites; +} + +namespace { + +struct RoleSite { + std::string text; + bool literal = false; + std::size_t line = 0, start = 0, end = 0; +}; + +struct ActionBlock { + std::size_t line = 0, actionStart = 0, actionEnd = 0; + std::optional role; + bool hasRoleField = false, hasOutputDir = false; +}; + +constexpr std::size_t kActionWindow = 40; + +// The literal written right after `after` (only spaces between), if any. +const StringLiteral* literal_after(const MaskedLine& m, std::size_t after) { + for (const auto& s : m.strings) + if (s.start >= after && trim(std::string_view(m.code).substr(after, s.start - after)).empty()) return &s; + return nullptr; +} + +std::vector find_action_blocks(const std::vector& masked) { + static const std::regex action(R"(\bmcpp::action\b)"), submit(R"(\.submit\s*\()"), outputDir(R"(\.output_dir\s*\()"), + roleConstant(R"(mcpp::roles::([A-Za-z_]\w*))"), roleAssign(R"(\brole\s*=)"); + std::vector blocks; + for (std::size_t i = 0; i < masked.size(); ++i) { + const auto token = search(masked[i].code, action); + if (!token) continue; + const auto limit = std::min(masked.size() - 1, i + kActionWindow); + auto end = limit; + for (auto j = i; j <= limit; ++j) + if (std::regex_search(masked[j].code, submit)) { + end = j; + break; + } + ActionBlock block; + block.line = i; + block.actionStart = static_cast(token->position(0)); + block.actionEnd = block.actionStart + static_cast(token->length(0)); + for (auto j = i; j <= end; ++j) { + const auto& [code, strings] = masked[j]; + if (!block.hasOutputDir && std::regex_search(code, outputDir)) block.hasOutputDir = true; + if (const auto c = search(code, roleConstant); c && !block.role) { + block.hasRoleField = true; + const auto start = static_cast(c->position(0)); + block.role = RoleSite{(*c)[1].str(), false, j, start, start + static_cast(c->length(0))}; + } + if (!block.role) { + if (const auto a = search(code, roleAssign)) { + block.hasRoleField = true; + const auto after = static_cast(a->position(0) + a->length(0)); + if (const auto* lit = literal_after(masked[j], after)) + block.role = RoleSite{lit->value, true, j, lit->start, lit->end}; + } + } + } + blocks.push_back(std::move(block)); + } + return blocks; +} + +// The narrowest span of `needle` in a literal, else the literal. +std::pair literal_span(const StringLiteral& literal, std::string_view needle) { + if (!needle.empty()) + if (const auto at = literal.raw.find(needle); at != std::string::npos) + return {literal.start + at, literal.start + at + needle.size()}; + return {literal.start, literal.end}; +} + +std::optional group1(const std::string& s, const std::regex& re) { + if (const auto m = search(s, re)) return (*m)[1].str(); + return std::nullopt; +} + +} // namespace + +std::vector analyse(const std::vector& lines, std::string_view severity, bool unknownSymbols) { + std::vector masked; + ScanState state; + for (const auto& l : lines) masked.push_back(mask_for_analysis(l, state)); + + std::vector found; + auto report = [&](std::string_view rule, std::string message, std::size_t line, std::size_t start, std::size_t end) { + const std::string_view text = line < lines.size() ? std::string_view(lines[line]) : std::string_view(); + found.push_back({std::format("mcpp.buildscript.{}", rule), std::move(message), std::string(severity), static_cast(line) + 1, + u16(text, start) + 1, u16(text, end) + 1}); + }; + const auto roles = roles_list(); + + // 1. A `mcpp::` the engine does not have, and no import of the file brings in. + if (unknownSymbols) { + static const std::regex imported(R"(\bimport\s+mcpp\.([A-Za-z_][A-Za-z0-9_]*))"), symbol(R"(\bmcpp::([A-Za-z_][A-Za-z0-9_]*))"); + std::set namespaces; + for (const auto& m : masked) + for (auto it = std::sregex_iterator(m.code.begin(), m.code.end(), imported); it != std::sregex_iterator(); ++it) + namespaces.insert((*it)[1].str()); + for (std::size_t i = 0; i < masked.size(); ++i) + for (auto it = std::sregex_iterator(masked[i].code.begin(), masked[i].code.end(), symbol); it != std::sregex_iterator(); ++it) { + const auto name = (*it)[1].str(); + if (known_symbols().contains(name) || namespaces.contains(name)) continue; + const auto at = static_cast(it->position(0)); + report("unknownSymbol", + std::format("unknown mcpp::{}: not a directive, action role or typed-API name in the mcpp build-script snapshot " + "(SPEC-007 §2; docs/30-build-mcpp.md)", name), + i, at, at + static_cast(it->length(0))); + } + } + + // 2. `role = ""` where the engine's constant exists (R3.6). + const auto blocks = find_action_blocks(masked); + static const std::regex roleAssign(R"(\brole\s*=)"); + for (std::size_t i = 0; i < masked.size(); ++i) + for (auto it = std::sregex_iterator(masked[i].code.begin(), masked[i].code.end(), roleAssign); it != std::sregex_iterator(); ++it) { + const auto after = static_cast(it->position(0) + it->length(0)); + const auto* lit = literal_after(masked[i], after); + if (!lit) continue; + if (is_role(lit->value)) + report("roleLiteral", + std::format("role is spelled as the string \"{0}\"; use mcpp::roles::{0} so an engine that does not know the role " + "refuses to compile it (SPEC-007 R3.6)", lit->value), + i, lit->start, lit->end); + else + report("actionNeedsRole", std::format("unknown action role \"{}\"; the five roles are {} (SPEC-007 R3.6)", lit->value, roles), + i, lit->start, lit->end); + } + + // 3 + 7, the typed form: a block with no role; a prepare block with no output_dir. + for (const auto& b : blocks) { + if (!b.hasRoleField) { + report("actionNeedsRole", + std::format("this mcpp::action declares no role; name one of {} (mcpp::roles::…) — a missing role is read as \"source\" " + "(SPEC-007 R3.2, R3.6)", roles), + b.line, b.actionStart, b.actionEnd); + continue; + } + if (!b.role || b.role->text != "prepare" || b.hasOutputDir) continue; + report("prepareNeedsOutputDir", + "a prepare action must declare the directory its command fills with output_dir(…): the engine refuses an action " + "without one (SPEC-007 R3.3)", + b.role->line, b.role->start, b.role->end); + } + + // 3 + 6 + 7, the raw `mcpp:action=` payload (the frozen printf surface; a role as a string is + // allowed there). A payload that does not parse is skipped. + static const std::regex shellEnv(R"((?:^|\s)([A-Za-z_][A-Za-z0-9_]*=\S*))"), shellCd(R"((?:^|\s)(cd\s+\S+\s*&&))"); + constexpr std::string_view importAction = "mcpp:action="; + for (std::size_t i = 0; i < masked.size(); ++i) + for (const auto& lit : masked[i].strings) { + const auto at = lit.value.find(importAction); + if (at == std::string::npos) continue; + const auto payload = Json::parse(trim(std::string_view(lit.value).substr(at + importAction.size())), nullptr, false); + if (payload.is_discarded() || !payload.is_object()) continue; + const auto role = payload.find("role"); + if (role == payload.end() || !role->is_string()) { + report("actionNeedsRole", std::format("this mcpp:action payload declares no role; the five roles are {} (SPEC-007 R3.6)", roles), + i, lit.start, lit.end); + } else if (!is_role(role->get())) { + report("actionNeedsRole", + std::format("unknown action role \"{}\"; the five roles are {} (SPEC-007 R3.6)", role->get(), roles), i, + lit.start, lit.end); + } else if (role->get() == "prepare" && !(payload.contains("output_dir") && payload["output_dir"].is_string())) { + report("prepareNeedsOutputDir", "a prepare action must declare output_dir: the engine refuses an action without one (SPEC-007 R3.3)", + i, lit.start, lit.end); + } + if (payload.contains("command") && payload["command"].is_array()) + for (const auto& entry : payload["command"]) { + if (!entry.is_string()) continue; + const auto e = entry.get(); + auto needle = group1(e, shellEnv); + if (!needle) needle = group1(e, shellCd); + if (needle) { + report("shellSyntaxInAction", + std::format("the command spells shell syntax (\"{}\"); declare it with env(…) and cwd(…) instead (SPEC-007 R3.8)", + *needle), + i, lit.start, lit.end); + break; + } + } + } + + // 4 + 5: flags R2.1 and R4.4 send to directives. + static const std::regex cxxCall(R"((?:^|[^\w])(?:cxxflag|cflag)\s*\()"), linkCall(R"((?:^|[^\w])link_flag\s*\()"), + rawPath(R"((?:^|\s)(-[IL]\S*))"); + constexpr std::string_view rpath = "-Wl,-rpath"; + for (std::size_t i = 0; i < masked.size(); ++i) { + const auto& [code, strings] = masked[i]; + const bool isCxxFlagCall = std::regex_search(code, cxxCall); + const bool isLinkFlagCall = std::regex_search(code, linkCall); + for (const auto& lit : strings) { + const auto& v = lit.value; + const bool isLinkFlag = isLinkFlagCall || v.find("mcpp:link-flag=") != std::string::npos; + const bool isCxxFlag = isCxxFlagCall || v.find("mcpp:cxxflag=") != std::string::npos || v.find("mcpp:cflag=") != std::string::npos; + if (isLinkFlag && v.find(rpath) != std::string::npos) { + const auto [s, e] = literal_span(lit, rpath); + report("rpathInLinkFlag", + "a run path must not be spelled in link_flag; declare the directory with mcpp::runtime_search_dir(…) (SPEC-007 R4.4)", + i, s, e); + } + if (isLinkFlag || isCxxFlag) + if (const auto token = group1(v, rawPath)) { + const auto [s, e] = literal_span(lit, *token); + report("rawPathFlag", + std::format("spelling \"{}\" in a raw flag bypasses the engine's rendering; use mcpp::include_dir / " + "mcpp::include_dir_after / mcpp::link_search (SPEC-007 R2.1)", *token), + i, s, e); + } + } + } + + // 6, the typed form: the strings of `.arg()` / `.command()` in an action block. + static const std::regex submit(R"(\.submit\s*\()"), argCall(R"(\.(?:arg|command)\s*\()"); + std::set actionLines; + for (const auto& b : blocks) + for (auto i = b.line; i < masked.size(); ++i) { + actionLines.insert(i); + if (std::regex_search(masked[i].code, submit)) break; + } + for (std::size_t i = 0; i < masked.size(); ++i) { + if (!actionLines.contains(i) || !std::regex_search(masked[i].code, argCall)) continue; + for (const auto& lit : masked[i].strings) { + auto needle = group1(lit.value, shellEnv); + if (!needle) needle = group1(lit.value, shellCd); + if (!needle) continue; + const auto [s, e] = literal_span(lit, *needle); + report("shellSyntaxInAction", + std::format("the command spells shell syntax (\"{}\"); declare the environment and working directory with env(…) and " + "cwd(…) instead (SPEC-007 R3.8)", *needle), + i, s, e); + } + } + + std::set seen; + std::vector unique; + for (auto& d : found) + if (seen.insert(std::format("{}:{}:{}:{}", d.code, d.line, d.start, d.end)).second) unique.push_back(std::move(d)); + std::ranges::stable_sort(unique, [](const Diagnostic& a, const Diagnostic& b) { + if (a.line != b.line) return a.line < b.line; + if (a.start != b.start) return a.start < b.start; + return a.code < b.code; + }); + return unique; +} + +std::vector snippets() { + const auto choice = roles_list(","); + std::vector out{ + {"action", "action ... (typed action block)", "mcpp::action with id, role, description, arg/input/output and submit", + std::format("The five roles (SPEC-007 R3.6): {}.", roles_list()), + std::format("mcpp::action ${{1:handle}};\n${{1:handle}}.id = \"${{2:id}}\";\n${{1:handle}}.role = mcpp::roles::${{3|{}|}};\n" + "${{1:handle}}.description = \"${{4:description}}\";\n${{1:handle}}.arg(\"${{5:argument}}\")\n .input(\"${{6:input}}\")\n" + " .output(\"${{7:output}}\")\n .submit();", + choice)}, + {"action", "action ... (prepare block)", "mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)", + "A prepare action must declare output_dir; the engine refuses one without it.", + "mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n" + "${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"}, + }; + // `mcpp::("…")` for every directive but `action`: one placeholder, since no arity is known. + std::set seen; + for (const auto& d : api()["directives"]) { + const auto name = underscored(str(d, "wire")); + if (name == "action" || !seen.insert(name).second) continue; + std::vector documentation; + std::vector rules; + if (d.contains("rules") && d["rules"].is_array()) + for (const auto& r : d["rules"]) rules.push_back(r.get()); + if (!rules.empty()) documentation.push_back("SPEC-007 " + joined(rules, ", ")); + if (d.contains("docsUrl") && d["docsUrl"].is_string()) + documentation.push_back(std::format("[docs/30 — build.mcpp]({})", d["docsUrl"].get())); + Snippet s{name, name + "(...)", std::format("mcpp:{}= · slot {}", str(d, "wire"), str(d, "slot")), std::nullopt, + name + "(\"${1:value}\")"}; + if (!documentation.empty()) s.documentation = joined(documentation, " · "); + out.push_back(std::move(s)); + } + return out; +} + +std::optional> completion(std::string_view line, int character, const Switches& sw) { + const std::string before(line.substr(0, text::byte_offset(line, character))); + static const std::regex afterImport(R"(\b(?:export\s+)?import\s+([A-Za-z0-9_.]*)$)"), afterScope(R"(mcpp::([A-Za-z_][A-Za-z0-9_]*)?$)"); + if (const auto m = search(before, afterImport)) { + // Only the list is optional; recognition (hover, the no-diagnostic rule) is not. C++ Modules + // completes module names from what the build sees, so it answers instead. + if (!sw.knownModules || sw.served) return std::nullopt; + std::vector items; + const auto typed = (*m)[1].str(); + for (const auto& k : known_modules()) + if (!k.name.ends_with(".*") && k.name.starts_with(typed)) + items.push_back({k.name, "module", "mcpp build-script module", k.description, std::nullopt, std::nullopt}); + return items; + } + if (const auto m = search(before, afterScope)) { + const auto typed = (*m)[1].matched ? (*m)[1].str() : std::string(); + std::vector items; + std::set seen; + for (const auto& d : api()["directives"]) { + const auto name = underscored(str(d, "wire")); + if (!name.starts_with(typed) || !seen.insert(name).second) continue; + items.push_back({name, "function", "mcpp build-script API", std::nullopt, std::nullopt, std::nullopt}); + } + if (sw.snippets) + for (auto& s : snippets()) + if (s.filterText.starts_with(typed)) items.push_back({s.label, "snippet", s.detail, s.documentation, s.filterText, s.insertText}); + return items; + } + return std::nullopt; +} + +std::optional hover(const std::vector& lines, int line, int character, bool served) { + // An import's hover is C++ Modules' while it serves the file. + if (!served) + for (const auto& site : scan_imports(lines)) + if (site.line == line && character >= site.start && character <= site.end) + if (const auto info = known_module(site.module)) return std::format("**{}**\n\n{}", info->name, info->description); + if (line < 0 || line >= static_cast(lines.size())) return std::nullopt; + const auto& text = lines[line]; + static const std::regex symbol(R"(mcpp::([A-Za-z_][A-Za-z0-9_]*))"); + std::optional name; + for (auto it = std::sregex_iterator(text.begin(), text.end(), symbol); it != std::sregex_iterator(); ++it) { + const auto start = u16(text, static_cast(it->position(0))); + const auto end = u16(text, static_cast(it->position(0) + it->length(0))); + if (character >= start && character <= end) { + name = (*it)[1].str(); + break; + } + } + if (!name) return std::nullopt; + auto wire = *name; + std::ranges::replace(wire, '_', '-'); + const auto* entry = directive(wire); + if (!entry) return std::nullopt; + std::vector details{ + std::format("**mcpp::{}**", *name), + std::format("wire `mcpp:{}=` · slot `{}` · scope `{}` · since protocol {}", str(*entry, "wire"), str(*entry, "slot"), + str(*entry, "scope"), entry->contains("since") ? (*entry)["since"].dump() : std::string("undefined"))}; + std::vector rules; + if (entry->contains("rules") && (*entry)["rules"].is_array()) + for (const auto& r : (*entry)["rules"]) rules.push_back(r.get()); + if (!rules.empty()) details.push_back("SPEC-007: " + joined(rules, ", ")); + if (entry->contains("docsUrl") && (*entry)["docsUrl"].is_string()) + details.push_back(std::format("[docs/30 — build.mcpp]({})", (*entry)["docsUrl"].get())); + return joined(details, "\n\n"); +} + +Json api_source() { + return Json{{"version", str(api(), "sourceVersion")}, {"commit", str(api(), "sourceCommit")}}; +} + +} // namespace mcppide::buildscript diff --git a/src/manifest.cppm b/src/manifest.cppm index f1851f8..d236098 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -1,49 +1,99 @@ -// Editing help for mcpp.toml and build.mcpp: completion, hover and an outline, -// from mcpp's own key registry and build-program directives (the snapshots in -// spec/, each naming the mcpp release and commit it was taken from). +// The rules layer for mcpp.toml: completion, hover, the three in-file jumps and the seven +// diagnostic rules, from the snapshot of mcpp's tables and keys (spec/toml-schema.json, naming the +// mcpp release and commit it was read from). // -// THIS IS TEXT, NOT MEANING. The analysis below finds where the cursor is -- -// which `[table]` it is under, whether it is on a key or after `=` -- in the -// text the editor sends; it resolves nothing. Whether the manifest is valid, -// and where it is wrong, is mcpp's to say (manifest/check, located -// diagnostics). Both editors call these methods, so CLion completes what VS -// Code completes. +// THIS IS TEXT, NOT MEANING. It reads what an edit in progress already says and answers from the +// snapshot; it runs nothing, so it serves an untrusted workspace too. Whether the manifest is valid +// is mcpp's to say (manifest/check). Every rule errs towards silence: a false positive costs more +// trust than a missed hint. +// +// Positions in: a 0-based line and a UTF-16 column, as the editors count. Positions out: the same, +// except a diagnostic's, which is 1-based (`line:column`, the form a terminal prints). A port of +// mcpp-vscode 0.9's src/toml/{completion,hover,navigation,diagnostics}.ts (design 2026-10-30 part 2 +// §8); tests/rules holds that implementation's answers over a corpus, and this one gives them. export module mcppide.manifest; import std; import nlohmann.json; +import mcppide.i18n; import mcppide.spec; +import mcppide.text; +import mcppide.toml; export namespace mcppide::manifest { using Json = nlohmann::json; -// Where the cursor is in a mcpp.toml. -struct Context { - enum class Where { header, key, value, comment, other } where = Where::other; - std::string table; // "build", "target.'cfg(os = \"linux\")'.build", ... - std::string section; // the schema section the table reads as: "build" - std::string key; // in a value: the key before `=` - std::set presentKeys; // keys already written in this table - int replaceStart = 0, replaceEnd = 0; // the word the suggestion replaces, on the cursor's line +// ── completion ───────────────────────────────────────────────────────────── + +struct Suggestion { + std::string label; + std::string kind; // section | template | version + std::string detail; + std::optional documentation; + std::optional insertSnippet; // a snippet (`${1:name}`); the label when none + int start = 0, end = 0; // the replaced columns on the cursor's line +}; + +// The suggestions at a cursor: table headers, a table's keys, an enum key's values, and the forms of +// a table whose keys the user names. `lines` are the document's up to the cursor's line, as an +// editor asks. `locale` picks the language of the details. +std::vector completions(const std::vector& lines, int line, int character, + std::string_view locale = "en"); + +// A dependency's version under the cursor (`zlib = "1.2|"`, `zlib = { version = "1.2|" }`). +struct VersionContext { + std::string name; + bool insideString = false; + int start = 0, end = 0; }; +std::optional dependency_version_context(const std::vector& lines, int line, int character); + +// A version an index offers, newest first. +struct PackageVersion { + std::string version; + std::optional summary; +}; +// The versions of the packages named `name` in mcpp's catalogue (`mcpp search --format json`). +std::vector catalogue_versions(const Json& searchEnvelope, std::string_view name); +std::vector version_suggestions(const VersionContext& at, const std::vector& versions); + +// ── hover and the jumps ──────────────────────────────────────────────────── + +struct HoverInfo { + std::string title, body; + std::optional documentation; // the section's reference +}; +std::optional hover_at(const std::vector& lines, int line, int character); + +struct Location { + int line = 0, start = 0, end = 0; +}; +// `workspace = true` to `[workspace.dependencies]`'s entry; `features = ["a"]` to `[features.a]`; +// `path = "../x"` to that manifest's `[package]`, whose line `resolve_manifest` answers (the +// file system is the caller's). +std::optional definition_at(const std::vector& lines, int line, int character, + const std::function(std::string_view)>& resolve_manifest); + +// ── diagnostics ──────────────────────────────────────────────────────────── + +struct Diagnostic { + std::string code, message, severity; // severity: error | warning | info + int line = 0, start = 0, end = 0; // 1-based + std::optional relatedKey; +}; +// Each rule's severity: error | warning | info | off. +struct DiagnosticSettings { + std::string syntax = "error", unknownSection = "warning", unknownKey = "warning", + planeSeparation = "warning", legacyKeys = "info", mcppFloor = "error", arrayTable = "error"; +}; +std::vector analyse(const std::vector& lines, const DiagnosticSettings& settings); + +// The outline: each table, and the keys under it (LSP DocumentSymbol). +Json document_symbols(const std::vector& lines); -Context context_at(std::string_view text, int line, int character); -// The schema section a table header reads as: `target..build` is `build`, -// `dependencies.` is `dependencies`, `targets.` is `targets`; a -// platform's table is the same section under `[platform]` and under the -// selector that overrides it (`platform.link` and `target..link` are -// `platform.link`). -std::string section_of(std::string_view table); - -// {text, line, character} -> {items: [{label, kind, detail, documentation, insertText, range}]} -Json complete(const Json& params); -// {text, line, character} -> {contents (markdown), range} or null -Json hover(const Json& params); -// {text} -> [{name, kind (table|key), line, children}] -Json symbols(const Json& params); -// {text, line, character} -> completion of `mcpp::` in build.mcpp -Json buildscript_complete(const Json& params); +// The snapshot's provenance. +Json schema_source(); } // namespace mcppide::manifest @@ -51,361 +101,1107 @@ namespace mcppide::manifest { namespace { -const Json& schema() { - return spec::toml_schema(); +using toml::Context; +using toml::Resolution; + +const Json& schema() { return spec::toml_schema(); } + +std::string str(const Json& j, std::string_view key) { + auto it = j.find(key); + return it != j.end() && it->is_string() ? it->get() : std::string(); +} +bool has_string(const Json& j, std::string_view key) { + auto it = j.find(key); + return it != j.end() && it->is_string() && !it->get_ref().empty(); +} + +// `[package]` / `package` is `package`; an array of tables is no section. +std::string normalise_section(std::string_view headerOrName) { + auto t = text::trim(headerOrName); + if (t.starts_with("[[")) return {}; + if (t.starts_with("[") && t.ends_with("]")) return std::string(text::trim(t.substr(1, t.size() - 2))); + return std::string(t); } -const Json& api() { - return spec::buildscript_api(); + +const Json* section_by_name(std::string_view name) { + const auto wanted = normalise_section(name); + if (wanted.empty()) return nullptr; + for (const auto& s : schema()["sections"]) + if (str(s, "name") == wanted) return &s; + return nullptr; +} + +const Json* keys_of(const Json* section) { + if (!section) return nullptr; + auto it = section->find("keys"); + return it != section->end() && it->is_array() ? &*it : nullptr; +} + +const Json* find_key(const Json* section, std::string_view key) { + if (const auto* keys = keys_of(section)) + for (const auto& k : *keys) + if (str(k, "key") == key) return &k; + return nullptr; +} + +std::string tr(std::string_view key, std::string_view locale) { return i18n::translate(key, locale); } + +int u16(const std::vector& lines, int line, int byte) { + if (line < 0 || line >= static_cast(lines.size())) return byte; + return text::utf16_column(lines[line], static_cast(std::max(0, byte))); +} + +// A cursor's column in bytes; past the line's end it stays past it. +int cursor_byte(const std::vector& lines, int line, int character) { + if (line < 0 || line >= static_cast(lines.size())) return character; + const auto& l = lines[line]; + const int length = text::utf16_length(l); + if (character > length) return static_cast(l.size()) + (character - length); + return static_cast(text::byte_offset(l, character)); } -std::vector split_lines(std::string_view text) { - std::vector out; - std::size_t pos = 0; - while (pos <= text.size()) { - auto end = text.find('\n', pos); - if (end == std::string_view::npos) end = text.size(); - auto line = text.substr(pos, end - pos); - if (!line.empty() && line.back() == '\r') line.remove_suffix(1); - out.push_back(line); - if (end == text.size()) break; - pos = end + 1; +const std::set> kDependencyGroups = { + "dependencies", "dev-dependencies", "build-dependencies", "workspace.dependencies", "feature-deps", +}; + +// ── completion tables ── + +struct HeaderSpec { + std::string_view group, label, header, detail; +}; + +// The wording of the tables' headers; the list of tables is the snapshot's. +constexpr HeaderSpec kHeaderSpecs[] = { + {"package", "[package]", "[package]", "Package metadata"}, + {"lib", "[lib]", "[lib]", "Library root-module convention"}, + {"build", "[build]", "[build]", "Build configuration"}, + {"generated_files", "[generated_files]", "[generated_files]", "Generated files (path → content)"}, + {"dependencies", "[dependencies]", "[dependencies]", "Runtime dependencies"}, + {"dev-dependencies", "[dev-dependencies]", "[dev-dependencies]", "Development/test dependencies"}, + {"build-dependencies", "[build-dependencies]", "[build-dependencies]", "Build-time dependencies (pulled at build time only, invisible at run time)"}, + {"workspace", "[workspace]", "[workspace]", "Workspace member declarations"}, + {"workspace.dependencies", "[workspace.dependencies]", "[workspace.dependencies]", "Declare dependency versions centrally; members inherit them with workspace = true"}, + {"features", "[features]", "[features]", "Feature definitions"}, + {"feature-deps", "[feature-deps.]", "[feature-deps.${1:name}]", "Optional dependencies pulled in by a feature"}, + {"capabilities", "[capabilities]", "[capabilities]", "Capability bindings (provider selection)"}, + {"targets", "[targets.]", "[targets.${1:name}]", "Build targets"}, + {"profile", "[profile.]", "[profile.${1:name}]", "Build profiles"}, + {"runtime", "[runtime]", "[runtime]", "Host runtime capabilities"}, + {"resources", "[resources]", "[resources]", "Metadata and assets compiled into the product (PE targets only)"}, + {"toolchain", "[toolchain]", "[toolchain]", "Compiler toolchain shorthand"}, + {"xlings", "[xlings]", "[xlings]", "Build environment (supplied by xlings)"}, + {"xlings.workspace", "[xlings.workspace]", "[xlings.workspace]", "Pin tool versions"}, + {"target", "[target.]", "[target.${1:x86_64-linux-gnu}]", "Configuration per target triple"}, + {"dependencies.", "[dependencies.]", "[dependencies.${1:mcpplibs}]", "Dependencies of one index namespace"}, + {"features.", "[features.]", "[features.${1:name}]", "A feature as a table"}, + {"target.cfg.build", "[target.'cfg(...)'.build]", "[target.'cfg(${1:os = \"linux\"})'.build]", "Build settings for the targets a selector matches"}, + {"target.cfg.dependencies", "[target.'cfg(...)'.dependencies]", "[target.'cfg(${1:os = \"linux\"})'.dependencies]", "Dependencies for the targets a selector matches"}, + {"pack", "[pack]", "[pack]", "mcpp pack packaging configuration"}, + {"pack.bundle-project", "[pack.bundle-project]", "[pack.bundle-project]", "Fine-tuning of the vendored filtering policy"}, + {"indices", "[indices]", "[indices]", "Project-level index redirection"}, + {"tools.overrides", "[tools.overrides]", "[tools.overrides]", "Host tool binary overrides"}, + {"language", "[language]", "[language]", "Legacy compatibility field; new projects should use [package].standard"}, +}; + +struct TemplateSpec { + std::string_view label, detail, documentation, insertSnippet; +}; + +constexpr TemplateSpec kDependencyTemplates[] = { + {"name = \"version\"", "SemVer version dependency", + "Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported.", + "${1:name} = \"${2:1.0.0}\""}, + {"name = { path = ... }", "Path dependency (local development)", "", "${1:name} = { path = \"${2:../mylib}\" }"}, + {"name = { git = ..., tag = ... }", "Git dependency (one of tag / branch / rev)", "", + "${1:name} = { git = \"${2:https://github.com/user/repo.git}\", tag = \"${3:v1.0.0}\" }"}, + {"name = { version = ..., features = [...] }", "Long dep spec: request a feature of that dependency", "", + "${1:name} = { version = \"${2:1.0}\", features = [\"${3:feature}\"] }"}, + {"name = { version = ..., tools = [...] }", "Host tools produced by the dependency (must be a bin target of that package)", "", + "${1:name} = { version = \"${2:1.0}\", tools = [\"${3:protoc}\"] }"}, +}; +constexpr TemplateSpec kFeatureTemplates[] = { + {"name = [...]", "Array shorthand: implies the feature only", "", "${1:name} = [${2}]"}, + {"name = { defines = [...] }", "Table form: contributes the package's own macros when activated", "", "${1:name} = { defines = [\"${2:MACRO}\"] }"}, + {"name = { requires = [...] }", "Table form: requires a capability", "", "${1:name} = { requires = [\"${2:blas}\"] }"}, + {"name = { sources = [...] }", "Table form: feature-gated source globs", "", "${1:name} = { sources = [\"${2:src/simd/**}\"] }"}, +}; +constexpr TemplateSpec kGeneratedFileTemplates[] = { + {"\"path\" = \"content\"", "Generated file (relative path → content, part of the fingerprint)", "", + "\"${1:src/gen/wrap.cppm}\" = \"\"\"\n${2:}\n\"\"\""}, +}; +constexpr TemplateSpec kCapabilityTemplates[] = { + {"capability = \"provider\"", "Capability binding (equivalent to --cap)", "", "${1:blas} = \"${2:compat.openblas}\""}, +}; +constexpr TemplateSpec kXlingsWorkspaceTemplates[] = { + {"tool = \"version\"", "Pin the xlings tool version", "", "${1:node} = \"${2:24.19.0}\""}, +}; +constexpr TemplateSpec kToolsOverridesTemplates[] = { + {"\"pkg:tool\" = \"path\"", "Override a host tool with an existing binary (skips the build)", "", + "\"${1:compat.protobuf:protoc}\" = \"${2:/usr/bin/protoc}\""}, +}; + +std::span templates_of(std::string_view group) { + if (group == "features") return kFeatureTemplates; + if (group == "generated_files") return kGeneratedFileTemplates; + if (group == "capabilities") return kCapabilityTemplates; + if (group == "xlings.workspace") return kXlingsWorkspaceTemplates; + if (group == "tools.overrides") return kToolsOverridesTemplates; + return {}; +} + +struct HeaderEntry { + std::string label, header, detail; + std::optional documentation; +}; + +// The tables to suggest: the snapshot's, worded by the table above where it has them, then the +// table's forms the snapshot does not list. +std::vector header_entries(std::string_view locale) { + std::vector entries; + const auto& sections = schema()["sections"]; + if (sections.empty()) { + for (const auto& s : kHeaderSpecs) entries.push_back({std::string(s.label), std::string(s.header), tr(s.detail, locale), {}}); + return entries; + } + for (const auto& section : sections) { + const auto header = str(section, "header"); + if (section.value("selector", false)) { + // Written only under a selector (`[target..abi]`): the selector to fill in. + const auto tail = header.substr(header.find("") + std::string_view("").size()); + entries.push_back({"[target.'cfg(...)'" + tail, "[target.'cfg(${1:os = \"linux\"})'" + tail, + "plane: " + str(section, "plane"), {}}); + continue; + } + const auto name = str(section, "name"); + const auto spec = std::ranges::find(kHeaderSpecs, name, &HeaderSpec::group); + HeaderEntry e; + if (spec != std::end(kHeaderSpecs)) { + e = {std::string(spec->label), std::string(spec->header), tr(spec->detail, locale), {}}; + } else { + e = {header, header, "plane: " + str(section, "plane"), {}}; + } + if (has_string(section, "deprecatedBy")) e.documentation = std::format("Deprecated; use `{}`.", str(section, "deprecatedBy")); + entries.push_back(std::move(e)); } + std::set suggested; + for (const auto& e : entries) suggested.insert(e.header); + for (const auto& s : kHeaderSpecs) + if (!suggested.contains(std::string(s.header))) + entries.push_back({std::string(s.label), std::string(s.header), tr(s.detail, locale), {}}); + return entries; +} + +std::vector header_suggestions(int start, int end, std::string_view locale) { + std::vector out; + for (auto& e : header_entries(locale)) + out.push_back({e.label, "section", e.detail, e.documentation, e.header, start, end}); return out; } -std::string_view trim(std::string_view s) { - while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.remove_prefix(1); - while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.remove_suffix(1); - return s; +std::vector template_suggestions(std::span templates, int start, int end, std::string_view locale) { + std::vector out; + for (const auto& t : templates) { + Suggestion s{std::string(t.label), "template", tr(t.detail, locale), std::nullopt, std::string(t.insertSnippet), start, end}; + if (!t.documentation.empty()) s.documentation = tr(t.documentation, locale); + out.push_back(std::move(s)); + } + return out; } -// The position of a `#` that starts a comment (outside a string), or npos. -std::size_t comment_at(std::string_view line) { - char quote = 0; - for (std::size_t i = 0; i < line.size(); ++i) { - const char c = line[i]; - if (quote) { if (c == '\\' && quote == '"') { ++i; continue; } if (c == quote) quote = 0; continue; } - if (c == '"' || c == '\'') quote = c; - else if (c == '#') return i; +std::string key_placeholder(const Json& key) { + const auto type = str(key, "type"); + if (type == "boolean") return "true"; + if (type == "number") return "0"; + if (type == "array") return "[]"; + if (type == "table") return "{ }"; + if (type == "enum") { + const auto it = key.find("values"); + return text::stringify(it != key.end() && it->is_array() && !it->empty() ? (*it)[0] : Json("")); } - return std::string_view::npos; + return "\"\""; } -// `[a.b]` / `[[a.b]]` -> "a.b"; nullopt for a line that is not a header. -std::optional header_of(std::string_view line) { - auto t = trim(line.substr(0, std::min(line.size(), comment_at(line)))); - if (t.size() < 2 || t.front() != '[') return std::nullopt; - const bool array = t.starts_with("[["); - t.remove_prefix(array ? 2 : 1); - const auto close = t.rfind(array ? "]]" : "]"); - if (close == std::string_view::npos) return std::nullopt; - return std::string(trim(t.substr(0, close))); +bool has_values(const Json& key) { + auto it = key.find("values"); + return it != key.end() && it->is_array() && !it->empty(); } -// The dotted segments of a table name, quotes respected. -std::vector segments(std::string_view table) { - std::vector out; - std::string cur; - char quote = 0; - for (char c : table) { - if (quote) { cur.push_back(c); if (c == quote) quote = 0; continue; } - if (c == '"' || c == '\'') { quote = c; cur.push_back(c); continue; } - if (c == '.') { out.push_back(std::string(trim(cur))); cur.clear(); continue; } - cur.push_back(c); +std::string key_detail(const Json& key) { + auto detail = str(key, "type"); + if (has_values(key)) { + std::string values; + for (const auto& v : key["values"]) values += (values.empty() ? "" : " | ") + v.get(); + detail += ": " + values; } - if (!trim(cur).empty() || !out.empty()) out.push_back(std::string(trim(cur))); + if (key.contains("default")) detail += std::format(" (default: {})", text::stringify(key["default"])); + return detail; +} + +std::optional key_documentation(const Json& key) { + std::vector lines; + if (has_string(key, "note")) lines.push_back(str(key, "note")); + if (key.contains("since") && key["since"].is_string()) lines.push_back(std::format("Since {}.", key["since"].get())); + if (key.value("legacy", false) == true) lines.push_back("Legacy key."); + if (lines.empty()) return std::nullopt; + std::string out; + for (const auto& l : lines) out += (out.empty() ? "" : " ") + l; return out; } -const Json* section_named(std::string_view name) { - for (const auto& s : schema()["sections"]) if (s.value("name", "") == name) return &s; - return nullptr; +std::vector segment_names(const std::vector& segments) { + std::vector out; + for (const auto& s : segments) out.push_back(s.name); + return out; } -const Json* key_in(const Json& section, std::string_view key) { - if (!section.contains("keys")) return nullptr; - for (const auto& k : section["keys"]) if (k.value("key", "") == key) return &k; - return nullptr; +// The top-level keys of `group` written before `line`, whichever of the group's tables holds them. +std::set keys_used_before(const std::vector& lines, int line, std::string_view group) { + std::set used; + std::optional current; + for (const auto& node : toml::parse(lines).nodes) { + if (const auto* s = std::get_if(&node)) { + const auto r = toml::resolve_section(segment_names(s->segments)); + current = r.kind == Resolution::Kind::known ? std::optional(r.group) : std::nullopt; + continue; + } + const auto& kv = std::get(node); + if (current != group || kv.range.startLine >= line) continue; + if (!kv.keyPath.empty()) used.insert(kv.keyPath[0].name); + } + return used; } -bool is_word(char c) { return std::isalnum(static_cast(c)) || c == '_' || c == '-' || c == '.'; } +std::string join(const std::vector& parts, std::string_view sep = ".") { + std::string out; + for (const auto& p : parts) out += (out.empty() ? "" : std::string(sep)) + p; + return out; +} -Json range_json(int line, int start, int end) { - return Json{{"start", {{"line", line}, {"character", start}}}, {"end", {{"line", line}, {"character", end}}}}; +// The enum key a value belongs to: the dotted path whole, else its first segment. +const Json* enum_key_of(const Json* section, const std::vector& keyPath) { + if (!keys_of(section) || keyPath.empty()) return nullptr; + const Json* key = find_key(section, join(keyPath)); + if (!key) key = find_key(section, keyPath[0]); + return key && str(*key, "type") == "enum" ? key : nullptr; } -std::string key_doc(const Json& section, const Json& key) { - std::string doc = std::format("**{}** `{}`", key.value("key", ""), key.value("type", "")); - if (key.contains("values") && key["values"].is_array()) { - std::string vals; - for (const auto& v : key["values"]) { if (!vals.empty()) vals += " | "; vals += "`" + v.get() + "`"; } - doc += "\n\n" + vals; +// The first character of a line that is not white space, or npos (`search(/\S/)`). +std::size_t first_non_space(std::string_view line) { + std::size_t i = 0; + while (i < line.size()) { + const auto n = text::space_at(line, i); + if (!n) return i; + i += n; } - if (key.contains("default") && !key["default"].is_null()) - doc += std::format("\n\ndefault: `{}`", key["default"].is_string() ? key["default"].get() : key["default"].dump()); - if (key.contains("note") && key["note"].is_string()) doc += "\n\n" + key["note"].get(); - if (section.contains("doc") && section["doc"].is_string()) doc += std::format("\n\n[{}]({})", section.value("header", ""), section["doc"].get()); - return doc; -} - -// The section the segments of a table name read as. The longest leading run -// that names a section of the schema is that section (`pack.bundle-project`, -// `runtime.requirements`, `platform.link.inputs`, `hooks.during_build`), else -// the first segment (`profile.release` is `profile`, `targets.app` is -// `targets`). Three tables read otherwise: -// - `workspace.
      ` is `
      `, the table it mirrors; -// - `target..
      ` is `
      `, and a platform's table there -// (carrier, layers, link, runners, tools) is the platform's; -// - a `requires_abi` table, of `[package]` or of a feature, is `requires_abi`. -// The layers a host triple takes (`platform.hosted.layers`) are `platform.layers`. -std::string section_of_segments(std::span seg) { - if (seg.empty()) return {}; - if (seg[0] == "workspace" && seg.size() >= 2) return section_of_segments(seg.subspan(1)); - if (seg[0] == "target" && seg.size() >= 3) { - static constexpr std::string_view kOverrides[] = {"carrier", "layers", "link", "runners", "tools"}; - if (std::ranges::find(kOverrides, seg[2]) == std::end(kOverrides)) return section_of_segments(seg.subspan(2)); - std::vector platform{"platform"}; - platform.insert(platform.end(), seg.begin() + 2, seg.end()); - return section_of_segments(platform); + return std::string_view::npos; +} + +} // namespace + +std::vector completions(const std::vector& lines, int line, int character, std::string_view locale) { + const auto context = toml::context_at(lines, line, character); + const int cl = std::clamp(line, 0, std::max(0, static_cast(lines.size()) - 1)); + auto out = [&](std::vector v) { + for (auto& s : v) { + s.start = u16(lines, cl, s.start); + s.end = u16(lines, cl, s.end); + } + return v; + }; + if (context.kind == Context::Kind::header) { + // `[[`: mcpp's tables are not arrays, and a suggestion would turn one into `[x]` unseen. + if (context.isArray) return {}; + // A suggestion is the whole `[x]`: from the line's `[` when it begins the line. + const std::string_view lineText = line >= 0 && line < static_cast(lines.size()) ? std::string_view(lines[line]) : std::string_view(); + const auto bracket = lineText.find('['); + const auto firstNonWs = first_non_space(lineText); + const int start = bracket != std::string_view::npos && bracket == firstNonWs ? static_cast(bracket) : context.replace.start; + return out(header_suggestions(start, context.replace.end, locale)); } - if (seg.size() >= 2 && seg.back() == "requires_abi") return "requires_abi"; - if (seg.size() >= 3 && seg[0] == "platform" && seg[1] == "hosted" && seg[2] == "layers") return "platform.layers"; - std::string name, longest; - for (const auto& s : seg) { - name += name.empty() ? s : "." + s; - if (section_named(name)) longest = name; + if (context.kind == Context::Kind::key) { + const auto& section = context.section; + if (section.kind == Resolution::Kind::top) return out(header_suggestions(context.replace.start, context.replace.end, locale)); + if (section.kind != Resolution::Kind::known || !context.containerPath.empty()) return {}; + if (kDependencyGroups.contains(section.group)) + return out(template_suggestions(kDependencyTemplates, context.replace.start, context.replace.end, locale)); + const auto* schemaSection = section_by_name(section.group); + // A table whose keys the user names has no fixed list. + if (schemaSection && !schemaSection->value("openKeys", false) && keys_of(schemaSection)) { + const auto used = keys_used_before(lines, line, section.group); + std::vector v; + for (const auto& key : *keys_of(schemaSection)) { + const auto name = str(key, "key"); + if (used.contains(name)) continue; + v.push_back({name, "template", key_detail(key), key_documentation(key), name + " = " + key_placeholder(key), + context.replace.start, context.replace.end}); + } + return out(std::move(v)); + } + return out(template_suggestions(templates_of(section.group), context.replace.start, context.replace.end, locale)); + } + // A value: only an enum key's values; a free value is not guessed. + if (context.section.kind == Resolution::Kind::known) { + const auto* key = enum_key_of(section_by_name(context.section.group), context.keyPath); + if (key) { + std::vector v; + for (const auto& value : (*key)["values"]) { + const auto s = value.get(); + // In a string only its content; a bare value gets its quotes. + v.push_back({s, "template", "value of " + str(*key, "key"), std::nullopt, + context.insideString ? s : text::stringify(value), context.replace.start, context.replace.end}); + } + return out(std::move(v)); + } } - return longest.empty() ? seg[0] : longest; + return {}; } -} // namespace +std::optional dependency_version_context(const std::vector& lines, int line, int character) { + const auto context = toml::context_at(lines, line, character); + if (context.kind != Context::Kind::value || context.section.kind != Resolution::Kind::known) return std::nullopt; + if (!kDependencyGroups.contains(context.section.group) || context.keyPath.empty()) return std::nullopt; + if (context.keyPath.size() > 1 && context.keyPath.back() != "version") return std::nullopt; + // A version is a string, or a value not typed yet. + if (context.valueKind && *context.valueKind != toml::ValueKind::string && *context.valueKind != toml::ValueKind::unknown) + return std::nullopt; + const int cl = std::clamp(line, 0, std::max(0, static_cast(lines.size()) - 1)); + return VersionContext{context.keyPath[0], context.insideString, u16(lines, cl, context.replace.start), + u16(lines, cl, context.replace.end)}; +} + +namespace { -std::string section_of(std::string_view table) { - return section_of_segments(segments(table)); -} - -Context context_at(std::string_view text, int line, int character) { - Context c; - const auto lines = split_lines(text); - if (line < 0 || line >= static_cast(lines.size())) return c; - // The table the cursor is under, and the keys it already holds. - int headerLine = -1; - for (int i = line; i >= 0; --i) - if (auto h = header_of(lines[i])) { c.table = *h; headerLine = i; break; } - if (headerLine >= 0 && headerLine == line) { - // On the header line itself: completing the header. - c.where = Context::Where::header; +// Numeric segments by value, a missing one first, others in collation order. +int compare_versions(std::string_view a, std::string_view b) { + auto parts = [](std::string_view s) { + std::vector out; + std::size_t at = 0; + for (std::size_t i = 0; i <= s.size(); ++i) + if (i == s.size() || s[i] == '.' || s[i] == '+' || s[i] == '-') { + out.push_back(s.substr(at, i - at)); + at = i + 1; + } + return out; + }; + const auto l = parts(a), r = parts(b); + auto numeric = [](std::string_view s) { + return !s.empty() && std::ranges::all_of(s, [](char c) { return c >= '0' && c <= '9'; }); + }; + for (std::size_t i = 0; i < std::max(l.size(), r.size()); ++i) { + if (i >= l.size()) return -1; + if (i >= r.size()) return 1; + if (numeric(l[i]) && numeric(r[i])) { + const auto x = std::stod(std::string(l[i])), y = std::stod(std::string(r[i])); + if (x != y) return x < y ? -1 : 1; + continue; + } + if (const int o = text::locale_compare(l[i], r[i]); o != 0) return o < 0 ? -1 : 1; } - for (int i = headerLine + 1; i < static_cast(lines.size()); ++i) { - if (i != headerLine && header_of(lines[i])) break; - auto l = lines[i]; - l = l.substr(0, std::min(l.size(), comment_at(l))); - if (auto eq = l.find('='); eq != std::string_view::npos) { - auto k = std::string(trim(l.substr(0, eq))); - if (!k.empty() && i != line) c.presentKeys.insert(k); + return 0; +} + +} // namespace + +std::vector catalogue_versions(const Json& searchEnvelope, std::string_view name) { + std::vector out; + std::set seen; + const auto& data = searchEnvelope.contains("data") ? searchEnvelope["data"] : searchEnvelope; + if (!data.contains("packages") || !data["packages"].is_array()) return out; + for (const auto& p : data["packages"]) { + if (str(p, "name") != name || !p.contains("versions") || !p["versions"].is_array()) continue; + const auto summary = std::string(text::trim(str(p, "description"))); + for (const auto& v : p["versions"]) { + if (!v.is_string() || !seen.insert(v.get()).second) continue; + out.push_back({v.get(), summary.empty() ? std::nullopt : std::optional(summary)}); } } - c.section = section_of(c.table); - const auto cur = lines[line]; - const int col = std::clamp(character, 0, static_cast(cur.size())); - if (comment_at(cur) <= static_cast(col) && comment_at(cur) != std::string_view::npos) { - c.where = Context::Where::comment; - return c; + std::ranges::stable_sort(out, [](const auto& x, const auto& y) { return compare_versions(y.version, x.version) < 0; }); + return out; +} + +std::vector version_suggestions(const VersionContext& at, const std::vector& versions) { + std::vector out; + for (const auto& v : versions) + out.push_back({v.version, "version", v.summary ? at.name + ": " + *v.summary : "version of " + at.name, std::nullopt, + at.insideString ? v.version : text::stringify(Json(v.version)), at.start, at.end}); + return out; +} + +// ── hover ── + +namespace { + +std::string unquote_or_self(std::string_view t) { + if (t.size() >= 2 && ((t.front() == '"' && t.back() == '"') || (t.front() == '\'' && t.back() == '\''))) + return std::string(t.substr(1, t.size() - 2)); + return std::string(t); +} + +// A header's segments to a section: the whole name, the reading of a table the rules layer knows, +// else the longest leading run that names one. +const Json* section_for_header(const std::vector& segments) { + if (segments.empty()) return nullptr; + if (const auto* exact = section_by_name(join(segments))) return exact; + const auto r = toml::resolve_section(segments); + // A table read as a group the snapshot lacks shows nothing rather than another table's keys. + if (r.kind == Resolution::Kind::known) return section_by_name(r.group); + for (auto length = segments.size() - 1; length >= 1; --length) + if (const auto* s = section_by_name(join(std::vector(segments.begin(), segments.begin() + static_cast(length))))) + return s; + return nullptr; +} + +std::string values_list(const Json& key) { + std::string out; + for (const auto& v : key["values"]) out += std::string(out.empty() ? "" : " | ") + "`" + v.get() + "`"; + return out; +} + +std::string paragraphs(const std::vector& lines) { return join(lines, "\n\n"); } + +HoverInfo section_hover(const Json& section) { + std::vector lines{std::format("plane: `{}`", str(section, "plane"))}; + if (has_string(section, "deprecatedBy")) lines.push_back(std::format("legacy: replaced by `{}`", str(section, "deprecatedBy"))); + const auto* keys = keys_of(§ion); + lines.push_back(!keys ? std::string("keys: none declared in the snapshot") + : std::format("keys: {}{}", keys->size(), section.value("openKeys", false) ? " (open: the user picks the names)" : "")); + HoverInfo info{str(section, "header"), paragraphs(lines), std::nullopt}; + if (section.contains("doc") && section["doc"].is_string()) info.documentation = section["doc"].get(); + return info; +} + +HoverInfo key_hover(const Json& key, const Json* section) { + std::vector lines{std::format("type: `{}`", str(key, "type"))}; + if (has_values(key)) lines.push_back("values: " + values_list(key)); + if (key.contains("default")) lines.push_back(std::format("default: `{}`", text::stringify(key["default"]))); + if (key.contains("since") && key["since"].is_string()) lines.push_back("since: " + key["since"].get()); + if (has_string(key, "note")) lines.push_back(str(key, "note")); + if (key.value("legacy", false) == true) lines.push_back("legacy: this key is deprecated"); + if (key.value("unmodelled", false) == true) lines.push_back("unmodelled: the type is inferred from the docs, not read from mcpp"); + HoverInfo info{str(key, "key"), paragraphs(lines), std::nullopt}; + if (section && section->contains("doc") && (*section)["doc"].is_string()) info.documentation = (*section)["doc"].get(); + return info; +} + +HoverInfo enum_value_hover(const Json& key, const std::string& value, const Json* section) { + std::vector lines{std::format("enum value of `{}`", str(key, "key"))}; + if (has_values(key)) lines.push_back("one of: " + values_list(key)); + if (key.contains("default")) lines.push_back(std::format("default: `{}`", text::stringify(key["default"]))); + if (has_string(key, "note")) lines.push_back(str(key, "note")); + HoverInfo info{value.empty() ? str(key, "key") : value, paragraphs(lines), std::nullopt}; + if (section && section->contains("doc") && (*section)["doc"].is_string()) info.documentation = (*section)["doc"].get(); + return info; +} + +// The text of a replaced range on a line, clamped. +std::string_view token_at(std::string_view lineText, toml::ReplaceRange r) { + const auto start = static_cast(std::clamp(r.start, 0, static_cast(lineText.size()))); + const auto end = static_cast(std::clamp(r.end, static_cast(start), static_cast(lineText.size()))); + return lineText.substr(start, end - start); +} + +} // namespace + +std::optional hover_at(const std::vector& lines, int line, int character) { + if (lines.empty()) return std::nullopt; + const auto context = toml::context_at(lines, line, character); + const std::string_view lineText = lines[std::clamp(line, 0, static_cast(lines.size()) - 1)]; + if (context.kind == Context::Kind::header) { + auto segments = context.segments; + // On a segment's token: the segments before it, and the token. + const auto token = text::trim(token_at(lineText, context.replace)); + if (!token.empty()) segments.push_back(unquote_or_self(token)); + const auto* section = section_for_header(segments); + if (!section) return std::nullopt; + return section_hover(*section); } - // The word the cursor is in. - int s = col, e = col; - while (s > 0 && is_word(cur[s - 1])) --s; - while (e < static_cast(cur.size()) && is_word(cur[e])) ++e; - c.replaceStart = s; - c.replaceEnd = e; - const auto before = trim(cur.substr(0, col)); - if (before.starts_with("[")) { - c.where = Context::Where::header; - c.replaceStart = static_cast(cur.find('[')); - c.replaceEnd = static_cast(cur.size()); - return c; + if (context.kind == Context::Kind::key) { + if (context.section.kind != Resolution::Kind::known || !context.containerPath.empty()) return std::nullopt; + const auto* section = section_by_name(context.section.group); + if (!keys_of(section)) return std::nullopt; + // A dotted key's root is the schema's key (`metadata.demo` is `metadata`). + const auto token = unquote_or_self(text::trim(token_at(lineText, context.replace))); + const auto name = !context.keyPrefix.empty() ? context.keyPrefix[0] : token; + const auto* key = find_key(section, name); + if (!key) return std::nullopt; + return key_hover(*key, section); } - if (c.where == Context::Where::header) return c; - if (auto eq = cur.substr(0, col).find('='); eq != std::string_view::npos) { - c.where = Context::Where::value; - c.key = std::string(trim(cur.substr(0, eq))); - // A quoted value is replaced inside its quotes. - auto open = cur.find_first_of("\"'", eq); - if (open != std::string_view::npos && static_cast(open) < col) { - auto close = cur.find(cur[open], open + 1); - c.replaceStart = static_cast(open + 1); - c.replaceEnd = close == std::string_view::npos ? static_cast(cur.size()) : static_cast(close); + if (context.section.kind != Resolution::Kind::known) return std::nullopt; + const auto* section = section_by_name(context.section.group); + if (!keys_of(section) || context.keyPath.empty()) return std::nullopt; + const Json* key = find_key(section, join(context.keyPath)); + if (!key) key = find_key(section, context.keyPath[0]); + if (!key || str(*key, "type") != "enum") return std::nullopt; + const auto value = std::string(text::trim(unquote_or_self(token_at(lineText, context.replace)))); + return enum_value_hover(*key, value, section); +} + +// ── navigation ── + +namespace { + +bool contains(const toml::Range& r, int line, int col) { + if (line < r.startLine || line > r.endLine) return false; + if (line == r.startLine && col < r.startCol) return false; + if (line == r.endLine && col > r.endCol) return false; + return true; +} + +struct Candidate { + const toml::KeyValue* node; + std::optional ownerKey; + std::vector sectionSegments; +}; + +void collect_nested(const std::optional& value, const std::string& owner, + const std::vector& sectionSegments, std::vector& out); + +void collect_nested_value(const toml::Value& value, const std::string& owner, const std::vector& sectionSegments, + std::vector& out) { + if (value.kind == toml::ValueKind::inlineTable) { + for (const auto& entry : value.entries) { + out.push_back({&entry, owner, sectionSegments}); + collect_nested(entry.value, owner, sectionSegments, out); } - return c; + return; } - c.where = Context::Where::key; - return c; -} - -Json complete(const Json& p) { - const auto text = p.value("text", ""); - const int line = p.value("line", 0), character = p.value("character", 0); - auto c = context_at(text, line, character); - Json items = Json::array(); - const auto range = range_json(line, c.replaceStart, c.replaceEnd); - switch (c.where) { - case Context::Where::header: - for (const auto& s : schema()["sections"]) { - if (!s.value("deprecatedBy", Json(nullptr)).is_null()) continue; - const auto header = s.value("header", ""); - const auto doc = s.contains("doc") && s["doc"].is_string() ? s["doc"] : Json(""); - // A table written only under a selector (`[target..abi]`) is - // offered with the selector to fill in. - if (s.value("selector", false)) { - const auto tail = header.substr(header.find("") + std::string_view("").size()); - items.push_back(Json{{"label", "[target.'cfg(...)'" + tail}, {"kind", "template"}, - {"detail", s.value("plane", "")}, {"documentation", doc}, - {"insertText", "[target.'cfg(${1:os = \"linux\"})'" + tail}, - {"snippet", true}, {"range", range}}); + if (value.kind == toml::ValueKind::array) + for (const auto& element : value.elements) collect_nested_value(element, owner, sectionSegments, out); +} + +void collect_nested(const std::optional& value, const std::string& owner, + const std::vector& sectionSegments, std::vector& out) { + if (value) collect_nested_value(*value, owner, sectionSegments, out); +} + +std::optional group_of(const std::vector& segments) { + if (segments.empty()) return std::nullopt; + const auto r = toml::resolve_section(segments); + return r.kind == Resolution::Kind::known ? std::optional(r.group) : std::nullopt; +} + +std::string dependency_name(const Candidate& c) { + if (c.ownerKey && !c.ownerKey->empty()) return *c.ownerKey; + if (c.sectionSegments.size() >= 2) return join(std::vector(c.sectionSegments.begin() + 1, c.sectionSegments.end())); + return {}; +} + +bool relative_path(std::string_view value) { + const auto t = text::trim(value); + if (t.empty() || t.starts_with("/") || t.starts_with("\\") || t.starts_with("~")) return false; + if (t.size() >= 3 && std::isalpha(static_cast(t[0])) && t[1] == ':' && (t[2] == '\\' || t[2] == '/')) return false; + // `://` + if (!t.empty() && std::isalpha(static_cast(t[0]))) { + std::size_t i = 1; + while (i < t.size() && (std::isalnum(static_cast(t[i])) || t[i] == '+' || t[i] == '.' || t[i] == '-')) ++i; + if (t.substr(i).starts_with("://")) return false; + } + return true; +} + +} // namespace + +std::optional definition_at(const std::vector& lines, int line, int character, + const std::function(std::string_view)>& resolve_manifest) { + const int cursorLine = std::max(0, line); + const int cursorCol = cursor_byte(lines, cursorLine, std::max(0, character)); + const auto doc = toml::parse(lines); + std::vector candidates; + { + std::vector sectionSegments; + for (const auto& node : doc.nodes) { + if (const auto* s = std::get_if(&node)) { + sectionSegments = segment_names(s->segments); + continue; + } + const auto& kv = std::get(node); + candidates.push_back({&kv, std::nullopt, sectionSegments}); + collect_nested(kv.value, join(segment_names(kv.keyPath)), sectionSegments, candidates); + } + } + auto location_of_key = [&](const toml::KeyValue& kv) -> std::optional { + if (kv.keyPath.empty()) return std::nullopt; + const auto& first = kv.keyPath.front().range; + const auto& last = kv.keyPath.back().range; + return Location{first.startLine, u16(lines, first.startLine, first.startCol), u16(lines, first.startLine, last.endCol)}; + }; + for (const auto& c : candidates) { + const auto& node = *c.node; + const bool onKey = std::ranges::any_of(node.keyPath, [&](const auto& s) { return contains(s.range, cursorLine, cursorCol); }); + const bool onValue = node.value && contains(node.value->range, cursorLine, cursorCol); + if (!onKey && !onValue) continue; + const auto keyName = node.keyPath.empty() ? std::string() : node.keyPath.back().name; + const auto group = group_of(c.sectionSegments); + const bool inDependencies = group && kDependencyGroups.contains(*group); + if (keyName == "workspace") { + const bool isTrue = node.value && node.value->kind == toml::ValueKind::boolean && node.value->text == "true"; + if (!isTrue || !inDependencies) continue; + const auto dependency = dependency_name(c); + if (dependency.empty()) continue; + bool inWorkspace = false; + for (const auto& n : doc.nodes) { + if (const auto* s = std::get_if(&n)) { + inWorkspace = group_of(segment_names(s->segments)) == std::optional("workspace.dependencies"); continue; } - items.push_back(Json{{"label", header}, {"kind", "section"}, {"detail", s.value("plane", "")}, - {"insertText", header}, {"range", range}, {"documentation", doc}}); + const auto& kv = std::get(n); + if (inWorkspace && join(segment_names(kv.keyPath)) == dependency) { + if (auto at = location_of_key(kv)) return at; + break; + } + } + continue; + } + if (keyName == "path") { + if (!inDependencies || !node.value || node.value->kind != toml::ValueKind::string) continue; + const auto relative = node.value->text.value_or(""); + if (!relative_path(relative)) continue; + std::optional resolved; + try { + resolved = resolve_manifest(relative); + } catch (...) { + resolved = std::nullopt; + } + if (!resolved || *resolved < 0) continue; + // Only the line is known (the other file is not read here): its `[package]`. + return Location{*resolved, 0, static_cast(std::string_view("[package]").size())}; + } + if (keyName == "features") { + if (!node.value || node.value->kind != toml::ValueKind::array) continue; + const auto element = std::ranges::find_if(node.value->elements, [&](const auto& e) { return contains(e.range, cursorLine, cursorCol); }); + if (element == node.value->elements.end() || element->kind != toml::ValueKind::string) continue; + const auto name = std::string(text::trim(element->text.value_or(""))); + if (name.empty()) continue; + for (const auto& n : doc.nodes) { + const auto* s = std::get_if(&n); + if (!s || s->isArray || s->segments.size() != 2) continue; + if (s->segments[0].name == "features" && s->segments[1].name == name) + return Location{s->line, u16(lines, s->line, s->range.startCol), u16(lines, s->line, s->range.endCol)}; + } + continue; + } + } + return std::nullopt; +} + +// ── diagnostics ── + +namespace { + +enum class Mode { normal, mlDouble, mlSingle }; +struct ParseState { + Mode mode = Mode::normal; + int depth = 0; +}; + +bool escaped(std::string_view t, std::size_t index) { + std::size_t n = 0; + for (std::size_t i = index; i > 0 && t[i - 1] == '\\'; --i) ++n; + return n % 2 == 1; +} + +// One line, the state across lines updated; the `#` that starts a comment, or npos. +std::size_t scan(std::string_view t, ParseState& state) { + std::size_t i = 0; + while (i < t.size()) { + if (state.mode == Mode::mlDouble) { + if (t.substr(i).starts_with("\"\"\"") && !escaped(t, i)) { + state.mode = Mode::normal; + i += 3; + continue; + } + ++i; + continue; + } + if (state.mode == Mode::mlSingle) { + if (t.substr(i).starts_with("'''")) { + state.mode = Mode::normal; + i += 3; + continue; + } + ++i; + continue; + } + const char ch = t[i]; + if (ch == '#') return i; + if (ch == '"') { + if (t.substr(i).starts_with("\"\"\"")) { + state.mode = Mode::mlDouble; + i += 3; + continue; + } + ++i; + while (i < t.size()) { + if (t[i] == '\\') { i += 2; continue; } + if (t[i] == '"') { ++i; break; } + ++i; + } + continue; + } + if (ch == '\'') { + if (t.substr(i).starts_with("'''")) { + state.mode = Mode::mlSingle; + i += 3; + continue; + } + ++i; + while (i < t.size()) { + if (t[i] == '\'') { ++i; break; } + ++i; } - // The forms that name something: a dependency namespace, a target, a selector. - for (auto [label, snippet, detail] : std::initializer_list>{ - {"[dependencies.]", "[dependencies.${1:mcpplibs}]", "dependencies of one index namespace"}, - {"[targets.]", "[targets.${1:name}]", "an artifact"}, - {"[target.'cfg(...)'.build]", "[target.'cfg(${1:os = \"linux\"})'.build]", "build settings for some targets"}, - {"[target.'cfg(...)'.dependencies]", "[target.'cfg(${1:os = \"linux\"})'.dependencies]", "dependencies for some targets"}, - {"[features.]", "[features.${1:name}]", "a feature"}, - {"[profile.]", "[profile.${1:release}]", "a build profile"}, - }) - items.push_back(Json{{"label", label}, {"kind", "template"}, {"detail", detail}, - {"insertText", snippet}, {"snippet", true}, {"range", range}}); - break; - case Context::Where::key: { - const auto* section = section_named(c.section); - if (!section || section->value("openKeys", false)) break; - if (section->contains("keys")) - for (const auto& k : (*section)["keys"]) { - const auto key = k.value("key", ""); - if (c.presentKeys.contains(key)) continue; - std::string insert = key + " = "; - const auto type = k.value("type", ""); - if (type == "string" || type == "enum") insert += "\"$1\""; - else if (type == "array") insert += "[$1]"; - else if (type == "boolean") insert += "${1|true,false|}"; - else if (type == "table") insert += "{ $1 }"; - else insert += "$1"; - items.push_back(Json{{"label", key}, {"kind", "key"}, {"detail", type}, - {"documentation", key_doc(*section, k)}, {"insertText", insert}, - {"snippet", true}, {"range", range}, - {"deprecated", k.contains("deprecatedBy") && !k["deprecatedBy"].is_null()}}); + continue; + } + if (ch == '[' || ch == '{') { state.depth += 1; ++i; continue; } + if (ch == ']' || ch == '}') { + if (state.depth > 0) state.depth -= 1; + ++i; + continue; + } + ++i; + } + return std::string_view::npos; +} + +std::string_view code_before_comment(std::string_view t) { + ParseState s; + const auto stop = scan(t, s); + return stop == std::string_view::npos ? t : t.substr(0, stop); +} + +// `needle` outside quotes, or npos. +std::size_t index_of_unquoted(std::string_view t, std::string_view needle) { + std::size_t i = 0; + while (i < t.size()) { + const char ch = t[i]; + if (ch == '"' || ch == '\'') { + const std::string delim(3, ch); + if (!t.substr(i).starts_with(delim)) { + ++i; + while (i < t.size()) { + if (ch == '"' && t[i] == '\\') { i += 2; continue; } + if (t[i] == ch) { ++i; break; } + ++i; } - break; + continue; + } + i += 3; + while (i < t.size()) { + if (t.substr(i).starts_with(delim) && !escaped(t, i)) { i += 3; break; } + ++i; + } + continue; + } + if (t.substr(i).starts_with(needle)) return i; + ++i; + } + return std::string_view::npos; +} + +// A table name or dotted key split at its dots, quotes kept and respected. +std::vector split_dots(std::string_view t) { + std::vector parts; + std::string current; + char quote = 0; + for (const char ch : t) { + if (quote) { + current += ch; + if (ch == quote) quote = 0; + continue; } - case Context::Where::value: { - const auto* section = section_named(c.section); - const Json* key = section ? key_in(*section, c.key) : nullptr; - if (!key) break; - if (key->contains("values") && (*key)["values"].is_array()) - for (const auto& v : (*key)["values"]) - items.push_back(Json{{"label", v}, {"kind", "value"}, {"detail", c.key}, {"insertText", v}, {"range", range}}); - else if (key->value("type", "") == "boolean") - for (const char* v : {"true", "false"}) - items.push_back(Json{{"label", v}, {"kind", "value"}, {"detail", c.key}, {"insertText", v}, {"range", range}}); - break; + if (ch == '"' || ch == '\'') { + quote = ch; + current += ch; + continue; } - default: - break; + if (ch == '.') { + parts.push_back(std::move(current)); + current.clear(); + continue; + } + current += ch; + } + parts.push_back(std::move(current)); + return parts; +} + +bool bare_key(std::string_view t) { + return !t.empty() && std::ranges::all_of(t, [](char c) { return std::isalnum(static_cast(c)) || c == '_' || c == '-'; }); +} +bool quoted_key(std::string_view t) { + return t.size() >= 2 && ((t.front() == '"' && t.back() == '"') || (t.front() == '\'' && t.back() == '\'')); +} +bool valid_key(std::string_view t) { + const auto parts = split_dots(t); + return !parts.empty() && std::ranges::all_of(parts, [](const auto& p) { return bare_key(p) || quoted_key(p); }); +} +std::optional unquote(std::string_view t) { + if (quoted_key(t)) return std::string(t.substr(1, t.size() - 2)); + return std::nullopt; +} + +const Json* lookup_section(std::string_view name) { + if (const auto* exact = section_by_name(name)) return exact; + const auto parts = split_dots(name); + for (auto i = parts.size() - 1; i >= 1; --i) + if (const auto* s = section_by_name(join(std::vector(parts.begin(), parts.begin() + static_cast(i))))) return s; + return nullptr; +} + +// The section whose keys a table takes: its own, or a row table's base (`[targets.]`, +// `[profile.]`, `[target.]`); deeper tables are another vocabulary, not checked. +const Json* key_section_of(std::string_view headerName) { + if (const auto* exact = section_by_name(headerName)) return exact; + const auto parts = split_dots(headerName); + if (parts.size() == 2 && (parts[0] == "targets" || parts[0] == "profile" || parts[0] == "target")) return section_by_name(parts[0]); + return nullptr; +} + +const Json* key_info_of(const Json* section, std::string_view key) { + if (!keys_of(section)) return nullptr; + if (const auto* direct = find_key(section, key)) return direct; + const auto parts = split_dots(key); + if (parts.size() > 1) return find_key(section, parts[0]); + return nullptr; +} + +bool dependency_section(std::string_view name) { + const auto parts = split_dots(name); + const auto& last = parts.back(); + return last == "dependencies" || last == "dev-dependencies" || last == "build-dependencies"; +} + +bool xlings_section(std::string_view name) { return std::ranges::contains(split_dots(name), std::string("xlings")); } + +bool tool_payload_name(std::string_view key) { + auto bare = unquote(key).value_or(std::string(key)); + std::ranges::transform(bare, bare.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return bare.starts_with("xim:") || bare.starts_with("xpm:") || bare.starts_with("xlings:") || bare.starts_with("xvm:"); +} + +// `namespace.name`: /^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)+$/ +bool mcpp_package_name(std::string_view key) { + const auto bare = unquote(key).value_or(std::string(key)); + if (tool_payload_name(bare)) return false; + if (bare.empty() || !(std::isalpha(static_cast(bare[0])) || bare[0] == '_')) return false; + auto word = [](char c) { return std::isalnum(static_cast(c)) || c == '_' || c == '-'; }; + std::size_t i = 1; + while (i < bare.size() && word(bare[i])) ++i; + int dots = 0; + while (i < bare.size()) { + if (bare[i] != '.') return false; + ++i; + const auto start = i; + while (i < bare.size() && word(bare[i])) ++i; + if (i == start) return false; + ++dots; + } + return dots >= 1; +} + +// /(^|[,{\s])\s*=/ +bool names_field(std::string_view t, std::string_view word) { + for (std::size_t at = t.find(word); at != std::string_view::npos; at = t.find(word, at + 1)) { + const bool boundary = at == 0 || t[at - 1] == ',' || t[at - 1] == '{' || text::space_at(t, at - 1) == 1 + || (at >= 2 && text::space_at(t, at - 2) == 2) || (at >= 3 && text::space_at(t, at - 3) == 3); + if (!boundary) continue; + std::size_t i = at + word.size(); + while (auto n = text::space_at(t, i)) i += n; + if (i < t.size() && t[i] == '=') return true; + } + return false; +} + +// A version, not a path: `"1.0"`, `{ version = "1.0" }`; not `{ path = ... }`, `""`, `{ linux = "" }`. +bool declares_version(std::string_view valueText) { + const auto t = text::trim(valueText); + if (t.starts_with("{")) return names_field(t, "version") && !names_field(t, "path"); + const auto bare = unquote(t); + if (!bare || bare->empty()) return false; + return bare->find('/') == std::string::npos && !bare->starts_with("."); +} + +// The arrays of tables mcpp reads (toml.cppm's kAllowedArraysOfTables, and the snapshot's own). +bool allowed_array_table(std::string_view name) { + static const std::vector> allowed = [] { + std::vector> out = { + {"build", "flags"}, {"build", "sources"}, {"features", "*", "flags"}, {"runtime", "requirements"}, + {"runtime", "artifacts"}, {"runtime", "deploy"}, {"target", "*", "build", "flags"}, + {"target", "*", "build", "sources"}, {"xlings", "deps"}, + }; + for (const auto& s : schema()["sections"]) + if (str(s, "header").starts_with("[[")) out.push_back(split_dots(str(s, "name"))); + return out; + }(); + const auto parts = split_dots(name); + return std::ranges::any_of(allowed, [&](const auto& pattern) { + if (pattern.size() != parts.size()) return false; + for (std::size_t i = 0; i < parts.size(); ++i) + if (pattern[i] != "*" && pattern[i] != parts[i]) return false; + return true; + }); +} + +// `">="`: /^>=\d+(\.\d+)+$/ +bool mcpp_floor(std::string_view valueText) { + const auto bare = unquote(text::trim(valueText)); + if (!bare || !bare->starts_with(">=")) return false; + const std::string_view v = std::string_view(*bare).substr(2); + int groups = 0; + std::size_t i = 0; + for (;;) { + const auto start = i; + while (i < v.size() && std::isdigit(static_cast(v[i]))) ++i; + if (i == start) return false; + ++groups; + if (i == v.size()) break; + if (v[i] != '.') return false; + ++i; } - return Json{{"items", items}, {"table", c.table}, {"section", c.section}}; -} - -Json hover(const Json& p) { - const auto text = p.value("text", ""); - const int line = p.value("line", 0), character = p.value("character", 0); - const auto lines = split_lines(text); - if (line < 0 || line >= static_cast(lines.size())) return nullptr; - const auto cur = lines[line]; - if (auto h = header_of(cur)) { - const auto* section = section_named(section_of(*h)); - if (!section) return nullptr; - std::string doc = std::format("**{}** · {}", section->value("header", ""), section->value("plane", "")); - if (section->contains("doc") && (*section)["doc"].is_string()) - doc += std::format("\n\n[{}]({})", section->value("header", ""), (*section)["doc"].get()); - if (!section->value("deprecatedBy", Json(nullptr)).is_null()) - doc += std::format("\n\nReplaced by `{}`.", (*section)["deprecatedBy"].get()); - return Json{{"contents", doc}, {"range", range_json(line, 0, static_cast(cur.size()))}}; + return groups >= 2; +} + +} // namespace + +std::vector analyse(const std::vector& lines, const DiagnosticSettings& settings) { + std::vector found; + auto push = [&](std::string_view rule, const std::string& level, std::string message, int line, int start, int end, + std::optional relatedKey = std::nullopt) { + if (level == "off") return; + found.push_back({std::format("mcpp.manifest.{}", rule), std::move(message), level, line, start, end, std::move(relatedKey)}); + }; + ParseState state; + std::optional headerName; + const Json* section = nullptr; + for (std::size_t index = 0; index < lines.size(); ++index) { + std::string_view raw = lines[index]; + if (!raw.empty() && raw.back() == '\r') raw.remove_suffix(1); + const int lineNumber = static_cast(index) + 1; + // A continuation of a multi-line string or an array: part of the statement above. + if (state.mode != Mode::normal || state.depth > 0) { + scan(raw, state); + continue; + } + const auto code = code_before_comment(raw); + const auto t = text::trim(code); + if (t.empty()) { + scan(raw, state); + continue; + } + const auto lead = code.size() - text::trim_start(code).size(); + const int lineStart = text::utf16_column(raw, lead) + 1; + const int lineEnd = text::utf16_column(raw, lead + t.size()) + 1; + const std::string tstr(t); + + if (t.starts_with("[")) { + const bool arrayHeader = t.size() >= 5 && t.starts_with("[[") && t.ends_with("]]"); + const bool plainHeader = t.size() >= 3 && t.ends_with("]"); + if (!arrayHeader && !plainHeader) { + push("syntax", settings.syntax, "not a [section] header: " + tstr, lineNumber, lineStart, lineEnd); + scan(raw, state); + continue; + } + const auto name = std::string(text::trim(arrayHeader ? t.substr(2, t.size() - 4) : t.substr(1, t.size() - 2))); + headerName = name; + section = key_section_of(name); + if (arrayHeader) { + if (!allowed_array_table(name)) + push("arrayTable", settings.arrayTable, std::format("[[{}]] is an array of tables, which mcpp does not use here", name), + lineNumber, lineStart, lineEnd, name); + } else if (!lookup_section(name)) { + push("unknownSection", settings.unknownSection, std::format("unknown section [{}]", name), lineNumber, lineStart, lineEnd, name); + } else if (section && has_string(*section, "deprecatedBy")) { + const auto by = str(*section, "deprecatedBy"); + push("legacyKey", settings.legacyKeys, std::format("[{}] is deprecated; use {}", name, by), lineNumber, lineStart, lineEnd, by); + } + scan(raw, state); + continue; + } + + const auto equals = index_of_unquoted(t, "="); + if (equals == std::string_view::npos || equals == 0) { + push("syntax", settings.syntax, "not a [section] header or key = value: " + tstr, lineNumber, lineStart, lineEnd); + scan(raw, state); + continue; + } + const auto key = std::string(text::trim(t.substr(0, equals))); + const auto value = std::string(text::trim(t.substr(equals + 1))); + const int keyStart = lineStart; + const int keyEnd = keyStart + text::utf16_length(key); + if (!valid_key(key)) { + push("syntax", settings.syntax, "invalid key: " + key, lineNumber, keyStart, keyEnd); + scan(raw, state); + continue; + } + if (value.empty()) { + push("syntax", settings.syntax, std::format("key {} has no value", key), lineNumber, keyStart, keyEnd, key); + scan(raw, state); + continue; + } + const auto* info = key_info_of(section, key); + if (keys_of(section) && !section->value("openKeys", false) && !info) + push("unknownKey", settings.unknownKey, std::format("unknown key {} in {}", key, str(*section, "header")), lineNumber, + keyStart, keyEnd, key); + if (info && info->value("legacy", false) == true && !(section && has_string(*section, "deprecatedBy"))) { + const auto replacement = info->contains("note") && (*info)["note"].is_string() ? (*info)["note"].get() + : std::string("no replacement documented"); + push("legacyKey", settings.legacyKeys, std::format("key {} is deprecated; {}", key, replacement), lineNumber, keyStart, keyEnd, key); + } + if (section && str(*section, "name") == "package" && split_dots(key)[0] == "mcpp" && !mcpp_floor(value)) + push("mcppFloor", settings.mcppFloor, "[package] mcpp must be a release floor like \">=2026.9.28.3\"", lineNumber, keyStart, + keyEnd, "mcpp"); + const auto bareKey = unquote(key).value_or(key); + if (dependency_section(headerName.value_or("")) && tool_payload_name(bareKey)) { + push("planeSeparation", settings.planeSeparation, + std::format("{} is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]", bareKey), + lineNumber, keyStart, keyEnd, bareKey); + } else if (xlings_section(headerName.value_or("")) && mcpp_package_name(bareKey) && declares_version(value)) { + push("planeSeparation", settings.planeSeparation, + std::format("{} is an mcpp package, not a tool payload; declare it in [dependencies]", bareKey), lineNumber, keyStart, + keyEnd, bareKey); + } + scan(raw, state); } - auto c = context_at(text, line, character); - auto eq = cur.find('='); - if (eq == std::string_view::npos || character > static_cast(eq)) return nullptr; - const auto key = std::string(trim(cur.substr(0, eq))); - const auto* section = section_named(c.section); - const Json* k = section ? key_in(*section, key) : nullptr; - if (!k) return nullptr; - const auto start = static_cast(cur.find(key)); - return Json{{"contents", key_doc(*section, *k)}, {"range", range_json(line, start, start + static_cast(key.size()))}}; -} - -Json symbols(const Json& p) { - // The views name the text, so the text outlives them. - const std::string text = p.value("text", std::string{}); - const auto lines = split_lines(text); + std::ranges::stable_sort(found, [](const Diagnostic& a, const Diagnostic& b) { + if (a.line != b.line) return a.line < b.line; + if (a.start != b.start) return a.start < b.start; + if (const int c = text::locale_compare(a.code, b.code); c != 0) return c < 0; + return text::locale_compare(a.message, b.message) < 0; + }); + return found; +} + +// ── outline ── + +Json document_symbols(const std::vector& lines) { Json out = Json::array(); Json* current = nullptr; - for (int i = 0; i < static_cast(lines.size()); ++i) { - if (auto h = header_of(lines[i])) { - out.push_back(Json{{"name", *h}, {"kind", "table"}, {"line", i}, {"children", Json::array()}}); + auto range = [&](const toml::Range& r) { + return Json{{"start", {{"line", r.startLine}, {"character", u16(lines, r.startLine, r.startCol)}}}, + {"end", {{"line", r.endLine}, {"character", u16(lines, r.endLine, r.endCol)}}}}; + }; + for (const auto& node : toml::parse(lines).nodes) { + if (const auto* s = std::get_if(&node)) { + if (s->segments.empty()) continue; + const auto name = join(segment_names(s->segments)); + // LSP SymbolKind: Namespace 3, Property 7. + out.push_back(Json{{"name", s->isArray ? "[[" + name + "]]" : "[" + name + "]"}, {"kind", 3}, + {"range", range(s->range)}, {"selectionRange", range(s->range)}, {"children", Json::array()}}); current = &out.back(); continue; } - auto l = lines[i].substr(0, std::min(lines[i].size(), comment_at(lines[i]))); - auto eq = l.find('='); - if (eq == std::string_view::npos) continue; - auto key = std::string(trim(l.substr(0, eq))); - if (key.empty() || key.find(' ') != std::string::npos) continue; - Json sym{{"name", key}, {"kind", "key"}, {"line", i}}; - if (current) (*current)["children"].push_back(sym); - else out.push_back(sym); + const auto& kv = std::get(node); + if (kv.keyPath.empty()) continue; + Json sym{{"name", join(segment_names(kv.keyPath))}, {"kind", 7}, {"range", range(kv.range)}, + {"selectionRange", range(toml::Range{kv.keyPath.front().range.startLine, kv.keyPath.front().range.startCol, + kv.keyPath.back().range.endLine, kv.keyPath.back().range.endCol})}}; + if (current) (*current)["children"].push_back(std::move(sym)); + else out.push_back(std::move(sym)); } return out; } -Json buildscript_complete(const Json& p) { - // The views name the text, so the text outlives them. - const std::string text = p.value("text", std::string{}); - const auto lines = split_lines(text); - const int line = p.value("line", 0), character = p.value("character", 0); - if (line < 0 || line >= static_cast(lines.size())) return Json{{"items", Json::array()}}; - const auto cur = lines[line]; - const int col = std::clamp(character, 0, static_cast(cur.size())); - const auto before = cur.substr(0, col); - const auto scope = before.rfind("mcpp::"); - if (scope == std::string_view::npos) return Json{{"items", Json::array()}}; - const auto typed = before.substr(scope + 6); - if (std::ranges::any_of(typed, [](char ch) { return !(std::isalnum(static_cast(ch)) || ch == '_'); })) - return Json{{"items", Json::array()}}; - Json items = Json::array(); - const auto range = range_json(line, static_cast(scope + 6), col); - std::set offered; - for (const auto& d : api()["directives"]) { - // The typed API's functions that emit the directive (`include-dir` is - // `mcpp::include_dir`, `cfg` is `mcpp::define`); `action` is the - // snippet below. - if (!d.contains("functions") || !d["functions"].is_array()) continue; - std::string doc = std::format("`mcpp:{}=` · slot `{}` · scope `{}` · since protocol {}", d.value("wire", ""), - d.value("slot", ""), d.value("scope", ""), d.value("since", 0)); - if (d.contains("docsUrl") && d["docsUrl"].is_string()) doc += std::format("\n\n[docs]({})", d["docsUrl"].get()); - for (const auto& f : d["functions"]) { - const auto name = f.get(); - if (!name.starts_with(typed) || !offered.insert(name).second) continue; - items.push_back(Json{{"label", name}, {"kind", "function"}, {"detail", std::format("mcpp:{}=", d.value("wire", ""))}, - {"documentation", doc}, {"insertText", name + "(\"$1\")"}, {"snippet", true}, {"range", range}}); - } - } - if (std::string_view("action").starts_with(typed)) - items.push_back(Json{{"label", "action"}, {"kind", "snippet"}, {"detail", "mcpp::action with id, role, inputs, outputs"}, - {"insertText", "action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,prepare,object,artifact|};\n${1:handle}.submit();"}, - {"snippet", true}, {"range", range}}); - return Json{{"items", items}}; +Json schema_source() { + return Json{{"version", str(schema(), "sourceVersion")}, {"commit", str(schema(), "sourceCommit")}}; } } // namespace mcppide::manifest diff --git a/src/server.cppm b/src/server.cppm index 7bcd26c..45de264 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -1603,10 +1603,6 @@ void Session::install() { return job_start(job); }); s.on("manifest/check", [this](const Json& p, const rpc::Context&) { return manifest_check(p); }); - s.on("toml/complete", [](const Json& p, const rpc::Context&) -> Result { return manifest::complete(p); }); - s.on("toml/hover", [](const Json& p, const rpc::Context&) -> Result { return manifest::hover(p); }); - s.on("toml/symbols", [](const Json& p, const rpc::Context&) -> Result { return manifest::symbols(p); }); - s.on("buildscript/complete", [](const Json& p, const rpc::Context&) -> Result { return manifest::buildscript_complete(p); }); } } // namespace mcppide::server diff --git a/src/text.cppm b/src/text.cppm new file mode 100644 index 0000000..51dd39d --- /dev/null +++ b/src/text.cppm @@ -0,0 +1,156 @@ +// Text as the editors hold it. A document is lines (split at `\n`, a `\r` before it dropped), and a +// position on a line is a count of UTF-16 code units, as LSP and every editor count it; the rules +// layer reads the UTF-8 bytes and turns a byte offset into that count where it answers. The +// whitespace JavaScript trims, JSON as JSON.stringify writes it, and the order localeCompare gives +// are here too: the rules layer answers what mcpp-vscode 0.9's TypeScript answered (design +// 2026-10-30 part 2 §8). +export module mcppide.text; + +import std; +import nlohmann.json; + +export namespace mcppide::text { + +// `text.split(/\r?\n/)`. +std::vector lines_of(std::string_view text); + +// The UTF-16 code units of `line`'s first `byte` bytes. +int utf16_column(std::string_view line, std::size_t byte); +// The UTF-16 length of `s`. +int utf16_length(std::string_view s); +// The byte offset of UTF-16 column `column`, clamped to the line; a column inside a character is +// the character's start. +std::size_t byte_offset(std::string_view line, int column); + +// The length of the JavaScript white space or line terminator at `s[i]`, or 0. +std::size_t space_at(std::string_view s, std::size_t i); +// String.prototype.trim, trimStart, trimEnd. +std::string_view trim(std::string_view s); +std::string_view trim_start(std::string_view s); +std::string_view trim_end(std::string_view s); + +// JSON.stringify of a value read from JSON (no spaces; a string's characters as they are). +std::string stringify(const nlohmann::json& value); + +// String.prototype.localeCompare as the root collation orders ASCII words: letters before their +// case (a < B < c), a lower-case letter before its capital, digits before letters; a negative, +// zero or positive result. +int locale_compare(std::string_view a, std::string_view b); + +} // namespace mcppide::text + +namespace mcppide::text { + +std::vector lines_of(std::string_view text) { + std::vector out; + std::size_t at = 0; + for (;;) { + const auto nl = text.find('\n', at); + auto line = text.substr(at, nl == std::string_view::npos ? std::string_view::npos : nl - at); + if (nl != std::string_view::npos && !line.empty() && line.back() == '\r') line.remove_suffix(1); + out.emplace_back(line); + if (nl == std::string_view::npos) break; + at = nl + 1; + } + return out; +} + +namespace { + +// The bytes of the UTF-8 sequence led by `c`, and the UTF-16 units it is. +std::pair sequence(unsigned char c) { + if (c < 0x80) return {1, 1}; + if ((c & 0xE0) == 0xC0) return {2, 1}; + if ((c & 0xF0) == 0xE0) return {3, 1}; + if ((c & 0xF8) == 0xF0) return {4, 2}; + return {1, 1}; +} + +} // namespace + +int utf16_column(std::string_view line, std::size_t byte) { + int units = 0; + for (std::size_t i = 0; i < line.size() && i < byte;) { + const auto [bytes, n] = sequence(static_cast(line[i])); + i += bytes; + units += n; + } + return units; +} + +int utf16_length(std::string_view s) { return utf16_column(s, s.size()); } + +std::size_t byte_offset(std::string_view line, int column) { + int units = 0; + std::size_t i = 0; + while (i < line.size()) { + const auto [bytes, n] = sequence(static_cast(line[i])); + if (units + n > column) break; + units += n; + i += bytes; + } + return std::min(i, line.size()); +} + +std::size_t space_at(std::string_view s, std::size_t i) { + if (i >= s.size()) return 0; + const auto c = static_cast(s[i]); + if (c == ' ' || (c >= 0x09 && c <= 0x0D)) return 1; + auto is = [&](std::string_view seq) { return s.substr(i, seq.size()) == seq; }; + if (c == 0xC2 && is("\xC2\xA0")) return 2; // U+00A0 + if (c == 0xE1 && is("\xE1\x9A\x80")) return 3; // U+1680 + if (c == 0xE2 && i + 2 < s.size() && static_cast(s[i + 1]) == 0x80) { + const auto t = static_cast(s[i + 2]); + if ((t >= 0x80 && t <= 0x8A) || t == 0xA8 || t == 0xA9 || t == 0xAF) return 3; // U+2000-200A, 2028, 2029, 202F + } + if (c == 0xE2 && is("\xE2\x81\x9F")) return 3; // U+205F + if (c == 0xE3 && is("\xE3\x80\x80")) return 3; // U+3000 + if (c == 0xEF && is("\xEF\xBB\xBF")) return 3; // U+FEFF + return 0; +} + +std::string_view trim_start(std::string_view s) { + std::size_t i = 0; + while (auto n = space_at(s, i)) i += n; + return s.substr(i); +} + +std::string_view trim_end(std::string_view s) { + // A space is at most three bytes; read backwards by trying each length. + for (;;) { + bool cut = false; + for (std::size_t n = 1; n <= 3 && n <= s.size(); ++n) + if (space_at(s, s.size() - n) == n) { + s.remove_suffix(n); + cut = true; + break; + } + if (!cut) return s; + } +} + +std::string_view trim(std::string_view s) { return trim_end(trim_start(s)); } + +std::string stringify(const nlohmann::json& value) { + return value.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); +} + +int locale_compare(std::string_view a, std::string_view b) { + auto rank = [](unsigned char c) -> int { + if (c >= '0' && c <= '9') return 1000 + c; + if (c >= 'a' && c <= 'z') return 2000 + (c - 'a') * 2; + if (c >= 'A' && c <= 'Z') return 2000 + (c - 'A') * 2; + return c; // punctuation and spaces before digits and letters, in code order + }; + for (std::size_t i = 0; i < a.size() && i < b.size(); ++i) { + const int x = rank(static_cast(a[i])), y = rank(static_cast(b[i])); + if (x != y) return x < y ? -1 : 1; + } + if (a.size() != b.size()) return a.size() < b.size() ? -1 : 1; + // Equal but for case: the lower-case one first, at its first difference. + for (std::size_t i = 0; i < a.size(); ++i) + if (a[i] != b[i]) return std::islower(static_cast(a[i])) ? -1 : 1; + return 0; +} + +} // namespace mcppide::text diff --git a/src/toml.cppm b/src/toml.cppm new file mode 100644 index 0000000..812b73d --- /dev/null +++ b/src/toml.cppm @@ -0,0 +1,712 @@ +// mcpp.toml read tolerantly, for an editor: the structure an edit in progress already has -- table +// headers, keys, values -- with where each one is, and the place of a cursor in it. Nothing here +// throws for a document being typed (an unclosed `[`, string, array or inline table): what was +// recognised is answered, and an open construct says so. It is not a TOML validator; mcpp is +// (manifest/check). +// +// The subset read: table headers (`[[...]]` arrays of tables, quoted segments), bare, quoted and +// dotted keys, strings (`"""` and `'''` multi-line ones too), integers, booleans, arrays and inline +// tables (nested, across lines), and comments. Positions are 0-based; a column is a byte offset in +// the line (mcppide.text turns it into the editor's UTF-16 column). +// +// A port of mcpp-vscode 0.9's src/toml/parser.ts (design 2026-10-30 part 2 §8); the goldens in +// tests/rules are that implementation's answers. +export module mcppide.toml; + +import std; +import mcppide.spec; +import mcppide.text; + +export namespace mcppide::toml { + +struct Pos { + int line = 0, col = 0; +}; + +// 0-based; the end is exclusive. +struct Range { + int startLine = 0, startCol = 0, endLine = 0, endCol = 0; +}; + +// One segment of a key or a table name: the name unquoted, and the token's range (quotes included). +struct KeySegment { + std::string name; + bool quoted = false; + Range range; +}; + +enum class ValueKind { string, integer, boolean, array, inlineTable, unknown }; +std::string_view name_of(ValueKind k); + +struct KeyValue; + +// A value. `open`: not closed (a string without its closing quote, an array without `]`, an inline +// table without `}`), its range running to where the scan stopped. +struct Value { + ValueKind kind = ValueKind::unknown; + Range range; + bool open = false; + std::optional text; // a string's content (escapes as written), a scalar's token + char quote = 0; // a string's quote + bool multiline = false; + std::optional contentRange; // a string's content, without the quotes + std::vector elements; // an array's + std::vector entries; // an inline table's +}; + +// `a.b.c = value`: the key's segments, and the value when one was written. +struct KeyValue { + std::vector keyPath; + Range range; + std::optional value; +}; + +// `[a.b]` or `[[a.b]]`; `open`: no `]` yet. +struct Section { + std::vector segments; + bool isArray = false; + bool open = false; + Range range; + int line = 0; +}; + +using Node = std::variant; + +struct Document { + std::vector nodes; +}; + +// What a table is: before any header (`top`), a table the rules layer does not know (`unknown`), or +// one it reads as `group` -- a section of spec/toml-schema.json, or a table that has its own reading +// (`workspace.dependencies`, `runtime.capability`). +struct Resolution { + enum class Kind { top, unknown, known } kind = Kind::top; + std::string group; + std::vector segments; +}; + +Resolution resolve_section(const std::vector& segments); + +// The token a suggestion replaces, on the cursor's line (bytes). +struct ReplaceRange { + int start = 0, end = 0; +}; + +// Where a cursor is. +struct Context { + enum class Kind { header, key, value } kind = Kind::key; + // header: the segments before the cursor's token; whether it is `[[`. + std::vector segments; + bool isArray = false; + // key and value: the table the statement is in. + Resolution section; + // key: inside an inline table, the keys leading to it; the dotted key's segments before the + // cursor's token. + std::vector containerPath, keyPrefix; + // value: the key's whole path (through inline tables and arrays), the kind of the value under + // the cursor (none before it starts), whether the cursor is inside a string and the string is open. + std::vector keyPath; + std::optional valueKind; + bool insideString = false, stringOpen = false; + ReplaceRange replace; +}; + +// The document's structure. `lines` as text::lines_of gives them. +Document parse(const std::vector& lines); +// The context of a cursor at `line` and UTF-16 column `character`, both clamped into the document. +Context context_at(const std::vector& lines, int line, int character); + +} // namespace mcppide::toml + +namespace mcppide::toml { + +std::string_view name_of(ValueKind k) { + switch (k) { + case ValueKind::string: return "string"; + case ValueKind::integer: return "integer"; + case ValueKind::boolean: return "boolean"; + case ValueKind::array: return "array"; + case ValueKind::inlineTable: return "inlineTable"; + case ValueKind::unknown: return "unknown"; + } + return "unknown"; +} + +namespace { + +// Tables the rules layer reads by name, whether or not the schema has them. +const std::set> kKnownSections = { + "package", "lib", "build", "generated_files", "dependencies", "dev-dependencies", "build-dependencies", + "features", "indices", "capabilities", "runtime", "resources", "toolchain", "xlings", "xlings.workspace", + "xlings.envs", "workspace", "workspace.dependencies", "pack", "pack.bundle-project", "language", + "tools.overrides", +}; + +// `.` is ``. +const std::set> kParameterizedBases = { + "targets", "profile", "feature-deps", "dependencies", "dev-dependencies", +}; + +// A platform's tables a target selector overrides (`[target..link]` is `[platform.link]`). +const std::set> kPlatformOverrides = {"carrier", "layers", "link", "runners", "tools"}; + +// Tables whose sub-tables are named entries. +bool names_entries(std::string_view head) { + return kParameterizedBases.contains(head) || head == "build-dependencies" || head == "features" || head == "platform"; +} + +const std::set>& schema_sections() { + static const auto names = [] { + std::set> out; + for (const auto& s : spec::toml_schema()["sections"]) out.insert(s.value("name", "")); + return out; + }(); + return names; +} + +std::string join(std::span parts) { + std::string out; + for (const auto& p : parts) { + if (!out.empty()) out += '.'; + out += p; + } + return out; +} + +std::optional resolve_group(std::span segments) { + const auto joined = join(segments); + if (kKnownSections.contains(joined) || schema_sections().contains(joined)) return joined; + const auto& head = segments[0]; + if (head == "target") { + if (segments.size() <= 2) return "target"; + auto rest = segments.subspan(2); + if (kPlatformOverrides.contains(rest[0])) { + std::vector platform{"platform"}; + platform.insert(platform.end(), rest.begin(), rest.end()); + return resolve_group(platform); + } + return resolve_group(rest); + } + if (head == "workspace" && segments.size() >= 3 && segments[1] == "dependencies") return "workspace.dependencies"; + if (head == "workspace" && segments.size() >= 2) return resolve_group(segments.subspan(1)); + if (segments.size() >= 2 && segments.back() == "requires_abi") return "requires_abi"; + if (head == "platform" && segments.size() >= 3 && segments[1] == "hosted" && segments[2] == "layers") + return "platform.layers"; + if (head == "runtime" && segments.size() == 2) return "runtime.capability"; + if (names_entries(head) && segments.size() >= 2) { + for (auto length = segments.size() - 1; length >= 1; --length) { + const auto name = join(segments.first(length)); + if (schema_sections().contains(name) || kKnownSections.contains(name)) return name; + } + return head; + } + return std::nullopt; +} + +bool bare_key_char(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; +} + +// The cursor's context was found: the scan stops. +struct Found {}; + +class Scanner { +public: + explicit Scanner(std::vector lines, std::optional cursor = std::nullopt) : lines_(std::move(lines)) { + for (auto& l : lines_) + if (!l.empty() && l.back() == '\r') l.pop_back(); + if (lines_.empty()) lines_.emplace_back(); + if (cursor) { + const int line = std::clamp(cursor->line, 0, static_cast(lines_.size()) - 1); + cursor_ = Pos{line, cursor->col}; + } + } + + // A cursor given as a UTF-16 column. + static Scanner at(const std::vector& lines, int line, int character) { + Scanner s(lines); + const int l = std::clamp(line, 0, static_cast(s.lines_.size()) - 1); + const auto byte = text::byte_offset(s.lines_[l], std::max(0, character)); + s.cursor_ = Pos{l, static_cast(byte)}; + return s; + } + + std::optional context; + + std::vector scan() { + std::vector nodes; + while (!eof()) { + skip_trivia(); + // Between statements: a key starts here. + if (cursor_reached()) capture_key({}, {}); + if (eof()) break; + const auto start = pos(); + if (peek() == '[') { + nodes.emplace_back(scan_header(start)); + } else if (auto kv = scan_key_value({})) { + nodes.emplace_back(std::move(*kv)); + } + // Whatever is not recognised is consumed, so the scan ends. + if (same(pos(), start)) advance(); + } + return nodes; + } + + // At the end of the document: a key of the current table. + Context final_context() const { + Context c; + c.kind = Context::Kind::key; + c.section = section_resolution(); + c.replace = empty_replace(); + return c; + } + +private: + std::vector lines_; + int line_ = 0, col_ = 0; + std::optional cursor_; + std::optional> currentSection_; + + Pos pos() const { return {line_, col_}; } + bool eof() const { return line_ >= static_cast(lines_.size()); } + bool at_eol() const { return eof() || col_ >= static_cast(lines_[line_].size()); } + char peek() const { return at_eol() ? '\0' : lines_[line_][col_]; } + char peek_at(int offset) const { + if (eof()) return '\0'; + const auto& t = lines_[line_]; + return col_ + offset < static_cast(t.size()) ? t[col_ + offset] : '\0'; + } + void advance() { + if (eof()) return; + col_ += 1; + if (col_ > static_cast(lines_[line_].size())) { + line_ += 1; + col_ = 0; + } + } + static bool same(Pos a, Pos b) { return a.line == b.line && a.col == b.col; } + static int cmp(Pos a, Pos b) { return a.line != b.line ? a.line - b.line : a.col - b.col; } + + void skip_inline_ws() { + while (peek() == ' ' || peek() == '\t') col_ += 1; + } + // Spaces, line ends and comments (a `#` inside a string never reaches here). + void skip_trivia() { + while (!eof()) { + const char ch = peek(); + if (ch == ' ' || ch == '\t') { col_ += 1; continue; } + if (at_eol()) { line_ += 1; col_ = 0; continue; } + if (ch == '#') { + while (!at_eol()) col_ += 1; + continue; + } + break; + } + } + + Range range_from(Pos start) const { return {start.line, start.col, line_, col_}; } + + std::string slice(Pos a, Pos b) const { + // A scan that ran to the end of the document (an unclosed multi-line string) ends one line + // past the last; its text ends where the last line does. + if (b.line >= static_cast(lines_.size())) { + const int last = static_cast(lines_.size()) - 1; + b = {last, static_cast(lines_[last].size())}; + if (a.line > last) return {}; + } + auto cut = [](const std::string& s, int from, int to) { + from = std::clamp(from, 0, static_cast(s.size())); + to = std::clamp(to, from, static_cast(s.size())); + return s.substr(from, to - from); + }; + if (a.line == b.line) return cut(lines_[a.line], a.col, b.col); + std::string out = cut(lines_[a.line], a.col, static_cast(lines_[a.line].size())); + for (int l = a.line + 1; l < b.line; ++l) out += "\n" + lines_[l]; + out += "\n" + cut(lines_[b.line], 0, b.col); + return out; + } + + bool cursor_reached() const { return cursor_ && cmp(pos(), *cursor_) >= 0; } + + // Whether the cursor is in the range, its end included (a cursor often sits right after a token). + bool cursor_within(const Range& r) const { + if (!cursor_) return false; + const auto& c = *cursor_; + if (c.line < r.startLine || c.line > r.endLine) return false; + if (c.line == r.startLine && c.col < r.startCol) return false; + if (c.line == r.endLine && c.col > r.endCol) return false; + return true; + } + + // The token around the cursor on its line, up to a stop character either side. + template + ReplaceRange expand_on_line(Stop stop) const { + if (!cursor_) return {}; + const auto& t = lines_[cursor_->line]; + const int at = std::min(cursor_->col, static_cast(t.size())); + int start = at, end = at; + while (start > 0 && !stop(t[start - 1])) --start; + while (end < static_cast(t.size()) && !stop(t[end])) ++end; + return {start, end}; + } + + ReplaceRange empty_replace() const { + const int col = cursor_ ? cursor_->col : 0; + return {col, col}; + } + static ReplaceRange token_replace(const Range& r) { return {r.startCol, r.endCol}; } + static std::optional single_line_replace(const Range& r) { + if (r.startLine != r.endLine) return std::nullopt; + return ReplaceRange{r.startCol, r.endCol}; + } + + Resolution section_resolution() const { + if (!currentSection_) return Resolution{}; + return resolve_section(*currentSection_); + } + + static std::vector names(const std::vector& segments) { + std::vector out; + for (const auto& s : segments) out.push_back(s.name); + return out; + } + + [[noreturn]] void capture(Context c) { + context = std::move(c); + throw Found{}; + } + [[noreturn]] void capture_key(std::vector containerPath, std::vector keyPrefix, + const Range* token = nullptr) { + Context c; + c.kind = Context::Kind::key; + c.section = section_resolution(); + c.containerPath = std::move(containerPath); + c.keyPrefix = std::move(keyPrefix); + c.replace = token ? token_replace(*token) : empty_replace(); + capture(std::move(c)); + } + [[noreturn]] void capture_value(std::vector keyPath, std::optional kind, + bool insideString = false, bool stringOpen = false, + std::optional replace = std::nullopt) { + Context c; + c.kind = Context::Kind::value; + c.section = section_resolution(); + c.keyPath = std::move(keyPath); + c.valueKind = kind; + c.insideString = insideString; + c.stringOpen = stringOpen; + c.replace = replace ? *replace : empty_replace(); + capture(std::move(c)); + } + [[noreturn]] void capture_header(const std::vector& segments, bool isArray, const Range* token = nullptr) { + Context c; + c.kind = Context::Kind::header; + c.segments = names(segments); + c.isArray = isArray; + c.replace = token ? token_replace(*token) : empty_replace(); + capture(std::move(c)); + } + + // `[a.b]`, `[[a.b]]`, quoted segments; without its `]` the header is open and the rest of the + // line is skipped. + Section scan_header(Pos start) { + advance(); // [ + bool isArray = false; + if (peek() == '[') { + isArray = true; + advance(); + } + std::vector segments; + bool open = true; + for (;;) { + skip_inline_ws(); + if (cursor_reached()) capture_header(segments, isArray); + auto segment = scan_key_segment(); + if (!segment) break; + if (cursor_within(segment->range)) capture_header(segments, isArray, &segment->range); + segments.push_back(std::move(*segment)); + skip_inline_ws(); + if (peek() == '.') { + advance(); + continue; + } + break; + } + skip_inline_ws(); + if (cursor_reached()) capture_header(segments, isArray); + if (peek() == ']') { + advance(); + if (isArray && peek() == ']') advance(); + // `[[x]` with one `]` is closed too. + open = false; + } + if (open) + while (!at_eol()) col_ += 1; + Section node{segments, isArray, open, range_from(start), start.line}; + currentSection_ = names(segments); + return node; + } + + // A bare or quoted key segment (a quoted one that runs past its line is cut there). + std::optional scan_key_segment() { + const auto start = pos(); + const char quote = peek(); + if (quote == '"' || quote == '\'') { + advance(); + const auto contentStart = pos(); + while (!at_eol() && peek() != quote) { + if (quote == '"' && peek() == '\\' && col_ + 1 < static_cast(lines_[line_].size())) { + col_ += 2; + continue; + } + col_ += 1; + } + const auto contentEnd = pos(); + if (peek() == quote) advance(); + return KeySegment{slice(contentStart, contentEnd), true, range_from(start)}; + } + if (!bare_key_char(quote)) return std::nullopt; + while (bare_key_char(peek())) col_ += 1; + return KeySegment{slice(start, pos()), false, range_from(start)}; + } + + // A dotted key and its value; `containerPath` the keys leading into an inline table. + std::optional scan_key_value(const std::vector& containerPath) { + const auto start = pos(); + std::vector segments; + for (;;) { + skip_inline_ws(); + if (cursor_reached()) capture_key(containerPath, names(segments)); + auto segment = scan_key_segment(); + if (!segment) break; + if (cursor_within(segment->range)) capture_key(containerPath, names(segments), &segment->range); + segments.push_back(std::move(*segment)); + skip_inline_ws(); + if (peek() == '.') { + advance(); + continue; + } + break; + } + skip_inline_ws(); + // Between the key and `=`: still the key. + if (cursor_reached()) capture_key(containerPath, names(segments)); + const auto keyNames = names(segments); + std::optional value; + if (peek() == '=') { + advance(); + skip_inline_ws(); + auto valuePath = containerPath; + valuePath.insert(valuePath.end(), keyNames.begin(), keyNames.end()); + // After `=`, before the value. + if (cursor_reached()) { + // A token after the cursor on its line is what a suggestion replaces whole. + if (!at_eol() && peek() != '#') { + auto ahead = scan_value(valuePath); // a cursor inside the token is captured there + const auto r = single_line_replace(ahead.range); + capture_value(valuePath, ahead.kind, false, false, r ? *r : empty_replace()); + } + capture_value(valuePath, std::nullopt); + } + // A value starts on the key's line, or there is none. + if (!at_eol() && peek() != '#') value = scan_value(valuePath); + } + if (segments.empty() && !value) return std::nullopt; + return KeyValue{std::move(segments), range_from(start), std::move(value)}; + } + + Value scan_value(const std::vector& keyPath) { + const char ch = peek(); + if (ch == '"' || ch == '\'') return scan_string(keyPath); + if (ch == '[') return scan_array(keyPath); + if (ch == '{') return scan_inline_table(keyPath); + return scan_bare(keyPath); + } + + Value scan_string(const std::vector& keyPath) { + const auto start = pos(); + const char quote = peek(); + advance(); + bool multiline = false, open = true; + auto contentStart = pos(), contentEnd = pos(); + if (peek() == quote) { + advance(); + if (peek() == quote) { + multiline = true; + advance(); + contentStart = pos(); + } else { + open = false; // "" or '' + } + } + std::optional closeStart; + if (open) { + while (!eof()) { + if (!multiline && at_eol()) break; // a one-line string runs past its line: open + const char ch = peek(); + if (ch == '\\' && quote == '"') { + advance(); + if (!eof() && (multiline || !at_eol())) advance(); + continue; + } + if (ch == quote) { + if (multiline) { + if (peek_at(1) == quote && peek_at(2) == quote) { + contentEnd = pos(); + closeStart = pos(); + advance(); + advance(); + advance(); + open = false; + break; + } + advance(); + continue; + } + contentEnd = pos(); + closeStart = pos(); + advance(); + open = false; + break; + } + advance(); + } + if (open) contentEnd = pos(); + } + Value node; + node.kind = ValueKind::string; + node.range = range_from(start); + node.open = open; + node.text = slice(contentStart, contentEnd); + node.quote = quote; + node.multiline = multiline; + node.contentRange = Range{contentStart.line, contentStart.col, contentEnd.line, contentEnd.col}; + if (cursor_within(node.range) && cursor_) { + const auto& cursor = *cursor_; + const bool afterOpen = cmp(cursor, contentStart) >= 0; + const bool beforeClose = !closeStart || cmp(cursor, *closeStart) <= 0; + const bool insideString = afterOpen && beforeClose; + // Inside the content: the token up to the quotes on this line, within the content. + auto replace = empty_replace(); + if (insideString) { + const auto expanded = expand_on_line([quote](char c) { return c == quote; }); + const int minCol = contentStart.line == cursor.line ? contentStart.col : 0; + const int maxCol = contentEnd.line == cursor.line ? contentEnd.col + : static_cast(lines_[cursor.line].size()); + replace = {std::max(expanded.start, minCol), std::min(std::max(expanded.end, minCol), maxCol)}; + } + capture_value(keyPath, ValueKind::string, insideString, open && afterOpen, replace); + } + return node; + } + + Value scan_array(const std::vector& keyPath) { + const auto start = pos(); + advance(); // [ + Value node; + node.kind = ValueKind::array; + node.open = true; + for (;;) { + skip_trivia(); + if (cursor_reached()) capture_value(keyPath, std::nullopt); + if (eof()) break; + const char ch = peek(); + if (ch == ']') { + advance(); + node.open = false; + break; + } + if (ch == ',') { + advance(); + continue; + } + const auto elementStart = pos(); + node.elements.push_back(scan_value(keyPath)); + if (same(pos(), elementStart)) advance(); + } + node.range = range_from(start); + return node; + } + + Value scan_inline_table(const std::vector& keyPath) { + const auto start = pos(); + advance(); // { + Value node; + node.kind = ValueKind::inlineTable; + node.open = true; + for (;;) { + skip_trivia(); + if (cursor_reached()) capture_key(keyPath, {}); + if (eof()) break; + const char ch = peek(); + if (ch == '}') { + advance(); + node.open = false; + break; + } + if (ch == ',') { + advance(); + continue; + } + const auto entryStart = pos(); + if (auto entry = scan_key_value(keyPath)) node.entries.push_back(std::move(*entry)); + if (same(pos(), entryStart)) advance(); + } + node.range = range_from(start); + return node; + } + + // A bare token: a boolean, an integer, anything else `unknown` (not validated). + Value scan_bare(const std::vector& keyPath) { + const auto start = pos(); + auto end = pos(); + while (!at_eol()) { + const char ch = peek(); + if (ch == ',' || ch == ']' || ch == '}' || ch == '[' || ch == '{' || ch == '#') break; + col_ += 1; + if (ch != ' ' && ch != '\t') end = pos(); + } + Value node; + node.text = slice(start, end); + const auto& t = *node.text; + if (t == "true" || t == "false") { + node.kind = ValueKind::boolean; + } else { + // /^[+-]?\d[\d_]*$/ + std::size_t i = (!t.empty() && (t[0] == '+' || t[0] == '-')) ? 1 : 0; + bool integer = i < t.size() && std::isdigit(static_cast(t[i])); + for (std::size_t j = i; integer && j < t.size(); ++j) + integer = std::isdigit(static_cast(t[j])) || t[j] == '_'; + node.kind = integer ? ValueKind::integer : ValueKind::unknown; + } + node.range = Range{start.line, start.col, end.line, end.col}; + if (cursor_within(node.range)) capture_value(keyPath, node.kind, false, false, token_replace(node.range)); + return node; + } +}; + +} // namespace + +Resolution resolve_section(const std::vector& segments) { + if (segments.empty()) return Resolution{}; + if (auto group = resolve_group(segments)) return Resolution{Resolution::Kind::known, *group, {}}; + return Resolution{Resolution::Kind::unknown, {}, segments}; +} + +Document parse(const std::vector& lines) { + Scanner s(lines); + return Document{s.scan()}; +} + +Context context_at(const std::vector& lines, int line, int character) { + auto s = Scanner::at(lines, line, character); + try { + s.scan(); + } catch (const Found&) { + if (s.context) return *s.context; + } + // Not reached: the cursor is in the space at the end, a key of the current table. + return s.final_context(); +} + +} // namespace mcppide::toml diff --git a/tests/rules/corpus/brackets.toml b/tests/rules/corpus/brackets.toml new file mode 100644 index 0000000..07961a2 --- /dev/null +++ b/tests/rules/corpus/brackets.toml @@ -0,0 +1,6 @@ +[package] +name = "b" +[ +version = "1" +[unclosed +[nosuch] diff --git a/tests/rules/corpus/broken.toml b/tests/rules/corpus/broken.toml new file mode 100644 index 0000000..aff2f19 --- /dev/null +++ b/tests/rules/corpus/broken.toml @@ -0,0 +1,36 @@ +[package] +name = "broken" +mcpp = "2026.9" +colour = "blue" +standard = +a b = 1 += "no key" +[package] trailing +[nosuch] +[nosuch.deeper] +[[build.flags]] +[[package]] +[language] +edition = "2023" + +[dependencies] +"xim:cmake" = "3.31.0" +"xpm:ninja" = { version = "1.12" } + +[xlings] +compat.zlib = "1.3.1" +mcpplibs.os = { path = "../os" } +tool.name = { version = "1.0" } + +[xlings.workspace] +"mcpplibs.os" = "0.2.3" + +[targets.app] +kind = "nonsense" +colour = 1 + +[profile.debug] +unknown_knob = true + +[toolchain] +anything = "goes" diff --git a/tests/rules/corpus/comments.toml b/tests/rules/corpus/comments.toml new file mode 100644 index 0000000..e4ad9ba --- /dev/null +++ b/tests/rules/corpus/comments.toml @@ -0,0 +1,3 @@ +# only a comment + + diff --git a/tests/rules/corpus/complete.toml b/tests/rules/corpus/complete.toml new file mode 100644 index 0000000..d0d0607 --- /dev/null +++ b/tests/rules/corpus/complete.toml @@ -0,0 +1,80 @@ +# A manifest that uses most of what mcpp reads. +[package] +name = "demo.app" +version = "0.3.0" +mcpp = ">=2026.10.30.1" +standard = "c++23" +description = "演示 a manifest — with 中文 text" +authors = ["A "] + +[build] +cxxflags = ["-O2", "-Wall"] +bmi_schedule = "auto" +sources = [ + "src/**/*.cppm", + "src/**/*.cpp", # a comment inside an array +] + +[dependencies] +fmt = "11.0.2" +compat.zlib = { version = "1.3.1" } +local = { path = "../lib" } +remote = { git = "https://github.com/user/repo.git", tag = "v1.0.0" } +shared = { workspace = true } +simdlib = { version = "2.0", features = ["avx2", "simd"] } + +[dependencies.mcpplibs] +os = "0.2.3" + +[dev-dependencies] +gtest = "1.15.0" + +[features] +default = ["simd"] +simd = { defines = ["USE_SIMD"] } +extra = [] + +[features.simd] +sources = ["src/simd/**"] + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[targets.tool] +kind = "entry" + +[profile.release] +opt = 3 + +[target.'cfg(os = "linux")'.build] +cxxflags = ["-pthread"] + +[target.x86_64-openkal] +platform = "mcpplibs.openkal-platform@0.18.0" + +[workspace] +members = ["libs/*"] + +[workspace.dependencies] +shared = "1.2.0" + +[xlings] +deps = ["cmake"] + +[xlings.workspace] +node = "24.19.0" + +[toolchain] +linux = "llvm@23.1.3" + +[pack] +format = "tar" + +[runtime] +libraries = ["m"] + +description2 = """ +a multi-line +string = with an equals sign +""" diff --git a/tests/rules/corpus/crlf.toml b/tests/rules/corpus/crlf.toml new file mode 100644 index 0000000..75c6c0b --- /dev/null +++ b/tests/rules/corpus/crlf.toml @@ -0,0 +1,6 @@ +[package] +name = "crlf" +version = "1.0.0" + +[build] +bmi_schedule = "on" diff --git a/tests/rules/corpus/editing.mcpp b/tests/rules/corpus/editing.mcpp new file mode 100644 index 0000000..b11a1be --- /dev/null +++ b/tests/rules/corpus/editing.mcpp @@ -0,0 +1,4 @@ +import mcpp.pl +import st +export import mcpp.deps. + mcpp::link_lib("x"); diff --git a/tests/rules/corpus/editing.toml b/tests/rules/corpus/editing.toml new file mode 100644 index 0000000..613f4ba --- /dev/null +++ b/tests/rules/corpus/editing.toml @@ -0,0 +1,24 @@ +[pack +[targets. +[[ +[package] +name = "edit" +standard = " +kind = +[targets.app] +kind = "b +main = +[build] +bmi_schedule = au +cxxflags = ["-O2", +[dependencies] +zlib = "1. +fmt = { version = "1 +foo = { git = "x", tag = " +bar = { features = [" + tabbed = "v" +名字 = "unicode key" +x = "名字" # 注释 +[features] +simd = { defines = ["A"], sources = [ { +[dependencies.ns diff --git a/tests/rules/corpus/empty.toml b/tests/rules/corpus/empty.toml new file mode 100644 index 0000000..e69de29 diff --git a/tests/rules/corpus/plain.mcpp b/tests/rules/corpus/plain.mcpp new file mode 100644 index 0000000..6ff630c --- /dev/null +++ b/tests/rules/corpus/plain.mcpp @@ -0,0 +1,14 @@ +import std; +import mcpp; + +int main() { + mcpp::link_search("lib"); + mcpp::include_dir("include"); + mcpp::runtime_search_dir("lib"); + mcpp::action a; + a.id = "obj"; + a.role = mcpp::roles::object; + a.input("x.c").output("x.o").submit(); + mcpp::li + mcpp:: +} diff --git a/tests/rules/corpus/rules.mcpp b/tests/rules/corpus/rules.mcpp new file mode 100644 index 0000000..3fad227 --- /dev/null +++ b/tests/rules/corpus/rules.mcpp @@ -0,0 +1,59 @@ +// A build program exercising every rule the analysis knows. +import std; +import mcpp; +import mcpp.plugins.tool; +import mcpp.rules.qt; +export import mcpp.core; +import ; +import :part; + +int main() { + obj.import(x); + mcpp::link_lib("z"); + mcpp::unknown_thing("x"); + mcpp::rules::qt::moc("a.h"); + mcpp::cxxflag("-I/usr/include/foo"); + mcpp::cflag("-L/opt/lib"); + mcpp::link_flag("-Wl,-rpath,/opt/lib"); + std::println("mcpp:link-flag=-L/x -Wl,-rpath,/y"); + std::println("mcpp:cxxflag=-I/raw"); + int big = 1'000'000; + /* mcpp::in_block_comment + mcpp::still_in_comment */ + auto raw = R"x(mcpp::in_raw_string + mcpp::still_raw )x"; + std::println("{}", "名字 mcpp::in_string"); + + mcpp::action untyped; + untyped.id = "u"; + untyped.command("make").submit(); + + mcpp::action lit; + lit.id = "l"; + lit.role = "prepare"; + lit.command("gen").submit(); + + mcpp::action bogus; + bogus.role = "compile"; + bogus.submit(); + + mcpp::action prep; + prep.id = "p"; + prep.role = mcpp::roles::prepare; + prep.arg("FOO=bar") + .command("cd build && make") + .submit(); + + mcpp::action ok; + ok.role = mcpp::roles::prepare; + ok.output_dir("gen"); + ok.command("gen").submit(); + + std::println("mcpp:action={{\"role\":\"prepare\",\"command\":[\"A=1 make\"]}}"); + std::println("mcpp:action={\"role\":\"prepare\",\"command\":[\"A=1 make\"]}"); + std::println("mcpp:action={\"command\":[\"cd x && make\"]}"); + std::println("mcpp:action={\"role\":\"weird\"}"); + std::println("mcpp:action={\"role\":\"object\",\"command\":[\"cc\"]}"); + std::println("mcpp:action=not json"); + mcpp:: +} diff --git a/tests/rules/corpus/unclosed.toml b/tests/rules/corpus/unclosed.toml new file mode 100644 index 0000000..00c01f2 --- /dev/null +++ b/tests/rules/corpus/unclosed.toml @@ -0,0 +1,6 @@ +[package] +name = "u" +text = """ +never closed +[build] +cxxflags = ["-O2" diff --git a/tests/rules/golden/brackets.toml.json b/tests/rules/golden/brackets.toml.json new file mode 100644 index 0000000..9d288cf --- /dev/null +++ b/tests/rules/golden/brackets.toml.json @@ -0,0 +1 @@ +{"diagnostics":{"default":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [","severity":"error","line":3,"startCharacter":1,"endCharacter":2}],"quiet":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [","severity":"info","line":3,"startCharacter":1,"endCharacter":2}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"2:0":[3,0,0],"2:1":[0,0,1],"2:2":[0,0,1],"2:3":[0,0,1],"3:0":[0,0,0],"3:1":[0,0,7],"3:2":[0,0,7],"3:3":[0,0,7],"3:4":[0,0,7],"3:5":[0,0,7],"3:6":[0,0,7],"3:7":[0,0,7],"3:8":[0,8,8],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"3:15":[2],"4:0":[0,0,0],"4:1":[0,0,1],"4:2":[0,0,9],"4:3":[0,0,9],"4:4":[0,0,9],"4:5":[0,0,9],"4:6":[0,0,9],"4:7":[0,0,9],"4:8":[0,0,9],"4:9":[0,0,9],"4:10":[0,0,9],"4:11":[0,0,9],"5:0":[2],"5:1":[0,0,1],"5:2":[0,0,7],"5:3":[0,0,7],"5:4":[0,0,7],"5:5":[0,0,7],"5:6":[0,0,7],"5:7":[0,0,7],"5:8":[2],"5:9":[2],"5:10":[2],"6:0":[2],"6:1":[2],"6:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"}]} diff --git a/tests/rules/golden/broken.toml.json b/tests/rules/golden/broken.toml.json new file mode 100644 index 0000000..50daf81 --- /dev/null +++ b/tests/rules/golden/broken.toml.json @@ -0,0 +1 @@ +{"diagnostics":{"default":[{"code":"mcpp.manifest.mcppFloor","message":"[package] mcpp must be a release floor like \">=2026.9.28.3\"","severity":"error","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"mcpp"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [package]","severity":"warning","line":4,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.syntax","message":"key standard has no value","severity":"error","line":5,"startCharacter":1,"endCharacter":9,"relatedKey":"standard"},{"code":"mcpp.manifest.syntax","message":"invalid key: a b","severity":"error","line":6,"startCharacter":1,"endCharacter":4},{"code":"mcpp.manifest.syntax","message":"not a [section] header or key = value: = \"no key\"","severity":"error","line":7,"startCharacter":1,"endCharacter":11},{"code":"mcpp.manifest.syntax","message":"not a [section] header: [package] trailing","severity":"error","line":8,"startCharacter":1,"endCharacter":19},{"code":"mcpp.manifest.unknownSection","message":"unknown section [nosuch]","severity":"warning","line":9,"startCharacter":1,"endCharacter":9,"relatedKey":"nosuch"},{"code":"mcpp.manifest.unknownSection","message":"unknown section [nosuch.deeper]","severity":"warning","line":10,"startCharacter":1,"endCharacter":16,"relatedKey":"nosuch.deeper"},{"code":"mcpp.manifest.arrayTable","message":"[[package]] is an array of tables, which mcpp does not use here","severity":"error","line":12,"startCharacter":1,"endCharacter":12,"relatedKey":"package"},{"code":"mcpp.manifest.legacyKey","message":"[language] is deprecated; use [package].standard","severity":"info","line":13,"startCharacter":1,"endCharacter":11,"relatedKey":"[package].standard"},{"code":"mcpp.manifest.unknownKey","message":"unknown key edition in [language]","severity":"warning","line":14,"startCharacter":1,"endCharacter":8,"relatedKey":"edition"},{"code":"mcpp.manifest.planeSeparation","message":"xim:cmake is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]","severity":"warning","line":17,"startCharacter":1,"endCharacter":12,"relatedKey":"xim:cmake"},{"code":"mcpp.manifest.planeSeparation","message":"xpm:ninja is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]","severity":"warning","line":18,"startCharacter":1,"endCharacter":12,"relatedKey":"xpm:ninja"},{"code":"mcpp.manifest.planeSeparation","message":"compat.zlib is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key compat.zlib in [xlings]","severity":"warning","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key mcpplibs.os in [xlings]","severity":"warning","line":22,"startCharacter":1,"endCharacter":12,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.planeSeparation","message":"tool.name is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.unknownKey","message":"unknown key tool.name in [xlings]","severity":"warning","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.planeSeparation","message":"mcpplibs.os is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":26,"startCharacter":1,"endCharacter":14,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [targets]","severity":"warning","line":30,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.unknownKey","message":"unknown key unknown_knob in [profile]","severity":"warning","line":33,"startCharacter":1,"endCharacter":13,"relatedKey":"unknown_knob"}],"quiet":[{"code":"mcpp.manifest.mcppFloor","message":"[package] mcpp must be a release floor like \">=2026.9.28.3\"","severity":"warning","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"mcpp"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [package]","severity":"info","line":4,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.syntax","message":"key standard has no value","severity":"info","line":5,"startCharacter":1,"endCharacter":9,"relatedKey":"standard"},{"code":"mcpp.manifest.syntax","message":"invalid key: a b","severity":"info","line":6,"startCharacter":1,"endCharacter":4},{"code":"mcpp.manifest.syntax","message":"not a [section] header or key = value: = \"no key\"","severity":"info","line":7,"startCharacter":1,"endCharacter":11},{"code":"mcpp.manifest.syntax","message":"not a [section] header: [package] trailing","severity":"info","line":8,"startCharacter":1,"endCharacter":19},{"code":"mcpp.manifest.legacyKey","message":"[language] is deprecated; use [package].standard","severity":"warning","line":13,"startCharacter":1,"endCharacter":11,"relatedKey":"[package].standard"},{"code":"mcpp.manifest.unknownKey","message":"unknown key edition in [language]","severity":"info","line":14,"startCharacter":1,"endCharacter":8,"relatedKey":"edition"},{"code":"mcpp.manifest.unknownKey","message":"unknown key compat.zlib in [xlings]","severity":"info","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key mcpplibs.os in [xlings]","severity":"info","line":22,"startCharacter":1,"endCharacter":12,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.unknownKey","message":"unknown key tool.name in [xlings]","severity":"info","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [targets]","severity":"info","line":30,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.unknownKey","message":"unknown key unknown_knob in [profile]","severity":"info","line":33,"startCharacter":1,"endCharacter":13,"relatedKey":"unknown_knob"}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"1:13":[2],"1:14":[2],"1:15":[2],"1:16":[2],"1:17":[2],"2:0":[3,0,0],"2:1":[3,0,4],"2:2":[3,0,4],"2:3":[3,0,4],"2:4":[3,0,4],"2:5":[3,5,5],"2:6":[2],"2:7":[2],"2:8":[2],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"3:0":[4,0,0],"3:1":[4,0,6],"3:2":[4,0,6],"3:3":[4,0,6],"3:4":[4,0,6],"3:5":[4,0,6],"3:6":[4,0,6],"3:7":[4,7,7],"3:8":[2],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"3:15":[2],"3:16":[2],"3:17":[2],"4:0":[4,0,0],"4:1":[4,0,8],"4:2":[4,0,8],"4:3":[4,0,8],"4:4":[4,0,8],"4:5":[4,0,8],"4:6":[4,0,8],"4:7":[4,0,8],"4:8":[4,0,8],"4:9":[4,9,9],"4:10":[5,10,10],"4:11":[5,10,10],"4:12":[5,10,10],"5:0":[6,0,0],"5:1":[6,0,1],"5:2":[6,2,2],"5:3":[6,2,3],"5:4":[6,4,4],"5:5":[2],"5:6":[2],"5:7":[2],"5:8":[2],"5:9":[2],"6:0":[6,0,0],"6:1":[2],"6:2":[2],"6:3":[2],"6:4":[2],"6:5":[2],"6:6":[2],"6:7":[2],"6:8":[2],"6:9":[2],"6:10":[2],"6:11":[2],"6:12":[2],"7:0":[6,0,0],"7:1":[0,0,1],"7:2":[0,0,8],"7:3":[0,0,8],"7:4":[0,0,8],"7:5":[0,0,8],"7:6":[0,0,8],"7:7":[0,0,8],"7:8":[0,0,8],"7:9":[6,9,9],"7:10":[6,10,10],"7:11":[6,10,18],"7:12":[6,10,18],"7:13":[6,10,18],"7:14":[6,10,18],"7:15":[6,10,18],"7:16":[6,10,18],"7:17":[6,10,18],"7:18":[6,10,18],"7:19":[6,10,18],"7:20":[6,10,18],"8:0":[6,0,0],"8:1":[0,0,1],"8:2":[0,0,7],"8:3":[0,0,7],"8:4":[0,0,7],"8:5":[0,0,7],"8:6":[0,0,7],"8:7":[0,0,7],"8:8":[2],"8:9":[2],"8:10":[2],"9:0":[2],"9:1":[0,0,1],"9:2":[0,0,7],"9:3":[0,0,7],"9:4":[0,0,7],"9:5":[0,0,7],"9:6":[0,0,7],"9:7":[0,0,7],"9:8":[0,0,8],"9:9":[0,0,14],"9:10":[0,0,14],"9:11":[0,0,14],"9:12":[0,0,14],"9:13":[0,0,14],"9:14":[0,0,14],"9:15":[2],"9:16":[2],"9:17":[2],"10:0":[2],"10:1":[2],"10:2":[2],"10:3":[2],"10:4":[2],"10:5":[2],"10:6":[2],"10:7":[2],"10:8":[2],"10:9":[2],"10:10":[2],"10:11":[2],"10:12":[2],"10:13":[2],"10:14":[2],"10:15":[2],"10:16":[2],"10:17":[2],"11:0":[2],"11:1":[2],"11:2":[2],"11:3":[2],"11:4":[2],"11:5":[2],"11:6":[2],"11:7":[2],"11:8":[2],"11:9":[2],"11:10":[6,10,10],"11:11":[6,11,11],"11:12":[6,11,11],"11:13":[6,11,11],"12:0":[6,0,0],"12:1":[0,0,1],"12:2":[0,0,9],"12:3":[0,0,9],"12:4":[0,0,9],"12:5":[0,0,9],"12:6":[0,0,9],"12:7":[0,0,9],"12:8":[0,0,9],"12:9":[0,0,9],"12:10":[7,10,10],"12:11":[7,10,10],"12:12":[7,10,10],"13:0":[7,0,0],"13:1":[7,0,7],"13:2":[7,0,7],"13:3":[7,0,7],"13:4":[7,0,7],"13:5":[7,0,7],"13:6":[7,0,7],"13:7":[7,0,7],"13:8":[7,8,8],"13:9":[2],"13:10":[2],"13:11":[2],"13:12":[2],"13:13":[2],"13:14":[2],"13:15":[2],"13:16":[2],"13:17":[2],"13:18":[2],"14:0":[7,0,0],"14:1":[7,0,0],"14:2":[7,0,0],"15:0":[7,0,0],"15:1":[0,0,1],"15:2":[0,0,13],"15:3":[0,0,13],"15:4":[0,0,13],"15:5":[0,0,13],"15:6":[0,0,13],"15:7":[0,0,13],"15:8":[0,0,13],"15:9":[0,0,13],"15:10":[0,0,13],"15:11":[0,0,13],"15:12":[0,0,13],"15:13":[0,0,13],"15:14":[8,14,14],"15:15":[8,14,14],"15:16":[8,14,14],"16:0":[8,0,0],"16:1":[8,0,11],"16:2":[8,0,11],"16:3":[8,0,11],"16:4":[8,0,11],"16:5":[8,0,11],"16:6":[8,0,11],"16:7":[8,0,11],"16:8":[8,0,11],"16:9":[8,0,11],"16:10":[8,0,11],"16:11":[8,0,11],"16:12":[8,12,12],"16:13":[2],"16:14":[2],"16:15":[2],"16:16":[2],"16:17":[2],"16:18":[2],"16:19":[2],"16:20":[2],"16:21":[2],"16:22":[2],"16:23":[2],"16:24":[2],"17:0":[8,0,0],"17:1":[8,0,11],"17:2":[8,0,11],"17:3":[8,0,11],"17:4":[8,0,11],"17:5":[8,0,11],"17:6":[8,0,11],"17:7":[8,0,11],"17:8":[8,0,11],"17:9":[8,0,11],"17:10":[8,0,11],"17:11":[8,0,11],"17:12":[8,12,12],"17:13":[2],"17:14":[2],"17:15":[2],"17:16":[2],"17:17":[2],"17:18":[2],"17:19":[2],"17:20":[2],"17:21":[2],"17:22":[2],"17:23":[2],"17:24":[2],"17:25":[2],"17:26":[2],"17:27":[2],"17:28":[2],"17:29":[2],"17:30":[2],"17:31":[2],"17:32":[2],"17:33":[2],"17:34":[8,34,34],"17:35":[8,34,34],"17:36":[8,34,34],"18:0":[8,0,0],"18:1":[8,0,0],"18:2":[8,0,0],"19:0":[8,0,0],"19:1":[0,0,1],"19:2":[0,0,7],"19:3":[0,0,7],"19:4":[0,0,7],"19:5":[0,0,7],"19:6":[0,0,7],"19:7":[0,0,7],"19:8":[9,8,8],"19:9":[9,8,8],"19:10":[9,8,8],"20:0":[9,0,0],"20:1":[9,0,6],"20:2":[9,0,6],"20:3":[9,0,6],"20:4":[9,0,6],"20:5":[9,0,6],"20:6":[9,0,6],"20:7":[9,7,7],"20:8":[9,7,11],"20:9":[9,7,11],"20:10":[9,7,11],"20:11":[9,7,11],"20:12":[9,12,12],"20:13":[2],"20:14":[2],"20:15":[2],"20:16":[2],"20:17":[2],"20:18":[2],"20:19":[2],"20:20":[2],"20:21":[2],"20:22":[2],"20:23":[2],"21:0":[9,0,0],"21:1":[9,0,8],"21:2":[9,0,8],"21:3":[9,0,8],"21:4":[9,0,8],"21:5":[9,0,8],"21:6":[9,0,8],"21:7":[9,0,8],"21:8":[9,0,8],"21:9":[9,9,9],"21:10":[9,9,11],"21:11":[9,9,11],"21:12":[9,12,12],"21:13":[2],"21:14":[2],"21:15":[2],"21:16":[2],"21:17":[2],"21:18":[2],"21:19":[2],"21:20":[2],"21:21":[2],"21:22":[2],"21:23":[2],"21:24":[2],"21:25":[2],"21:26":[2],"21:27":[2],"21:28":[2],"21:29":[2],"21:30":[2],"21:31":[2],"21:32":[9,32,32],"21:33":[9,32,32],"21:34":[9,32,32],"22:0":[9,0,0],"22:1":[9,0,4],"22:2":[9,0,4],"22:3":[9,0,4],"22:4":[9,0,4],"22:5":[9,5,5],"22:6":[9,5,9],"22:7":[9,5,9],"22:8":[9,5,9],"22:9":[9,5,9],"22:10":[9,10,10],"22:11":[2],"22:12":[2],"22:13":[2],"22:14":[2],"22:15":[2],"22:16":[2],"22:17":[2],"22:18":[2],"22:19":[2],"22:20":[2],"22:21":[2],"22:22":[2],"22:23":[2],"22:24":[2],"22:25":[2],"22:26":[2],"22:27":[2],"22:28":[2],"22:29":[2],"22:30":[2],"22:31":[9,31,31],"22:32":[9,31,31],"22:33":[9,31,31],"23:0":[9,0,0],"23:1":[9,0,0],"23:2":[9,0,0],"24:0":[9,0,0],"24:1":[0,0,1],"24:2":[0,0,7],"24:3":[0,0,7],"24:4":[0,0,7],"24:5":[0,0,7],"24:6":[0,0,7],"24:7":[0,0,7],"24:8":[0,0,8],"24:9":[0,0,17],"24:10":[0,0,17],"24:11":[0,0,17],"24:12":[0,0,17],"24:13":[0,0,17],"24:14":[0,0,17],"24:15":[0,0,17],"24:16":[0,0,17],"24:17":[0,0,17],"24:18":[10,18,18],"24:19":[10,18,18],"24:20":[10,18,18],"25:0":[10,0,0],"25:1":[10,0,13],"25:2":[10,0,13],"25:3":[10,0,13],"25:4":[10,0,13],"25:5":[10,0,13],"25:6":[10,0,13],"25:7":[10,0,13],"25:8":[10,0,13],"25:9":[10,0,13],"25:10":[10,0,13],"25:11":[10,0,13],"25:12":[10,0,13],"25:13":[10,0,13],"25:14":[10,14,14],"25:15":[2],"25:16":[2],"25:17":[2],"25:18":[2],"25:19":[2],"25:20":[2],"25:21":[2],"25:22":[2],"25:23":[2],"25:24":[2],"25:25":[2],"26:0":[10,0,0],"26:1":[10,0,0],"26:2":[10,0,0],"27:0":[10,0,0],"27:1":[0,0,1],"27:2":[0,0,8],"27:3":[0,0,8],"27:4":[0,0,8],"27:5":[0,0,8],"27:6":[0,0,8],"27:7":[0,0,8],"27:8":[0,0,8],"27:9":[0,0,9],"27:10":[0,0,12],"27:11":[0,0,12],"27:12":[0,0,12],"27:13":[11,13,13],"27:14":[11,13,13],"27:15":[11,13,13],"28:0":[11,0,0],"28:1":[11,0,4],"28:2":[11,0,4],"28:3":[11,0,4],"28:4":[11,0,4],"28:5":[11,5,5],"28:6":[12,7,17],"28:7":[12,7,7],"28:8":[13,8,16],"28:9":[13,8,16],"28:10":[13,8,16],"28:11":[13,8,16],"28:12":[13,8,16],"28:13":[13,8,16],"28:14":[13,8,16],"28:15":[13,8,16],"28:16":[13,8,16],"28:17":[12,17,17],"28:18":[12,17,17],"28:19":[12,17,17],"29:0":[14,0,0],"29:1":[14,0,6],"29:2":[14,0,6],"29:3":[14,0,6],"29:4":[14,0,6],"29:5":[14,0,6],"29:6":[14,0,6],"29:7":[14,7,7],"29:8":[2],"29:9":[2],"29:10":[2],"29:11":[2],"29:12":[2],"30:0":[14,0,0],"30:1":[14,0,0],"30:2":[14,0,0],"31:0":[14,0,0],"31:1":[0,0,1],"31:2":[0,0,8],"31:3":[0,0,8],"31:4":[0,0,8],"31:5":[0,0,8],"31:6":[0,0,8],"31:7":[0,0,8],"31:8":[0,0,8],"31:9":[0,0,9],"31:10":[0,0,14],"31:11":[0,0,14],"31:12":[0,0,14],"31:13":[0,0,14],"31:14":[0,0,14],"31:15":[15,15,15],"31:16":[15,15,15],"31:17":[15,15,15],"32:0":[15,0,0],"32:1":[15,0,12],"32:2":[15,0,12],"32:3":[15,0,12],"32:4":[15,0,12],"32:5":[15,0,12],"32:6":[15,0,12],"32:7":[15,0,12],"32:8":[15,0,12],"32:9":[15,0,12],"32:10":[15,0,12],"32:11":[15,0,12],"32:12":[15,0,12],"32:13":[15,13,13],"32:14":[2],"32:15":[2],"32:16":[2],"32:17":[2],"32:18":[2],"32:19":[2],"32:20":[2],"32:21":[2],"33:0":[15,0,0],"33:1":[15,0,0],"33:2":[15,0,0],"34:0":[15,0,0],"34:1":[0,0,1],"34:2":[0,0,10],"34:3":[0,0,10],"34:4":[0,0,10],"34:5":[0,0,10],"34:6":[0,0,10],"34:7":[0,0,10],"34:8":[0,0,10],"34:9":[0,0,10],"34:10":[0,0,10],"34:11":[2],"34:12":[2],"34:13":[2],"35:0":[2],"35:1":[2],"35:2":[2],"35:3":[2],"35:4":[2],"35:5":[2],"35:6":[2],"35:7":[2],"35:8":[2],"35:9":[2],"35:10":[2],"35:11":[2],"35:12":[2],"35:13":[2],"35:14":[2],"35:15":[2],"35:16":[2],"35:17":[2],"35:18":[2],"35:19":[2],"36:0":[2],"36:1":[2],"36:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"2:1":2,"2:2":2,"2:3":2,"2:4":2,"2:5":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"4:7":3,"4:8":3,"4:9":3,"4:10":4,"4:11":4,"4:12":4,"7:2":0,"7:3":0,"7:4":0,"7:5":0,"7:6":0,"7:7":0,"7:8":0,"10:3":5,"10:4":5,"10:5":5,"10:6":5,"10:7":5,"10:8":5,"10:9":5,"10:10":5,"10:11":5,"10:12":5,"10:13":5,"11:3":0,"11:4":0,"11:5":0,"11:6":0,"11:7":0,"11:8":0,"11:9":0,"12:2":6,"12:3":6,"12:4":6,"12:5":6,"12:6":6,"12:7":6,"12:8":6,"12:9":6,"15:2":7,"15:3":7,"15:4":7,"15:5":7,"15:6":7,"15:7":7,"15:8":7,"15:9":7,"15:10":7,"15:11":7,"15:12":7,"15:13":7,"19:2":8,"19:3":8,"19:4":8,"19:5":8,"19:6":8,"19:7":8,"24:2":8,"24:3":8,"24:4":8,"24:5":8,"24:6":8,"24:7":8,"24:8":8,"24:9":9,"24:10":9,"24:11":9,"24:12":9,"24:13":9,"24:14":9,"24:15":9,"24:16":9,"24:17":9,"27:2":10,"27:3":10,"27:4":10,"27:5":10,"27:6":10,"27:7":10,"27:8":10,"27:9":10,"27:10":10,"27:11":10,"27:12":10,"28:1":11,"28:2":11,"28:3":11,"28:4":11,"28:5":11,"28:6":12,"28:7":13,"28:8":12,"28:9":12,"28:10":12,"28:11":12,"28:12":12,"28:13":12,"28:14":12,"28:15":12,"28:16":12,"28:17":13,"28:18":13,"28:19":13,"31:2":14,"31:3":14,"31:4":14,"31:5":14,"31:6":14,"31:7":14,"31:8":14,"31:9":14,"31:10":14,"31:11":14,"31:12":14,"31:13":14,"31:14":14,"34:2":15,"34:3":15,"34:4":15,"34:5":15,"34:6":15,"34:7":15,"34:8":15,"34:9":15,"34:10":15},"definition":{},"versionContext":{"16:13":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":14,"endCharacter":22}},"16:14":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":14,"endCharacter":14}},"16:15":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:16":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:17":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:18":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:19":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:20":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:21":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:22":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"16:23":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"16:24":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"17:25":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":26,"endCharacter":32}},"17:26":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":26,"endCharacter":26}},"17:27":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:28":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:29":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:30":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:31":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:32":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":32,"endCharacter":32}}},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"import_std","kind":"template","detail":"boolean","insertSnippet":"import_std = true","documentation":"replaced by [package].standard and the module scan Legacy key."},{"label":"modules","kind":"template","detail":"boolean","insertSnippet":"modules = true","documentation":"replaced by the module scan Legacy key."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"replaced by [package].standard Legacy key."}],[{"label":"name = \"version\"","kind":"template","detail":"SemVer version dependency","documentation":"Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported.","insertSnippet":"${1:name} = \"${2:1.0.0}\""},{"label":"name = { path = ... }","kind":"template","detail":"Path dependency (local development)","insertSnippet":"${1:name} = { path = \"${2:../mylib}\" }"},{"label":"name = { git = ..., tag = ... }","kind":"template","detail":"Git dependency (one of tag / branch / rev)","insertSnippet":"${1:name} = { git = \"${2:https://github.com/user/repo.git}\", tag = \"${3:v1.0.0}\" }"},{"label":"name = { version = ..., features = [...] }","kind":"template","detail":"Long dep spec: request a feature of that dependency","insertSnippet":"${1:name} = { version = \"${2:1.0}\", features = [\"${3:feature}\"] }"},{"label":"name = { version = ..., tools = [...] }","kind":"template","detail":"Host tools produced by the dependency (must be a bin target of that package)","insertSnippet":"${1:name} = { version = \"${2:1.0}\", tools = [\"${3:protoc}\"] }"}],[{"label":"deps","kind":"template","detail":"array","insertSnippet":"deps = []","documentation":"the project's host build tools: \"@\" or tables Since 0.0.77."},{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"tool = \"version\"","kind":"template","detail":"Pin the xlings tool version","insertSnippet":"${1:node} = \"${2:24.19.0}\""}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"opt","kind":"template","detail":"enum: s | z","insertSnippet":"opt = \"s\"","documentation":"an -O level: a number, or \"s\"/\"z\""},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"mcpp","body":"type: `string`\n\nsince: 2026.9.28.3\n\nrelease floor, only the \">=\" form is accepted (docs/04 §2.1)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"[language]","body":"plane: `unclassified`\n\nlegacy: replaced by `[package].standard`\n\nkeys: 3","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#41-legacy-language-compatibility-layer"},{"title":"[dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[xlings]","body":"plane: `tool`\n\nkeys: 4","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[xlings.workspace]","body":"plane: `tool`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"nonsense","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[profile]","body":"plane: `compile`\n\nkeys: 10","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"[toolchain]","body":"plane: `compile`\n\nkeys: 3 (open: the user picks the names)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#27-toolchain--toolchain-configuration"}]} diff --git a/tests/rules/golden/catalogue.json b/tests/rules/golden/catalogue.json new file mode 100644 index 0000000..d621fd3 --- /dev/null +++ b/tests/rules/golden/catalogue.json @@ -0,0 +1 @@ +{"snippets":[{"filterText":"action","label":"action ... (typed action block)","detail":"mcpp::action with id, role, description, arg/input/output and submit","documentation":"The five roles (SPEC-007 R3.6): source, check, object, artifact, prepare.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,object,artifact,prepare|};\n${1:handle}.description = \"${4:description}\";\n${1:handle}.arg(\"${5:argument}\")\n .input(\"${6:input}\")\n .output(\"${7:output}\")\n .submit();"},{"filterText":"action","label":"action ... (prepare block)","detail":"mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)","documentation":"A prepare action must declare output_dir; the engine refuses one without it.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"},{"filterText":"cxxflag","label":"cxxflag(...)","detail":"mcpp:cxxflag= · slot CxxFlags","insertText":"cxxflag(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"cflag","label":"cflag(...)","detail":"mcpp:cflag= · slot CFlags","insertText":"cflag(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"link_lib","label":"link_lib(...)","detail":"mcpp:link-lib= · slot LdFlags","insertText":"link_lib(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"link_search","label":"link_search(...)","detail":"mcpp:link-search= · slot LdFlags","insertText":"link_search(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"cfg","label":"cfg(...)","detail":"mcpp:cfg= · slot Defines","insertText":"cfg(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"generated","label":"generated(...)","detail":"mcpp:generated= · slot Generated","insertText":"generated(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"source","label":"source(...)","detail":"mcpp:source= · slot Sources","insertText":"source(\"${1:value}\")","documentation":"SPEC-007 R2.1, R3.3, R3.6, R3.9"},{"filterText":"runner","label":"runner(...)","detail":"mcpp:runner= · slot Runner","insertText":"runner(\"${1:value}\")","documentation":"SPEC-007 R9.10 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192)"},{"filterText":"runner_named","label":"runner_named(...)","detail":"mcpp:runner-named= · slot NamedRunner","insertText":"runner_named(\"${1:value}\")"},{"filterText":"runner_longlived","label":"runner_longlived(...)","detail":"mcpp:runner-longlived= · slot RunnerLongLived","insertText":"runner_longlived(\"${1:value}\")"},{"filterText":"run_exclusive","label":"run_exclusive(...)","detail":"mcpp:run-exclusive= · slot RunExclusive","insertText":"run_exclusive(\"${1:value}\")"},{"filterText":"link_script","label":"link_script(...)","detail":"mcpp:link-script= · slot LdFlags","insertText":"link_script(\"${1:value}\")"},{"filterText":"link_flag","label":"link_flag(...)","detail":"mcpp:link-flag= · slot LdFlags","insertText":"link_flag(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"include_dir","label":"include_dir(...)","detail":"mcpp:include-dir= · slot IncludeDirs","insertText":"include_dir(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"include_dir_after","label":"include_dir_after(...)","detail":"mcpp:include-dir-after= · slot IncludeDirsAfter","insertText":"include_dir_after(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"rerun_if_changed","label":"rerun_if_changed(...)","detail":"mcpp:rerun-if-changed= · slot RerunFiles","insertText":"rerun_if_changed(\"${1:value}\")"},{"filterText":"rerun_if_env_changed","label":"rerun_if_env_changed(...)","detail":"mcpp:rerun-if-env-changed= · slot RerunEnv","insertText":"rerun_if_env_changed(\"${1:value}\")"},{"filterText":"rerun_if_changed_glob","label":"rerun_if_changed_glob(...)","detail":"mcpp:rerun-if-changed-glob= · slot RerunGlobs","insertText":"rerun_if_changed_glob(\"${1:value}\")"},{"filterText":"warning","label":"warning(...)","detail":"mcpp:warning= · slot Warnings","insertText":"warning(\"${1:value}\")","documentation":"SPEC-007 R1.2, R2.1, R9.4 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212)"},{"filterText":"diagnostic","label":"diagnostic(...)","detail":"mcpp:diagnostic= · slot Diagnostics","insertText":"diagnostic(\"${1:value}\")"},{"filterText":"fact","label":"fact(...)","detail":"mcpp:fact= · slot Facts","insertText":"fact(\"${1:value}\")","documentation":"SPEC-007 R2.1 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952)"},{"filterText":"floor","label":"floor(...)","detail":"mcpp:floor= · slot Floors","insertText":"floor(\"${1:value}\")","documentation":"SPEC-007 R2.1 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952)"},{"filterText":"pack_format","label":"pack_format(...)","detail":"mcpp:pack-format= · slot PackFormats","insertText":"pack_format(\"${1:value}\")","documentation":"SPEC-007 R9.10"},{"filterText":"windows_subsystem","label":"windows_subsystem(...)","detail":"mcpp:windows-subsystem= · slot WindowsSubsystem","insertText":"windows_subsystem(\"${1:value}\")"},{"filterText":"windows_entry","label":"windows_entry(...)","detail":"mcpp:windows-entry= · slot WindowsEntry","insertText":"windows_entry(\"${1:value}\")"},{"filterText":"deploy","label":"deploy(...)","detail":"mcpp:deploy= · slot Deploy","insertText":"deploy(\"${1:value}\")","documentation":"SPEC-007 R2.1, R4.2, R9.2, R9.5 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11)"},{"filterText":"runtime_search_dir","label":"runtime_search_dir(...)","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","insertText":"runtime_search_dir(\"${1:value}\")","documentation":"SPEC-007 R2.1"},{"filterText":"decision","label":"decision(...)","detail":"mcpp:decision= · slot ToolDecisions","insertText":"decision(\"${1:value}\")","documentation":"SPEC-007 R6.5, R9.2"},{"filterText":"xpkg_request","label":"xpkg_request(...)","detail":"mcpp:xpkg-request= · slot XpkgRequests","insertText":"xpkg_request(\"${1:value}\")"},{"filterText":"toolchain","label":"toolchain(...)","detail":"mcpp:toolchain= · slot ToolchainStatement","insertText":"toolchain(\"${1:value}\")","documentation":"SPEC-007 R9.2, R9.9"},{"filterText":"link_start","label":"link_start(...)","detail":"mcpp:link-start= · slot LinkStart","insertText":"link_start(\"${1:value}\")","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)"},{"filterText":"link_driver","label":"link_driver(...)","detail":"mcpp:link-driver= · slot LinkDriver","insertText":"link_driver(\"${1:value}\")","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)"},{"filterText":"link_input","label":"link_input(...)","detail":"mcpp:link-input= · slot LinkInputs","insertText":"link_input(\"${1:value}\")","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)"},{"filterText":"artifact_name","label":"artifact_name(...)","detail":"mcpp:artifact-name= · slot ArtifactNames","insertText":"artifact_name(\"${1:value}\")","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)"},{"filterText":"seal","label":"seal(...)","detail":"mcpp:seal= · slot Seal","insertText":"seal(\"${1:value}\")","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)"},{"filterText":"run_host","label":"run_host(...)","detail":"mcpp:run-host= · slot RunHost","insertText":"run_host(\"${1:value}\")","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)"}],"modules":[{"name":"std","description":"The C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+).","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#import-std-mcpp-2026821"},{"name":"std.compat","description":"The C++ standard library plus the C library names in the global namespace; `import std;` and `import std.compat;` may be used together.","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#import-std-mcpp-2026821"},{"name":"mcpp","description":"The mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1).","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#the-interfaces-name-mcppcore-protocol-14"},{"name":"mcpp.core","description":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2).","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#the-interfaces-name-mcppcore-protocol-14"},{"name":"mcpp.plugins.*","description":"The official general-purpose library (L2), provided by the `plugins-core` feature and built only on the L1 engine interface, e.g. `mcpp.plugins.tool` (SPEC-007 §9).","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#the-names-of-a-plugins-modules"},{"name":"mcpp.deps.*","description":"Official dependency-adapter plugins (L3), e.g. a package manager or external build system brought into the graph (SPEC-007 §0, §9).","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#the-names-of-a-plugins-modules"},{"name":"mcpp.rules.*","description":"Official rule packages (L3): code generation and device-language toolchains (SPEC-007 §0, §9).","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#the-names-of-a-plugins-modules"},{"name":"mcpp.dist.*","description":"Official distribution members (L3): they turn a link artifact into something installable, e.g. a WiX or APK payload (SPEC-007 §0, §9).","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#the-names-of-a-plugins-modules"},{"name":"mcpp.tools.*","description":"Official tool namespaces (L3), e.g. `mcpp.tools.island` helpers described in docs/31 (SPEC-007 §9).","docsUrl":"https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md#the-names-of-a-plugins-modules"}],"known":[["std","std"],["std.compat","std.compat"],["mcpp","mcpp"],["mcpp.core","mcpp.core"],["mcpp.plugins.tool","mcpp.plugins.tool"],["mcpp.plugins",null],["mcpp.deps.vcpkg","mcpp.deps.vcpkg"],["mcpp.rules.qt.x","mcpp.rules.qt.x"],["mcpp.toml",null],["mcpp.tools.",null],["mcpp.dist.wix","mcpp.dist.wix"]]} diff --git a/tests/rules/golden/comments.toml.json b/tests/rules/golden/comments.toml.json new file mode 100644 index 0000000..e7dc28f --- /dev/null +++ b/tests/rules/golden/comments.toml.json @@ -0,0 +1 @@ +{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,1,1],"0:2":[0,2,2],"0:3":[0,3,3],"0:4":[0,4,4],"0:5":[0,5,5],"0:6":[0,6,6],"0:7":[0,7,7],"0:8":[0,8,8],"0:9":[0,9,9],"0:10":[0,10,10],"0:11":[0,11,11],"0:12":[0,12,12],"0:13":[0,13,13],"0:14":[0,14,14],"0:15":[0,15,15],"0:16":[0,16,16],"0:17":[0,16,16],"0:18":[0,16,16],"1:0":[0,0,0],"1:1":[0,0,0],"1:2":[0,0,0],"2:0":[0,0,0],"2:1":[0,1,1],"2:2":[0,2,2],"2:3":[0,3,3],"2:4":[0,3,3],"2:5":[0,3,3],"3:0":[0,0,0],"3:1":[0,0,0],"3:2":[0,0,0]},"hover":{},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}]],"hovers":[]} diff --git a/tests/rules/golden/complete.toml.json b/tests/rules/golden/complete.toml.json new file mode 100644 index 0000000..34d5209 --- /dev/null +++ b/tests/rules/golden/complete.toml.json @@ -0,0 +1 @@ +{"diagnostics":{"default":[{"code":"mcpp.manifest.unknownKey","message":"unknown key simd in [features]","severity":"warning","line":34,"startCharacter":1,"endCharacter":5,"relatedKey":"simd"},{"code":"mcpp.manifest.unknownKey","message":"unknown key extra in [features]","severity":"warning","line":35,"startCharacter":1,"endCharacter":6,"relatedKey":"extra"},{"code":"mcpp.manifest.unknownKey","message":"unknown key description2 in [runtime]","severity":"warning","line":77,"startCharacter":1,"endCharacter":13,"relatedKey":"description2"}],"quiet":[{"code":"mcpp.manifest.unknownKey","message":"unknown key simd in [features]","severity":"info","line":34,"startCharacter":1,"endCharacter":5,"relatedKey":"simd"},{"code":"mcpp.manifest.unknownKey","message":"unknown key extra in [features]","severity":"info","line":35,"startCharacter":1,"endCharacter":6,"relatedKey":"extra"},{"code":"mcpp.manifest.unknownKey","message":"unknown key description2 in [runtime]","severity":"info","line":77,"startCharacter":1,"endCharacter":13,"relatedKey":"description2"}]},"completion":{"0:0":[0,0,0],"0:1":[0,1,1],"0:2":[0,2,2],"0:3":[0,3,3],"0:4":[0,4,4],"0:5":[0,5,5],"0:6":[0,6,6],"0:7":[0,7,7],"0:8":[0,8,8],"0:9":[0,9,9],"0:10":[0,10,10],"0:11":[0,11,11],"0:12":[0,12,12],"0:13":[0,13,13],"0:14":[0,14,14],"0:15":[0,15,15],"0:16":[0,16,16],"0:17":[0,17,17],"0:18":[0,18,18],"0:19":[0,19,19],"0:20":[0,20,20],"0:21":[0,21,21],"0:22":[0,22,22],"0:23":[0,23,23],"0:24":[0,24,24],"0:25":[0,25,25],"0:26":[0,26,26],"0:27":[0,27,27],"0:28":[0,28,28],"0:29":[0,29,29],"0:30":[0,30,30],"0:31":[0,31,31],"0:32":[0,32,32],"0:33":[0,33,33],"0:34":[0,34,34],"0:35":[0,35,35],"0:36":[0,36,36],"0:37":[0,37,37],"0:38":[0,38,38],"0:39":[0,39,39],"0:40":[0,40,40],"0:41":[0,41,41],"0:42":[0,42,42],"0:43":[0,43,43],"0:44":[0,44,44],"0:45":[0,45,45],"0:46":[0,46,46],"0:47":[0,47,47],"0:48":[0,47,47],"0:49":[0,47,47],"1:0":[0,0,0],"1:1":[0,0,1],"1:2":[0,0,8],"1:3":[0,0,8],"1:4":[0,0,8],"1:5":[0,0,8],"1:6":[0,0,8],"1:7":[0,0,8],"1:8":[0,0,8],"1:9":[1,9,9],"1:10":[1,9,9],"1:11":[1,9,9],"2:0":[1,0,0],"2:1":[1,0,4],"2:2":[1,0,4],"2:3":[1,0,4],"2:4":[1,0,4],"2:5":[1,5,5],"2:6":[1,6,6],"2:7":[1,7,7],"2:8":[1,8,8],"2:9":[1,9,9],"2:10":[1,10,10],"2:11":[1,11,11],"2:12":[1,12,12],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"2:18":[2],"2:19":[2],"2:20":[2],"2:21":[2],"2:22":[2],"2:23":[2],"2:24":[2],"2:25":[2],"2:26":[2],"3:0":[3,0,0],"3:1":[3,0,7],"3:2":[3,0,7],"3:3":[3,0,7],"3:4":[3,0,7],"3:5":[3,0,7],"3:6":[3,0,7],"3:7":[3,0,7],"3:8":[3,8,8],"3:9":[3,9,9],"3:10":[3,10,10],"3:11":[3,11,11],"3:12":[3,12,12],"3:13":[2],"3:14":[2],"3:15":[2],"3:16":[2],"3:17":[2],"3:18":[2],"3:19":[2],"3:20":[2],"3:21":[2],"3:22":[2],"3:23":[2],"4:0":[4,0,0],"4:1":[4,0,4],"4:2":[4,0,4],"4:3":[4,0,4],"4:4":[4,0,4],"4:5":[4,5,5],"4:6":[4,6,6],"4:7":[4,7,7],"4:8":[4,8,8],"4:9":[4,9,9],"4:10":[4,10,10],"4:11":[4,11,11],"4:12":[4,12,12],"4:13":[2],"4:14":[2],"4:15":[2],"4:16":[2],"4:17":[2],"4:18":[2],"4:19":[2],"4:20":[2],"4:21":[2],"4:22":[2],"4:23":[2],"4:24":[2],"4:25":[2],"4:26":[2],"4:27":[2],"4:28":[2],"4:29":[2],"4:30":[2],"4:31":[2],"4:32":[2],"5:0":[5,0,0],"5:1":[5,0,8],"5:2":[5,0,8],"5:3":[5,0,8],"5:4":[5,0,8],"5:5":[5,0,8],"5:6":[5,0,8],"5:7":[5,0,8],"5:8":[5,0,8],"5:9":[5,9,9],"5:10":[5,10,10],"5:11":[5,11,11],"5:12":[5,12,12],"5:13":[6,14,21],"5:14":[6,14,14],"5:15":[7,15,20],"5:16":[7,15,20],"5:17":[7,15,20],"5:18":[7,15,20],"5:19":[7,15,20],"5:20":[7,15,20],"5:21":[6,21,21],"5:22":[6,21,21],"5:23":[6,21,21],"6:0":[8,0,0],"6:1":[8,0,11],"6:2":[8,0,11],"6:3":[8,0,11],"6:4":[8,0,11],"6:5":[8,0,11],"6:6":[8,0,11],"6:7":[8,0,11],"6:8":[8,0,11],"6:9":[8,0,11],"6:10":[8,0,11],"6:11":[8,0,11],"6:12":[8,12,12],"6:13":[2],"6:14":[2],"6:15":[2],"6:16":[2],"6:17":[2],"6:18":[2],"6:19":[2],"6:20":[2],"6:21":[2],"6:22":[2],"6:23":[2],"6:24":[2],"6:25":[2],"6:26":[2],"6:27":[2],"6:28":[2],"6:29":[2],"6:30":[2],"6:31":[2],"6:32":[2],"6:33":[2],"6:34":[2],"6:35":[2],"6:36":[2],"6:37":[2],"6:38":[2],"6:39":[2],"6:40":[2],"6:41":[2],"6:42":[2],"6:43":[2],"6:44":[2],"6:45":[2],"6:46":[2],"7:0":[9,0,0],"7:1":[9,0,7],"7:2":[9,0,7],"7:3":[9,0,7],"7:4":[9,0,7],"7:5":[9,0,7],"7:6":[9,0,7],"7:7":[9,0,7],"7:8":[9,8,8],"7:9":[9,9,9],"7:10":[9,10,10],"7:11":[9,11,11],"7:12":[9,12,12],"7:13":[2],"7:14":[2],"7:15":[2],"7:16":[2],"7:17":[2],"7:18":[2],"7:19":[2],"7:20":[2],"7:21":[2],"7:22":[2],"7:23":[2],"7:24":[2],"7:25":[2],"7:26":[2],"7:27":[2],"7:28":[2],"7:29":[2],"7:30":[2],"7:31":[2],"7:32":[2],"7:33":[2],"7:34":[2],"7:35":[9,35,35],"7:36":[9,35,35],"7:37":[9,35,35],"8:0":[10,0,0],"8:1":[10,0,0],"8:2":[10,0,0],"9:0":[10,0,0],"9:1":[0,0,1],"9:2":[0,0,6],"9:3":[0,0,6],"9:4":[0,0,6],"9:5":[0,0,6],"9:6":[0,0,6],"9:7":[11,7,7],"9:8":[11,7,7],"9:9":[11,7,7],"10:0":[11,0,0],"10:1":[11,0,8],"10:2":[11,0,8],"10:3":[11,0,8],"10:4":[11,0,8],"10:5":[11,0,8],"10:6":[11,0,8],"10:7":[11,0,8],"10:8":[11,0,8],"10:9":[11,9,9],"10:10":[2],"10:11":[2],"10:12":[2],"10:13":[2],"10:14":[2],"10:15":[2],"10:16":[2],"10:17":[2],"10:18":[2],"10:19":[2],"10:20":[2],"10:21":[2],"10:22":[2],"10:23":[2],"10:24":[2],"10:25":[2],"10:26":[2],"10:27":[11,27,27],"10:28":[11,27,27],"10:29":[11,27,27],"11:0":[12,0,0],"11:1":[12,0,12],"11:2":[12,0,12],"11:3":[12,0,12],"11:4":[12,0,12],"11:5":[12,0,12],"11:6":[12,0,12],"11:7":[12,0,12],"11:8":[12,0,12],"11:9":[12,0,12],"11:10":[12,0,12],"11:11":[12,0,12],"11:12":[12,0,12],"11:13":[12,13,13],"11:14":[13,15,21],"11:15":[13,15,15],"11:16":[14,16,20],"11:17":[14,16,20],"11:18":[14,16,20],"11:19":[14,16,20],"11:20":[14,16,20],"11:21":[13,21,21],"11:22":[13,21,21],"11:23":[13,21,21],"12:0":[15,0,0],"12:1":[15,0,7],"12:2":[15,0,7],"12:3":[15,0,7],"12:4":[15,0,7],"12:5":[15,0,7],"12:6":[15,0,7],"12:7":[15,0,7],"12:8":[15,8,8],"12:9":[15,9,9],"12:10":[2],"12:11":[2],"12:12":[2],"12:13":[2],"12:14":[2],"13:0":[2],"13:1":[2],"13:2":[2],"13:3":[2],"13:4":[2],"13:5":[2],"13:6":[2],"13:7":[2],"13:8":[2],"13:9":[2],"13:10":[2],"13:11":[2],"13:12":[2],"13:13":[2],"13:14":[2],"13:15":[2],"13:16":[2],"13:17":[2],"13:18":[2],"13:19":[2],"13:20":[2],"13:21":[2],"13:22":[2],"14:0":[2],"14:1":[2],"14:2":[2],"14:3":[2],"14:4":[2],"14:5":[2],"14:6":[2],"14:7":[2],"14:8":[2],"14:9":[2],"14:10":[2],"14:11":[2],"14:12":[2],"14:13":[2],"14:14":[2],"14:15":[2],"14:16":[2],"14:17":[2],"14:18":[2],"14:19":[2],"14:20":[2],"14:21":[2],"14:22":[2],"14:23":[2],"14:24":[2],"14:25":[2],"14:26":[2],"14:27":[2],"14:28":[2],"14:29":[2],"14:30":[2],"14:31":[2],"14:32":[2],"14:33":[2],"14:34":[2],"14:35":[2],"14:36":[2],"14:37":[2],"14:38":[2],"14:39":[2],"14:40":[2],"14:41":[2],"14:42":[2],"14:43":[2],"14:44":[2],"14:45":[2],"14:46":[2],"14:47":[2],"14:48":[2],"14:49":[2],"14:50":[2],"14:51":[2],"15:0":[2],"15:1":[16,1,1],"15:2":[16,1,1],"15:3":[16,1,1],"16:0":[16,0,0],"16:1":[16,0,0],"16:2":[16,0,0],"17:0":[16,0,0],"17:1":[0,0,1],"17:2":[0,0,13],"17:3":[0,0,13],"17:4":[0,0,13],"17:5":[0,0,13],"17:6":[0,0,13],"17:7":[0,0,13],"17:8":[0,0,13],"17:9":[0,0,13],"17:10":[0,0,13],"17:11":[0,0,13],"17:12":[0,0,13],"17:13":[0,0,13],"17:14":[17,14,14],"17:15":[17,14,14],"17:16":[17,14,14],"18:0":[17,0,0],"18:1":[17,0,3],"18:2":[17,0,3],"18:3":[17,0,3],"18:4":[17,4,4],"18:5":[17,5,5],"18:6":[17,6,6],"18:7":[17,7,7],"18:8":[17,8,8],"18:9":[17,9,9],"18:10":[17,10,10],"18:11":[17,11,11],"18:12":[17,12,12],"18:13":[17,13,13],"18:14":[2],"18:15":[2],"18:16":[2],"18:17":[2],"18:18":[2],"18:19":[2],"18:20":[2],"18:21":[2],"18:22":[2],"18:23":[2],"18:24":[2],"18:25":[2],"19:0":[17,0,0],"19:1":[17,0,6],"19:2":[17,0,6],"19:3":[17,0,6],"19:4":[17,0,6],"19:5":[17,0,6],"19:6":[17,0,6],"19:7":[17,7,7],"19:8":[17,7,11],"19:9":[17,7,11],"19:10":[17,7,11],"19:11":[17,7,11],"19:12":[17,12,12],"19:13":[17,13,13],"19:14":[2],"19:15":[2],"19:16":[2],"19:17":[2],"19:18":[2],"19:19":[2],"19:20":[2],"19:21":[2],"19:22":[2],"19:23":[2],"19:24":[2],"19:25":[2],"19:26":[2],"19:27":[2],"19:28":[2],"19:29":[2],"19:30":[2],"19:31":[2],"19:32":[2],"19:33":[2],"19:34":[2],"19:35":[2],"19:36":[17,36,36],"19:37":[17,36,36],"19:38":[17,36,36],"20:0":[17,0,0],"20:1":[17,0,5],"20:2":[17,0,5],"20:3":[17,0,5],"20:4":[17,0,5],"20:5":[17,0,5],"20:6":[17,6,6],"20:7":[17,7,7],"20:8":[17,8,8],"20:9":[17,9,9],"20:10":[17,10,10],"20:11":[17,11,11],"20:12":[17,12,12],"20:13":[17,13,13],"20:14":[2],"20:15":[2],"20:16":[2],"20:17":[2],"20:18":[2],"20:19":[2],"20:20":[2],"20:21":[2],"20:22":[2],"20:23":[2],"20:24":[2],"20:25":[2],"20:26":[2],"20:27":[2],"20:28":[2],"20:29":[2],"20:30":[2],"20:31":[2],"20:32":[2],"20:33":[2],"20:34":[17,34,34],"20:35":[17,34,34],"20:36":[17,34,34],"21:0":[17,0,0],"21:1":[17,0,6],"21:2":[17,0,6],"21:3":[17,0,6],"21:4":[17,0,6],"21:5":[17,0,6],"21:6":[17,0,6],"21:7":[17,7,7],"21:8":[17,8,8],"21:9":[17,9,9],"21:10":[17,10,10],"21:11":[17,11,11],"21:12":[17,12,12],"21:13":[17,13,13],"21:14":[2],"21:15":[2],"21:16":[2],"21:17":[2],"21:18":[2],"21:19":[2],"21:20":[2],"21:21":[2],"21:22":[2],"21:23":[2],"21:24":[2],"21:25":[2],"21:26":[2],"21:27":[2],"21:28":[2],"21:29":[2],"21:30":[2],"21:31":[2],"21:32":[2],"21:33":[2],"21:34":[2],"21:35":[2],"21:36":[2],"21:37":[2],"21:38":[2],"21:39":[2],"21:40":[2],"21:41":[2],"21:42":[2],"21:43":[2],"21:44":[2],"21:45":[2],"21:46":[2],"21:47":[2],"21:48":[2],"21:49":[2],"21:50":[2],"21:51":[2],"21:52":[2],"21:53":[2],"21:54":[2],"21:55":[2],"21:56":[2],"21:57":[2],"21:58":[2],"21:59":[2],"21:60":[2],"21:61":[2],"21:62":[2],"21:63":[2],"21:64":[2],"21:65":[2],"21:66":[2],"21:67":[2],"21:68":[2],"21:69":[2],"21:70":[2],"21:71":[2],"21:72":[2],"21:73":[2],"21:74":[2],"21:75":[17,75,75],"21:76":[17,75,75],"21:77":[17,75,75],"22:0":[17,0,0],"22:1":[17,0,6],"22:2":[17,0,6],"22:3":[17,0,6],"22:4":[17,0,6],"22:5":[17,0,6],"22:6":[17,0,6],"22:7":[17,7,7],"22:8":[17,8,8],"22:9":[17,9,9],"22:10":[17,10,10],"22:11":[17,11,11],"22:12":[17,12,12],"22:13":[17,13,13],"22:14":[2],"22:15":[2],"22:16":[2],"22:17":[2],"22:18":[2],"22:19":[2],"22:20":[2],"22:21":[2],"22:22":[2],"22:23":[2],"22:24":[2],"22:25":[2],"22:26":[2],"22:27":[2],"22:28":[2],"22:29":[2],"22:30":[2],"22:31":[2],"22:32":[2],"22:33":[2],"22:34":[2],"22:35":[17,35,35],"22:36":[17,35,35],"22:37":[17,35,35],"23:0":[17,0,0],"23:1":[17,0,7],"23:2":[17,0,7],"23:3":[17,0,7],"23:4":[17,0,7],"23:5":[17,0,7],"23:6":[17,0,7],"23:7":[17,0,7],"23:8":[17,8,8],"23:9":[17,9,9],"23:10":[17,10,10],"23:11":[17,11,11],"23:12":[17,12,12],"23:13":[17,13,13],"23:14":[2],"23:15":[2],"23:16":[2],"23:17":[2],"23:18":[2],"23:19":[2],"23:20":[2],"23:21":[2],"23:22":[2],"23:23":[2],"23:24":[2],"23:25":[2],"23:26":[2],"23:27":[2],"23:28":[2],"23:29":[2],"23:30":[2],"23:31":[2],"23:32":[2],"23:33":[2],"23:34":[2],"23:35":[2],"23:36":[2],"23:37":[2],"23:38":[2],"23:39":[2],"23:40":[2],"23:41":[2],"23:42":[2],"23:43":[2],"23:44":[2],"23:45":[2],"23:46":[2],"23:47":[2],"23:48":[2],"23:49":[2],"23:50":[2],"23:51":[2],"23:52":[2],"23:53":[2],"23:54":[2],"23:55":[2],"23:56":[2],"23:57":[2],"23:58":[2],"23:59":[2],"23:60":[2],"23:61":[2],"23:62":[2],"23:63":[17,63,63],"23:64":[17,63,63],"23:65":[17,63,63],"24:0":[17,0,0],"24:1":[17,0,0],"24:2":[17,0,0],"25:0":[17,0,0],"25:1":[0,0,1],"25:2":[0,0,13],"25:3":[0,0,13],"25:4":[0,0,13],"25:5":[0,0,13],"25:6":[0,0,13],"25:7":[0,0,13],"25:8":[0,0,13],"25:9":[0,0,13],"25:10":[0,0,13],"25:11":[0,0,13],"25:12":[0,0,13],"25:13":[0,0,13],"25:14":[0,0,14],"25:15":[0,0,22],"25:16":[0,0,22],"25:17":[0,0,22],"25:18":[0,0,22],"25:19":[0,0,22],"25:20":[0,0,22],"25:21":[0,0,22],"25:22":[0,0,22],"25:23":[17,23,23],"25:24":[17,23,23],"25:25":[17,23,23],"26:0":[17,0,0],"26:1":[17,0,2],"26:2":[17,0,2],"26:3":[17,3,3],"26:4":[2],"26:5":[2],"26:6":[2],"26:7":[2],"26:8":[2],"26:9":[2],"26:10":[2],"26:11":[2],"26:12":[2],"26:13":[2],"26:14":[2],"27:0":[17,0,0],"27:1":[17,0,0],"27:2":[17,0,0],"28:0":[17,0,0],"28:1":[0,0,1],"28:2":[0,0,17],"28:3":[0,0,17],"28:4":[0,0,17],"28:5":[0,0,17],"28:6":[0,0,17],"28:7":[0,0,17],"28:8":[0,0,17],"28:9":[0,0,17],"28:10":[0,0,17],"28:11":[0,0,17],"28:12":[0,0,17],"28:13":[0,0,17],"28:14":[0,0,17],"28:15":[0,0,17],"28:16":[0,0,17],"28:17":[0,0,17],"28:18":[17,18,18],"28:19":[17,18,18],"28:20":[17,18,18],"29:0":[17,0,0],"29:1":[17,0,5],"29:2":[17,0,5],"29:3":[17,0,5],"29:4":[17,0,5],"29:5":[17,0,5],"29:6":[17,6,6],"29:7":[2],"29:8":[2],"29:9":[2],"29:10":[2],"29:11":[2],"29:12":[2],"29:13":[2],"29:14":[2],"29:15":[2],"29:16":[2],"29:17":[2],"29:18":[2],"30:0":[17,0,0],"30:1":[17,0,0],"30:2":[17,0,0],"31:0":[17,0,0],"31:1":[0,0,1],"31:2":[0,0,9],"31:3":[0,0,9],"31:4":[0,0,9],"31:5":[0,0,9],"31:6":[0,0,9],"31:7":[0,0,9],"31:8":[0,0,9],"31:9":[0,0,9],"31:10":[18,10,10],"31:11":[18,10,10],"31:12":[18,10,10],"32:0":[18,0,0],"32:1":[18,0,7],"32:2":[18,0,7],"32:3":[18,0,7],"32:4":[18,0,7],"32:5":[18,0,7],"32:6":[18,0,7],"32:7":[18,0,7],"32:8":[18,8,8],"32:9":[2],"32:10":[2],"32:11":[2],"32:12":[2],"32:13":[2],"32:14":[2],"32:15":[2],"32:16":[2],"32:17":[2],"32:18":[18,18,18],"32:19":[18,18,18],"32:20":[18,18,18],"33:0":[19,0,0],"33:1":[19,0,4],"33:2":[19,0,4],"33:3":[19,0,4],"33:4":[19,0,4],"33:5":[19,5,5],"33:6":[19,6,6],"33:7":[19,7,7],"33:8":[19,8,8],"33:9":[2],"33:10":[2],"33:11":[2],"33:12":[2],"33:13":[2],"33:14":[2],"33:15":[2],"33:16":[2],"33:17":[2],"33:18":[2],"33:19":[2],"33:20":[2],"33:21":[2],"33:22":[2],"33:23":[2],"33:24":[2],"33:25":[2],"33:26":[2],"33:27":[2],"33:28":[2],"33:29":[2],"33:30":[2],"33:31":[2],"33:32":[2],"33:33":[2],"33:34":[2],"33:35":[2],"33:36":[19,36,36],"33:37":[19,36,36],"33:38":[19,36,36],"34:0":[19,0,0],"34:1":[19,0,5],"34:2":[19,0,5],"34:3":[19,0,5],"34:4":[19,0,5],"34:5":[19,0,5],"34:6":[19,6,6],"34:7":[19,7,7],"34:8":[19,8,8],"34:9":[2],"34:10":[2],"34:11":[2],"34:12":[19,12,12],"34:13":[19,12,12],"34:14":[19,12,12],"35:0":[19,0,0],"35:1":[19,0,0],"35:2":[19,0,0],"36:0":[19,0,0],"36:1":[0,0,1],"36:2":[0,0,9],"36:3":[0,0,9],"36:4":[0,0,9],"36:5":[0,0,9],"36:6":[0,0,9],"36:7":[0,0,9],"36:8":[0,0,9],"36:9":[0,0,9],"36:10":[0,0,10],"36:11":[0,0,14],"36:12":[0,0,14],"36:13":[0,0,14],"36:14":[0,0,14],"36:15":[19,15,15],"36:16":[19,15,15],"36:17":[19,15,15],"37:0":[19,0,0],"37:1":[19,0,7],"37:2":[19,0,7],"37:3":[19,0,7],"37:4":[19,0,7],"37:5":[19,0,7],"37:6":[19,0,7],"37:7":[19,0,7],"37:8":[19,8,8],"37:9":[2],"37:10":[2],"37:11":[2],"37:12":[2],"37:13":[2],"37:14":[2],"37:15":[2],"37:16":[2],"37:17":[2],"37:18":[2],"37:19":[2],"37:20":[2],"37:21":[2],"37:22":[2],"37:23":[2],"37:24":[2],"37:25":[19,25,25],"37:26":[19,25,25],"37:27":[19,25,25],"38:0":[20,0,0],"38:1":[20,0,0],"38:2":[20,0,0],"39:0":[20,0,0],"39:1":[0,0,1],"39:2":[0,0,8],"39:3":[0,0,8],"39:4":[0,0,8],"39:5":[0,0,8],"39:6":[0,0,8],"39:7":[0,0,8],"39:8":[0,0,8],"39:9":[0,0,9],"39:10":[0,0,12],"39:11":[0,0,12],"39:12":[0,0,12],"39:13":[21,13,13],"39:14":[21,13,13],"39:15":[21,13,13],"40:0":[21,0,0],"40:1":[21,0,4],"40:2":[21,0,4],"40:3":[21,0,4],"40:4":[21,0,4],"40:5":[21,5,5],"40:6":[22,7,12],"40:7":[22,7,7],"40:8":[23,8,11],"40:9":[23,8,11],"40:10":[23,8,11],"40:11":[23,8,11],"40:12":[22,12,12],"40:13":[22,12,12],"40:14":[22,12,12],"41:0":[24,0,0],"41:1":[24,0,4],"41:2":[24,0,4],"41:3":[24,0,4],"41:4":[24,0,4],"41:5":[24,5,5],"41:6":[2],"41:7":[2],"41:8":[2],"41:9":[2],"41:10":[2],"41:11":[2],"41:12":[2],"41:13":[2],"41:14":[2],"41:15":[2],"41:16":[2],"41:17":[2],"41:18":[2],"41:19":[2],"41:20":[2],"41:21":[2],"41:22":[2],"41:23":[2],"42:0":[25,0,0],"42:1":[25,0,0],"42:2":[25,0,0],"43:0":[25,0,0],"43:1":[0,0,1],"43:2":[0,0,8],"43:3":[0,0,8],"43:4":[0,0,8],"43:5":[0,0,8],"43:6":[0,0,8],"43:7":[0,0,8],"43:8":[0,0,8],"43:9":[0,0,9],"43:10":[0,0,13],"43:11":[0,0,13],"43:12":[0,0,13],"43:13":[0,0,13],"43:14":[25,14,14],"43:15":[25,14,14],"43:16":[25,14,14],"44:0":[25,0,0],"44:1":[25,0,4],"44:2":[25,0,4],"44:3":[25,0,4],"44:4":[25,0,4],"44:5":[25,5,5],"44:6":[22,7,14],"44:7":[22,7,7],"44:8":[23,8,13],"44:9":[23,8,13],"44:10":[23,8,13],"44:11":[23,8,13],"44:12":[23,8,13],"44:13":[23,8,13],"44:14":[22,14,14],"44:15":[22,14,14],"44:16":[22,14,14],"45:0":[25,0,0],"45:1":[25,0,0],"45:2":[25,0,0],"46:0":[25,0,0],"46:1":[0,0,1],"46:2":[0,0,8],"46:3":[0,0,8],"46:4":[0,0,8],"46:5":[0,0,8],"46:6":[0,0,8],"46:7":[0,0,8],"46:8":[0,0,8],"46:9":[0,0,9],"46:10":[0,0,16],"46:11":[0,0,16],"46:12":[0,0,16],"46:13":[0,0,16],"46:14":[0,0,16],"46:15":[0,0,16],"46:16":[0,0,16],"46:17":[26,17,17],"46:18":[26,17,17],"46:19":[26,17,17],"47:0":[26,0,0],"47:1":[26,0,3],"47:2":[26,0,3],"47:3":[26,0,3],"47:4":[26,4,4],"47:5":[27,6,7],"47:6":[27,6,7],"47:7":[27,6,7],"47:8":[27,6,7],"47:9":[27,6,7],"48:0":[28,0,0],"48:1":[28,0,0],"48:2":[28,0,0],"49:0":[28,0,0],"49:1":[0,0,1],"49:2":[0,0,7],"49:3":[0,0,7],"49:4":[0,0,7],"49:5":[0,0,7],"49:6":[0,0,7],"49:7":[0,0,7],"49:8":[0,0,8],"49:9":[0,0,27],"49:10":[0,0,27],"49:11":[0,0,27],"49:12":[0,0,27],"49:13":[0,0,27],"49:14":[0,0,27],"49:15":[0,0,27],"49:16":[0,0,27],"49:17":[0,0,27],"49:18":[0,0,27],"49:19":[0,0,27],"49:20":[0,0,27],"49:21":[0,0,27],"49:22":[0,0,27],"49:23":[0,0,27],"49:24":[0,0,27],"49:25":[0,0,27],"49:26":[0,0,27],"49:27":[0,0,27],"49:28":[0,0,28],"49:29":[0,0,33],"49:30":[0,0,33],"49:31":[0,0,33],"49:32":[0,0,33],"49:33":[0,0,33],"49:34":[16,34,34],"49:35":[16,34,34],"49:36":[16,34,34],"50:0":[16,0,0],"50:1":[16,0,8],"50:2":[16,0,8],"50:3":[16,0,8],"50:4":[16,0,8],"50:5":[16,0,8],"50:6":[16,0,8],"50:7":[16,0,8],"50:8":[16,0,8],"50:9":[16,9,9],"50:10":[2],"50:11":[2],"50:12":[2],"50:13":[2],"50:14":[2],"50:15":[2],"50:16":[2],"50:17":[2],"50:18":[2],"50:19":[2],"50:20":[2],"50:21":[2],"50:22":[2],"50:23":[16,23,23],"50:24":[16,23,23],"50:25":[16,23,23],"51:0":[16,0,0],"51:1":[16,0,0],"51:2":[16,0,0],"52:0":[16,0,0],"52:1":[0,0,1],"52:2":[0,0,7],"52:3":[0,0,7],"52:4":[0,0,7],"52:5":[0,0,7],"52:6":[0,0,7],"52:7":[0,0,7],"52:8":[0,0,8],"52:9":[0,0,22],"52:10":[0,0,22],"52:11":[0,0,22],"52:12":[0,0,22],"52:13":[0,0,22],"52:14":[0,0,22],"52:15":[0,0,22],"52:16":[0,0,22],"52:17":[0,0,22],"52:18":[0,0,22],"52:19":[0,0,22],"52:20":[0,0,22],"52:21":[0,0,22],"52:22":[0,0,22],"52:23":[29,23,23],"52:24":[29,23,23],"52:25":[29,23,23],"53:0":[29,0,0],"53:1":[29,0,8],"53:2":[29,0,8],"53:3":[29,0,8],"53:4":[29,0,8],"53:5":[29,0,8],"53:6":[29,0,8],"53:7":[29,0,8],"53:8":[29,0,8],"53:9":[29,9,9],"53:10":[2],"53:11":[2],"53:12":[2],"53:13":[2],"53:14":[2],"53:15":[2],"53:16":[2],"53:17":[2],"53:18":[2],"53:19":[2],"53:20":[2],"53:21":[2],"53:22":[2],"53:23":[2],"53:24":[2],"53:25":[2],"53:26":[2],"53:27":[2],"53:28":[2],"53:29":[2],"53:30":[2],"53:31":[2],"53:32":[2],"53:33":[2],"53:34":[2],"53:35":[2],"53:36":[2],"53:37":[2],"53:38":[2],"53:39":[2],"53:40":[2],"53:41":[2],"53:42":[2],"53:43":[2],"53:44":[2],"53:45":[2],"53:46":[2],"53:47":[2],"54:0":[30,0,0],"54:1":[30,0,0],"54:2":[30,0,0],"55:0":[30,0,0],"55:1":[0,0,1],"55:2":[0,0,10],"55:3":[0,0,10],"55:4":[0,0,10],"55:5":[0,0,10],"55:6":[0,0,10],"55:7":[0,0,10],"55:8":[0,0,10],"55:9":[0,0,10],"55:10":[0,0,10],"55:11":[31,11,11],"55:12":[31,11,11],"55:13":[31,11,11],"56:0":[31,0,0],"56:1":[31,0,7],"56:2":[31,0,7],"56:3":[31,0,7],"56:4":[31,0,7],"56:5":[31,0,7],"56:6":[31,0,7],"56:7":[31,0,7],"56:8":[31,8,8],"56:9":[2],"56:10":[2],"56:11":[2],"56:12":[2],"56:13":[2],"56:14":[2],"56:15":[2],"56:16":[2],"56:17":[2],"56:18":[2],"56:19":[2],"56:20":[31,20,20],"56:21":[31,20,20],"56:22":[31,20,20],"57:0":[32,0,0],"57:1":[32,0,0],"57:2":[32,0,0],"58:0":[32,0,0],"58:1":[0,0,1],"58:2":[0,0,10],"58:3":[0,0,10],"58:4":[0,0,10],"58:5":[0,0,10],"58:6":[0,0,10],"58:7":[0,0,10],"58:8":[0,0,10],"58:9":[0,0,10],"58:10":[0,0,10],"58:11":[0,0,11],"58:12":[0,0,23],"58:13":[0,0,23],"58:14":[0,0,23],"58:15":[0,0,23],"58:16":[0,0,23],"58:17":[0,0,23],"58:18":[0,0,23],"58:19":[0,0,23],"58:20":[0,0,23],"58:21":[0,0,23],"58:22":[0,0,23],"58:23":[0,0,23],"58:24":[17,24,24],"58:25":[17,24,24],"58:26":[17,24,24],"59:0":[17,0,0],"59:1":[17,0,6],"59:2":[17,0,6],"59:3":[17,0,6],"59:4":[17,0,6],"59:5":[17,0,6],"59:6":[17,0,6],"59:7":[17,7,7],"59:8":[2],"59:9":[2],"59:10":[2],"59:11":[2],"59:12":[2],"59:13":[2],"59:14":[2],"59:15":[2],"59:16":[2],"59:17":[2],"59:18":[2],"60:0":[17,0,0],"60:1":[17,0,0],"60:2":[17,0,0],"61:0":[17,0,0],"61:1":[0,0,1],"61:2":[0,0,7],"61:3":[0,0,7],"61:4":[0,0,7],"61:5":[0,0,7],"61:6":[0,0,7],"61:7":[0,0,7],"61:8":[33,8,8],"61:9":[33,8,8],"61:10":[33,8,8],"62:0":[33,0,0],"62:1":[33,0,4],"62:2":[33,0,4],"62:3":[33,0,4],"62:4":[33,0,4],"62:5":[33,5,5],"62:6":[2],"62:7":[2],"62:8":[2],"62:9":[2],"62:10":[2],"62:11":[2],"62:12":[2],"62:13":[2],"62:14":[2],"62:15":[2],"62:16":[33,16,16],"62:17":[33,16,16],"62:18":[33,16,16],"63:0":[34,0,0],"63:1":[34,0,0],"63:2":[34,0,0],"64:0":[34,0,0],"64:1":[0,0,1],"64:2":[0,0,7],"64:3":[0,0,7],"64:4":[0,0,7],"64:5":[0,0,7],"64:6":[0,0,7],"64:7":[0,0,7],"64:8":[0,0,8],"64:9":[0,0,17],"64:10":[0,0,17],"64:11":[0,0,17],"64:12":[0,0,17],"64:13":[0,0,17],"64:14":[0,0,17],"64:15":[0,0,17],"64:16":[0,0,17],"64:17":[0,0,17],"64:18":[35,18,18],"64:19":[35,18,18],"64:20":[35,18,18],"65:0":[35,0,0],"65:1":[35,0,4],"65:2":[35,0,4],"65:3":[35,0,4],"65:4":[35,0,4],"65:5":[35,5,5],"65:6":[2],"65:7":[2],"65:8":[2],"65:9":[2],"65:10":[2],"65:11":[2],"65:12":[2],"65:13":[2],"65:14":[2],"65:15":[2],"65:16":[2],"65:17":[2],"65:18":[2],"66:0":[35,0,0],"66:1":[35,0,0],"66:2":[35,0,0],"67:0":[35,0,0],"67:1":[0,0,1],"67:2":[0,0,10],"67:3":[0,0,10],"67:4":[0,0,10],"67:5":[0,0,10],"67:6":[0,0,10],"67:7":[0,0,10],"67:8":[0,0,10],"67:9":[0,0,10],"67:10":[0,0,10],"67:11":[2],"67:12":[2],"67:13":[2],"68:0":[2],"68:1":[2],"68:2":[2],"68:3":[2],"68:4":[2],"68:5":[2],"68:6":[2],"68:7":[2],"68:8":[2],"68:9":[2],"68:10":[2],"68:11":[2],"68:12":[2],"68:13":[2],"68:14":[2],"68:15":[2],"68:16":[2],"68:17":[2],"68:18":[2],"68:19":[2],"68:20":[2],"68:21":[2],"68:22":[2],"68:23":[2],"69:0":[2],"69:1":[2],"69:2":[2],"70:0":[2],"70:1":[0,0,1],"70:2":[0,0,5],"70:3":[0,0,5],"70:4":[0,0,5],"70:5":[0,0,5],"70:6":[36,6,6],"70:7":[36,6,6],"70:8":[36,6,6],"71:0":[36,0,0],"71:1":[36,0,6],"71:2":[36,0,6],"71:3":[36,0,6],"71:4":[36,0,6],"71:5":[36,0,6],"71:6":[36,0,6],"71:7":[36,7,7],"71:8":[2],"71:9":[2],"71:10":[2],"71:11":[2],"71:12":[2],"71:13":[2],"71:14":[2],"71:15":[2],"71:16":[2],"72:0":[37,0,0],"72:1":[37,0,0],"72:2":[37,0,0],"73:0":[37,0,0],"73:1":[0,0,1],"73:2":[0,0,8],"73:3":[0,0,8],"73:4":[0,0,8],"73:5":[0,0,8],"73:6":[0,0,8],"73:7":[0,0,8],"73:8":[0,0,8],"73:9":[38,9,9],"73:10":[38,9,9],"73:11":[38,9,9],"74:0":[38,0,0],"74:1":[38,0,9],"74:2":[38,0,9],"74:3":[38,0,9],"74:4":[38,0,9],"74:5":[38,0,9],"74:6":[38,0,9],"74:7":[38,0,9],"74:8":[38,0,9],"74:9":[38,0,9],"74:10":[38,10,10],"74:11":[2],"74:12":[2],"74:13":[2],"74:14":[2],"74:15":[2],"74:16":[2],"74:17":[38,17,17],"74:18":[38,17,17],"74:19":[38,17,17],"75:0":[39,0,0],"75:1":[39,0,0],"75:2":[39,0,0],"76:0":[39,0,0],"76:1":[39,0,12],"76:2":[39,0,12],"76:3":[39,0,12],"76:4":[39,0,12],"76:5":[39,0,12],"76:6":[39,0,12],"76:7":[39,0,12],"76:8":[39,0,12],"76:9":[39,0,12],"76:10":[39,0,12],"76:11":[39,0,12],"76:12":[39,0,12],"76:13":[39,13,13],"76:14":[2],"76:15":[2],"76:16":[2],"76:17":[2],"76:18":[2],"76:19":[2],"76:20":[2],"77:0":[2],"77:1":[2],"77:2":[2],"77:3":[2],"77:4":[2],"77:5":[2],"77:6":[2],"77:7":[2],"77:8":[2],"77:9":[2],"77:10":[2],"77:11":[2],"77:12":[2],"77:13":[2],"77:14":[2],"78:0":[2],"78:1":[2],"78:2":[2],"78:3":[2],"78:4":[2],"78:5":[2],"78:6":[2],"78:7":[2],"78:8":[2],"78:9":[2],"78:10":[2],"78:11":[2],"78:12":[2],"78:13":[2],"78:14":[2],"78:15":[2],"78:16":[2],"78:17":[2],"78:18":[2],"78:19":[2],"78:20":[2],"78:21":[2],"78:22":[2],"78:23":[2],"78:24":[2],"78:25":[2],"78:26":[2],"78:27":[2],"78:28":[2],"78:29":[2],"78:30":[2],"79:0":[2],"79:1":[2],"79:2":[2],"79:3":[2],"79:4":[2],"79:5":[2],"80:0":[39,0,0],"80:1":[39,0,0],"80:2":[39,0,0]},"hover":{"1:2":0,"1:3":0,"1:4":0,"1:5":0,"1:6":0,"1:7":0,"1:8":0,"2:1":1,"2:2":1,"2:3":1,"2:4":1,"2:5":1,"2:6":1,"2:7":1,"2:8":1,"2:9":1,"2:10":1,"2:11":1,"2:12":1,"3:1":2,"3:2":2,"3:3":2,"3:4":2,"3:5":2,"3:6":2,"3:7":2,"3:8":2,"3:9":2,"3:10":2,"3:11":2,"3:12":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"4:7":3,"4:8":3,"4:9":3,"4:10":3,"4:11":3,"4:12":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":4,"5:11":4,"5:12":4,"5:13":5,"5:14":6,"5:15":5,"5:16":5,"5:17":5,"5:18":5,"5:19":5,"5:20":5,"5:21":6,"5:22":6,"5:23":6,"6:1":7,"6:2":7,"6:3":7,"6:4":7,"6:5":7,"6:6":7,"6:7":7,"6:8":7,"6:9":7,"6:10":7,"6:11":7,"6:12":7,"7:1":8,"7:2":8,"7:3":8,"7:4":8,"7:5":8,"7:6":8,"7:7":8,"7:8":8,"7:9":8,"7:10":8,"7:11":8,"7:12":8,"9:2":9,"9:3":9,"9:4":9,"9:5":9,"9:6":9,"10:1":10,"10:2":10,"10:3":10,"10:4":10,"10:5":10,"10:6":10,"10:7":10,"10:8":10,"10:9":10,"11:1":11,"11:2":11,"11:3":11,"11:4":11,"11:5":11,"11:6":11,"11:7":11,"11:8":11,"11:9":11,"11:10":11,"11:11":11,"11:12":11,"11:13":11,"11:14":12,"11:15":13,"11:16":12,"11:17":12,"11:18":12,"11:19":12,"11:20":12,"11:21":13,"11:22":13,"11:23":13,"12:1":14,"12:2":14,"12:3":14,"12:4":14,"12:5":14,"12:6":14,"12:7":14,"12:8":14,"12:9":14,"17:2":15,"17:3":15,"17:4":15,"17:5":15,"17:6":15,"17:7":15,"17:8":15,"17:9":15,"17:10":15,"17:11":15,"17:12":15,"17:13":15,"25:2":15,"25:3":15,"25:4":15,"25:5":15,"25:6":15,"25:7":15,"25:8":15,"25:9":15,"25:10":15,"25:11":15,"25:12":15,"25:13":15,"25:14":15,"25:15":15,"25:16":15,"25:17":15,"25:18":15,"25:19":15,"25:20":15,"25:21":15,"25:22":15,"28:2":16,"28:3":16,"28:4":16,"28:5":16,"28:6":16,"28:7":16,"28:8":16,"28:9":16,"28:10":16,"28:11":16,"28:12":16,"28:13":16,"28:14":16,"28:15":16,"28:16":16,"28:17":16,"31:2":17,"31:3":17,"31:4":17,"31:5":17,"31:6":17,"31:7":17,"31:8":17,"31:9":17,"32:1":18,"32:2":18,"32:3":18,"32:4":18,"32:5":18,"32:6":18,"32:7":18,"32:8":18,"36:2":17,"36:3":17,"36:4":17,"36:5":17,"36:6":17,"36:7":17,"36:8":17,"36:9":17,"36:10":17,"36:11":17,"36:12":17,"36:13":17,"36:14":17,"37:1":19,"37:2":19,"37:3":19,"37:4":19,"37:5":19,"37:6":19,"37:7":19,"37:8":19,"39:2":20,"39:3":20,"39:4":20,"39:5":20,"39:6":20,"39:7":20,"39:8":20,"39:9":20,"39:10":20,"39:11":20,"39:12":20,"40:1":21,"40:2":21,"40:3":21,"40:4":21,"40:5":21,"40:6":22,"40:7":23,"40:8":22,"40:9":22,"40:10":22,"40:11":22,"40:12":23,"40:13":23,"40:14":23,"41:1":24,"41:2":24,"41:3":24,"41:4":24,"41:5":24,"43:2":20,"43:3":20,"43:4":20,"43:5":20,"43:6":20,"43:7":20,"43:8":20,"43:9":20,"43:10":20,"43:11":20,"43:12":20,"43:13":20,"44:1":21,"44:2":21,"44:3":21,"44:4":21,"44:5":21,"44:6":25,"44:7":23,"44:8":25,"44:9":25,"44:10":25,"44:11":25,"44:12":25,"44:13":25,"44:14":23,"44:15":23,"44:16":23,"46:2":26,"46:3":26,"46:4":26,"46:5":26,"46:6":26,"46:7":26,"46:8":26,"46:9":26,"46:10":26,"46:11":26,"46:12":26,"46:13":26,"46:14":26,"46:15":26,"46:16":26,"47:1":27,"47:2":27,"47:3":27,"47:4":27,"47:5":28,"47:6":28,"47:7":28,"47:8":28,"47:9":28,"49:2":29,"49:3":29,"49:4":29,"49:5":29,"49:6":29,"49:7":29,"49:8":29,"49:9":29,"49:10":29,"49:11":29,"49:12":29,"49:13":29,"49:14":29,"49:15":29,"49:16":29,"49:17":29,"49:18":29,"49:19":29,"49:20":29,"49:21":29,"49:22":29,"49:23":29,"49:24":29,"49:25":29,"49:26":29,"49:27":29,"49:28":29,"49:29":9,"49:30":9,"49:31":9,"49:32":9,"49:33":9,"50:1":10,"50:2":10,"50:3":10,"50:4":10,"50:5":10,"50:6":10,"50:7":10,"50:8":10,"50:9":10,"52:2":29,"52:3":29,"52:4":29,"52:5":29,"52:6":29,"52:7":29,"52:8":29,"52:9":29,"52:10":29,"52:11":29,"52:12":29,"52:13":29,"52:14":29,"52:15":29,"52:16":29,"52:17":29,"52:18":29,"52:19":29,"52:20":29,"52:21":29,"52:22":29,"53:1":30,"53:2":30,"53:3":30,"53:4":30,"53:5":30,"53:6":30,"53:7":30,"53:8":30,"53:9":30,"55:2":31,"55:3":31,"55:4":31,"55:5":31,"55:6":31,"55:7":31,"55:8":31,"55:9":31,"55:10":31,"56:1":32,"56:2":32,"56:3":32,"56:4":32,"56:5":32,"56:6":32,"56:7":32,"56:8":32,"58:2":31,"58:3":31,"58:4":31,"58:5":31,"58:6":31,"58:7":31,"58:8":31,"58:9":31,"58:10":31,"58:11":31,"61:2":33,"61:3":33,"61:4":33,"61:5":33,"61:6":33,"61:7":33,"62:1":34,"62:2":34,"62:3":34,"62:4":34,"62:5":34,"64:2":33,"64:3":33,"64:4":33,"64:5":33,"64:6":33,"64:7":33,"64:8":33,"64:9":35,"64:10":35,"64:11":35,"64:12":35,"64:13":35,"64:14":35,"64:15":35,"64:16":35,"64:17":35,"67:2":36,"67:3":36,"67:4":36,"67:5":36,"67:6":36,"67:7":36,"67:8":36,"67:9":36,"67:10":36,"70:2":37,"70:3":37,"70:4":37,"70:5":37,"71:1":38,"71:2":38,"71:3":38,"71:4":38,"71:5":38,"71:6":38,"71:7":38,"73:2":39,"73:3":39,"73:4":39,"73:5":39,"73:6":39,"73:7":39,"73:8":39,"74:1":40,"74:2":40,"74:3":40,"74:4":40,"74:5":40,"74:6":40,"74:7":40,"74:8":40,"74:9":40,"74:10":40},"definition":{"20:17":{"line":2,"startCharacter":0,"endCharacter":9},"20:18":{"line":2,"startCharacter":0,"endCharacter":9},"20:19":{"line":2,"startCharacter":0,"endCharacter":9},"20:20":{"line":2,"startCharacter":0,"endCharacter":9},"20:21":{"line":2,"startCharacter":0,"endCharacter":9},"20:24":{"line":2,"startCharacter":0,"endCharacter":9},"20:25":{"line":2,"startCharacter":0,"endCharacter":9},"20:26":{"line":2,"startCharacter":0,"endCharacter":9},"20:27":{"line":2,"startCharacter":0,"endCharacter":9},"20:28":{"line":2,"startCharacter":0,"endCharacter":9},"20:29":{"line":2,"startCharacter":0,"endCharacter":9},"20:30":{"line":2,"startCharacter":0,"endCharacter":9},"20:31":{"line":2,"startCharacter":0,"endCharacter":9},"20:32":{"line":2,"startCharacter":0,"endCharacter":9},"22:17":{"line":59,"startCharacter":0,"endCharacter":6},"22:18":{"line":59,"startCharacter":0,"endCharacter":6},"22:19":{"line":59,"startCharacter":0,"endCharacter":6},"22:20":{"line":59,"startCharacter":0,"endCharacter":6},"22:21":{"line":59,"startCharacter":0,"endCharacter":6},"22:22":{"line":59,"startCharacter":0,"endCharacter":6},"22:23":{"line":59,"startCharacter":0,"endCharacter":6},"22:24":{"line":59,"startCharacter":0,"endCharacter":6},"22:25":{"line":59,"startCharacter":0,"endCharacter":6},"22:26":{"line":59,"startCharacter":0,"endCharacter":6},"22:29":{"line":59,"startCharacter":0,"endCharacter":6},"22:30":{"line":59,"startCharacter":0,"endCharacter":6},"22:31":{"line":59,"startCharacter":0,"endCharacter":6},"22:32":{"line":59,"startCharacter":0,"endCharacter":6},"22:33":{"line":59,"startCharacter":0,"endCharacter":6},"23:54":{"line":36,"startCharacter":0,"endCharacter":15},"23:55":{"line":36,"startCharacter":0,"endCharacter":15},"23:56":{"line":36,"startCharacter":0,"endCharacter":15},"23:57":{"line":36,"startCharacter":0,"endCharacter":15},"23:58":{"line":36,"startCharacter":0,"endCharacter":15},"23:59":{"line":36,"startCharacter":0,"endCharacter":15},"23:60":{"line":36,"startCharacter":0,"endCharacter":15}},"versionContext":{"18:14":{"name":"fmt","insideString":false,"range":{"startCharacter":15,"endCharacter":23}},"18:15":{"name":"fmt","insideString":false,"range":{"startCharacter":15,"endCharacter":15}},"18:16":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:17":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:18":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:19":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:20":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:21":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:22":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:23":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"18:24":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"18:25":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"19:26":{"name":"compat","insideString":false,"range":{"startCharacter":27,"endCharacter":34}},"19:27":{"name":"compat","insideString":false,"range":{"startCharacter":27,"endCharacter":27}},"19:28":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:29":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:30":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:31":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:32":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:33":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:34":{"name":"compat","insideString":false,"range":{"startCharacter":34,"endCharacter":34}},"23:26":{"name":"simdlib","insideString":false,"range":{"startCharacter":27,"endCharacter":32}},"23:27":{"name":"simdlib","insideString":false,"range":{"startCharacter":27,"endCharacter":27}},"23:28":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:29":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:30":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:31":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:32":{"name":"simdlib","insideString":false,"range":{"startCharacter":32,"endCharacter":32}},"26:4":{"name":"os","insideString":false,"range":{"startCharacter":5,"endCharacter":12}},"26:5":{"name":"os","insideString":false,"range":{"startCharacter":5,"endCharacter":5}},"26:6":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:7":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:8":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:9":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:10":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:11":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:12":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"26:13":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"26:14":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"29:7":{"name":"gtest","insideString":false,"range":{"startCharacter":8,"endCharacter":16}},"29:8":{"name":"gtest","insideString":false,"range":{"startCharacter":8,"endCharacter":8}},"29:9":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:10":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:11":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:12":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:13":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:14":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:15":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:16":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"29:17":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"29:18":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:8":{"name":"shared","insideString":false,"range":{"startCharacter":9,"endCharacter":16}},"59:9":{"name":"shared","insideString":false,"range":{"startCharacter":9,"endCharacter":9}},"59:10":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:11":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:12":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:13":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:14":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:15":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:16":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:17":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:18":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}}},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"c++20"},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"c++23"},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"c++26"},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"c++2a"},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"c++2c"},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"gnu++20"},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"gnu++23"},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"gnu++26"},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"c++latest"},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"c++fly"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"auto"},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"on"},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"off"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"name = \"version\"","kind":"template","detail":"SemVer version dependency","documentation":"Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported.","insertSnippet":"${1:name} = \"${2:1.0.0}\""},{"label":"name = { path = ... }","kind":"template","detail":"Path dependency (local development)","insertSnippet":"${1:name} = { path = \"${2:../mylib}\" }"},{"label":"name = { git = ..., tag = ... }","kind":"template","detail":"Git dependency (one of tag / branch / rev)","insertSnippet":"${1:name} = { git = \"${2:https://github.com/user/repo.git}\", tag = \"${3:v1.0.0}\" }"},{"label":"name = { version = ..., features = [...] }","kind":"template","detail":"Long dep spec: request a feature of that dependency","insertSnippet":"${1:name} = { version = \"${2:1.0}\", features = [\"${3:feature}\"] }"},{"label":"name = { version = ..., tools = [...] }","kind":"template","detail":"Host tools produced by the dependency (must be a bin target of that package)","insertSnippet":"${1:name} = { version = \"${2:1.0}\", tools = [\"${3:protoc}\"] }"}],[{"label":"default","kind":"template","detail":"array","insertSnippet":"default = []","documentation":"the features a consumer gets unless it opts out Since 0.0.47."},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []","documentation":"[features.]: sources compiled only when the feature is active Since 0.0.95."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []","documentation":"[features.]: sources compiled only when the feature is active Since 0.0.95."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"opt","kind":"template","detail":"enum: s | z","insertSnippet":"opt = \"s\"","documentation":"an -O level: a number, or \"s\"/\"z\""},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}],[{"label":"s","kind":"template","detail":"value of opt","insertSnippet":"\"s\""},{"label":"z","kind":"template","detail":"value of opt","insertSnippet":"\"z\""}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}],[{"label":"carrier","kind":"template","detail":"table","insertSnippet":"carrier = { }","documentation":"{ package, section, main }: the program mcpp pack builds of a loaded artifact for a system (docs/35) Since 2026.10.27.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled","insertSnippet":"cxx_runtime = \"self-contained\""},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\""},{"label":"min_api_level","kind":"template","detail":"number","insertSnippet":"min_api_level = 0"},{"label":"platform","kind":"template","detail":"string","insertSnippet":"platform = \"\"","documentation":"the platform of the selected targets: \"@\", { path = \"...\" }, { git = ... }, or \"none\"; not accepted under a cfg(platform = ...) selector Since 2026.10.27.1."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"the target-axis form of [package] requires, unioned in when the selector matches Since 2026.10.26.1."},{"label":"run_under","kind":"template","detail":"array","insertSnippet":"run_under = []","documentation":"an argv placed before the runner: what reaches the machine the artifact runs on Since 2026.10.24.1."},{"label":"runner","kind":"template","detail":"array","insertSnippet":"runner = []","documentation":"argv template for mcpp run/test on this target, or { cmd, host = \"same-arch\" | \"any\", exclusive } (2026.10.27.1+)"},{"label":"runners","kind":"template","detail":"table","insertSnippet":"runners = { }","documentation":"named runners, = [argv]; also written as [target..runners] Since 2026.9.4.2."},{"label":"sysroot","kind":"template","detail":"string","insertSnippet":"sysroot = \"\""},{"label":"toolchain","kind":"template","detail":"string","insertSnippet":"toolchain = \"\""}],[{"label":"carrier","kind":"template","detail":"table","insertSnippet":"carrier = { }","documentation":"{ package, section, main }: the program mcpp pack builds of a loaded artifact for a system (docs/35) Since 2026.10.27.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled","insertSnippet":"cxx_runtime = \"self-contained\""},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\""},{"label":"min_api_level","kind":"template","detail":"number","insertSnippet":"min_api_level = 0"},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"the target-axis form of [package] requires, unioned in when the selector matches Since 2026.10.26.1."},{"label":"run_under","kind":"template","detail":"array","insertSnippet":"run_under = []","documentation":"an argv placed before the runner: what reaches the machine the artifact runs on Since 2026.10.24.1."},{"label":"runner","kind":"template","detail":"array","insertSnippet":"runner = []","documentation":"argv template for mcpp run/test on this target, or { cmd, host = \"same-arch\" | \"any\", exclusive } (2026.10.27.1+)"},{"label":"runners","kind":"template","detail":"table","insertSnippet":"runners = { }","documentation":"named runners, = [argv]; also written as [target..runners] Since 2026.9.4.2."},{"label":"sysroot","kind":"template","detail":"string","insertSnippet":"sysroot = \"\""},{"label":"toolchain","kind":"template","detail":"string","insertSnippet":"toolchain = \"\""}],[{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []","documentation":"member globs left out Since 0.0.11."},{"label":"members","kind":"template","detail":"array","insertSnippet":"members = []","documentation":"member directories or globs Since 0.0.11."}],[{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []","documentation":"member globs left out Since 0.0.11."}],[{"label":"deps","kind":"template","detail":"array","insertSnippet":"deps = []","documentation":"the project's host build tools: \"@\" or tables Since 0.0.77."},{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"tool = \"version\"","kind":"template","detail":"Pin the xlings tool version","insertSnippet":"${1:node} = \"${2:24.19.0}\""}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"format","kind":"template","detail":"string","insertSnippet":"format = \"\"","documentation":"the default of --pkg-format: tar, zip, dir, single-file, or a format a package provides Since 2026.10.27.1."},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"artifacts","kind":"template","detail":"array","insertSnippet":"artifacts = []","documentation":"[[runtime.artifacts]]: prebuilt artifacts the package provides Since 2026.8.10.1."},{"label":"capabilities","kind":"template","detail":"array","insertSnippet":"capabilities = []","documentation":"Since 0.0.46."},{"label":"deploy","kind":"template","detail":"array","insertSnippet":"deploy = []","documentation":"[[runtime.deploy]]: { from, to } files placed beside the artifact Since 2026.9.12.2."},{"label":"deploy_files","kind":"template","detail":"array","insertSnippet":"deploy_files = []","documentation":"files placed beside the artifact Since 2026.8.10.1."},{"label":"dlopen_libs","kind":"template","detail":"array","insertSnippet":"dlopen_libs = []","documentation":"libraries the program opens at run time Since 0.0.46."},{"label":"frameworks","kind":"template","detail":"array","insertSnippet":"frameworks = []","documentation":"Since 2026.8.10.1."},{"label":"libraries","kind":"template","detail":"array","insertSnippet":"libraries = []","documentation":"Since 2026.8.10.1."},{"label":"library_dirs","kind":"template","detail":"array","insertSnippet":"library_dirs = []","documentation":"Since 0.0.46."},{"label":"link_library_dirs","kind":"template","detail":"array","insertSnippet":"link_library_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"Since 0.0.48."},{"label":"requirements","kind":"template","detail":"array","insertSnippet":"requirements = []","documentation":"[[runtime.requirements]]: { kind, value, phase, required, discovery } Since 2026.8.10.1."},{"label":"runtime_search_dirs","kind":"template","detail":"array","insertSnippet":"runtime_search_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"transitive_needed_dirs","kind":"template","detail":"array","insertSnippet":"transitive_needed_dirs = []","documentation":"Since 2026.8.10.1."}],[{"label":"artifacts","kind":"template","detail":"array","insertSnippet":"artifacts = []","documentation":"[[runtime.artifacts]]: prebuilt artifacts the package provides Since 2026.8.10.1."},{"label":"capabilities","kind":"template","detail":"array","insertSnippet":"capabilities = []","documentation":"Since 0.0.46."},{"label":"deploy","kind":"template","detail":"array","insertSnippet":"deploy = []","documentation":"[[runtime.deploy]]: { from, to } files placed beside the artifact Since 2026.9.12.2."},{"label":"deploy_files","kind":"template","detail":"array","insertSnippet":"deploy_files = []","documentation":"files placed beside the artifact Since 2026.8.10.1."},{"label":"dlopen_libs","kind":"template","detail":"array","insertSnippet":"dlopen_libs = []","documentation":"libraries the program opens at run time Since 0.0.46."},{"label":"frameworks","kind":"template","detail":"array","insertSnippet":"frameworks = []","documentation":"Since 2026.8.10.1."},{"label":"library_dirs","kind":"template","detail":"array","insertSnippet":"library_dirs = []","documentation":"Since 0.0.46."},{"label":"link_library_dirs","kind":"template","detail":"array","insertSnippet":"link_library_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"Since 0.0.48."},{"label":"requirements","kind":"template","detail":"array","insertSnippet":"requirements = []","documentation":"[[runtime.requirements]]: { kind, value, phase, required, discovery } Since 2026.8.10.1."},{"label":"runtime_search_dirs","kind":"template","detail":"array","insertSnippet":"runtime_search_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"transitive_needed_dirs","kind":"template","detail":"array","insertSnippet":"transitive_needed_dirs = []","documentation":"Since 2026.8.10.1."}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"version","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"mcpp","body":"type: `string`\n\nsince: 2026.9.28.3\n\nrelease floor, only the \">=\" form is accepted (docs/04 §2.1)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"c++23","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"description","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"authors","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"cxxflags","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"auto","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"sources","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"[dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[dev-dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[features]","body":"plane: `gate`\n\nkeys: 12","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"default","body":"type: `array`\n\nsince: 0.0.47\n\nthe features a consumer gets unless it opts out","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"sources","body":"type: `array`\n\nsince: 0.0.95\n\n[features.]: sources compiled only when the feature is active","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"bin","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"main","body":"type: `string`\n\na file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"entry","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[profile]","body":"plane: `compile`\n\nkeys: 10","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"opt","body":"type: `enum`\n\nvalues: `s` | `z`\n\nan -O level: a number, or \"s\"/\"z\"\n\nunmodelled: the type is inferred from the docs, not read from mcpp","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"3","body":"enum value of `opt`\n\none of: `s` | `z`\n\nan -O level: a number, or \"s\"/\"z\"","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"[target]","body":"plane: `condition`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#271-target--platform-conditional-dependencies--flags"},{"title":"platform","body":"type: `string`\n\nsince: 2026.10.27.1\n\nthe platform of the selected targets: \"@\", { path = \"...\" }, { git = ... }, or \"none\"; not accepted under a cfg(platform = ...) selector","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#271-target--platform-conditional-dependencies--flags"},{"title":"[workspace]","body":"plane: `unclassified`\n\nkeys: 2","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#218-workspace--tables-a-workspace-shares-202610101"},{"title":"members","body":"type: `array`\n\nsince: 0.0.11\n\nmember directories or globs","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#218-workspace--tables-a-workspace-shares-202610101"},{"title":"[xlings]","body":"plane: `tool`\n\nkeys: 4","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"deps","body":"type: `array`\n\nsince: 0.0.77\n\nthe project's host build tools: \"@\" or tables\n\nunmodelled: the type is inferred from the docs, not read from mcpp","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[xlings.workspace]","body":"plane: `tool`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[toolchain]","body":"plane: `compile`\n\nkeys: 3 (open: the user picks the names)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#27-toolchain--toolchain-configuration"},{"title":"[pack]","body":"plane: `unclassified`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"format","body":"type: `string`\n\nsince: 2026.10.27.1\n\nthe default of --pkg-format: tar, zip, dir, single-file, or a format a package provides","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"[runtime]","body":"plane: `metadata`\n\nkeys: 13","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#211-runtime--provider-neutral-runtime-contract"},{"title":"libraries","body":"type: `array`\n\nsince: 2026.8.10.1","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#211-runtime--provider-neutral-runtime-contract"}]} diff --git a/tests/rules/golden/crlf.toml.json b/tests/rules/golden/crlf.toml.json new file mode 100644 index 0000000..10c0465 --- /dev/null +++ b/tests/rules/golden/crlf.toml.json @@ -0,0 +1 @@ +{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"1:13":[2],"1:14":[2],"1:15":[2],"2:0":[3,0,0],"2:1":[3,0,7],"2:2":[3,0,7],"2:3":[3,0,7],"2:4":[3,0,7],"2:5":[3,0,7],"2:6":[3,0,7],"2:7":[3,0,7],"2:8":[3,8,8],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"2:18":[2],"2:19":[2],"3:0":[4,0,0],"3:1":[4,0,0],"3:2":[4,0,0],"4:0":[4,0,0],"4:1":[0,0,1],"4:2":[0,0,6],"4:3":[0,0,6],"4:4":[0,0,6],"4:5":[0,0,6],"4:6":[0,0,6],"4:7":[5,7,7],"4:8":[5,7,7],"4:9":[5,7,7],"5:0":[5,0,0],"5:1":[5,0,12],"5:2":[5,0,12],"5:3":[5,0,12],"5:4":[5,0,12],"5:5":[5,0,12],"5:6":[5,0,12],"5:7":[5,0,12],"5:8":[5,0,12],"5:9":[5,0,12],"5:10":[5,0,12],"5:11":[5,0,12],"5:12":[5,0,12],"5:13":[5,13,13],"5:14":[6,15,19],"5:15":[6,15,15],"5:16":[7,16,18],"5:17":[7,16,18],"5:18":[7,16,18],"5:19":[6,19,19],"5:20":[6,19,19],"5:21":[6,19,19],"6:0":[8,0,0],"6:1":[8,0,0],"6:2":[8,0,0]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"2:1":2,"2:2":2,"2:3":2,"2:4":2,"2:5":2,"2:6":2,"2:7":2,"2:8":2,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":4,"5:11":4,"5:12":4,"5:13":4,"5:14":5,"5:15":6,"5:16":5,"5:17":5,"5:18":5,"5:19":6,"5:20":6,"5:21":6},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"auto"},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"on"},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"off"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"version","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"on","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"}]} diff --git a/tests/rules/golden/editing.mcpp.json b/tests/rules/golden/editing.mcpp.json new file mode 100644 index 0000000..6f7a410 --- /dev/null +++ b/tests/rules/golden/editing.mcpp.json @@ -0,0 +1 @@ +{"diagnostics":{"warning":[],"served":[]},"imports":[],"completion":{"0:7":0,"0:8":1,"0:9":1,"0:10":1,"0:11":1,"0:12":2,"0:13":3,"0:14":3,"1:7":0,"1:8":4,"1:9":4,"2:14":0,"2:15":1,"2:16":1,"2:17":1,"2:18":1,"2:19":2,"2:20":3,"2:21":3,"2:22":3,"2:23":3,"2:24":3,"3:8":5,"3:9":6,"3:10":6,"3:11":6,"3:12":6,"3:13":6,"3:14":7,"3:15":7,"3:16":7},"hover":{"3:2":0,"3:3":0,"3:4":0,"3:5":0,"3:6":0,"3:7":0,"3:8":0,"3:9":0,"3:10":0,"3:11":0,"3:12":0,"3:13":0,"3:14":0,"3:15":0,"3:16":0},"lists":[[{"label":"std","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+)."},{"label":"std.compat","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library plus the C library names in the global namespace; `import std;` and `import std.compat;` may be used together."},{"label":"mcpp","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1)."},{"label":"mcpp.core","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2)."}],[{"label":"mcpp","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1)."},{"label":"mcpp.core","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2)."}],[{"label":"mcpp.core","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2)."}],[],[{"label":"std","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+)."},{"label":"std.compat","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library plus the C library names in the global namespace; `import std;` and `import std.compat;` may be used together."}],[{"label":"cxxflag","kind":"function","detail":"mcpp build-script API"},{"label":"cflag","kind":"function","detail":"mcpp build-script API"},{"label":"link_lib","kind":"function","detail":"mcpp build-script API"},{"label":"link_search","kind":"function","detail":"mcpp build-script API"},{"label":"cfg","kind":"function","detail":"mcpp build-script API"},{"label":"generated","kind":"function","detail":"mcpp build-script API"},{"label":"source","kind":"function","detail":"mcpp build-script API"},{"label":"runner","kind":"function","detail":"mcpp build-script API"},{"label":"runner_named","kind":"function","detail":"mcpp build-script API"},{"label":"runner_longlived","kind":"function","detail":"mcpp build-script API"},{"label":"run_exclusive","kind":"function","detail":"mcpp build-script API"},{"label":"link_script","kind":"function","detail":"mcpp build-script API"},{"label":"link_flag","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir_after","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_env_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed_glob","kind":"function","detail":"mcpp build-script API"},{"label":"warning","kind":"function","detail":"mcpp build-script API"},{"label":"diagnostic","kind":"function","detail":"mcpp build-script API"},{"label":"action","kind":"function","detail":"mcpp build-script API"},{"label":"fact","kind":"function","detail":"mcpp build-script API"},{"label":"floor","kind":"function","detail":"mcpp build-script API"},{"label":"pack_format","kind":"function","detail":"mcpp build-script API"},{"label":"windows_subsystem","kind":"function","detail":"mcpp build-script API"},{"label":"windows_entry","kind":"function","detail":"mcpp build-script API"},{"label":"deploy","kind":"function","detail":"mcpp build-script API"},{"label":"runtime_search_dir","kind":"function","detail":"mcpp build-script API"},{"label":"decision","kind":"function","detail":"mcpp build-script API"},{"label":"xpkg_request","kind":"function","detail":"mcpp build-script API"},{"label":"toolchain","kind":"function","detail":"mcpp build-script API"},{"label":"link_start","kind":"function","detail":"mcpp build-script API"},{"label":"link_driver","kind":"function","detail":"mcpp build-script API"},{"label":"link_input","kind":"function","detail":"mcpp build-script API"},{"label":"artifact_name","kind":"function","detail":"mcpp build-script API"},{"label":"seal","kind":"function","detail":"mcpp build-script API"},{"label":"run_host","kind":"function","detail":"mcpp build-script API"},{"label":"action ... (typed action block)","kind":"snippet","filterText":"action","detail":"mcpp::action with id, role, description, arg/input/output and submit","documentation":"The five roles (SPEC-007 R3.6): source, check, object, artifact, prepare.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,object,artifact,prepare|};\n${1:handle}.description = \"${4:description}\";\n${1:handle}.arg(\"${5:argument}\")\n .input(\"${6:input}\")\n .output(\"${7:output}\")\n .submit();"},{"label":"action ... (prepare block)","kind":"snippet","filterText":"action","detail":"mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)","documentation":"A prepare action must declare output_dir; the engine refuses one without it.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"},{"label":"cxxflag(...)","kind":"snippet","filterText":"cxxflag","detail":"mcpp:cxxflag= · slot CxxFlags","documentation":"SPEC-007 R2.1","insertText":"cxxflag(\"${1:value}\")"},{"label":"cflag(...)","kind":"snippet","filterText":"cflag","detail":"mcpp:cflag= · slot CFlags","documentation":"SPEC-007 R2.1","insertText":"cflag(\"${1:value}\")"},{"label":"link_lib(...)","kind":"snippet","filterText":"link_lib","detail":"mcpp:link-lib= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_lib(\"${1:value}\")"},{"label":"link_search(...)","kind":"snippet","filterText":"link_search","detail":"mcpp:link-search= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_search(\"${1:value}\")"},{"label":"cfg(...)","kind":"snippet","filterText":"cfg","detail":"mcpp:cfg= · slot Defines","documentation":"SPEC-007 R2.1","insertText":"cfg(\"${1:value}\")"},{"label":"generated(...)","kind":"snippet","filterText":"generated","detail":"mcpp:generated= · slot Generated","documentation":"SPEC-007 R2.1","insertText":"generated(\"${1:value}\")"},{"label":"source(...)","kind":"snippet","filterText":"source","detail":"mcpp:source= · slot Sources","documentation":"SPEC-007 R2.1, R3.3, R3.6, R3.9","insertText":"source(\"${1:value}\")"},{"label":"runner(...)","kind":"snippet","filterText":"runner","detail":"mcpp:runner= · slot Runner","documentation":"SPEC-007 R9.10 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192)","insertText":"runner(\"${1:value}\")"},{"label":"runner_named(...)","kind":"snippet","filterText":"runner_named","detail":"mcpp:runner-named= · slot NamedRunner","insertText":"runner_named(\"${1:value}\")"},{"label":"runner_longlived(...)","kind":"snippet","filterText":"runner_longlived","detail":"mcpp:runner-longlived= · slot RunnerLongLived","insertText":"runner_longlived(\"${1:value}\")"},{"label":"run_exclusive(...)","kind":"snippet","filterText":"run_exclusive","detail":"mcpp:run-exclusive= · slot RunExclusive","insertText":"run_exclusive(\"${1:value}\")"},{"label":"link_script(...)","kind":"snippet","filterText":"link_script","detail":"mcpp:link-script= · slot LdFlags","insertText":"link_script(\"${1:value}\")"},{"label":"link_flag(...)","kind":"snippet","filterText":"link_flag","detail":"mcpp:link-flag= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_flag(\"${1:value}\")"},{"label":"include_dir(...)","kind":"snippet","filterText":"include_dir","detail":"mcpp:include-dir= · slot IncludeDirs","documentation":"SPEC-007 R2.1","insertText":"include_dir(\"${1:value}\")"},{"label":"include_dir_after(...)","kind":"snippet","filterText":"include_dir_after","detail":"mcpp:include-dir-after= · slot IncludeDirsAfter","documentation":"SPEC-007 R2.1","insertText":"include_dir_after(\"${1:value}\")"},{"label":"rerun_if_changed(...)","kind":"snippet","filterText":"rerun_if_changed","detail":"mcpp:rerun-if-changed= · slot RerunFiles","insertText":"rerun_if_changed(\"${1:value}\")"},{"label":"rerun_if_env_changed(...)","kind":"snippet","filterText":"rerun_if_env_changed","detail":"mcpp:rerun-if-env-changed= · slot RerunEnv","insertText":"rerun_if_env_changed(\"${1:value}\")"},{"label":"rerun_if_changed_glob(...)","kind":"snippet","filterText":"rerun_if_changed_glob","detail":"mcpp:rerun-if-changed-glob= · slot RerunGlobs","insertText":"rerun_if_changed_glob(\"${1:value}\")"},{"label":"warning(...)","kind":"snippet","filterText":"warning","detail":"mcpp:warning= · slot Warnings","documentation":"SPEC-007 R1.2, R2.1, R9.4 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212)","insertText":"warning(\"${1:value}\")"},{"label":"diagnostic(...)","kind":"snippet","filterText":"diagnostic","detail":"mcpp:diagnostic= · slot Diagnostics","insertText":"diagnostic(\"${1:value}\")"},{"label":"fact(...)","kind":"snippet","filterText":"fact","detail":"mcpp:fact= · slot Facts","documentation":"SPEC-007 R2.1 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952)","insertText":"fact(\"${1:value}\")"},{"label":"floor(...)","kind":"snippet","filterText":"floor","detail":"mcpp:floor= · slot Floors","documentation":"SPEC-007 R2.1 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952)","insertText":"floor(\"${1:value}\")"},{"label":"pack_format(...)","kind":"snippet","filterText":"pack_format","detail":"mcpp:pack-format= · slot PackFormats","documentation":"SPEC-007 R9.10","insertText":"pack_format(\"${1:value}\")"},{"label":"windows_subsystem(...)","kind":"snippet","filterText":"windows_subsystem","detail":"mcpp:windows-subsystem= · slot WindowsSubsystem","insertText":"windows_subsystem(\"${1:value}\")"},{"label":"windows_entry(...)","kind":"snippet","filterText":"windows_entry","detail":"mcpp:windows-entry= · slot WindowsEntry","insertText":"windows_entry(\"${1:value}\")"},{"label":"deploy(...)","kind":"snippet","filterText":"deploy","detail":"mcpp:deploy= · slot Deploy","documentation":"SPEC-007 R2.1, R4.2, R9.2, R9.5 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11)","insertText":"deploy(\"${1:value}\")"},{"label":"runtime_search_dir(...)","kind":"snippet","filterText":"runtime_search_dir","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","documentation":"SPEC-007 R2.1","insertText":"runtime_search_dir(\"${1:value}\")"},{"label":"decision(...)","kind":"snippet","filterText":"decision","detail":"mcpp:decision= · slot ToolDecisions","documentation":"SPEC-007 R6.5, R9.2","insertText":"decision(\"${1:value}\")"},{"label":"xpkg_request(...)","kind":"snippet","filterText":"xpkg_request","detail":"mcpp:xpkg-request= · slot XpkgRequests","insertText":"xpkg_request(\"${1:value}\")"},{"label":"toolchain(...)","kind":"snippet","filterText":"toolchain","detail":"mcpp:toolchain= · slot ToolchainStatement","documentation":"SPEC-007 R9.2, R9.9","insertText":"toolchain(\"${1:value}\")"},{"label":"link_start(...)","kind":"snippet","filterText":"link_start","detail":"mcpp:link-start= · slot LinkStart","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_start(\"${1:value}\")"},{"label":"link_driver(...)","kind":"snippet","filterText":"link_driver","detail":"mcpp:link-driver= · slot LinkDriver","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_driver(\"${1:value}\")"},{"label":"link_input(...)","kind":"snippet","filterText":"link_input","detail":"mcpp:link-input= · slot LinkInputs","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_input(\"${1:value}\")"},{"label":"artifact_name(...)","kind":"snippet","filterText":"artifact_name","detail":"mcpp:artifact-name= · slot ArtifactNames","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"artifact_name(\"${1:value}\")"},{"label":"seal(...)","kind":"snippet","filterText":"seal","detail":"mcpp:seal= · slot Seal","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"seal(\"${1:value}\")"},{"label":"run_host(...)","kind":"snippet","filterText":"run_host","detail":"mcpp:run-host= · slot RunHost","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"run_host(\"${1:value}\")"}],[{"label":"link_lib","kind":"function","detail":"mcpp build-script API"},{"label":"link_search","kind":"function","detail":"mcpp build-script API"},{"label":"link_script","kind":"function","detail":"mcpp build-script API"},{"label":"link_flag","kind":"function","detail":"mcpp build-script API"},{"label":"link_start","kind":"function","detail":"mcpp build-script API"},{"label":"link_driver","kind":"function","detail":"mcpp build-script API"},{"label":"link_input","kind":"function","detail":"mcpp build-script API"},{"label":"link_lib(...)","kind":"snippet","filterText":"link_lib","detail":"mcpp:link-lib= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_lib(\"${1:value}\")"},{"label":"link_search(...)","kind":"snippet","filterText":"link_search","detail":"mcpp:link-search= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_search(\"${1:value}\")"},{"label":"link_script(...)","kind":"snippet","filterText":"link_script","detail":"mcpp:link-script= · slot LdFlags","insertText":"link_script(\"${1:value}\")"},{"label":"link_flag(...)","kind":"snippet","filterText":"link_flag","detail":"mcpp:link-flag= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_flag(\"${1:value}\")"},{"label":"link_start(...)","kind":"snippet","filterText":"link_start","detail":"mcpp:link-start= · slot LinkStart","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_start(\"${1:value}\")"},{"label":"link_driver(...)","kind":"snippet","filterText":"link_driver","detail":"mcpp:link-driver= · slot LinkDriver","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_driver(\"${1:value}\")"},{"label":"link_input(...)","kind":"snippet","filterText":"link_input","detail":"mcpp:link-input= · slot LinkInputs","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_input(\"${1:value}\")"}],[{"label":"link_lib","kind":"function","detail":"mcpp build-script API"},{"label":"link_lib(...)","kind":"snippet","filterText":"link_lib","detail":"mcpp:link-lib= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_lib(\"${1:value}\")"}]],"hovers":["**mcpp::link_lib**\n\nwire `mcpp:link-lib=` · slot `LdFlags` · scope `LinkGlobal` · since protocol 1\n\nSPEC-007: R2.1"]} diff --git a/tests/rules/golden/editing.toml.json b/tests/rules/golden/editing.toml.json new file mode 100644 index 0000000..04e1ec0 --- /dev/null +++ b/tests/rules/golden/editing.toml.json @@ -0,0 +1 @@ +{"diagnostics":{"default":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [pack","severity":"error","line":1,"startCharacter":1,"endCharacter":6}],"quiet":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [pack","severity":"info","line":1,"startCharacter":1,"endCharacter":6}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,5],"0:3":[0,0,5],"0:4":[0,0,5],"0:5":[0,0,5],"0:6":[0,0,5],"0:7":[0,0,5],"1:0":[1,0,0],"1:1":[0,0,1],"1:2":[0,0,8],"1:3":[0,0,8],"1:4":[0,0,8],"1:5":[0,0,8],"1:6":[0,0,8],"1:7":[0,0,8],"1:8":[0,0,8],"1:9":[0,0,9],"1:10":[0,0,9],"1:11":[0,0,9],"2:0":[2,0,0],"2:1":[3],"2:2":[3],"2:3":[3],"2:4":[3],"3:0":[0,0,0],"3:1":[0,0,1],"3:2":[0,0,8],"3:3":[0,0,8],"3:4":[0,0,8],"3:5":[0,0,8],"3:6":[0,0,8],"3:7":[0,0,8],"3:8":[0,0,8],"3:9":[4,9,9],"3:10":[4,9,9],"3:11":[4,9,9],"4:0":[4,0,0],"4:1":[4,0,4],"4:2":[4,0,4],"4:3":[4,0,4],"4:4":[4,0,4],"4:5":[4,5,5],"4:6":[3],"4:7":[3],"4:8":[3],"4:9":[3],"4:10":[3],"4:11":[3],"4:12":[3],"4:13":[3],"4:14":[3],"4:15":[3],"5:0":[5,0,0],"5:1":[5,0,8],"5:2":[5,0,8],"5:3":[5,0,8],"5:4":[5,0,8],"5:5":[5,0,8],"5:6":[5,0,8],"5:7":[5,0,8],"5:8":[5,0,8],"5:9":[5,9,9],"5:10":[6,11,12],"5:11":[6,11,11],"5:12":[7,12,12],"5:13":[7,12,12],"5:14":[7,12,12],"6:0":[8,0,0],"6:1":[8,0,4],"6:2":[8,0,4],"6:3":[8,0,4],"6:4":[8,0,4],"6:5":[8,5,5],"6:6":[3],"6:7":[3],"6:8":[3],"6:9":[3],"7:0":[8,0,0],"7:1":[0,0,1],"7:2":[0,0,8],"7:3":[0,0,8],"7:4":[0,0,8],"7:5":[0,0,8],"7:6":[0,0,8],"7:7":[0,0,8],"7:8":[0,0,8],"7:9":[0,0,9],"7:10":[0,0,12],"7:11":[0,0,12],"7:12":[0,0,12],"7:13":[2,13,13],"7:14":[2,13,13],"7:15":[2,13,13],"8:0":[2,0,0],"8:1":[2,0,4],"8:2":[2,0,4],"8:3":[2,0,4],"8:4":[2,0,4],"8:5":[2,5,5],"8:6":[9,7,9],"8:7":[9,7,7],"8:8":[10,8,9],"8:9":[10,8,9],"8:10":[10,8,9],"8:11":[10,8,9],"9:0":[11,0,0],"9:1":[11,0,4],"9:2":[11,0,4],"9:3":[11,0,4],"9:4":[11,0,4],"9:5":[11,5,5],"9:6":[3],"9:7":[3],"9:8":[3],"9:9":[3],"10:0":[12,0,0],"10:1":[0,0,1],"10:2":[0,0,6],"10:3":[0,0,6],"10:4":[0,0,6],"10:5":[0,0,6],"10:6":[0,0,6],"10:7":[13,7,7],"10:8":[13,7,7],"10:9":[13,7,7],"11:0":[13,0,0],"11:1":[13,0,12],"11:2":[13,0,12],"11:3":[13,0,12],"11:4":[13,0,12],"11:5":[13,0,12],"11:6":[13,0,12],"11:7":[13,0,12],"11:8":[13,0,12],"11:9":[13,0,12],"11:10":[13,0,12],"11:11":[13,0,12],"11:12":[13,0,12],"11:13":[13,13,13],"11:14":[14,15,17],"11:15":[14,15,17],"11:16":[14,15,17],"11:17":[14,15,17],"11:18":[14,15,17],"11:19":[14,15,17],"12:0":[15,0,0],"12:1":[15,0,8],"12:2":[15,0,8],"12:3":[15,0,8],"12:4":[15,0,8],"12:5":[15,0,8],"12:6":[15,0,8],"12:7":[15,0,8],"12:8":[15,0,8],"12:9":[15,9,9],"12:10":[3],"12:11":[3],"12:12":[3],"12:13":[3],"12:14":[3],"12:15":[3],"12:16":[3],"12:17":[3],"12:18":[3],"12:19":[3],"12:20":[3],"12:21":[3],"13:0":[3],"13:1":[3],"13:2":[3],"13:3":[3],"13:4":[3],"13:5":[3],"13:6":[3],"13:7":[3],"13:8":[3],"13:9":[3],"13:10":[3],"13:11":[3],"13:12":[3],"13:13":[3],"13:14":[3],"13:15":[3],"13:16":[3],"14:0":[3],"14:1":[3],"14:2":[3],"14:3":[3],"14:4":[3],"14:5":[3],"14:6":[3],"14:7":[3],"14:8":[3],"14:9":[3],"14:10":[3],"14:11":[3],"14:12":[3],"15:0":[3],"15:1":[3],"15:2":[3],"15:3":[3],"15:4":[3],"15:5":[3],"15:6":[3],"15:7":[3],"15:8":[3],"15:9":[3],"15:10":[3],"15:11":[3],"15:12":[3],"15:13":[3],"15:14":[3],"15:15":[3],"15:16":[3],"15:17":[3],"15:18":[3],"15:19":[3],"15:20":[3],"15:21":[3],"15:22":[3],"16:0":[3],"16:1":[3],"16:2":[3],"16:3":[3],"16:4":[3],"16:5":[3],"16:6":[3],"16:7":[3],"16:8":[3],"16:9":[3],"16:10":[3],"16:11":[3],"16:12":[3],"16:13":[3],"16:14":[3],"16:15":[3],"16:16":[3],"16:17":[3],"16:18":[3],"16:19":[3],"16:20":[3],"16:21":[3],"16:22":[3],"16:23":[3],"16:24":[3],"16:25":[3],"16:26":[3],"16:27":[3],"16:28":[3],"17:0":[3],"17:1":[3],"17:2":[3],"17:3":[3],"17:4":[3],"17:5":[3],"17:6":[3],"17:7":[3],"17:8":[3],"17:9":[3],"17:10":[3],"17:11":[3],"17:12":[3],"17:13":[3],"17:14":[3],"17:15":[3],"17:16":[3],"17:17":[3],"17:18":[3],"17:19":[3],"17:20":[3],"17:21":[3],"17:22":[3],"17:23":[3],"18:0":[3],"18:1":[3],"18:2":[3],"18:3":[3],"18:4":[3],"18:5":[3],"18:6":[3],"18:7":[3],"18:8":[3],"18:9":[3],"18:10":[3],"18:11":[3],"18:12":[3],"18:13":[3],"18:14":[3],"18:15":[3],"19:0":[3],"19:1":[3],"19:2":[3],"19:3":[3],"19:4":[3],"19:5":[3],"19:6":[3],"19:7":[3],"19:8":[3],"19:9":[3],"19:10":[3],"19:11":[3],"19:12":[3],"19:13":[3],"19:14":[3],"19:15":[3],"19:16":[3],"19:17":[3],"19:18":[3],"19:19":[3],"19:20":[3],"20:0":[3],"20:1":[3],"20:2":[3],"20:3":[3],"20:4":[3],"20:5":[3],"20:6":[3],"20:7":[3],"20:8":[3],"20:9":[3],"20:10":[3],"20:11":[3],"20:12":[3],"20:13":[3],"20:14":[3],"20:15":[3],"21:0":[3],"21:1":[3],"21:2":[3],"21:3":[3],"21:4":[3],"21:5":[3],"21:6":[3],"21:7":[3],"21:8":[3],"21:9":[3],"21:10":[3],"21:11":[3],"21:12":[3],"22:0":[3],"22:1":[3],"22:2":[3],"22:3":[3],"22:4":[3],"22:5":[3],"22:6":[3],"22:7":[3],"22:8":[3],"22:9":[3],"22:10":[3],"22:11":[3],"22:12":[3],"22:13":[3],"22:14":[3],"22:15":[3],"22:16":[3],"22:17":[3],"22:18":[3],"22:19":[3],"22:20":[3],"22:21":[3],"22:22":[3],"22:23":[3],"22:24":[3],"22:25":[3],"22:26":[3],"22:27":[3],"22:28":[3],"22:29":[3],"22:30":[3],"22:31":[3],"22:32":[3],"22:33":[3],"22:34":[3],"22:35":[3],"22:36":[3],"22:37":[3],"22:38":[3],"22:39":[3],"22:40":[3],"22:41":[3],"22:42":[3],"23:0":[3],"23:1":[3],"23:2":[3],"23:3":[3],"23:4":[3],"23:5":[3],"23:6":[3],"23:7":[3],"23:8":[3],"23:9":[3],"23:10":[3],"23:11":[3],"23:12":[3],"23:13":[3],"23:14":[3],"23:15":[3],"23:16":[3],"23:17":[3],"23:18":[3],"24:0":[3],"24:1":[3],"24:2":[3]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"1:6":1,"1:7":1,"1:8":1,"1:9":1,"1:10":1,"1:11":1,"3:2":2,"3:3":2,"3:4":2,"3:5":2,"3:6":2,"3:7":2,"3:8":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":5,"5:11":6,"5:12":6,"5:13":6,"5:14":6,"7:2":1,"7:3":1,"7:4":1,"7:5":1,"7:6":1,"7:7":1,"7:8":1,"7:9":1,"7:10":1,"7:11":1,"7:12":1,"8:1":7,"8:2":7,"8:3":7,"8:4":7,"8:5":7,"8:6":8,"8:7":9,"8:8":10,"8:9":10,"8:10":10,"8:11":10,"9:1":11,"9:2":11,"9:3":11,"9:4":11,"9:5":11,"10:2":12,"10:3":12,"10:4":12,"10:5":12,"10:6":12,"11:1":13,"11:2":13,"11:3":13,"11:4":13,"11:5":13,"11:6":13,"11:7":13,"11:8":13,"11:9":13,"11:10":13,"11:11":13,"11:12":13,"11:13":13,"11:14":14,"11:15":14,"11:16":14,"11:17":14,"11:18":14,"11:19":14,"12:1":15,"12:2":15,"12:3":15,"12:4":15,"12:5":15,"12:6":15,"12:7":15,"12:8":15,"12:9":15},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"format","kind":"template","detail":"string","insertSnippet":"format = \"\"","documentation":"the default of --pkg-format: tar, zip, dir, single-file, or a format a package provides Since 2026.10.27.1."},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"c++20"},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"c++23"},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"c++26"},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"c++2a"},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"c++2c"},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"gnu++20"},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"gnu++23"},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"gnu++26"},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"c++latest"},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"c++fly"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}]],"hovers":[{"title":"[pack]","body":"plane: `unclassified`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"\"","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"\"b","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"b","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"main","body":"type: `string`\n\na file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"au","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"cxxflags","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"}]} diff --git a/tests/rules/golden/empty.toml.json b/tests/rules/golden/empty.toml.json new file mode 100644 index 0000000..8690ddf --- /dev/null +++ b/tests/rules/golden/empty.toml.json @@ -0,0 +1 @@ +{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,0,0],"0:2":[0,0,0]},"hover":{},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}]],"hovers":[]} diff --git a/tests/rules/golden/plain.mcpp.json b/tests/rules/golden/plain.mcpp.json new file mode 100644 index 0000000..a27e270 --- /dev/null +++ b/tests/rules/golden/plain.mcpp.json @@ -0,0 +1 @@ +{"diagnostics":{"warning":[{"code":"mcpp.buildscript.unknownSymbol","message":"unknown mcpp::li: not a directive, action role or typed-API name in the mcpp build-script snapshot (SPEC-007 §2; docs/30-build-mcpp.md)","severity":"warning","line":12,"startCharacter":5,"endCharacter":13}],"served":[]},"imports":[{"module":"std","line":0,"startCharacter":7,"endCharacter":10},{"module":"mcpp","line":1,"startCharacter":7,"endCharacter":11}],"completion":{"0:7":0,"0:8":1,"0:9":1,"0:10":1,"1:7":0,"1:8":2,"1:9":2,"1:10":2,"1:11":2,"4:10":3,"4:11":4,"4:12":4,"4:13":4,"4:14":4,"4:15":4,"4:16":5,"4:17":6,"4:18":6,"4:19":6,"4:20":6,"4:21":6,"5:10":3,"5:11":7,"5:12":7,"5:13":7,"5:14":7,"5:15":7,"5:16":7,"5:17":7,"5:18":7,"5:19":7,"5:20":7,"5:21":7,"6:10":3,"6:11":8,"6:12":9,"6:13":9,"6:14":10,"6:15":10,"6:16":10,"6:17":10,"6:18":10,"6:19":10,"6:20":10,"6:21":10,"6:22":10,"6:23":10,"6:24":10,"6:25":10,"6:26":10,"6:27":10,"6:28":10,"7:10":3,"7:11":11,"7:12":12,"7:13":12,"7:14":12,"7:15":12,"7:16":12,"9:19":3,"9:20":8,"9:21":13,"9:22":13,"9:23":13,"9:24":13,"11:10":3,"11:11":4,"11:12":4,"12:10":3},"hover":{"0:7":0,"0:8":0,"0:9":0,"0:10":0,"1:7":1,"1:8":1,"1:9":1,"1:10":1,"1:11":1,"4:4":2,"4:5":2,"4:6":2,"4:7":2,"4:8":2,"4:9":2,"4:10":2,"4:11":2,"4:12":2,"4:13":2,"4:14":2,"4:15":2,"4:16":2,"4:17":2,"4:18":2,"4:19":2,"4:20":2,"4:21":2,"5:4":3,"5:5":3,"5:6":3,"5:7":3,"5:8":3,"5:9":3,"5:10":3,"5:11":3,"5:12":3,"5:13":3,"5:14":3,"5:15":3,"5:16":3,"5:17":3,"5:18":3,"5:19":3,"5:20":3,"5:21":3,"6:4":4,"6:5":4,"6:6":4,"6:7":4,"6:8":4,"6:9":4,"6:10":4,"6:11":4,"6:12":4,"6:13":4,"6:14":4,"6:15":4,"6:16":4,"6:17":4,"6:18":4,"6:19":4,"6:20":4,"6:21":4,"6:22":4,"6:23":4,"6:24":4,"6:25":4,"6:26":4,"6:27":4,"6:28":4,"7:4":5,"7:5":5,"7:6":5,"7:7":5,"7:8":5,"7:9":5,"7:10":5,"7:11":5,"7:12":5,"7:13":5,"7:14":5,"7:15":5,"7:16":5},"lists":[[{"label":"std","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+)."},{"label":"std.compat","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library plus the C library names in the global namespace; `import std;` and `import std.compat;` may be used together."},{"label":"mcpp","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1)."},{"label":"mcpp.core","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2)."}],[{"label":"std","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+)."},{"label":"std.compat","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library plus the C library names in the global namespace; `import std;` and `import std.compat;` may be used together."}],[{"label":"mcpp","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1)."},{"label":"mcpp.core","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2)."}],[{"label":"cxxflag","kind":"function","detail":"mcpp build-script API"},{"label":"cflag","kind":"function","detail":"mcpp build-script API"},{"label":"link_lib","kind":"function","detail":"mcpp build-script API"},{"label":"link_search","kind":"function","detail":"mcpp build-script API"},{"label":"cfg","kind":"function","detail":"mcpp build-script API"},{"label":"generated","kind":"function","detail":"mcpp build-script API"},{"label":"source","kind":"function","detail":"mcpp build-script API"},{"label":"runner","kind":"function","detail":"mcpp build-script API"},{"label":"runner_named","kind":"function","detail":"mcpp build-script API"},{"label":"runner_longlived","kind":"function","detail":"mcpp build-script API"},{"label":"run_exclusive","kind":"function","detail":"mcpp build-script API"},{"label":"link_script","kind":"function","detail":"mcpp build-script API"},{"label":"link_flag","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir_after","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_env_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed_glob","kind":"function","detail":"mcpp build-script API"},{"label":"warning","kind":"function","detail":"mcpp build-script API"},{"label":"diagnostic","kind":"function","detail":"mcpp build-script API"},{"label":"action","kind":"function","detail":"mcpp build-script API"},{"label":"fact","kind":"function","detail":"mcpp build-script API"},{"label":"floor","kind":"function","detail":"mcpp build-script API"},{"label":"pack_format","kind":"function","detail":"mcpp build-script API"},{"label":"windows_subsystem","kind":"function","detail":"mcpp build-script API"},{"label":"windows_entry","kind":"function","detail":"mcpp build-script API"},{"label":"deploy","kind":"function","detail":"mcpp build-script API"},{"label":"runtime_search_dir","kind":"function","detail":"mcpp build-script API"},{"label":"decision","kind":"function","detail":"mcpp build-script API"},{"label":"xpkg_request","kind":"function","detail":"mcpp build-script API"},{"label":"toolchain","kind":"function","detail":"mcpp build-script API"},{"label":"link_start","kind":"function","detail":"mcpp build-script API"},{"label":"link_driver","kind":"function","detail":"mcpp build-script API"},{"label":"link_input","kind":"function","detail":"mcpp build-script API"},{"label":"artifact_name","kind":"function","detail":"mcpp build-script API"},{"label":"seal","kind":"function","detail":"mcpp build-script API"},{"label":"run_host","kind":"function","detail":"mcpp build-script API"},{"label":"action ... (typed action block)","kind":"snippet","filterText":"action","detail":"mcpp::action with id, role, description, arg/input/output and submit","documentation":"The five roles (SPEC-007 R3.6): source, check, object, artifact, prepare.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,object,artifact,prepare|};\n${1:handle}.description = \"${4:description}\";\n${1:handle}.arg(\"${5:argument}\")\n .input(\"${6:input}\")\n .output(\"${7:output}\")\n .submit();"},{"label":"action ... (prepare block)","kind":"snippet","filterText":"action","detail":"mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)","documentation":"A prepare action must declare output_dir; the engine refuses one without it.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"},{"label":"cxxflag(...)","kind":"snippet","filterText":"cxxflag","detail":"mcpp:cxxflag= · slot CxxFlags","documentation":"SPEC-007 R2.1","insertText":"cxxflag(\"${1:value}\")"},{"label":"cflag(...)","kind":"snippet","filterText":"cflag","detail":"mcpp:cflag= · slot CFlags","documentation":"SPEC-007 R2.1","insertText":"cflag(\"${1:value}\")"},{"label":"link_lib(...)","kind":"snippet","filterText":"link_lib","detail":"mcpp:link-lib= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_lib(\"${1:value}\")"},{"label":"link_search(...)","kind":"snippet","filterText":"link_search","detail":"mcpp:link-search= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_search(\"${1:value}\")"},{"label":"cfg(...)","kind":"snippet","filterText":"cfg","detail":"mcpp:cfg= · slot Defines","documentation":"SPEC-007 R2.1","insertText":"cfg(\"${1:value}\")"},{"label":"generated(...)","kind":"snippet","filterText":"generated","detail":"mcpp:generated= · slot Generated","documentation":"SPEC-007 R2.1","insertText":"generated(\"${1:value}\")"},{"label":"source(...)","kind":"snippet","filterText":"source","detail":"mcpp:source= · slot Sources","documentation":"SPEC-007 R2.1, R3.3, R3.6, R3.9","insertText":"source(\"${1:value}\")"},{"label":"runner(...)","kind":"snippet","filterText":"runner","detail":"mcpp:runner= · slot Runner","documentation":"SPEC-007 R9.10 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192)","insertText":"runner(\"${1:value}\")"},{"label":"runner_named(...)","kind":"snippet","filterText":"runner_named","detail":"mcpp:runner-named= · slot NamedRunner","insertText":"runner_named(\"${1:value}\")"},{"label":"runner_longlived(...)","kind":"snippet","filterText":"runner_longlived","detail":"mcpp:runner-longlived= · slot RunnerLongLived","insertText":"runner_longlived(\"${1:value}\")"},{"label":"run_exclusive(...)","kind":"snippet","filterText":"run_exclusive","detail":"mcpp:run-exclusive= · slot RunExclusive","insertText":"run_exclusive(\"${1:value}\")"},{"label":"link_script(...)","kind":"snippet","filterText":"link_script","detail":"mcpp:link-script= · slot LdFlags","insertText":"link_script(\"${1:value}\")"},{"label":"link_flag(...)","kind":"snippet","filterText":"link_flag","detail":"mcpp:link-flag= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_flag(\"${1:value}\")"},{"label":"include_dir(...)","kind":"snippet","filterText":"include_dir","detail":"mcpp:include-dir= · slot IncludeDirs","documentation":"SPEC-007 R2.1","insertText":"include_dir(\"${1:value}\")"},{"label":"include_dir_after(...)","kind":"snippet","filterText":"include_dir_after","detail":"mcpp:include-dir-after= · slot IncludeDirsAfter","documentation":"SPEC-007 R2.1","insertText":"include_dir_after(\"${1:value}\")"},{"label":"rerun_if_changed(...)","kind":"snippet","filterText":"rerun_if_changed","detail":"mcpp:rerun-if-changed= · slot RerunFiles","insertText":"rerun_if_changed(\"${1:value}\")"},{"label":"rerun_if_env_changed(...)","kind":"snippet","filterText":"rerun_if_env_changed","detail":"mcpp:rerun-if-env-changed= · slot RerunEnv","insertText":"rerun_if_env_changed(\"${1:value}\")"},{"label":"rerun_if_changed_glob(...)","kind":"snippet","filterText":"rerun_if_changed_glob","detail":"mcpp:rerun-if-changed-glob= · slot RerunGlobs","insertText":"rerun_if_changed_glob(\"${1:value}\")"},{"label":"warning(...)","kind":"snippet","filterText":"warning","detail":"mcpp:warning= · slot Warnings","documentation":"SPEC-007 R1.2, R2.1, R9.4 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212)","insertText":"warning(\"${1:value}\")"},{"label":"diagnostic(...)","kind":"snippet","filterText":"diagnostic","detail":"mcpp:diagnostic= · slot Diagnostics","insertText":"diagnostic(\"${1:value}\")"},{"label":"fact(...)","kind":"snippet","filterText":"fact","detail":"mcpp:fact= · slot Facts","documentation":"SPEC-007 R2.1 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952)","insertText":"fact(\"${1:value}\")"},{"label":"floor(...)","kind":"snippet","filterText":"floor","detail":"mcpp:floor= · slot Floors","documentation":"SPEC-007 R2.1 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952)","insertText":"floor(\"${1:value}\")"},{"label":"pack_format(...)","kind":"snippet","filterText":"pack_format","detail":"mcpp:pack-format= · slot PackFormats","documentation":"SPEC-007 R9.10","insertText":"pack_format(\"${1:value}\")"},{"label":"windows_subsystem(...)","kind":"snippet","filterText":"windows_subsystem","detail":"mcpp:windows-subsystem= · slot WindowsSubsystem","insertText":"windows_subsystem(\"${1:value}\")"},{"label":"windows_entry(...)","kind":"snippet","filterText":"windows_entry","detail":"mcpp:windows-entry= · slot WindowsEntry","insertText":"windows_entry(\"${1:value}\")"},{"label":"deploy(...)","kind":"snippet","filterText":"deploy","detail":"mcpp:deploy= · slot Deploy","documentation":"SPEC-007 R2.1, R4.2, R9.2, R9.5 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11)","insertText":"deploy(\"${1:value}\")"},{"label":"runtime_search_dir(...)","kind":"snippet","filterText":"runtime_search_dir","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","documentation":"SPEC-007 R2.1","insertText":"runtime_search_dir(\"${1:value}\")"},{"label":"decision(...)","kind":"snippet","filterText":"decision","detail":"mcpp:decision= · slot ToolDecisions","documentation":"SPEC-007 R6.5, R9.2","insertText":"decision(\"${1:value}\")"},{"label":"xpkg_request(...)","kind":"snippet","filterText":"xpkg_request","detail":"mcpp:xpkg-request= · slot XpkgRequests","insertText":"xpkg_request(\"${1:value}\")"},{"label":"toolchain(...)","kind":"snippet","filterText":"toolchain","detail":"mcpp:toolchain= · slot ToolchainStatement","documentation":"SPEC-007 R9.2, R9.9","insertText":"toolchain(\"${1:value}\")"},{"label":"link_start(...)","kind":"snippet","filterText":"link_start","detail":"mcpp:link-start= · slot LinkStart","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_start(\"${1:value}\")"},{"label":"link_driver(...)","kind":"snippet","filterText":"link_driver","detail":"mcpp:link-driver= · slot LinkDriver","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_driver(\"${1:value}\")"},{"label":"link_input(...)","kind":"snippet","filterText":"link_input","detail":"mcpp:link-input= · slot LinkInputs","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_input(\"${1:value}\")"},{"label":"artifact_name(...)","kind":"snippet","filterText":"artifact_name","detail":"mcpp:artifact-name= · slot ArtifactNames","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"artifact_name(\"${1:value}\")"},{"label":"seal(...)","kind":"snippet","filterText":"seal","detail":"mcpp:seal= · slot Seal","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"seal(\"${1:value}\")"},{"label":"run_host(...)","kind":"snippet","filterText":"run_host","detail":"mcpp:run-host= · slot RunHost","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"run_host(\"${1:value}\")"}],[{"label":"link_lib","kind":"function","detail":"mcpp build-script API"},{"label":"link_search","kind":"function","detail":"mcpp build-script API"},{"label":"link_script","kind":"function","detail":"mcpp build-script API"},{"label":"link_flag","kind":"function","detail":"mcpp build-script API"},{"label":"link_start","kind":"function","detail":"mcpp build-script API"},{"label":"link_driver","kind":"function","detail":"mcpp build-script API"},{"label":"link_input","kind":"function","detail":"mcpp build-script API"},{"label":"link_lib(...)","kind":"snippet","filterText":"link_lib","detail":"mcpp:link-lib= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_lib(\"${1:value}\")"},{"label":"link_search(...)","kind":"snippet","filterText":"link_search","detail":"mcpp:link-search= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_search(\"${1:value}\")"},{"label":"link_script(...)","kind":"snippet","filterText":"link_script","detail":"mcpp:link-script= · slot LdFlags","insertText":"link_script(\"${1:value}\")"},{"label":"link_flag(...)","kind":"snippet","filterText":"link_flag","detail":"mcpp:link-flag= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_flag(\"${1:value}\")"},{"label":"link_start(...)","kind":"snippet","filterText":"link_start","detail":"mcpp:link-start= · slot LinkStart","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_start(\"${1:value}\")"},{"label":"link_driver(...)","kind":"snippet","filterText":"link_driver","detail":"mcpp:link-driver= · slot LinkDriver","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_driver(\"${1:value}\")"},{"label":"link_input(...)","kind":"snippet","filterText":"link_input","detail":"mcpp:link-input= · slot LinkInputs","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_input(\"${1:value}\")"}],[{"label":"link_search","kind":"function","detail":"mcpp build-script API"},{"label":"link_script","kind":"function","detail":"mcpp build-script API"},{"label":"link_start","kind":"function","detail":"mcpp build-script API"},{"label":"link_search(...)","kind":"snippet","filterText":"link_search","detail":"mcpp:link-search= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_search(\"${1:value}\")"},{"label":"link_script(...)","kind":"snippet","filterText":"link_script","detail":"mcpp:link-script= · slot LdFlags","insertText":"link_script(\"${1:value}\")"},{"label":"link_start(...)","kind":"snippet","filterText":"link_start","detail":"mcpp:link-start= · slot LinkStart","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_start(\"${1:value}\")"}],[{"label":"link_search","kind":"function","detail":"mcpp build-script API"},{"label":"link_search(...)","kind":"snippet","filterText":"link_search","detail":"mcpp:link-search= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_search(\"${1:value}\")"}],[{"label":"include_dir","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir_after","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir(...)","kind":"snippet","filterText":"include_dir","detail":"mcpp:include-dir= · slot IncludeDirs","documentation":"SPEC-007 R2.1","insertText":"include_dir(\"${1:value}\")"},{"label":"include_dir_after(...)","kind":"snippet","filterText":"include_dir_after","detail":"mcpp:include-dir-after= · slot IncludeDirsAfter","documentation":"SPEC-007 R2.1","insertText":"include_dir_after(\"${1:value}\")"}],[{"label":"runner","kind":"function","detail":"mcpp build-script API"},{"label":"runner_named","kind":"function","detail":"mcpp build-script API"},{"label":"runner_longlived","kind":"function","detail":"mcpp build-script API"},{"label":"run_exclusive","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_env_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed_glob","kind":"function","detail":"mcpp build-script API"},{"label":"runtime_search_dir","kind":"function","detail":"mcpp build-script API"},{"label":"run_host","kind":"function","detail":"mcpp build-script API"},{"label":"runner(...)","kind":"snippet","filterText":"runner","detail":"mcpp:runner= · slot Runner","documentation":"SPEC-007 R9.10 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192)","insertText":"runner(\"${1:value}\")"},{"label":"runner_named(...)","kind":"snippet","filterText":"runner_named","detail":"mcpp:runner-named= · slot NamedRunner","insertText":"runner_named(\"${1:value}\")"},{"label":"runner_longlived(...)","kind":"snippet","filterText":"runner_longlived","detail":"mcpp:runner-longlived= · slot RunnerLongLived","insertText":"runner_longlived(\"${1:value}\")"},{"label":"run_exclusive(...)","kind":"snippet","filterText":"run_exclusive","detail":"mcpp:run-exclusive= · slot RunExclusive","insertText":"run_exclusive(\"${1:value}\")"},{"label":"rerun_if_changed(...)","kind":"snippet","filterText":"rerun_if_changed","detail":"mcpp:rerun-if-changed= · slot RerunFiles","insertText":"rerun_if_changed(\"${1:value}\")"},{"label":"rerun_if_env_changed(...)","kind":"snippet","filterText":"rerun_if_env_changed","detail":"mcpp:rerun-if-env-changed= · slot RerunEnv","insertText":"rerun_if_env_changed(\"${1:value}\")"},{"label":"rerun_if_changed_glob(...)","kind":"snippet","filterText":"rerun_if_changed_glob","detail":"mcpp:rerun-if-changed-glob= · slot RerunGlobs","insertText":"rerun_if_changed_glob(\"${1:value}\")"},{"label":"runtime_search_dir(...)","kind":"snippet","filterText":"runtime_search_dir","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","documentation":"SPEC-007 R2.1","insertText":"runtime_search_dir(\"${1:value}\")"},{"label":"run_host(...)","kind":"snippet","filterText":"run_host","detail":"mcpp:run-host= · slot RunHost","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"run_host(\"${1:value}\")"}],[{"label":"runner","kind":"function","detail":"mcpp build-script API"},{"label":"runner_named","kind":"function","detail":"mcpp build-script API"},{"label":"runner_longlived","kind":"function","detail":"mcpp build-script API"},{"label":"run_exclusive","kind":"function","detail":"mcpp build-script API"},{"label":"runtime_search_dir","kind":"function","detail":"mcpp build-script API"},{"label":"run_host","kind":"function","detail":"mcpp build-script API"},{"label":"runner(...)","kind":"snippet","filterText":"runner","detail":"mcpp:runner= · slot Runner","documentation":"SPEC-007 R9.10 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192)","insertText":"runner(\"${1:value}\")"},{"label":"runner_named(...)","kind":"snippet","filterText":"runner_named","detail":"mcpp:runner-named= · slot NamedRunner","insertText":"runner_named(\"${1:value}\")"},{"label":"runner_longlived(...)","kind":"snippet","filterText":"runner_longlived","detail":"mcpp:runner-longlived= · slot RunnerLongLived","insertText":"runner_longlived(\"${1:value}\")"},{"label":"run_exclusive(...)","kind":"snippet","filterText":"run_exclusive","detail":"mcpp:run-exclusive= · slot RunExclusive","insertText":"run_exclusive(\"${1:value}\")"},{"label":"runtime_search_dir(...)","kind":"snippet","filterText":"runtime_search_dir","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","documentation":"SPEC-007 R2.1","insertText":"runtime_search_dir(\"${1:value}\")"},{"label":"run_host(...)","kind":"snippet","filterText":"run_host","detail":"mcpp:run-host= · slot RunHost","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"run_host(\"${1:value}\")"}],[{"label":"runtime_search_dir","kind":"function","detail":"mcpp build-script API"},{"label":"runtime_search_dir(...)","kind":"snippet","filterText":"runtime_search_dir","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","documentation":"SPEC-007 R2.1","insertText":"runtime_search_dir(\"${1:value}\")"}],[{"label":"action","kind":"function","detail":"mcpp build-script API"},{"label":"artifact_name","kind":"function","detail":"mcpp build-script API"},{"label":"action ... (typed action block)","kind":"snippet","filterText":"action","detail":"mcpp::action with id, role, description, arg/input/output and submit","documentation":"The five roles (SPEC-007 R3.6): source, check, object, artifact, prepare.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,object,artifact,prepare|};\n${1:handle}.description = \"${4:description}\";\n${1:handle}.arg(\"${5:argument}\")\n .input(\"${6:input}\")\n .output(\"${7:output}\")\n .submit();"},{"label":"action ... (prepare block)","kind":"snippet","filterText":"action","detail":"mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)","documentation":"A prepare action must declare output_dir; the engine refuses one without it.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"},{"label":"artifact_name(...)","kind":"snippet","filterText":"artifact_name","detail":"mcpp:artifact-name= · slot ArtifactNames","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"artifact_name(\"${1:value}\")"}],[{"label":"action","kind":"function","detail":"mcpp build-script API"},{"label":"action ... (typed action block)","kind":"snippet","filterText":"action","detail":"mcpp::action with id, role, description, arg/input/output and submit","documentation":"The five roles (SPEC-007 R3.6): source, check, object, artifact, prepare.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,object,artifact,prepare|};\n${1:handle}.description = \"${4:description}\";\n${1:handle}.arg(\"${5:argument}\")\n .input(\"${6:input}\")\n .output(\"${7:output}\")\n .submit();"},{"label":"action ... (prepare block)","kind":"snippet","filterText":"action","detail":"mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)","documentation":"A prepare action must declare output_dir; the engine refuses one without it.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"}],[]],"hovers":["**std**\n\nThe C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+).","**mcpp**\n\nThe mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1).","**mcpp::link_search**\n\nwire `mcpp:link-search=` · slot `LdFlags` · scope `LinkGlobal` · since protocol 1\n\nSPEC-007: R2.1","**mcpp::include_dir**\n\nwire `mcpp:include-dir=` · slot `IncludeDirs` · scope `PackagePrivate` · since protocol 1\n\nSPEC-007: R2.1","**mcpp::runtime_search_dir**\n\nwire `mcpp:runtime-search-dir=` · slot `RuntimeSearchDir` · scope `LinkGlobal` · since protocol 12\n\nSPEC-007: R2.1","**mcpp::action**\n\nwire `mcpp:action=` · slot `Actions` · scope `GraphNode` · since protocol 1\n\nSPEC-007: R1.1, R1.2, R1.3, R2.1, R3.1, R3.2, R3.3, R3.5, R3.7, R3.8, R3.9, R5.1, R5.3, R6.4, R9.2, R9.10"]} diff --git a/tests/rules/golden/rules.mcpp.json b/tests/rules/golden/rules.mcpp.json new file mode 100644 index 0000000..2ddb460 --- /dev/null +++ b/tests/rules/golden/rules.mcpp.json @@ -0,0 +1 @@ +{"diagnostics":{"warning":[{"code":"mcpp.buildscript.unknownSymbol","message":"unknown mcpp::unknown_thing: not a directive, action role or typed-API name in the mcpp build-script snapshot (SPEC-007 §2; docs/30-build-mcpp.md)","severity":"warning","line":13,"startCharacter":5,"endCharacter":24},{"code":"mcpp.buildscript.rawPathFlag","message":"spelling \"-I/usr/include/foo\" in a raw flag bypasses the engine's rendering; use mcpp::include_dir / mcpp::include_dir_after / mcpp::link_search (SPEC-007 R2.1)","severity":"warning","line":15,"startCharacter":20,"endCharacter":38},{"code":"mcpp.buildscript.rawPathFlag","message":"spelling \"-L/opt/lib\" in a raw flag bypasses the engine's rendering; use mcpp::include_dir / mcpp::include_dir_after / mcpp::link_search (SPEC-007 R2.1)","severity":"warning","line":16,"startCharacter":18,"endCharacter":28},{"code":"mcpp.buildscript.rpathInLinkFlag","message":"a run path must not be spelled in link_flag; declare the directory with mcpp::runtime_search_dir(…) (SPEC-007 R4.4)","severity":"warning","line":17,"startCharacter":22,"endCharacter":32},{"code":"mcpp.buildscript.rpathInLinkFlag","message":"a run path must not be spelled in link_flag; declare the directory with mcpp::runtime_search_dir(…) (SPEC-007 R4.4)","severity":"warning","line":18,"startCharacter":39,"endCharacter":49},{"code":"mcpp.buildscript.actionNeedsRole","message":"this mcpp::action declares no role; name one of source, check, object, artifact, prepare (mcpp::roles::…) — a missing role is read as \"source\" (SPEC-007 R3.2, R3.6)","severity":"warning","line":27,"startCharacter":5,"endCharacter":17},{"code":"mcpp.buildscript.prepareNeedsOutputDir","message":"a prepare action must declare the directory its command fills with output_dir(…): the engine refuses an action without one (SPEC-007 R3.3)","severity":"warning","line":33,"startCharacter":16,"endCharacter":25},{"code":"mcpp.buildscript.roleLiteral","message":"role is spelled as the string \"prepare\"; use mcpp::roles::prepare so an engine that does not know the role refuses to compile it (SPEC-007 R3.6)","severity":"warning","line":33,"startCharacter":16,"endCharacter":25},{"code":"mcpp.buildscript.actionNeedsRole","message":"unknown action role \"compile\"; the five roles are source, check, object, artifact, prepare (SPEC-007 R3.6)","severity":"warning","line":37,"startCharacter":18,"endCharacter":27},{"code":"mcpp.buildscript.prepareNeedsOutputDir","message":"a prepare action must declare the directory its command fills with output_dir(…): the engine refuses an action without one (SPEC-007 R3.3)","severity":"warning","line":42,"startCharacter":17,"endCharacter":37},{"code":"mcpp.buildscript.shellSyntaxInAction","message":"the command spells shell syntax (\"FOO=bar\"); declare the environment and working directory with env(…) and cwd(…) instead (SPEC-007 R3.8)","severity":"warning","line":43,"startCharacter":15,"endCharacter":22},{"code":"mcpp.buildscript.shellSyntaxInAction","message":"the command spells shell syntax (\"cd build &&\"); declare the environment and working directory with env(…) and cwd(…) instead (SPEC-007 R3.8)","severity":"warning","line":44,"startCharacter":19,"endCharacter":30},{"code":"mcpp.buildscript.prepareNeedsOutputDir","message":"a prepare action must declare output_dir: the engine refuses an action without one (SPEC-007 R3.3)","severity":"warning","line":53,"startCharacter":18,"endCharacter":81},{"code":"mcpp.buildscript.shellSyntaxInAction","message":"the command spells shell syntax (\"A=1\"); declare it with env(…) and cwd(…) instead (SPEC-007 R3.8)","severity":"warning","line":53,"startCharacter":18,"endCharacter":81},{"code":"mcpp.buildscript.actionNeedsRole","message":"this mcpp:action payload declares no role; the five roles are source, check, object, artifact, prepare (SPEC-007 R3.6)","severity":"warning","line":54,"startCharacter":18,"endCharacter":64},{"code":"mcpp.buildscript.shellSyntaxInAction","message":"the command spells shell syntax (\"cd x &&\"); declare it with env(…) and cwd(…) instead (SPEC-007 R3.8)","severity":"warning","line":54,"startCharacter":18,"endCharacter":64},{"code":"mcpp.buildscript.actionNeedsRole","message":"unknown action role \"weird\"; the five roles are source, check, object, artifact, prepare (SPEC-007 R3.6)","severity":"warning","line":55,"startCharacter":18,"endCharacter":52}],"served":[{"code":"mcpp.buildscript.rawPathFlag","message":"spelling \"-I/usr/include/foo\" in a raw flag bypasses the engine's rendering; use mcpp::include_dir / mcpp::include_dir_after / mcpp::link_search (SPEC-007 R2.1)","severity":"info","line":15,"startCharacter":20,"endCharacter":38},{"code":"mcpp.buildscript.rawPathFlag","message":"spelling \"-L/opt/lib\" in a raw flag bypasses the engine's rendering; use mcpp::include_dir / mcpp::include_dir_after / mcpp::link_search (SPEC-007 R2.1)","severity":"info","line":16,"startCharacter":18,"endCharacter":28},{"code":"mcpp.buildscript.rpathInLinkFlag","message":"a run path must not be spelled in link_flag; declare the directory with mcpp::runtime_search_dir(…) (SPEC-007 R4.4)","severity":"info","line":17,"startCharacter":22,"endCharacter":32},{"code":"mcpp.buildscript.rpathInLinkFlag","message":"a run path must not be spelled in link_flag; declare the directory with mcpp::runtime_search_dir(…) (SPEC-007 R4.4)","severity":"info","line":18,"startCharacter":39,"endCharacter":49},{"code":"mcpp.buildscript.actionNeedsRole","message":"this mcpp::action declares no role; name one of source, check, object, artifact, prepare (mcpp::roles::…) — a missing role is read as \"source\" (SPEC-007 R3.2, R3.6)","severity":"info","line":27,"startCharacter":5,"endCharacter":17},{"code":"mcpp.buildscript.prepareNeedsOutputDir","message":"a prepare action must declare the directory its command fills with output_dir(…): the engine refuses an action without one (SPEC-007 R3.3)","severity":"info","line":33,"startCharacter":16,"endCharacter":25},{"code":"mcpp.buildscript.roleLiteral","message":"role is spelled as the string \"prepare\"; use mcpp::roles::prepare so an engine that does not know the role refuses to compile it (SPEC-007 R3.6)","severity":"info","line":33,"startCharacter":16,"endCharacter":25},{"code":"mcpp.buildscript.actionNeedsRole","message":"unknown action role \"compile\"; the five roles are source, check, object, artifact, prepare (SPEC-007 R3.6)","severity":"info","line":37,"startCharacter":18,"endCharacter":27},{"code":"mcpp.buildscript.prepareNeedsOutputDir","message":"a prepare action must declare the directory its command fills with output_dir(…): the engine refuses an action without one (SPEC-007 R3.3)","severity":"info","line":42,"startCharacter":17,"endCharacter":37},{"code":"mcpp.buildscript.shellSyntaxInAction","message":"the command spells shell syntax (\"FOO=bar\"); declare the environment and working directory with env(…) and cwd(…) instead (SPEC-007 R3.8)","severity":"info","line":43,"startCharacter":15,"endCharacter":22},{"code":"mcpp.buildscript.shellSyntaxInAction","message":"the command spells shell syntax (\"cd build &&\"); declare the environment and working directory with env(…) and cwd(…) instead (SPEC-007 R3.8)","severity":"info","line":44,"startCharacter":19,"endCharacter":30},{"code":"mcpp.buildscript.prepareNeedsOutputDir","message":"a prepare action must declare output_dir: the engine refuses an action without one (SPEC-007 R3.3)","severity":"info","line":53,"startCharacter":18,"endCharacter":81},{"code":"mcpp.buildscript.shellSyntaxInAction","message":"the command spells shell syntax (\"A=1\"); declare it with env(…) and cwd(…) instead (SPEC-007 R3.8)","severity":"info","line":53,"startCharacter":18,"endCharacter":81},{"code":"mcpp.buildscript.actionNeedsRole","message":"this mcpp:action payload declares no role; the five roles are source, check, object, artifact, prepare (SPEC-007 R3.6)","severity":"info","line":54,"startCharacter":18,"endCharacter":64},{"code":"mcpp.buildscript.shellSyntaxInAction","message":"the command spells shell syntax (\"cd x &&\"); declare it with env(…) and cwd(…) instead (SPEC-007 R3.8)","severity":"info","line":54,"startCharacter":18,"endCharacter":64},{"code":"mcpp.buildscript.actionNeedsRole","message":"unknown action role \"weird\"; the five roles are source, check, object, artifact, prepare (SPEC-007 R3.6)","severity":"info","line":55,"startCharacter":18,"endCharacter":52}]},"imports":[{"module":"std","line":1,"startCharacter":7,"endCharacter":10},{"module":"mcpp","line":2,"startCharacter":7,"endCharacter":11},{"module":"mcpp.plugins.tool","line":3,"startCharacter":7,"endCharacter":24},{"module":"mcpp.rules.qt","line":4,"startCharacter":7,"endCharacter":20},{"module":"mcpp.core","line":5,"startCharacter":14,"endCharacter":23}],"completion":{"1:7":0,"1:8":1,"1:9":1,"1:10":1,"2:7":0,"2:8":2,"2:9":2,"2:10":2,"2:11":2,"3:7":0,"3:8":2,"3:9":2,"3:10":2,"3:11":2,"3:12":3,"3:13":4,"3:14":4,"3:15":4,"3:16":4,"3:17":4,"3:18":4,"3:19":4,"3:20":4,"3:21":4,"3:22":4,"3:23":4,"3:24":4,"4:7":0,"4:8":2,"4:9":2,"4:10":2,"4:11":2,"4:12":3,"4:13":4,"4:14":4,"4:15":4,"4:16":4,"4:17":4,"4:18":4,"4:19":4,"4:20":4,"5:14":0,"5:15":2,"5:16":2,"5:17":2,"5:18":2,"5:19":3,"5:20":3,"5:21":3,"5:22":3,"5:23":3,"6:7":0,"7:7":0,"11:10":5,"11:11":6,"11:12":6,"11:13":6,"11:14":6,"11:15":6,"11:16":7,"11:17":7,"11:18":7,"12:10":5,"12:11":4,"12:12":4,"12:13":4,"12:14":4,"12:15":4,"12:16":4,"12:17":4,"12:18":4,"12:19":4,"12:20":4,"12:21":4,"12:22":4,"12:23":4,"13:10":5,"13:11":8,"13:12":9,"13:13":4,"13:14":4,"13:15":4,"14:10":5,"14:11":10,"14:12":11,"14:13":11,"14:14":11,"14:15":11,"14:16":11,"14:17":11,"15:10":5,"15:11":10,"15:12":12,"15:13":13,"15:14":13,"15:15":13,"16:10":5,"16:11":6,"16:12":6,"16:13":6,"16:14":6,"16:15":6,"16:16":14,"16:17":14,"16:18":14,"16:19":14,"20:13":5,"20:14":15,"20:15":15,"20:16":4,"20:17":4,"20:18":4,"20:19":4,"20:20":4,"20:21":4,"20:22":4,"20:23":4,"20:24":4,"20:25":4,"20:26":4,"20:27":4,"20:28":4,"20:29":4,"21:13":5,"21:14":16,"21:15":4,"21:16":4,"21:17":4,"21:18":4,"21:19":4,"21:20":4,"21:21":4,"21:22":4,"21:23":4,"21:24":4,"21:25":4,"21:26":4,"21:27":4,"21:28":4,"21:29":4,"22:25":5,"22:26":15,"22:27":15,"22:28":4,"22:29":4,"22:30":4,"22:31":4,"22:32":4,"22:33":4,"22:34":4,"22:35":4,"22:36":4,"22:37":4,"22:38":4,"23:10":5,"23:11":16,"23:12":4,"23:13":4,"23:14":4,"23:15":4,"23:16":4,"23:17":4,"23:18":4,"23:19":4,"24:33":5,"24:34":15,"24:35":15,"24:36":4,"24:37":4,"24:38":4,"24:39":4,"24:40":4,"24:41":4,"24:42":4,"26:10":5,"26:11":17,"26:12":18,"26:13":18,"26:14":18,"26:15":18,"26:16":18,"30:10":5,"30:11":17,"30:12":18,"30:13":18,"30:14":18,"30:15":18,"30:16":18,"35:10":5,"35:11":17,"35:12":18,"35:13":18,"35:14":18,"35:15":18,"35:16":18,"39:10":5,"39:11":17,"39:12":18,"39:13":18,"39:14":18,"39:15":18,"39:16":18,"41:22":5,"41:23":8,"41:24":4,"41:25":4,"41:26":4,"41:27":4,"46:10":5,"46:11":17,"46:12":18,"46:13":18,"46:14":18,"46:15":18,"46:16":18,"47:20":5,"47:21":8,"47:22":4,"47:23":4,"47:24":4,"47:25":4,"57:10":5},"hover":{"1:7":0,"1:8":0,"1:9":0,"1:10":0,"2:7":1,"2:8":1,"2:9":1,"2:10":1,"2:11":1,"3:7":2,"3:8":2,"3:9":2,"3:10":2,"3:11":2,"3:12":2,"3:13":2,"3:14":2,"3:15":2,"3:16":2,"3:17":2,"3:18":2,"3:19":2,"3:20":2,"3:21":2,"3:22":2,"3:23":2,"3:24":2,"4:7":3,"4:8":3,"4:9":3,"4:10":3,"4:11":3,"4:12":3,"4:13":3,"4:14":3,"4:15":3,"4:16":3,"4:17":3,"4:18":3,"4:19":3,"4:20":3,"5:14":4,"5:15":4,"5:16":4,"5:17":4,"5:18":4,"5:19":4,"5:20":4,"5:21":4,"5:22":4,"5:23":4,"11:4":5,"11:5":5,"11:6":5,"11:7":5,"11:8":5,"11:9":5,"11:10":5,"11:11":5,"11:12":5,"11:13":5,"11:14":5,"11:15":5,"11:16":5,"11:17":5,"11:18":5,"14:4":6,"14:5":6,"14:6":6,"14:7":6,"14:8":6,"14:9":6,"14:10":6,"14:11":6,"14:12":6,"14:13":6,"14:14":6,"14:15":6,"14:16":6,"14:17":6,"15:4":7,"15:5":7,"15:6":7,"15:7":7,"15:8":7,"15:9":7,"15:10":7,"15:11":7,"15:12":7,"15:13":7,"15:14":7,"15:15":7,"16:4":8,"16:5":8,"16:6":8,"16:7":8,"16:8":8,"16:9":8,"16:10":8,"16:11":8,"16:12":8,"16:13":8,"16:14":8,"16:15":8,"16:16":8,"16:17":8,"16:18":8,"16:19":8,"26:4":9,"26:5":9,"26:6":9,"26:7":9,"26:8":9,"26:9":9,"26:10":9,"26:11":9,"26:12":9,"26:13":9,"26:14":9,"26:15":9,"26:16":9,"30:4":9,"30:5":9,"30:6":9,"30:7":9,"30:8":9,"30:9":9,"30:10":9,"30:11":9,"30:12":9,"30:13":9,"30:14":9,"30:15":9,"30:16":9,"35:4":9,"35:5":9,"35:6":9,"35:7":9,"35:8":9,"35:9":9,"35:10":9,"35:11":9,"35:12":9,"35:13":9,"35:14":9,"35:15":9,"35:16":9,"39:4":9,"39:5":9,"39:6":9,"39:7":9,"39:8":9,"39:9":9,"39:10":9,"39:11":9,"39:12":9,"39:13":9,"39:14":9,"39:15":9,"39:16":9,"46:4":9,"46:5":9,"46:6":9,"46:7":9,"46:8":9,"46:9":9,"46:10":9,"46:11":9,"46:12":9,"46:13":9,"46:14":9,"46:15":9,"46:16":9},"lists":[[{"label":"std","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+)."},{"label":"std.compat","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library plus the C library names in the global namespace; `import std;` and `import std.compat;` may be used together."},{"label":"mcpp","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1)."},{"label":"mcpp.core","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2)."}],[{"label":"std","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+)."},{"label":"std.compat","kind":"module","detail":"mcpp build-script module","documentation":"The C++ standard library plus the C library names in the global namespace; `import std;` and `import std.compat;` may be used together."}],[{"label":"mcpp","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1)."},{"label":"mcpp.core","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2)."}],[{"label":"mcpp.core","kind":"module","detail":"mcpp build-script module","documentation":"The mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2)."}],[],[{"label":"cxxflag","kind":"function","detail":"mcpp build-script API"},{"label":"cflag","kind":"function","detail":"mcpp build-script API"},{"label":"link_lib","kind":"function","detail":"mcpp build-script API"},{"label":"link_search","kind":"function","detail":"mcpp build-script API"},{"label":"cfg","kind":"function","detail":"mcpp build-script API"},{"label":"generated","kind":"function","detail":"mcpp build-script API"},{"label":"source","kind":"function","detail":"mcpp build-script API"},{"label":"runner","kind":"function","detail":"mcpp build-script API"},{"label":"runner_named","kind":"function","detail":"mcpp build-script API"},{"label":"runner_longlived","kind":"function","detail":"mcpp build-script API"},{"label":"run_exclusive","kind":"function","detail":"mcpp build-script API"},{"label":"link_script","kind":"function","detail":"mcpp build-script API"},{"label":"link_flag","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir_after","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_env_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed_glob","kind":"function","detail":"mcpp build-script API"},{"label":"warning","kind":"function","detail":"mcpp build-script API"},{"label":"diagnostic","kind":"function","detail":"mcpp build-script API"},{"label":"action","kind":"function","detail":"mcpp build-script API"},{"label":"fact","kind":"function","detail":"mcpp build-script API"},{"label":"floor","kind":"function","detail":"mcpp build-script API"},{"label":"pack_format","kind":"function","detail":"mcpp build-script API"},{"label":"windows_subsystem","kind":"function","detail":"mcpp build-script API"},{"label":"windows_entry","kind":"function","detail":"mcpp build-script API"},{"label":"deploy","kind":"function","detail":"mcpp build-script API"},{"label":"runtime_search_dir","kind":"function","detail":"mcpp build-script API"},{"label":"decision","kind":"function","detail":"mcpp build-script API"},{"label":"xpkg_request","kind":"function","detail":"mcpp build-script API"},{"label":"toolchain","kind":"function","detail":"mcpp build-script API"},{"label":"link_start","kind":"function","detail":"mcpp build-script API"},{"label":"link_driver","kind":"function","detail":"mcpp build-script API"},{"label":"link_input","kind":"function","detail":"mcpp build-script API"},{"label":"artifact_name","kind":"function","detail":"mcpp build-script API"},{"label":"seal","kind":"function","detail":"mcpp build-script API"},{"label":"run_host","kind":"function","detail":"mcpp build-script API"},{"label":"action ... (typed action block)","kind":"snippet","filterText":"action","detail":"mcpp::action with id, role, description, arg/input/output and submit","documentation":"The five roles (SPEC-007 R3.6): source, check, object, artifact, prepare.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,object,artifact,prepare|};\n${1:handle}.description = \"${4:description}\";\n${1:handle}.arg(\"${5:argument}\")\n .input(\"${6:input}\")\n .output(\"${7:output}\")\n .submit();"},{"label":"action ... (prepare block)","kind":"snippet","filterText":"action","detail":"mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)","documentation":"A prepare action must declare output_dir; the engine refuses one without it.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"},{"label":"cxxflag(...)","kind":"snippet","filterText":"cxxflag","detail":"mcpp:cxxflag= · slot CxxFlags","documentation":"SPEC-007 R2.1","insertText":"cxxflag(\"${1:value}\")"},{"label":"cflag(...)","kind":"snippet","filterText":"cflag","detail":"mcpp:cflag= · slot CFlags","documentation":"SPEC-007 R2.1","insertText":"cflag(\"${1:value}\")"},{"label":"link_lib(...)","kind":"snippet","filterText":"link_lib","detail":"mcpp:link-lib= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_lib(\"${1:value}\")"},{"label":"link_search(...)","kind":"snippet","filterText":"link_search","detail":"mcpp:link-search= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_search(\"${1:value}\")"},{"label":"cfg(...)","kind":"snippet","filterText":"cfg","detail":"mcpp:cfg= · slot Defines","documentation":"SPEC-007 R2.1","insertText":"cfg(\"${1:value}\")"},{"label":"generated(...)","kind":"snippet","filterText":"generated","detail":"mcpp:generated= · slot Generated","documentation":"SPEC-007 R2.1","insertText":"generated(\"${1:value}\")"},{"label":"source(...)","kind":"snippet","filterText":"source","detail":"mcpp:source= · slot Sources","documentation":"SPEC-007 R2.1, R3.3, R3.6, R3.9","insertText":"source(\"${1:value}\")"},{"label":"runner(...)","kind":"snippet","filterText":"runner","detail":"mcpp:runner= · slot Runner","documentation":"SPEC-007 R9.10 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192)","insertText":"runner(\"${1:value}\")"},{"label":"runner_named(...)","kind":"snippet","filterText":"runner_named","detail":"mcpp:runner-named= · slot NamedRunner","insertText":"runner_named(\"${1:value}\")"},{"label":"runner_longlived(...)","kind":"snippet","filterText":"runner_longlived","detail":"mcpp:runner-longlived= · slot RunnerLongLived","insertText":"runner_longlived(\"${1:value}\")"},{"label":"run_exclusive(...)","kind":"snippet","filterText":"run_exclusive","detail":"mcpp:run-exclusive= · slot RunExclusive","insertText":"run_exclusive(\"${1:value}\")"},{"label":"link_script(...)","kind":"snippet","filterText":"link_script","detail":"mcpp:link-script= · slot LdFlags","insertText":"link_script(\"${1:value}\")"},{"label":"link_flag(...)","kind":"snippet","filterText":"link_flag","detail":"mcpp:link-flag= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_flag(\"${1:value}\")"},{"label":"include_dir(...)","kind":"snippet","filterText":"include_dir","detail":"mcpp:include-dir= · slot IncludeDirs","documentation":"SPEC-007 R2.1","insertText":"include_dir(\"${1:value}\")"},{"label":"include_dir_after(...)","kind":"snippet","filterText":"include_dir_after","detail":"mcpp:include-dir-after= · slot IncludeDirsAfter","documentation":"SPEC-007 R2.1","insertText":"include_dir_after(\"${1:value}\")"},{"label":"rerun_if_changed(...)","kind":"snippet","filterText":"rerun_if_changed","detail":"mcpp:rerun-if-changed= · slot RerunFiles","insertText":"rerun_if_changed(\"${1:value}\")"},{"label":"rerun_if_env_changed(...)","kind":"snippet","filterText":"rerun_if_env_changed","detail":"mcpp:rerun-if-env-changed= · slot RerunEnv","insertText":"rerun_if_env_changed(\"${1:value}\")"},{"label":"rerun_if_changed_glob(...)","kind":"snippet","filterText":"rerun_if_changed_glob","detail":"mcpp:rerun-if-changed-glob= · slot RerunGlobs","insertText":"rerun_if_changed_glob(\"${1:value}\")"},{"label":"warning(...)","kind":"snippet","filterText":"warning","detail":"mcpp:warning= · slot Warnings","documentation":"SPEC-007 R1.2, R2.1, R9.4 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212)","insertText":"warning(\"${1:value}\")"},{"label":"diagnostic(...)","kind":"snippet","filterText":"diagnostic","detail":"mcpp:diagnostic= · slot Diagnostics","insertText":"diagnostic(\"${1:value}\")"},{"label":"fact(...)","kind":"snippet","filterText":"fact","detail":"mcpp:fact= · slot Facts","documentation":"SPEC-007 R2.1 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952)","insertText":"fact(\"${1:value}\")"},{"label":"floor(...)","kind":"snippet","filterText":"floor","detail":"mcpp:floor= · slot Floors","documentation":"SPEC-007 R2.1 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952)","insertText":"floor(\"${1:value}\")"},{"label":"pack_format(...)","kind":"snippet","filterText":"pack_format","detail":"mcpp:pack-format= · slot PackFormats","documentation":"SPEC-007 R9.10","insertText":"pack_format(\"${1:value}\")"},{"label":"windows_subsystem(...)","kind":"snippet","filterText":"windows_subsystem","detail":"mcpp:windows-subsystem= · slot WindowsSubsystem","insertText":"windows_subsystem(\"${1:value}\")"},{"label":"windows_entry(...)","kind":"snippet","filterText":"windows_entry","detail":"mcpp:windows-entry= · slot WindowsEntry","insertText":"windows_entry(\"${1:value}\")"},{"label":"deploy(...)","kind":"snippet","filterText":"deploy","detail":"mcpp:deploy= · slot Deploy","documentation":"SPEC-007 R2.1, R4.2, R9.2, R9.5 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11)","insertText":"deploy(\"${1:value}\")"},{"label":"runtime_search_dir(...)","kind":"snippet","filterText":"runtime_search_dir","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","documentation":"SPEC-007 R2.1","insertText":"runtime_search_dir(\"${1:value}\")"},{"label":"decision(...)","kind":"snippet","filterText":"decision","detail":"mcpp:decision= · slot ToolDecisions","documentation":"SPEC-007 R6.5, R9.2","insertText":"decision(\"${1:value}\")"},{"label":"xpkg_request(...)","kind":"snippet","filterText":"xpkg_request","detail":"mcpp:xpkg-request= · slot XpkgRequests","insertText":"xpkg_request(\"${1:value}\")"},{"label":"toolchain(...)","kind":"snippet","filterText":"toolchain","detail":"mcpp:toolchain= · slot ToolchainStatement","documentation":"SPEC-007 R9.2, R9.9","insertText":"toolchain(\"${1:value}\")"},{"label":"link_start(...)","kind":"snippet","filterText":"link_start","detail":"mcpp:link-start= · slot LinkStart","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_start(\"${1:value}\")"},{"label":"link_driver(...)","kind":"snippet","filterText":"link_driver","detail":"mcpp:link-driver= · slot LinkDriver","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_driver(\"${1:value}\")"},{"label":"link_input(...)","kind":"snippet","filterText":"link_input","detail":"mcpp:link-input= · slot LinkInputs","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_input(\"${1:value}\")"},{"label":"artifact_name(...)","kind":"snippet","filterText":"artifact_name","detail":"mcpp:artifact-name= · slot ArtifactNames","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"artifact_name(\"${1:value}\")"},{"label":"seal(...)","kind":"snippet","filterText":"seal","detail":"mcpp:seal= · slot Seal","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"seal(\"${1:value}\")"},{"label":"run_host(...)","kind":"snippet","filterText":"run_host","detail":"mcpp:run-host= · slot RunHost","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"run_host(\"${1:value}\")"}],[{"label":"link_lib","kind":"function","detail":"mcpp build-script API"},{"label":"link_search","kind":"function","detail":"mcpp build-script API"},{"label":"link_script","kind":"function","detail":"mcpp build-script API"},{"label":"link_flag","kind":"function","detail":"mcpp build-script API"},{"label":"link_start","kind":"function","detail":"mcpp build-script API"},{"label":"link_driver","kind":"function","detail":"mcpp build-script API"},{"label":"link_input","kind":"function","detail":"mcpp build-script API"},{"label":"link_lib(...)","kind":"snippet","filterText":"link_lib","detail":"mcpp:link-lib= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_lib(\"${1:value}\")"},{"label":"link_search(...)","kind":"snippet","filterText":"link_search","detail":"mcpp:link-search= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_search(\"${1:value}\")"},{"label":"link_script(...)","kind":"snippet","filterText":"link_script","detail":"mcpp:link-script= · slot LdFlags","insertText":"link_script(\"${1:value}\")"},{"label":"link_flag(...)","kind":"snippet","filterText":"link_flag","detail":"mcpp:link-flag= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_flag(\"${1:value}\")"},{"label":"link_start(...)","kind":"snippet","filterText":"link_start","detail":"mcpp:link-start= · slot LinkStart","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_start(\"${1:value}\")"},{"label":"link_driver(...)","kind":"snippet","filterText":"link_driver","detail":"mcpp:link-driver= · slot LinkDriver","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_driver(\"${1:value}\")"},{"label":"link_input(...)","kind":"snippet","filterText":"link_input","detail":"mcpp:link-input= · slot LinkInputs","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"link_input(\"${1:value}\")"}],[{"label":"link_lib","kind":"function","detail":"mcpp build-script API"},{"label":"link_lib(...)","kind":"snippet","filterText":"link_lib","detail":"mcpp:link-lib= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_lib(\"${1:value}\")"}],[{"label":"runner","kind":"function","detail":"mcpp build-script API"},{"label":"runner_named","kind":"function","detail":"mcpp build-script API"},{"label":"runner_longlived","kind":"function","detail":"mcpp build-script API"},{"label":"run_exclusive","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_env_changed","kind":"function","detail":"mcpp build-script API"},{"label":"rerun_if_changed_glob","kind":"function","detail":"mcpp build-script API"},{"label":"runtime_search_dir","kind":"function","detail":"mcpp build-script API"},{"label":"run_host","kind":"function","detail":"mcpp build-script API"},{"label":"runner(...)","kind":"snippet","filterText":"runner","detail":"mcpp:runner= · slot Runner","documentation":"SPEC-007 R9.10 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192)","insertText":"runner(\"${1:value}\")"},{"label":"runner_named(...)","kind":"snippet","filterText":"runner_named","detail":"mcpp:runner-named= · slot NamedRunner","insertText":"runner_named(\"${1:value}\")"},{"label":"runner_longlived(...)","kind":"snippet","filterText":"runner_longlived","detail":"mcpp:runner-longlived= · slot RunnerLongLived","insertText":"runner_longlived(\"${1:value}\")"},{"label":"run_exclusive(...)","kind":"snippet","filterText":"run_exclusive","detail":"mcpp:run-exclusive= · slot RunExclusive","insertText":"run_exclusive(\"${1:value}\")"},{"label":"rerun_if_changed(...)","kind":"snippet","filterText":"rerun_if_changed","detail":"mcpp:rerun-if-changed= · slot RerunFiles","insertText":"rerun_if_changed(\"${1:value}\")"},{"label":"rerun_if_env_changed(...)","kind":"snippet","filterText":"rerun_if_env_changed","detail":"mcpp:rerun-if-env-changed= · slot RerunEnv","insertText":"rerun_if_env_changed(\"${1:value}\")"},{"label":"rerun_if_changed_glob(...)","kind":"snippet","filterText":"rerun_if_changed_glob","detail":"mcpp:rerun-if-changed-glob= · slot RerunGlobs","insertText":"rerun_if_changed_glob(\"${1:value}\")"},{"label":"runtime_search_dir(...)","kind":"snippet","filterText":"runtime_search_dir","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","documentation":"SPEC-007 R2.1","insertText":"runtime_search_dir(\"${1:value}\")"},{"label":"run_host(...)","kind":"snippet","filterText":"run_host","detail":"mcpp:run-host= · slot RunHost","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"run_host(\"${1:value}\")"}],[{"label":"runner","kind":"function","detail":"mcpp build-script API"},{"label":"runner_named","kind":"function","detail":"mcpp build-script API"},{"label":"runner_longlived","kind":"function","detail":"mcpp build-script API"},{"label":"run_exclusive","kind":"function","detail":"mcpp build-script API"},{"label":"runtime_search_dir","kind":"function","detail":"mcpp build-script API"},{"label":"run_host","kind":"function","detail":"mcpp build-script API"},{"label":"runner(...)","kind":"snippet","filterText":"runner","detail":"mcpp:runner= · slot Runner","documentation":"SPEC-007 R9.10 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192)","insertText":"runner(\"${1:value}\")"},{"label":"runner_named(...)","kind":"snippet","filterText":"runner_named","detail":"mcpp:runner-named= · slot NamedRunner","insertText":"runner_named(\"${1:value}\")"},{"label":"runner_longlived(...)","kind":"snippet","filterText":"runner_longlived","detail":"mcpp:runner-longlived= · slot RunnerLongLived","insertText":"runner_longlived(\"${1:value}\")"},{"label":"run_exclusive(...)","kind":"snippet","filterText":"run_exclusive","detail":"mcpp:run-exclusive= · slot RunExclusive","insertText":"run_exclusive(\"${1:value}\")"},{"label":"runtime_search_dir(...)","kind":"snippet","filterText":"runtime_search_dir","detail":"mcpp:runtime-search-dir= · slot RuntimeSearchDir","documentation":"SPEC-007 R2.1","insertText":"runtime_search_dir(\"${1:value}\")"},{"label":"run_host(...)","kind":"snippet","filterText":"run_host","detail":"mcpp:run-host= · slot RunHost","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"run_host(\"${1:value}\")"}],[{"label":"cxxflag","kind":"function","detail":"mcpp build-script API"},{"label":"cflag","kind":"function","detail":"mcpp build-script API"},{"label":"cfg","kind":"function","detail":"mcpp build-script API"},{"label":"cxxflag(...)","kind":"snippet","filterText":"cxxflag","detail":"mcpp:cxxflag= · slot CxxFlags","documentation":"SPEC-007 R2.1","insertText":"cxxflag(\"${1:value}\")"},{"label":"cflag(...)","kind":"snippet","filterText":"cflag","detail":"mcpp:cflag= · slot CFlags","documentation":"SPEC-007 R2.1","insertText":"cflag(\"${1:value}\")"},{"label":"cfg(...)","kind":"snippet","filterText":"cfg","detail":"mcpp:cfg= · slot Defines","documentation":"SPEC-007 R2.1","insertText":"cfg(\"${1:value}\")"}],[{"label":"cxxflag","kind":"function","detail":"mcpp build-script API"},{"label":"cxxflag(...)","kind":"snippet","filterText":"cxxflag","detail":"mcpp:cxxflag= · slot CxxFlags","documentation":"SPEC-007 R2.1","insertText":"cxxflag(\"${1:value}\")"}],[{"label":"cflag","kind":"function","detail":"mcpp build-script API"},{"label":"cfg","kind":"function","detail":"mcpp build-script API"},{"label":"cflag(...)","kind":"snippet","filterText":"cflag","detail":"mcpp:cflag= · slot CFlags","documentation":"SPEC-007 R2.1","insertText":"cflag(\"${1:value}\")"},{"label":"cfg(...)","kind":"snippet","filterText":"cfg","detail":"mcpp:cfg= · slot Defines","documentation":"SPEC-007 R2.1","insertText":"cfg(\"${1:value}\")"}],[{"label":"cflag","kind":"function","detail":"mcpp build-script API"},{"label":"cflag(...)","kind":"snippet","filterText":"cflag","detail":"mcpp:cflag= · slot CFlags","documentation":"SPEC-007 R2.1","insertText":"cflag(\"${1:value}\")"}],[{"label":"link_flag","kind":"function","detail":"mcpp build-script API"},{"label":"link_flag(...)","kind":"snippet","filterText":"link_flag","detail":"mcpp:link-flag= · slot LdFlags","documentation":"SPEC-007 R2.1","insertText":"link_flag(\"${1:value}\")"}],[{"label":"include_dir","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir_after","kind":"function","detail":"mcpp build-script API"},{"label":"include_dir(...)","kind":"snippet","filterText":"include_dir","detail":"mcpp:include-dir= · slot IncludeDirs","documentation":"SPEC-007 R2.1","insertText":"include_dir(\"${1:value}\")"},{"label":"include_dir_after(...)","kind":"snippet","filterText":"include_dir_after","detail":"mcpp:include-dir-after= · slot IncludeDirsAfter","documentation":"SPEC-007 R2.1","insertText":"include_dir_after(\"${1:value}\")"}],[{"label":"source","kind":"function","detail":"mcpp build-script API"},{"label":"seal","kind":"function","detail":"mcpp build-script API"},{"label":"source(...)","kind":"snippet","filterText":"source","detail":"mcpp:source= · slot Sources","documentation":"SPEC-007 R2.1, R3.3, R3.6, R3.9","insertText":"source(\"${1:value}\")"},{"label":"seal(...)","kind":"snippet","filterText":"seal","detail":"mcpp:seal= · slot Seal","documentation":"SPEC-007 R9.2 · [docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"seal(\"${1:value}\")"}],[{"label":"action","kind":"function","detail":"mcpp build-script API"},{"label":"artifact_name","kind":"function","detail":"mcpp build-script API"},{"label":"action ... (typed action block)","kind":"snippet","filterText":"action","detail":"mcpp::action with id, role, description, arg/input/output and submit","documentation":"The five roles (SPEC-007 R3.6): source, check, object, artifact, prepare.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,object,artifact,prepare|};\n${1:handle}.description = \"${4:description}\";\n${1:handle}.arg(\"${5:argument}\")\n .input(\"${6:input}\")\n .output(\"${7:output}\")\n .submit();"},{"label":"action ... (prepare block)","kind":"snippet","filterText":"action","detail":"mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)","documentation":"A prepare action must declare output_dir; the engine refuses one without it.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"},{"label":"artifact_name(...)","kind":"snippet","filterText":"artifact_name","detail":"mcpp:artifact-name= · slot ArtifactNames","documentation":"[docs/30 — build.mcpp](https://github.com/mcpp-community/mcpp/blob/e0b1133dd2912f539ee68a06724cb524e3531f46/docs/30-build-mcpp.md#typed-api-import-mcpp-recommended)","insertText":"artifact_name(\"${1:value}\")"}],[{"label":"action","kind":"function","detail":"mcpp build-script API"},{"label":"action ... (typed action block)","kind":"snippet","filterText":"action","detail":"mcpp::action with id, role, description, arg/input/output and submit","documentation":"The five roles (SPEC-007 R3.6): source, check, object, artifact, prepare.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::${3|source,check,object,artifact,prepare|};\n${1:handle}.description = \"${4:description}\";\n${1:handle}.arg(\"${5:argument}\")\n .input(\"${6:input}\")\n .output(\"${7:output}\")\n .submit();"},{"label":"action ... (prepare block)","kind":"snippet","filterText":"action","detail":"mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)","documentation":"A prepare action must declare output_dir; the engine refuses one without it.","insertText":"mcpp::action ${1:handle};\n${1:handle}.id = \"${2:id}\";\n${1:handle}.role = mcpp::roles::prepare;\n${1:handle}.output_dir(\"${3:dir}\");\n${1:handle}.command(\"${4:command}\").submit();"}]],"hovers":["**std**\n\nThe C++ standard library module. mcpp stages the same `std` its own build uses, keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+).","**mcpp**\n\nThe mcpp engine interface, bundled in the mcpp binary so it always matches that engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1).","**mcpp.plugins.tool**\n\nThe official general-purpose library (L2), provided by the `plugins-core` feature and built only on the L1 engine interface, e.g. `mcpp.plugins.tool` (SPEC-007 §9).","**mcpp.rules.qt**\n\nOfficial rule packages (L3): code generation and device-language toolchains (SPEC-007 §0, §9).","**mcpp.core**\n\nThe mcpp engine interface (L1). Only grows; a symbol leaves only after six months of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2).","**mcpp::link_lib**\n\nwire `mcpp:link-lib=` · slot `LdFlags` · scope `LinkGlobal` · since protocol 1\n\nSPEC-007: R2.1","**mcpp::cxxflag**\n\nwire `mcpp:cxxflag=` · slot `CxxFlags` · scope `PackagePrivate` · since protocol 1\n\nSPEC-007: R2.1","**mcpp::cflag**\n\nwire `mcpp:cflag=` · slot `CFlags` · scope `PackagePrivate` · since protocol 1\n\nSPEC-007: R2.1","**mcpp::link_flag**\n\nwire `mcpp:link-flag=` · slot `LdFlags` · scope `LinkGlobal` · since protocol 8\n\nSPEC-007: R2.1","**mcpp::action**\n\nwire `mcpp:action=` · slot `Actions` · scope `GraphNode` · since protocol 1\n\nSPEC-007: R1.1, R1.2, R1.3, R2.1, R3.1, R3.2, R3.3, R3.5, R3.7, R3.8, R3.9, R5.1, R5.3, R6.4, R9.2, R9.10"]} diff --git a/tests/rules/golden/unclosed.toml.json b/tests/rules/golden/unclosed.toml.json new file mode 100644 index 0000000..9165b01 --- /dev/null +++ b/tests/rules/golden/unclosed.toml.json @@ -0,0 +1 @@ +{"diagnostics":{"default":[{"code":"mcpp.manifest.unknownKey","message":"unknown key text in [package]","severity":"warning","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"text"}],"quiet":[{"code":"mcpp.manifest.unknownKey","message":"unknown key text in [package]","severity":"info","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"text"}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"2:0":[3,0,0],"2:1":[3,0,4],"2:2":[3,0,4],"2:3":[3,0,4],"2:4":[3,0,4],"2:5":[3,5,5],"2:6":[2],"2:7":[2],"2:8":[2],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"3:0":[2],"3:1":[2],"3:2":[2],"3:3":[2],"3:4":[2],"3:5":[2],"3:6":[2],"3:7":[2],"3:8":[2],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"4:0":[2],"4:1":[2],"4:2":[2],"4:3":[2],"4:4":[2],"4:5":[2],"4:6":[2],"4:7":[2],"4:8":[2],"4:9":[2],"5:0":[2],"5:1":[2],"5:2":[2],"5:3":[2],"5:4":[2],"5:5":[2],"5:6":[2],"5:7":[2],"5:8":[2],"5:9":[2],"5:10":[2],"5:11":[2],"5:12":[2],"5:13":[2],"5:14":[2],"5:15":[2],"5:16":[2],"5:17":[2],"5:18":[2],"5:19":[2],"6:0":[2],"6:1":[2],"6:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"}]} diff --git a/tests/test_manifest.cpp b/tests/test_manifest.cpp deleted file mode 100644 index 21f6a4d..0000000 --- a/tests/test_manifest.cpp +++ /dev/null @@ -1,97 +0,0 @@ -// mcpp.toml editing help: where the cursor is, what is offered there. -#include "support.hpp" -import std; -import nlohmann.json; -import mcppide.manifest; - -using namespace mcppide; -using Json = nlohmann::json; - -static std::vector labels(const Json& r) { - std::vector out; - for (const auto& i : r["items"]) out.push_back(i["label"].get()); - return out; -} -static bool has(const std::vector& v, std::string_view s) { return std::ranges::find(v, s) != v.end(); } - -int main() { - CHECK(manifest::section_of("target.'cfg(os = \"linux\")'.build") == "build"); - CHECK(manifest::section_of("dependencies.mcpplibs") == "dependencies"); - CHECK(manifest::section_of("targets.app") == "targets"); - // A platform's tables, under [platform] and under the selector that overrides it. - CHECK(manifest::section_of("platform") == "platform"); - CHECK(manifest::section_of("platform.link") == "platform.link"); - CHECK(manifest::section_of("platform.link.inputs") == "platform.link.inputs"); - CHECK(manifest::section_of("platform.build.'cfg(arch = \"aarch64\")'") == "platform.build"); - CHECK(manifest::section_of("platform.hosted.layers") == "platform.layers"); - CHECK(manifest::section_of("target.'cfg(platform = \"openkal\")'.layers") == "platform.layers"); - CHECK(manifest::section_of("target.x86_64-openkal.link") == "platform.link"); - CHECK(manifest::section_of("target.x86_64-openkal.versions") == "versions"); - CHECK(manifest::section_of("tools.overrides") == "tools.overrides"); - // A sub-table that is a section of its own, a mirrored table, the ABI switches. - CHECK(manifest::section_of("pack.bundle-project") == "pack.bundle-project"); - CHECK(manifest::section_of("runtime.requirements") == "runtime.requirements"); - CHECK(manifest::section_of("hooks.during_build") == "hooks.during_build"); - CHECK(manifest::section_of("resources.version-info") == "resources.version-info"); - CHECK(manifest::section_of("xlings.workspace") == "xlings.workspace"); - CHECK(manifest::section_of("profile.release") == "profile"); - CHECK(manifest::section_of("features.gpu") == "features"); - CHECK(manifest::section_of("workspace.package") == "package"); - CHECK(manifest::section_of("workspace.target.'cfg(os = \"linux\")'.build") == "build"); - CHECK(manifest::section_of("target.'cfg(os = \"linux\")'.abi") == "abi"); - CHECK(manifest::section_of("target.x86_64-linux-gnu.feature-requires-abi") == "feature-requires-abi"); - CHECK(manifest::section_of("package.requires_abi") == "requires_abi"); - CHECK(manifest::section_of("features.mt.requires_abi") == "requires_abi"); - - const std::string text = "[package]\nname = \"a\"\n\n[build]\nbmi_schedule = \"\"\n\n"; - auto keys = labels(manifest::complete(Json{{"text", text}, {"line", 5}, {"character", 0}})); - CHECK(has(keys, "sources") && !has(keys, "bmi_schedule")); // a key already written is not offered again - auto values = labels(manifest::complete(Json{{"text", text}, {"line", 4}, {"character", 16}})); - CHECK(has(values, "auto") && has(values, "on") && has(values, "off")); - auto headers = labels(manifest::complete(Json{{"text", "[bu"}, {"line", 0}, {"character", 3}})); - CHECK(has(headers, "[build]") && has(headers, "[targets.]")); - auto hover = manifest::hover(Json{{"text", text}, {"line", 4}, {"character", 2}}); - CHECK(hover.is_object() && hover["contents"].get().find("bmi_schedule") != std::string::npos); - auto syms = manifest::symbols(Json{{"text", text}}); - CHECK(syms.size() == 2 && syms[1]["name"] == "build" && syms[1]["children"].size() == 1); - auto bs = labels(manifest::buildscript_complete(Json{{"text", "int main(){ mcpp::cxx"}, {"line", 0}, {"character", 21}})); - CHECK(has(bs, "cxxflag")); - // The typed API's names, not the wire's (`mcpp:link-driver=` is `mcpp::link_driver`). - auto link = labels(manifest::buildscript_complete(Json{{"text", "int main(){ mcpp::link"}, {"line", 0}, {"character", 22}})); - CHECK(has(link, "link_driver") && has(link, "link_input_from") && has(link, "link_lib") && !has(link, "link-driver")); - auto def = labels(manifest::buildscript_complete(Json{{"text", "int main(){ mcpp::de"}, {"line", 0}, {"character", 20}})); - CHECK(has(def, "define") && has(def, "deploy")); - - // The platform keys of mcpp 2026.10.27.1. - const std::string plat = "[platform]\ntargets = []\n\n[platform.link]\n\n[target.x86_64-openkal]\n\n[host]\n\n[pack]\n"; - auto pk = labels(manifest::complete(Json{{"text", plat}, {"line", 2}, {"character", 0}})); - CHECK(has(pk, "universal") && has(pk, "plugin") && !has(pk, "targets")); - auto lk = labels(manifest::complete(Json{{"text", plat}, {"line", 4}, {"character", 0}})); - CHECK(has(lk, "driver") && has(lk, "seal") && has(lk, "exe")); - auto drv = labels(manifest::complete(Json{{"text", "[platform.link]\ndriver = \"\"\n"}, {"line", 1}, {"character", 10}})); - CHECK(has(drv, "lld") && has(drv, "compiler")); - auto tk = labels(manifest::complete(Json{{"text", plat}, {"line", 6}, {"character", 0}})); - CHECK(has(tk, "platform") && has(tk, "carrier") && has(tk, "run_under")); - auto hk = labels(manifest::complete(Json{{"text", plat}, {"line", 8}, {"character", 0}})); - CHECK(has(hk, "platform")); - auto pack = labels(manifest::complete(Json{{"text", plat}, {"line", 10}, {"character", 0}})); - CHECK(has(pack, "format") && has(pack, "compress") && has(pack, "universal") && has(pack, "sign")); - auto ph = labels(manifest::complete(Json{{"text", "[plat"}, {"line", 0}, {"character", 5}})); - CHECK(has(ph, "[platform]") && has(ph, "[platform.link]") && has(ph, "[[platform.link.inputs]]") && has(ph, "[host]")); - // Every table mcpp 2026.10.27.1 reads has its keys. - const std::string more = "[c-abi]\n\n[profile.release]\n\n[hooks.during_build]\n\n[target.'cfg(os = \"linux\")'.abi]\n\n" - "[[runtime.requirements]]\n\n[features.mt]\n\n[xlings]\n\n[test]\n"; - auto keysAt = [&](int line) { return labels(manifest::complete(Json{{"text", more}, {"line", line}, {"character", 0}})); }; - auto cabi = keysAt(1); - CHECK(has(cabi, "presents") && has(cabi, "data-model") && has(cabi, "wchar") && has(cabi, "builtins")); - CHECK(has(keysAt(3), "ndebug") && has(keysAt(3), "trim_paths")); - CHECK(has(keysAt(5), "loop") && !has(keysAt(5), "timeout_seconds")); - CHECK(has(keysAt(7), "threads") && has(keysAt(7), "exceptions")); - CHECK(has(keysAt(9), "phase") && has(keysAt(9), "discovery")); - CHECK(has(keysAt(11), "implies") && has(keysAt(11), "requires_abi") && has(keysAt(11), "rule_module")); - CHECK(has(keysAt(13), "subos") && has(keysAt(13), "deps")); - CHECK(has(keysAt(15), "windows_code_page") && has(keysAt(15), "windows_long_paths")); - auto th = labels(manifest::complete(Json{{"text", "[tar"}, {"line", 0}, {"character", 4}})); - CHECK(has(th, "[target.'cfg(...)'.abi]") && has(th, "[target.'cfg(...)'.requires_abi]") && !has(th, "[target..abi]")); - std::println("test_manifest: ok"); -} diff --git a/tests/test_rules.cpp b/tests/test_rules.cpp new file mode 100644 index 0000000..1b9ef51 --- /dev/null +++ b/tests/test_rules.cpp @@ -0,0 +1,195 @@ +// The rules layer answers what mcpp-vscode 0.9's TypeScript answered (design 2026-10-30 part 2 §8): +// over each file of tests/rules/corpus, at every position of every line (and past them), the +// completions, hovers, jumps, version contexts and diagnostics equal tests/rules/golden, which that +// implementation wrote. A difference is printed with its file and position. +#include "support.hpp" +import std; +import nlohmann.json; +import mcppide.text; +import mcppide.manifest; +import mcppide.buildscript; + +using namespace mcppide; +using Json = nlohmann::json; + +namespace { + +int failures = 0; + +void expect(const Json& got, const Json& want, const std::string& where) { + if (got == want) return; + if (++failures <= 25) + std::println(std::cerr, "MISMATCH {}\n got: {}\n want: {}", where, got.dump().substr(0, 1500), want.dump().substr(0, 1500)); +} + +Json opt(const std::optional& s) { return s ? Json(*s) : Json(); } + +void put(Json& j, std::string_view key, const std::optional& v) { + if (v) j[std::string(key)] = *v; +} + +Json toml_diagnostics(const std::vector& ds) { + Json out = Json::array(); + for (const auto& d : ds) { + Json j{{"code", d.code}, {"message", d.message}, {"severity", d.severity}, {"line", d.line}, {"startCharacter", d.start}, + {"endCharacter", d.end}}; + put(j, "relatedKey", d.relatedKey); + out.push_back(j); + } + return out; +} + +void check_manifest(const std::filesystem::path& file, const Json& golden) { + const auto name = file.filename().string(); + const auto lines = text::lines_of(testsupport::read(file)); + manifest::DiagnosticSettings quiet; + quiet.syntax = "info"; + quiet.unknownSection = "off"; + quiet.unknownKey = "info"; + quiet.planeSeparation = "off"; + quiet.legacyKeys = "warning"; + quiet.mcppFloor = "warning"; + quiet.arrayTable = "off"; + expect(toml_diagnostics(manifest::analyse(lines, {})), golden["diagnostics"]["default"], name + " diagnostics default"); + expect(toml_diagnostics(manifest::analyse(lines, quiet)), golden["diagnostics"]["quiet"], name + " diagnostics quiet"); + const std::map> manifests{{"../lib", 2}, {"libs/core", 0}}; + auto resolve = [&](std::string_view rel) -> std::optional { + auto it = manifests.find(rel); + return it == manifests.end() ? std::nullopt : std::optional(it->second); + }; + const int n = static_cast(lines.size()); + for (int l = 0; l <= n + 1; ++l) { + const int len = l < n ? text::utf16_length(lines[l]) : 0; + for (int c = 0; c <= len + 2; ++c) { + const auto at = std::format("{}:{}", l, c); + const auto where = std::format("{} {}", name, at); + if (l < n) { + const std::vector upTo(lines.begin(), lines.begin() + l + 1); + const auto items = manifest::completions(upTo, l, c); + Json list = Json::array(); + for (const auto& s : items) { + Json j{{"label", s.label}, {"kind", s.kind}, {"detail", s.detail}}; + put(j, "documentation", s.documentation); + put(j, "insertSnippet", s.insertSnippet); + list.push_back(j); + } + const auto& want = golden["completion"][at]; + expect(list, golden["lists"][want[0].get()], where + " completion"); + if (!items.empty()) expect(Json{items[0].start, items[0].end}, Json{want[1], want[2]}, where + " completion range"); + const auto v = manifest::dependency_version_context(upTo, l, c); + const Json wantV = golden.contains("versionContext") && golden["versionContext"].contains(at) ? golden["versionContext"][at] : Json(); + expect(v ? Json{{"name", v->name}, {"insideString", v->insideString}, + {"range", {{"startCharacter", v->start}, {"endCharacter", v->end}}}} + : Json(), + wantV, where + " version context"); + } + const auto h = manifest::hover_at(lines, l, c); + Json hj; + if (h) { + hj = Json{{"title", h->title}, {"body", h->body}}; + put(hj, "documentation", h->documentation); + } + expect(hj, golden["hover"].contains(at) ? golden["hovers"][golden["hover"][at].get()] : Json(), where + " hover"); + const auto d = manifest::definition_at(lines, l, c, resolve); + expect(d ? Json{{"line", d->line}, {"startCharacter", d->start}, {"endCharacter", d->end}} : Json(), + golden["definition"].contains(at) ? golden["definition"][at] : Json(), where + " definition"); + } + } +} + +Json bs_diagnostics(const std::vector& ds) { + Json out = Json::array(); + for (const auto& d : ds) + out.push_back(Json{{"code", d.code}, {"message", d.message}, {"severity", d.severity}, {"line", d.line}, {"startCharacter", d.start}, + {"endCharacter", d.end}}); + return out; +} + +void check_buildscript(const std::filesystem::path& file, const Json& golden) { + const auto name = file.filename().string(); + const auto lines = text::lines_of(testsupport::read(file)); + expect(bs_diagnostics(buildscript::analyse(lines, "warning")), golden["diagnostics"]["warning"], name + " diagnostics"); + expect(bs_diagnostics(buildscript::analyse(lines, "info", false)), golden["diagnostics"]["served"], name + " diagnostics served"); + Json imports = Json::array(); + for (const auto& s : buildscript::scan_imports(lines)) + imports.push_back(Json{{"module", s.module}, {"line", s.line}, {"startCharacter", s.start}, {"endCharacter", s.end}}); + expect(imports, golden["imports"], name + " imports"); + for (int l = 0; l < static_cast(lines.size()); ++l) + for (int c = 0; c <= text::utf16_length(lines[l]); ++c) { + const auto at = std::format("{}:{}", l, c); + const auto where = std::format("{} {}", name, at); + const auto items = buildscript::completion(lines[l], c); + Json got; + if (items) { + got = Json::array(); + for (const auto& i : *items) { + Json j{{"label", i.label}, {"kind", i.kind}, {"detail", i.detail}}; + put(j, "documentation", i.documentation); + put(j, "filterText", i.filterText); + put(j, "insertText", i.insertText); + got.push_back(j); + } + } + expect(got, golden["completion"].contains(at) ? golden["lists"][golden["completion"][at].get()] : Json(), + where + " completion"); + const auto h = buildscript::hover(lines, l, c); + expect(opt(h), golden["hover"].contains(at) ? golden["hovers"][golden["hover"][at].get()] : Json(), where + " hover"); + } +} + +void check_catalogue(const Json& golden) { + Json snippets = Json::array(); + for (const auto& s : buildscript::snippets()) { + Json j{{"filterText", s.filterText}, {"label", s.label}, {"detail", s.detail}, {"insertText", s.insertText}}; + put(j, "documentation", s.documentation); + snippets.push_back(j); + } + expect(snippets, golden["snippets"], "catalogue snippets"); + Json modules = Json::array(); + for (const auto& m : buildscript::known_modules()) + modules.push_back(Json{{"name", m.name}, {"description", m.description}, {"docsUrl", m.docsUrl}}); + expect(modules, golden["modules"], "catalogue modules"); + for (const auto& pair : golden["known"]) { + const auto name = pair[0].get(); + const auto m = buildscript::known_module(name); + expect(m ? Json(m->name) : Json(), pair[1], "known module " + name); + } +} + +} // namespace + +int main() { + const auto root = testsupport::repo() / "tests" / "rules"; + std::vector files; + for (const auto& e : std::filesystem::directory_iterator(root / "corpus")) files.push_back(e.path()); + std::ranges::sort(files); + CHECK(!files.empty()); + for (const auto& f : files) { + const auto golden = Json::parse(testsupport::read(root / "golden" / (f.filename().string() + ".json"))); + if (f.extension() == ".toml") check_manifest(f, golden); + else check_buildscript(f, golden); + } + check_catalogue(Json::parse(testsupport::read(root / "golden" / "catalogue.json"))); + + // Past the goldens: the catalogue's versions, newest first, a package's own only. + const Json envelope{{"data", {{"packages", Json::array({ + Json{{"name", "zlib"}, {"namespace", "compat"}, {"description", " A compression library "}, {"versions", {"1.2.13", "1.3.1", "1.3.0"}}}, + Json{{"name", "zlibx"}, {"namespace", "x"}, {"versions", {"9.0.0"}}}, + })}}}}; + const auto versions = manifest::catalogue_versions(envelope, "zlib"); + CHECK(versions.size() == 3 && versions[0].version == "1.3.1" && versions[2].version == "1.2.13"); + CHECK(versions[0].summary && *versions[0].summary == "A compression library"); + const auto lines = text::lines_of("[dependencies]\nzlib = \"1.\n"); + const auto at = manifest::dependency_version_context(lines, 1, 9); + CHECK(at && at->name == "zlib" && at->insideString); + const auto suggestions = manifest::version_suggestions(*at, versions); + CHECK(suggestions.size() == 3 && suggestions[0].insertSnippet == "1.3.1" && suggestions[0].kind == "version"); + // A suggestion's detail in the user's language. + const auto zh = manifest::completions(text::lines_of("[dependencies]\n"), 1, 0, "zh-cn"); + CHECK(!zh.empty() && zh[0].detail != "SemVer version dependency"); + + if (failures) std::println(std::cerr, "{} mismatch(es)", failures); + CHECK(failures == 0); + std::println("rules: the goldens hold"); + return 0; +} From fb29e37c80b8279dafd12d9c6d24f3d0c8811eec Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 23:19:52 +0900 Subject: [PATCH 097/119] =?UTF-8?q?checkpoint=205b:=20the=20session=20is?= =?UTF-8?q?=20an=20LSP=20server=20(protocol=202),=20and=20every=20editor?= =?UTF-8?q?=20reaches=20the=20rules=20layer=20of=20mcpp.toml=20and=20build?= =?UTF-8?q?.mcpp=20through=20it=20(mcpp=20design=202026-10-30=20part=202?= =?UTF-8?q?=20=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit initialize in LSP's shape, mcpp-ide's methods and notifications under mcpp-ide/, textDocument/* for the two files (full synchronisation, UTF-16 columns, answered in order). VS Code connects with its language client, bundled into dist/vendor; CLion over the session's own connection through its extension points (the platform's LSP client starts a server of its own); Neovim with vim.lsp. mcpp-vscode's TypeScript rules layer, its snapshots and their generators are removed; the scanner's goldens cover its parser tests. The declared degraded mode where okrun does not run, with its Extension Host run in CI. test-launch is mcpp 2026.10.31.2's. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 30 +- CHANGELOG.md | 23 +- docs/README.md | 1 + docs/architecture.md | 24 +- docs/rules.md | 97 ++ mide-plugins/clion/README.md | 5 +- .../mcppcommunity/mcppclion/McppSession.kt | 63 +- .../mcppcommunity/mcppclion/McppStartup.kt | 2 + .../mcppclion/backend/Backend.kt | 24 +- .../mcppclion/editor/McppEditor.kt | 378 ++++++++ .../src/main/resources/META-INF/plugin.xml | 13 + .../mcppcommunity/mcppclion/BackendTest.kt | 41 +- mide-plugins/nvim/README.md | 9 +- mide-plugins/nvim/lua/mcpp/init.lua | 16 +- mide-plugins/nvim/lua/mcpp/session.lua | 174 +++- mide-plugins/nvim/tests/smoke.lua | 28 +- mide-plugins/vscode/CHANGELOG.md | 9 +- mide-plugins/vscode/README.md | 4 +- mide-plugins/vscode/data/buildscript-api.json | 509 ---------- mide-plugins/vscode/data/i18n/zh-cn.json | 3 +- mide-plugins/vscode/data/toml-schema.json | 871 ------------------ mide-plugins/vscode/docs/architecture.md | 19 +- mide-plugins/vscode/docs/build-script.md | 135 --- mide-plugins/vscode/docs/mcpp-toml.md | 134 --- mide-plugins/vscode/package-lock.json | 76 +- mide-plugins/vscode/package.json | 8 +- .../vscode/src/buildscript/analysis.ts | 701 -------------- mide-plugins/vscode/src/buildscript/api.ts | 111 --- .../vscode/src/buildscript/modules.ts | 294 ------ .../vscode/src/buildscript/providers.ts | 251 ----- .../vscode/src/buildscript/settings.ts | 70 -- .../vscode/src/buildscript/snippets.ts | 116 --- mide-plugins/vscode/src/cli/process.ts | 24 +- mide-plugins/vscode/src/cli/search.ts | 175 ---- mide-plugins/vscode/src/extension.ts | 31 +- mide-plugins/vscode/src/ide/backend/client.ts | 164 ---- mide-plugins/vscode/src/ide/backend/locate.ts | 18 + mide-plugins/vscode/src/ide/session.ts | 180 +++- mide-plugins/vscode/src/toml/completion.ts | 539 ----------- mide-plugins/vscode/src/toml/diagnostics.ts | 634 ------------- mide-plugins/vscode/src/toml/hover.ts | 206 ----- mide-plugins/vscode/src/toml/navigation.ts | 306 ------ mide-plugins/vscode/src/toml/parser.ts | 501 +--------- mide-plugins/vscode/src/toml/providers.ts | 284 ------ mide-plugins/vscode/src/toml/schema.ts | 125 --- mide-plugins/vscode/test/architecture.test.ts | 11 - mide-plugins/vscode/test/artifacts.test.ts | 34 +- .../vscode/test/buildscript/analysis.test.ts | 233 ----- .../vscode/test/buildscript/api.test.ts | 114 --- .../vscode/test/buildscript/modules.test.ts | 118 --- .../vscode/test/buildscript/settings.test.ts | 136 --- .../vscode/test/buildscript/snippets.test.ts | 74 -- mide-plugins/vscode/test/cli/search.test.ts | 141 --- .../vscode/test/config/wiring.test.ts | 81 ++ mide-plugins/vscode/test/e2e/runTest.ts | 19 +- mide-plugins/vscode/test/e2e/suite/index.ts | 3 +- .../vscode/test/e2e/suite/nobackend.e2e.ts | 43 + .../vscode/test/i18n/hardcoded.test.ts | 2 +- mide-plugins/vscode/test/ide/client.test.ts | 100 +- mide-plugins/vscode/test/ide/locate.test.ts | 9 +- .../vscode/test/toml/completion.test.ts | 300 ------ .../vscode/test/toml/contract.test.ts | 231 ----- .../vscode/test/toml/diagnostics.test.ts | 235 ----- mide-plugins/vscode/test/toml/hover.test.ts | 83 -- .../vscode/test/toml/navigation.test.ts | 103 --- mide-plugins/vscode/test/toml/parser.test.ts | 249 +---- mide-plugins/vscode/test/toml/schema.test.ts | 98 -- .../vscode/tools/bundle-languageclient.mjs | 20 + .../vscode/tools/check-generators.mjs | 94 -- .../vscode/tools/generate-buildscript-api.mjs | 374 -------- .../vscode/tools/generate-toml-schema.mjs | 581 ------------ protocol/protocol.schema.json | 172 ++-- src/documents.cppm | 337 +++++++ src/rpc.cppm | 11 +- src/server.cppm | 329 +++++-- tests/rules/golden/brackets.toml.json | 2 +- tests/rules/golden/broken.toml.json | 2 +- tests/rules/golden/comments.toml.json | 2 +- tests/rules/golden/complete.toml.json | 2 +- tests/rules/golden/crlf.toml.json | 2 +- tests/rules/golden/editing.toml.json | 2 +- tests/rules/golden/empty.toml.json | 2 +- tests/rules/golden/snippets.json | 1 + tests/rules/golden/unclosed.toml.json | 2 +- tests/support.hpp | 5 + tests/test_mcppls_status.cpp | 26 +- tests/test_probe.cpp | 34 +- tests/test_protocol.cpp | 102 +- tests/test_rules.cpp | 128 ++- tests/test_testing.cpp | 14 +- 90 files changed, 2324 insertions(+), 9763 deletions(-) create mode 100644 docs/rules.md create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/editor/McppEditor.kt delete mode 100644 mide-plugins/vscode/data/buildscript-api.json delete mode 100644 mide-plugins/vscode/data/toml-schema.json delete mode 100644 mide-plugins/vscode/docs/build-script.md delete mode 100644 mide-plugins/vscode/docs/mcpp-toml.md delete mode 100644 mide-plugins/vscode/src/buildscript/analysis.ts delete mode 100644 mide-plugins/vscode/src/buildscript/api.ts delete mode 100644 mide-plugins/vscode/src/buildscript/modules.ts delete mode 100644 mide-plugins/vscode/src/buildscript/providers.ts delete mode 100644 mide-plugins/vscode/src/buildscript/settings.ts delete mode 100644 mide-plugins/vscode/src/buildscript/snippets.ts delete mode 100644 mide-plugins/vscode/src/cli/search.ts delete mode 100644 mide-plugins/vscode/src/ide/backend/client.ts delete mode 100644 mide-plugins/vscode/src/toml/completion.ts delete mode 100644 mide-plugins/vscode/src/toml/diagnostics.ts delete mode 100644 mide-plugins/vscode/src/toml/hover.ts delete mode 100644 mide-plugins/vscode/src/toml/navigation.ts delete mode 100644 mide-plugins/vscode/src/toml/providers.ts delete mode 100644 mide-plugins/vscode/src/toml/schema.ts delete mode 100644 mide-plugins/vscode/test/buildscript/analysis.test.ts delete mode 100644 mide-plugins/vscode/test/buildscript/api.test.ts delete mode 100644 mide-plugins/vscode/test/buildscript/modules.test.ts delete mode 100644 mide-plugins/vscode/test/buildscript/settings.test.ts delete mode 100644 mide-plugins/vscode/test/buildscript/snippets.test.ts delete mode 100644 mide-plugins/vscode/test/cli/search.test.ts create mode 100644 mide-plugins/vscode/test/e2e/suite/nobackend.e2e.ts delete mode 100644 mide-plugins/vscode/test/toml/completion.test.ts delete mode 100644 mide-plugins/vscode/test/toml/contract.test.ts delete mode 100644 mide-plugins/vscode/test/toml/diagnostics.test.ts delete mode 100644 mide-plugins/vscode/test/toml/hover.test.ts delete mode 100644 mide-plugins/vscode/test/toml/navigation.test.ts delete mode 100644 mide-plugins/vscode/test/toml/schema.test.ts create mode 100644 mide-plugins/vscode/tools/bundle-languageclient.mjs delete mode 100644 mide-plugins/vscode/tools/check-generators.mjs delete mode 100644 mide-plugins/vscode/tools/generate-buildscript-api.mjs delete mode 100644 mide-plugins/vscode/tools/generate-toml-schema.mjs create mode 100644 src/documents.cppm create mode 100644 tests/rules/golden/snippets.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 759a014..6282c83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,7 @@ jobs: run: npm test vscode-drift: - name: VS Code manifest and snapshots + name: VS Code manifest and settings documentation runs-on: ubuntu-24.04 timeout-minutes: 45 defaults: @@ -121,11 +121,6 @@ jobs: shell: bash steps: - uses: actions/checkout@v5 - - uses: actions/checkout@v5 - with: - repository: mcpp-community/mcpp - path: .mcpp-source - fetch-depth: 1 - uses: ./.github/actions/setup-mcpp - uses: actions/setup-node@v5 with: @@ -138,11 +133,6 @@ jobs: git diff --exit-code -- mide-plugins/vscode spec - run: npm ci working-directory: mide-plugins/vscode - - name: The committed snapshots match the current mcpp - working-directory: mide-plugins/vscode - env: - MCPP_REPO: ${{ github.workspace }}/.mcpp-source - run: npm run check:generated - name: The settings documentation matches the registry working-directory: mide-plugins/vscode run: | @@ -199,7 +189,8 @@ jobs: const fail = (message) => { console.error(message); process.exit(1); }; if (p.extensionDependencies?.[0] !== "sunrisepeak.mcpp-language-server") fail("the dependency moved"); if (p.extensionDependencies.some((id) => id.includes("clangd"))) fail("clangd is back in the dependency list"); - if (p.dependencies?.["vscode-languageclient"] || p.devDependencies?.["vscode-languageclient"]) fail("a second language client crept in"); + // The backend's language client is bundled into dist/vendor; nothing is a runtime dependency. + if (Object.keys(p.dependencies ?? {}).length) fail("a runtime dependency crept in: the VSIX carries no node_modules"); if ((p.activationEvents ?? []).some((event) => event.startsWith("onCommand:"))) fail("onCommand activations are derived since 1.74"); if (!p.contributes?.viewsContainers?.activitybar?.length) fail("the view container disappeared"); console.log("manifest ok"); @@ -243,11 +234,13 @@ jobs: exit 1 fi done - - name: No second language client in the build output + - name: One language client, the backend's, bundled working-directory: mide-plugins/vscode run: | test ! -e dist/src/languageClient.js ! grep -rqsE "require\(['\"]vscode-languageclient['\"]\)|from ['\"]vscode-languageclient['\"]" dist/src package.json + test "$(grep -rlE "vendor/languageclient" dist/src)" = "dist/src/ide/session.js" + test -s dist/vendor/languageclient.js - uses: actions/upload-artifact@v5 with: name: mcpp-vscode-vsix @@ -297,6 +290,17 @@ jobs: else npm run test:e2e:one fi + # The declared degraded mode (part 2 §8, F15): the bundle without a runner for this system, + # as on a platform okrun does not cover. + - name: Extension Host without its backend + env: + MCPP_E2E_NO_BACKEND: "1" + run: | + if [ "$(uname)" = "Linux" ]; then + xvfb-run -a npm run test:e2e:one + else + npm run test:e2e:one + fi # The only check that proves the dependency resolves end to end: install the packed VSIX into a # private profile and let VS Code pull the platform build of the language service. diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5e4f0..a855892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,28 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 runs some of them with the framework's filter and reads its report, each outcome sent as `test/caseResult`. VS Code's Testing view has the cases under their program, located at their macros, and the program at its `main`; CLion has a ▶ on each case and runs cases in its test tree. - Debugging a test's program is offered where the project's mcpp describes its launch (`test-launch`). + Debugging a test's program is offered where the project's mcpp describes its launch (`test-launch`, + mcpp 2026.10.31.2). +- **The rules layer of mcpp.toml and build.mcpp, in the backend (§8; docs/rules.md).** The session + is an LSP server, protocol 2: `initialize` in LSP's shape with mcpp-ide's parameters in + `initializationOptions`, mcpp.toml's and build.mcpp's completion, hover, jumps and diagnostics as + `textDocument/*` (full synchronisation, UTF-16 columns), and mcpp-ide's own methods and + notifications under `mcpp-ide/`. The tolerant scanner, the seven manifest rules and the SPEC-007 + checks are ported from mcpp-vscode 0.9's TypeScript, whose answers over a corpus at every position + are the tests' goldens (`tests/rules`). A table is read through the schema (a workspace table as the + table it mirrors, a platform's table under a selector as the platform's); a selector-only table is + offered with its selector; the schema's own arrays of tables are allowed; a dependency's version is + completed from mcpp's catalogue (`mcpp search --format json`) when `mcpp.toml.indexCompletion` is + on. VS Code connects with its language client (bundled into `dist/vendor`); CLion over the + session's connection, through a completion contributor, an annotator, a documentation provider + and a go-to-declaration handler; Neovim with its LSP client, attached to mcpp.toml and build.mcpp + buffers. While C++ Modules serves build.mcpp (`mcpplsServes`) it is left the unknownSymbol rule + and the module names after `import`. One snapshot of each: `spec/toml-schema.json`, + `spec/buildscript-api.json` (mcpp 2026.10.29.1's directives). +- **The declared degraded mode (§8, F15).** Where the package's okrun does not run (macOS x86_64, + Windows arm64, Linux armhf) VS Code says the backend does not run on this platform and goes on + with build, run, test and the project pages, running mcpp's queries itself; an Extension Host run + in CI holds it. - **mcpp-vscode 1.1.0 is mcpp-vscode 0.9.0 over the backend.** 0.9.0's extension is imported with its history (`mide-plugins/vscode`); every view, page, command, menu, setting and string a user sees is 0.9.0's. What 1.0 added stays, in 0.9.0's style: debugging (`mcpp.debug`, the `mcpp` diff --git a/docs/README.md b/docs/README.md index 25d3983..a692e64 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ # mcpp-ide documentation - [architecture.md](architecture.md) -- the three layers, what each holds, and how a request travels +- [rules.md](rules.md) -- mcpp.toml and build.mcpp: completion, hover, jumps and diagnostics over LSP - [../protocol/protocol.schema.json](../protocol/protocol.schema.json) -- the backend's protocol - [../agents/README.md](../agents/README.md) -- driving the backend from an agent - [../mide-plugins/vscode/README.md](../mide-plugins/vscode/README.md) -- the VS Code extension diff --git a/docs/architecture.md b/docs/architecture.md index 83be62c..fbedd30 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,7 +5,7 @@ | Layer | Holds | Does not hold | |---|---|---| | mcpp | the machine contract: event streams, result records, launch descriptors, JSON queries, `--deny` (mcpp docs/50) | any IDE concept | -| mcpp-ide backend (`src/`) | sessions and projects (which mcpp.toml a file belongs to), watching (result records, manifests), jobs (one at a time per project, cancellation, a newer job replacing a waiting one), view models (project, targets and platforms, cache, libraries, graphs), cleanup plans and their danger levels, settings validation and migration, the command table, runtime strings, the self-check, the handshakes with mcpp (`--protocol-version`) and mcppls (`--version`) | parsing mcpp.toml, resolving dependencies | +| mcpp-ide backend (`src/`) | sessions and projects (which mcpp.toml a file belongs to), watching (result records, manifests), jobs (one at a time per project, cancellation, a newer job replacing a waiting one), view models (project, targets and platforms, cache, libraries, graphs), cleanup plans and their danger levels, settings validation and migration, the command table, runtime strings, the self-check, the handshakes with mcpp (`--protocol-version`) and mcppls (`--version`), the rules layer of mcpp.toml and build.mcpp as an LSP server ([rules.md](rules.md)) | a manifest's meaning, resolving dependencies | | adapters (`mide-plugins/`) | declarations generated from `spec/`, the protocol mapped onto native UI, the pages' host, the backend process, finding the backend, workspace trust | any decision | ## The pages @@ -51,22 +51,28 @@ forward to mcppls's commands only VS Code) and, where an editor is left out, why backend answers each editor's table (`commands/list`) with the rows it offers, their titles in the session's language and their subcommands, the names Neovim types. -The Neovim plugin is Lua over Neovim's own JSON-RPC client (`vim.lsp.rpc`, the LSP base protocol's -framing). It carries no backend and no web view: a panel's view model is text in a floating window +The Neovim plugin is Lua over Neovim's own LSP client (`vim.lsp`), attached to mcpp.toml and +build.mcpp buffers. It carries no backend and no web view: a panel's view model is text in a floating window or a choice in `vim.ui.select`, and a panel with no textual equivalent (the settings, edited in `setup()`) is left out of its table. ## Language features -mcppls provides every language feature of C++ files; mcpp-ide registers no language service. The -VS Code extension installs mcppls as an extension pack (it can be removed); the CLion plugin depends -on the mcppls CLion plugin; in Neovim mcppls has its own plugin (`xlings install mcppls-nvim`), whose -client's server the Neovim plugin names to the backend. The backend reads `mcppls --version`: 0.0.18 and later refresh after any -build through mcpp's result record, and an older or missing mcppls is reported with what to do. +mcppls provides every language feature of C++ files; mcpp-ide registers no C++ language service. +The VS Code extension installs mcppls as an extension pack (it can be removed); the CLion plugin +depends on the mcppls CLion plugin; in Neovim mcppls has its own plugin (`xlings install +mcppls-nvim`), whose client's server the Neovim plugin names to the backend. The backend reads +`mcppls --version`: 0.0.18 and later refresh after any build through mcpp's result record, and an +older or missing mcppls is reported with what to do. + +mcpp.toml and build.mcpp are the backend's: the session is an LSP server (protocol 2), and its rules +layer answers their completion, hover, jumps and diagnostics in every editor +([rules.md](rules.md)). In build.mcpp, which mcppls compiles as a build program, the backend leaves +mcppls what a compiler answers better. ## A build, end to end -1. The user runs Build. The adapter sends `job/start {kind: "build", root}`. +1. The user runs Build. The adapter sends `mcpp-ide/job/start {kind: "build", root}`. 2. The backend queues it on the project's lane and runs `mcpp build --message-format json` with `MCPP_CLIENT`, colour for a terminal, and `MCPP_DENY_EFFECTS` in an untrusted workspace. 3. mcpp's narration becomes `job/output`; each event becomes `job/plan`, `job/progress`, diff --git a/docs/rules.md b/docs/rules.md new file mode 100644 index 0000000..8620059 --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,97 @@ +# The rules layer: mcpp.toml and build.mcpp + +The backend reads the two files mcpp defines beside C++ sources, `mcpp.toml` and `build.mcpp`, and +answers completion, hover, the jumps of `mcpp.toml` and the diagnostics of both (mcpp design +2026-10-30 part 2 §8). The answers are text analysis: nothing is run, so they hold in an untrusted +workspace too. Whether a manifest is valid is mcpp's to say (`mcpp-ide/manifest/check`, located +diagnostics); the rules layer says what the text in the editor already shows. + +## The connection + +`mcpp-ide serve` is an LSP server (protocol 2). `initialize` has LSP's shape, with mcpp-ide's +parameters in `initializationOptions`; the rules layer answers LSP's `textDocument/*` methods, and +mcpp-ide's own methods and notifications are under `mcpp-ide/`. Every editor connects to the one +backend process and so to one project state: + +| Editor | Client | The rules layer reaches the editor as | +|---|---|---| +| VS Code | VS Code's language client (bundled into `dist/vendor`) | the client's completion, hover, definition and the `mcpp` diagnostic collection | +| CLion | the session's connection (the platform's LSP client starts a server of its own) | a completion contributor, an external annotator, a documentation provider, a go-to-declaration handler | +| Neovim | Neovim's LSP client (`vim.lsp`) | the client attached to `mcpp.toml` and `build.mcpp` buffers | + +The documents are synchronised whole (`didOpen`, then `didChange` with each new text). A position is +a 0-based line and a UTF-16 column. Completion, hover, definition and the outline are answered in +the order the messages arrive, so an answer sees the document the notifications before it left. + +## mcpp.toml + +| Cursor | Suggestions | +|---|---| +| On a table header, or before the first one | every table of the snapshot (a table written only under a selector, `[target.'cfg(...)'.abi]`, with its selector to fill in), and the forms that name something (`[targets.]`, `[dependencies.]`, `[features.]`, `[target.'cfg(...)'.build]`, ...) | +| Inside `[[` | nothing: a suggestion would turn an array of tables into a table | +| On a key of a table with a closed vocabulary | its keys, less those already written in the table's group; a key's detail names its type, values and default | +| On a key of a dependency table | the five dependency forms | +| On a key of `[features]`, `[generated_files]`, `[capabilities]`, `[xlings.workspace]`, `[tools.overrides]` | that table's forms | +| On an enum key's value | its values (inside a string only the content is replaced) | +| On a dependency's version, with `mcpp.toml.indexCompletion` on and the workspace trusted | the versions mcpp's catalogue (`mcpp search --format json`) lists for that package, newest first; the catalogue is read once in the background (the answer is incomplete until then) and kept for ten minutes | + +A table is read as the snapshot's section its name denotes: the whole name when the snapshot has +it; `[workspace.
      ]` as the table it mirrors; a platform's table under a target selector +(`[target..link]`) as the platform's (`[platform.link]`); `[target..
      ]` as +`
      `; a named entry (`[targets.app]`, `[platform.build.'cfg(...)']`) as the longest leading +run that names a section. + +Hover describes a table (its plane, its keys), a key (type, values, default, the release that added +it, its note) and an enum value. Definition jumps from `workspace = true` to the entry of +`[workspace.dependencies]`, from an element of `features = [...]` to `[features.]`, and from +`path = "../x"` to that manifest's `[package]`. + +| Code | Default severity | Setting | What it reports | +|---|---|---|---| +| `mcpp.manifest.syntax` | error | `mcpp.toml.diagnostics.syntax` | a line that is neither a table header nor `key = value`; an invalid key; a key without a value | +| `mcpp.manifest.unknownSection` | warning | `mcpp.toml.diagnostics.unknownSection` | a table no section of the snapshot names, nor a prefix of it | +| `mcpp.manifest.unknownKey` | warning | `mcpp.toml.diagnostics.unknownKey` | a key a table with a closed vocabulary does not have | +| `mcpp.manifest.planeSeparation` | warning | `mcpp.toml.diagnostics.planeSeparation` | a tool payload (`xim:`, `xpm:`, `xlings:`, `xvm:`) as a dependency; an mcpp package with a version under `[xlings]` | +| `mcpp.manifest.legacyKey` | info | `mcpp.toml.diagnostics.legacyKeys` | a deprecated table or key, with its replacement | +| `mcpp.manifest.mcppFloor` | error | -- | `[package] mcpp` that is not a release floor (`">=2026.9.28.3"`) | +| `mcpp.manifest.arrayTable` | error | -- | an array of tables mcpp does not read (it reads `[[build.flags]]`, `[[runtime.requirements]]`, the snapshot's own) | + +The scanner errs towards silence: the lines of an unclosed array, inline table or multi-line string +are not checked. `mcpp.toml.completion`, `mcpp.toml.hover`, `mcpp.toml.navigation` and +`mcpp.toml.diagnostics.enabled` turn each feature off. + +## build.mcpp + +| Code | What it reports (SPEC-007) | +|---|---| +| `mcpp.buildscript.unknownSymbol` | a `mcpp::` that is no directive, action role or typed-API name of the snapshot, nor a namespace the file imports | +| `mcpp.buildscript.roleLiteral` | `role = ""` where `mcpp::roles::` exists (R3.6) | +| `mcpp.buildscript.actionNeedsRole` | an action without a role, or with a role the engine does not know (R3.2, R3.6) | +| `mcpp.buildscript.prepareNeedsOutputDir` | a prepare action without `output_dir` (R3.3) | +| `mcpp.buildscript.rpathInLinkFlag` | a run path in `link_flag` (R4.4) | +| `mcpp.buildscript.rawPathFlag` | `-I`/`-L` in a raw flag (R2.1) | +| `mcpp.buildscript.shellSyntaxInAction` | `VAR=value` or `cd x &&` in an action's command (R3.8) | + +The analysis is line-oriented: comments and string literals (raw strings too) are masked, and an +action block runs from `mcpp::action` to `.submit(` within 40 lines. Completion offers the +directives' typed names and the API's snippets after `mcpp::`, and the modules a build program may +import (`std`, `std.compat`, `mcpp`, `mcpp.core`, `mcpp.plugins.*`, ...) after `import`; hover +describes a directive and a known module. The layer recognises module names and never reports one +as missing. + +While C++ Modules (mcppls) serves the file -- the editor says so with `mcpplsServes` -- the backend +leaves it what a compiler answers better: the `unknownSymbol` rule, the module names after `import` +and their hover. `mcpp.buildScript.intelligence`, `.diagnostics` (with `.diagnostics.severity`), +`.snippets` and `.imports.knownModules` turn each part off. + +## The snapshots and the tests + +The rules read two snapshots of mcpp: `spec/toml-schema.json` (every table and key, each with the +release that added it) and `spec/buildscript-api.json` (the directives, roles and protocol), each +naming the mcpp release and commit it was taken from. + +The layer is a port of mcpp-vscode 0.9's TypeScript implementation. `tests/rules/golden` holds that +implementation's answers over `tests/rules/corpus` -- the scanner's structure and the cursor's context +at every position of every line, completion, hover, the jumps, the version contexts and the +diagnostics under two sets of severities -- and over the documents of its parser tests +(`golden/snippets.json`); `test_rules` compares the backend's answers with them. diff --git a/mide-plugins/clion/README.md b/mide-plugins/clion/README.md index 6f01fa8..57cdf88 100644 --- a/mide-plugins/clion/README.md +++ b/mide-plugins/clion/README.md @@ -14,8 +14,9 @@ mcpp's project features in CLion 2026.1 and later, over the mcpp-ide backend: - **Select the Build Target** (target sets among the rows) and **Pack** as popups, packing as a job; - the **Tools | mcpp** menu, generated from `spec/commands.json`. -Language features of C++ files come from the mcppls CLion plugin, which this plugin depends on; it -registers no language service. +Language features of C++ files come from the mcppls CLion plugin, which this plugin depends on. +Those of `mcpp.toml` and `build.mcpp` -- completion, documentation, the jumps and the diagnostics -- +come from the backend over the session's connection (mcpp-ide's `docs/rules.md`). ## Build diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index 797e90d..f5fb4e6 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -25,6 +25,19 @@ import java.io.File import java.util.concurrent.CompletableFuture import java.util.concurrent.CopyOnWriteArrayList +/** A path as the `file:` URI the backend names a document by (`file:///C:/x` for a drive path). */ +fun uriOf(path: String): String { + val p = path.replace('\\', '/') + val out = StringBuilder("file://") + if (!p.startsWith("/")) out.append('/') + for (b in p.toByteArray(Charsets.UTF_8)) { + val c = b.toInt() and 0xff + val ch = c.toChar() + if (ch.isLetterOrDigit() && c < 0x80 || ch in "/-_.~:") out.append(ch) else out.append("%%%02X".format(c)) + } + return out.toString() +} + @Service(Service.Level.PROJECT) class McppSession(private val project: Project) : Disposable { private val log = logger() @@ -88,12 +101,29 @@ class McppSession(private val project: Project) : Disposable { val strings: Map get() = bundle + private val startedListeners = CopyOnWriteArrayList<() -> Unit>() + + /** Called each time a backend has started and answered `initialize`. */ + fun whenStarted(listener: () -> Unit) { + startedListeners += listener + } + fun on(method: String, listener: (JsonElement) -> Unit): () -> Unit { val entry = method to listener listeners += entry return { listeners -= entry } } + /** + * Whether C++ Modules serves build.mcpp here: the mcppls CLion plugin is installed and enabled, + * and the backend leaves it what it answers better (the unknownSymbol rule, the module names + * after `import`). + */ + fun mcpplsServes(): Boolean = runCatching { + val id = com.intellij.openapi.extensions.PluginId.getId(MCPPLS_PLUGIN_ID) + com.intellij.ide.plugins.PluginManagerCore.getPlugin(id) != null && !com.intellij.ide.plugins.PluginManagerCore.isDisabled(id) + }.getOrDefault(false) + /** The plugin's directory: the parent of the lib/ holding this class's jar. */ private fun pluginRoot(): File? = pluginDirectory() @@ -127,17 +157,26 @@ class McppSession(private val project: Project) : Disposable { runCatching { fn(if (m == "*") JsonObject().apply { addProperty("method", method); add("params", params) } else params) } } } b.exited.thenAccept { code -> if (backend === b) onExit(code) } + // LSP's `initialize`: the client and the folder in LSP's fields, mcpp-ide's own in initializationOptions. val params = JsonObject().apply { - add("client", JsonObject().apply { addProperty("name", ApplicationInfo.getInstance().versionName); addProperty("version", ApplicationInfo.getInstance().fullVersion) }) - addProperty("editor", "jetbrains") - addProperty("locale", DynamicBundle.getLocale().toLanguageTag()) - addProperty("trusted", TrustedProjects.isProjectTrusted(project)) - add("settings", McppSettings.get().values()) - add("workspaceFolders", JsonArray().apply { project.basePath?.let { add(it) } }) - add("languageService", JsonObject().apply { add("candidates", mcpplsCandidates()) }) + addProperty("processId", ProcessHandle.current().pid()) + add("clientInfo", JsonObject().apply { addProperty("name", ApplicationInfo.getInstance().versionName); addProperty("version", ApplicationInfo.getInstance().fullVersion) }) + add("workspaceFolders", JsonArray().apply { + project.basePath?.let { base -> add(JsonObject().apply { addProperty("uri", uriOf(base)); addProperty("name", project.name) }) } + }) + add("capabilities", JsonObject()) + add("initializationOptions", JsonObject().apply { + addProperty("editor", "jetbrains") + addProperty("locale", DynamicBundle.getLocale().toLanguageTag()) + addProperty("trusted", TrustedProjects.isProjectTrusted(project)) + add("settings", McppSettings.get().values()) + add("languageService", JsonObject().apply { add("candidates", mcpplsCandidates()) }) + addProperty("mcpplsServes", mcpplsServes()) + }) } b.request("initialize", params).thenCompose { init -> initialized = init.asJsonObject + b.notify("initialized", JsonObject()) b.request("i18n/bundle") }.thenCompose { strings -> bundle = strings.asJsonObject.entrySet().associate { it.key to it.value.asString } @@ -146,6 +185,7 @@ class McppSession(private val project: Project) : Disposable { if (error != null) { future.completeExceptionally(error); return@whenComplete } commands = table.asJsonObject.getAsJsonArray("commands").map { it.asJsonObject } future.complete(initialized!!) + startedListeners.forEach { runCatching { it() } } } return future } @@ -262,9 +302,18 @@ class McppSession(private val project: Project) : Disposable { add("settings", McppSettings.get().values()) addProperty("trusted", TrustedProjects.isProjectTrusted(project)) addProperty("locale", DynamicBundle.getLocale().toLanguageTag()) + addProperty("mcpplsServes", mcpplsServes()) }) } + /** A notification to the backend (LSP's document notifications), when it is running. */ + fun notifyBackend(method: String, params: JsonObject) { + backend?.notify(method, params) + } + + /** Whether the backend is running now (nothing is started to answer this). */ + val running: Boolean get() = backend?.running == true + /** What the pages write to their output channel: the IDE's log. */ fun log(text: String) { text.trimEnd().lines().forEach { log.info("[mcpp] $it") } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStartup.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStartup.kt index 2020cda..97654fe 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStartup.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppStartup.kt @@ -16,6 +16,8 @@ class McppStartup : ProjectActivity { val base = project.basePath ?: return if (!File(base, "mcpp.toml").isFile) return val session = McppSession.get(project) + // mcpp.toml and build.mcpp reach the backend's rules layer as they are opened and edited. + io.github.mcppcommunity.mcppclion.editor.McppDocuments.get(project).install() session.start().thenAccept { init -> init.get("languageService")?.takeIf { it.isJsonObject }?.asJsonObject?.let { ls -> if (ls["state"].asString == "ok") return@let diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/backend/Backend.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/backend/Backend.kt index 5288b05..3cf7492 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/backend/Backend.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/backend/Backend.kt @@ -1,6 +1,8 @@ -// The backend's wire, as the plugin speaks it: `mcpp-ide serve` as a child process, JSON-RPC 2.0 -// framed as the LSP base protocol on its standard streams. Requests answer futures; notifications -// go to the listeners of their method ("*" receives every one). +// The backend's wire, as the plugin speaks it: `mcpp-ide serve` as a child process, an LSP server +// (protocol 2, mcpp design 2026-10-30 part 2 §8) on its standard streams. LSP's methods go as they +// are named; mcpp-ide's own are under `mcpp-ide/`, which this class adds to a request and takes off +// a notification. Requests answer futures; notifications go to the listeners of their method ("*" +// receives every one). package io.github.mcppcommunity.mcppclion.backend import com.google.gson.JsonElement @@ -61,6 +63,16 @@ fun encode(message: JsonObject): ByteArray { return "Content-Length: ${body.size}\r\n\r\n".toByteArray(StandardCharsets.US_ASCII) + body } +/** mcpp-ide's methods and notifications are under this namespace; LSP's are not. */ +const val NAMESPACE = "mcpp-ide/" + +/** The name a method has on the wire: LSP's own as it is, mcpp-ide's under its namespace. */ +fun wireName(method: String): String = + if (method in setOf("initialize", "initialized", "shutdown", "exit") || method.startsWith("$/") || + method.startsWith("textDocument/") || method.startsWith("workspace/") || method.startsWith("window/") || + method.startsWith(NAMESPACE)) method + else NAMESPACE + method + class Backend(command: List, private val log: (String) -> Unit = {}) : AutoCloseable { private val process: Process = ProcessBuilder(command).redirectErrorStream(false).start() private val input: OutputStream = process.outputStream @@ -109,7 +121,7 @@ class Backend(command: List, private val log: (String) -> Unit = {}) : A } else future.complete(message.get("result") ?: JsonNull.INSTANCE) return } - val method = message.get("method")?.asString ?: return + val method = message.get("method")?.asString?.removePrefix(NAMESPACE) ?: return val params = message.get("params") ?: JsonObject() listeners[method]?.forEach { runCatching { it(method, params) }.onFailure { e -> log("$method: ${e.message}") } } listeners["*"]?.forEach { runCatching { it(method, params) } } @@ -123,12 +135,12 @@ class Backend(command: List, private val log: (String) -> Unit = {}) : A } val id = nextId.getAndIncrement() pending[id] = method to future - send(JsonObject().apply { addProperty("jsonrpc", "2.0"); addProperty("id", id); addProperty("method", method); add("params", params) }) + send(JsonObject().apply { addProperty("jsonrpc", "2.0"); addProperty("id", id); addProperty("method", wireName(method)); add("params", params) }) return future } fun notify(method: String, params: JsonElement = JsonObject()) { - if (process.isAlive) send(JsonObject().apply { addProperty("jsonrpc", "2.0"); addProperty("method", method); add("params", params) }) + if (process.isAlive) send(JsonObject().apply { addProperty("jsonrpc", "2.0"); addProperty("method", wireName(method)); add("params", params) }) } private fun send(message: JsonObject) { diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/editor/McppEditor.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/editor/McppEditor.kt new file mode 100644 index 0000000..0207c90 --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/editor/McppEditor.kt @@ -0,0 +1,378 @@ +// mcpp.toml and build.mcpp in CLion: the backend's rules layer (mcpp design 2026-10-30 part 2 §8) -- +// completion, documentation, the jumps of mcpp.toml and the diagnostics of both -- over the +// session's own connection to `mcpp-ide serve`, which is an LSP server. The platform's LSP client +// starts the server it talks to, and a second backend process is what §8 rules out, so the requests +// are LSP's, sent by the session, and their answers reach CLion through its own extension points. +// +// The editor's text reaches the backend whole (`didOpen`, then `didChange` with each new version); +// positions are a 0-based line and a UTF-16 column, which a Document's offsets already count. +package io.github.mcppcommunity.mcppclion.editor + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.intellij.codeInsight.completion.CompletionContributor +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.ExternalAnnotator +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.lang.documentation.AbstractDocumentationProvider +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import io.github.mcppcommunity.mcppclion.McppSession +import io.github.mcppcommunity.mcppclion.uriOf +import java.net.URLDecoder +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +/** The files the rules layer reads. */ +internal fun served(file: VirtualFile?): Boolean = + file != null && file.isInLocalFileSystem && (file.name == "mcpp.toml" || file.name == "build.mcpp") + +/** A `file:` URI's path, as the backend wrote it (`file:///C:/x` is `C:/x`). */ +internal fun pathOf(uri: String): String { + var p = URLDecoder.decode(uri.removePrefix("file://").replace("+", "%2B"), Charsets.UTF_8) + if (p.length >= 3 && p[0] == '/' && p[1].isLetter() && p[2] == ':') p = p.substring(1) + return p +} + +private fun position(document: Document, offset: Int): JsonObject { + val line = document.getLineNumber(offset) + return JsonObject().apply { + addProperty("line", line) + addProperty("character", offset - document.getLineStartOffset(line)) + } +} + +private fun offsetOf(document: Document, position: JsonObject): Int { + val line = position["line"].asInt + if (line >= document.lineCount) return document.textLength + val start = document.getLineStartOffset(line) + return (start + position["character"].asInt).coerceIn(start, document.getLineEndOffset(line)) +} + +/** The documents the backend holds, and what it published about each. */ +@Service(Service.Level.PROJECT) +class McppDocuments(private val project: Project) : Disposable { + private val versions = ConcurrentHashMap() + private val sent = ConcurrentHashMap() + private val published = ConcurrentHashMap() + private val waiting = ConcurrentHashMap>>>() + @Volatile private var installed = false + + fun install() { + if (installed) return + installed = true + val session = McppSession.get(project) + session.on("textDocument/publishDiagnostics") { p -> onPublished(p.asJsonObject) } + // A backend that started again knows no document: each open one is sent again. + session.whenStarted { reopen() } + project.messageBus.connect(this).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { + override fun fileOpened(source: FileEditorManager, file: VirtualFile) { + if (served(file)) FileDocumentManager.getInstance().getDocument(file)?.let { sync(file, it) } + } + + override fun fileClosed(source: FileEditorManager, file: VirtualFile) { + if (served(file)) close(file) + } + }) + EditorFactory.getInstance().eventMulticaster.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + val file = FileDocumentManager.getInstance().getFile(event.document) ?: return + if (served(file) && sent.containsKey(uriOf(file.path))) sync(file, event.document) + } + }, this) + reopen() + } + + private fun reopen() { + sent.clear() + for (file in FileEditorManager.getInstance(project).openFiles) + if (served(file)) FileDocumentManager.getInstance().getDocument(file)?.let { sync(file, it) } + } + + /** Sends the document's text when it differs from what the backend holds: its uri and version. */ + fun sync(file: VirtualFile, document: Document): Pair? { + val session = McppSession.get(project) + if (!session.running) return null + val uri = uriOf(file.path) + val text = document.immutableCharSequence.toString() + synchronized(this) { + val last = sent[uri] + if (last == text) return uri to (versions[uri] ?: 0) + val version = (versions[uri] ?: 0) + 1 + versions[uri] = version + if (last == null) { + session.notifyBackend("textDocument/didOpen", JsonObject().apply { + add("textDocument", JsonObject().apply { + addProperty("uri", uri) + addProperty("languageId", if (file.name == "mcpp.toml") "mcpp-toml" else "cpp") + addProperty("version", version) + addProperty("text", text) + }) + }) + } else { + session.notifyBackend("textDocument/didChange", JsonObject().apply { + add("textDocument", JsonObject().apply { addProperty("uri", uri); addProperty("version", version) }) + add("contentChanges", JsonArray().apply { add(JsonObject().apply { addProperty("text", text) }) }) + }) + } + sent[uri] = text + return uri to version + } + } + + fun close(file: VirtualFile) { + val uri = uriOf(file.path) + if (sent.remove(uri) != null) + McppSession.get(project).notifyBackend("textDocument/didClose", JsonObject().apply { + add("textDocument", JsonObject().apply { addProperty("uri", uri) }) + }) + published.remove(uri) + } + + private fun onPublished(p: JsonObject) { + val uri = p["uri"]?.asString ?: return + published[uri] = p + val version = p["version"]?.takeIf { !it.isJsonNull }?.asInt ?: Int.MAX_VALUE + waiting[uri]?.let { list -> + synchronized(list) { + list.removeIf { (wanted, future) -> (version >= wanted).also { if (it) future.complete(p) } } + } + } + } + + /** The diagnostics the backend published for `version` of the document or a later one. */ + fun diagnostics(uri: String, version: Int, timeoutMs: Long): JsonArray? { + published[uri]?.let { p -> if ((p["version"]?.takeIf { !it.isJsonNull }?.asInt ?: 0) >= version) return p.getAsJsonArray("diagnostics") } + val future = CompletableFuture() + val list = waiting.computeIfAbsent(uri) { mutableListOf() } + synchronized(list) { list += version to future } + // Published while this waiter was being registered. + published[uri]?.let { onPublished(it) } + return runCatching { future.get(timeoutMs, TimeUnit.MILLISECONDS).getAsJsonArray("diagnostics") }.getOrNull() + } + + override fun dispose() {} + + companion object { + fun get(project: Project): McppDocuments = project.getService(McppDocuments::class.java) + } +} + +/** The rules' diagnostics as CLion's annotations. */ +class McppAnnotator : ExternalAnnotator() { + class Input(val project: Project, val file: VirtualFile, val document: Document) + + override fun collectInformation(file: PsiFile, editor: Editor, hasErrors: Boolean): Input? { + val vf = file.virtualFile ?: return null + if (!served(vf)) return null + return Input(file.project, vf, editor.document) + } + + override fun doAnnotate(input: Input): JsonArray? { + val documents = McppDocuments.get(input.project) + documents.install() + val (uri, version) = documents.sync(input.file, input.document) ?: return null + return documents.diagnostics(uri, version, 2000) + } + + override fun apply(file: PsiFile, result: JsonArray?, holder: AnnotationHolder) { + val document = PsiDocumentManager.getInstance(file.project).getDocument(file) ?: return + for (element in result ?: return) { + val d = element.asJsonObject + val range = d.getAsJsonObject("range") + val start = offsetOf(document, range.getAsJsonObject("start")) + var end = offsetOf(document, range.getAsJsonObject("end")) + if (end <= start) end = minOf(start + 1, document.textLength) + if (end <= start) continue + val severity = when (d["severity"]?.asInt) { + 1 -> HighlightSeverity.ERROR + 2 -> HighlightSeverity.WARNING + else -> HighlightSeverity.WEAK_WARNING + } + val code = d["code"]?.takeIf { it.isJsonPrimitive }?.asString + holder.newAnnotation(severity, d["message"].asString).range(TextRange(start, end)) + .apply { if (code != null) tooltip("${d["message"].asString} ($code)") }.create() + } + } +} + +/** A snippet (`${1:name}`, `$1`, `${2|a,b|}`) as plain text and where the caret goes: its first stop. */ +internal fun expandSnippet(snippet: String): Pair { + val out = StringBuilder() + var caret: Int? = null + var firstStop = Int.MAX_VALUE + var i = 0 + while (i < snippet.length) { + val c = snippet[i] + if (c == '\\' && i + 1 < snippet.length) { out.append(snippet[i + 1]); i += 2; continue } + if (c == '$' && i + 1 < snippet.length) { + val brace = snippet[i + 1] == '{' + var j = if (brace) i + 2 else i + 1 + val digits = StringBuilder() + while (j < snippet.length && snippet[j].isDigit()) digits.append(snippet[j++]) + if (digits.isNotEmpty()) { + val stop = digits.toString().toInt() + var text = "" + if (brace) { + var depth = 1 + val body = StringBuilder() + while (j < snippet.length && depth > 0) { + val d = snippet[j] + if (d == '{') depth++ + if (d == '}') depth-- + if (depth > 0) body.append(d) + j++ + } + text = when { + body.startsWith(":") -> expandSnippet(body.substring(1)).first + body.startsWith("|") -> body.removePrefix("|").removeSuffix("|").split(',').first() + else -> "" + } + } + if (stop in 1 until firstStop) { firstStop = stop; caret = out.length } + out.append(text) + i = j + continue + } + } + out.append(c) + i++ + } + return out.toString() to caret +} + +/** The backend's completion in mcpp.toml and build.mcpp. */ +class McppCompletion : CompletionContributor() { + override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { + val vf = parameters.originalFile.virtualFile ?: return + if (!served(vf)) return + val project = parameters.originalFile.project + val document = parameters.editor.document + val documents = McppDocuments.get(project) + documents.install() + val (uri, _) = documents.sync(vf, document) ?: return + val offset = parameters.offset + val params = JsonObject().apply { + add("textDocument", JsonObject().apply { addProperty("uri", uri) }) + add("position", position(document, offset)) + } + val answer = runCatching { McppSession.get(project).request("textDocument/completion", params).get(2, TimeUnit.SECONDS) } + .getOrNull()?.takeIf { it.isJsonObject }?.asJsonObject ?: return + val lineStart = document.getLineStartOffset(document.getLineNumber(offset)) + for (element in answer.getAsJsonArray("items") ?: return) { + val item = element.asJsonObject + val label = item["label"].asString + val edit = item["textEdit"]?.takeIf { it.isJsonObject }?.asJsonObject + val start = edit?.getAsJsonObject("range")?.getAsJsonObject("start")?.let { lineStart + it["character"].asInt } + val end = edit?.getAsJsonObject("range")?.getAsJsonObject("end")?.let { lineStart + it["character"].asInt } + val text = edit?.get("newText")?.asString ?: item["insertText"]?.asString ?: label + val (plain, caret) = if (item["insertTextFormat"]?.asInt == 2) expandSnippet(text) else text to null + val lookup = item["filterText"]?.asString ?: label + val built = LookupElementBuilder.create(lookup) + .withPresentableText(label) + .withTypeText(item["detail"]?.asString, true) + .withInsertHandler { context, _ -> + // What the lookup inserted is replaced by the item's text, and what the edit + // covered past the cursor goes too. + val past = if (end != null) (end - offset).coerceAtLeast(0) else 0 + val tail = minOf(context.tailOffset + past, context.document.textLength) + context.document.replaceString(context.startOffset, tail, plain) + context.editor.caretModel.moveToOffset(context.startOffset + (caret ?: plain.length)) + } + val prefix = if (start != null && start <= offset) document.getText(TextRange(start, offset)) else result.prefixMatcher.prefix + result.withPrefixMatcher(prefix).addElement(built) + } + } +} + +/** The backend's hover as CLion's documentation, in mcpp.toml and build.mcpp. */ +class McppDocumentation : AbstractDocumentationProvider() { + override fun getCustomDocumentationElement(editor: Editor, file: PsiFile, contextElement: PsiElement?, targetOffset: Int): PsiElement? { + if (!served(file.virtualFile)) return null + file.putUserData(OFFSET, targetOffset) + return contextElement ?: file + } + + override fun generateDoc(element: PsiElement, originalElement: PsiElement?): String? { + val file = element.containingFile ?: return null + val vf = file.virtualFile + if (!served(vf)) return null + val project = file.project + val document = PsiDocumentManager.getInstance(project).getDocument(file) ?: return null + val documents = McppDocuments.get(project) + documents.install() + val (uri, _) = documents.sync(vf, document) ?: return null + val offset = file.getUserData(OFFSET) ?: (originalElement ?: element).textOffset + val params = JsonObject().apply { + add("textDocument", JsonObject().apply { addProperty("uri", uri) }) + add("position", position(document, offset.coerceIn(0, document.textLength))) + } + val answer = runCatching { McppSession.get(project).request("textDocument/hover", params).get(2, TimeUnit.SECONDS) }.getOrNull() + val value = answer?.takeIf { it.isJsonObject }?.asJsonObject?.getAsJsonObject("contents")?.get("value")?.asString ?: return null + return markdownToHtml(value) + } + + companion object { + private val OFFSET = Key.create("mcpp.documentation.offset") + } +} + +/** The little markdown the rules write: paragraphs, **bold**, `code`, [text](link). */ +internal fun markdownToHtml(markdown: String): String { + fun inline(text: String): String { + var s = text.replace("&", "&").replace("<", "<").replace(">", ">") + s = Regex("\\[([^\\]]+)]\\(([^)]+)\\)").replace(s) { "${it.groupValues[1]}" } + s = Regex("\\*\\*([^*]+)\\*\\*").replace(s) { "${it.groupValues[1]}" } + s = Regex("`([^`]+)`").replace(s) { "${it.groupValues[1]}" } + return s + } + return markdown.split(Regex("\\n\\s*\\n")).joinToString("") { "

      ${inline(it.trim()).replace("\n", "
      ")}

      " } +} + +/** mcpp.toml's jumps: a workspace dependency, a feature, a path dependency's manifest. */ +class McppGotoDeclaration : GotoDeclarationHandler { + override fun getGotoDeclarationTargets(sourceElement: PsiElement?, offset: Int, editor: Editor): Array? { + val file = sourceElement?.containingFile ?: return null + val vf = file.virtualFile + if (!served(vf) || vf.name != "mcpp.toml") return null + val project = file.project + val documents = McppDocuments.get(project) + documents.install() + val (uri, _) = documents.sync(vf, editor.document) ?: return null + val params = JsonObject().apply { + add("textDocument", JsonObject().apply { addProperty("uri", uri) }) + add("position", position(editor.document, offset)) + } + val answer: JsonElement = runCatching { McppSession.get(project).request("textDocument/definition", params).get(2, TimeUnit.SECONDS) } + .getOrNull() ?: return null + val location = answer.takeIf { it.isJsonObject }?.asJsonObject ?: return null + val target = LocalFileSystem.getInstance().findFileByPath(pathOf(location["uri"].asString)) ?: return null + val psi = PsiManager.getInstance(project).findFile(target) ?: return null + val document = FileDocumentManager.getInstance().getDocument(target) ?: return null + val at = offsetOf(document, location.getAsJsonObject("range").getAsJsonObject("start")) + return arrayOf(psi.findElementAt(at) ?: psi) + } +} diff --git a/mide-plugins/clion/src/main/resources/META-INF/plugin.xml b/mide-plugins/clion/src/main/resources/META-INF/plugin.xml index 998c94a..a16a802 100644 --- a/mide-plugins/clion/src/main/resources/META-INF/plugin.xml +++ b/mide-plugins/clion/src/main/resources/META-INF/plugin.xml @@ -52,5 +52,18 @@ + + + + + + + + + + + + diff --git a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt index 28f6d24..c3ccd60 100644 --- a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt +++ b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/BackendTest.kt @@ -1,5 +1,6 @@ // The wire and the backend, without an IDE: frames reassemble whatever the chunking; a session over -// the mock mcpp finds the project, runs a build to `ok`, and lists the commands this editor offers +// the mock mcpp (protocol 2, an LSP server) finds the project, publishes mcpp.toml's diagnostics and +// completes it, runs a build to `ok`, and lists the commands this editor offers // (a forward to one of mcppls's actions only under the JetBrains id mcppls states). The session test runs when the build passes // the backend and the mock mcpp (MCPP_IDE_BACKEND, MCPP_IDE_MOCK_MCPP). package io.github.mcppcommunity.mcppclion @@ -78,11 +79,32 @@ class BackendTest { File(root, "mcpp.toml").writeText("[package]\nname = \"app\"\nversion = \"0.1.0\"\n") Backend(listOf(backend!!, "serve")).use { b -> val init = b.request("initialize", JsonObject().apply { - addProperty("editor", "jetbrains") - add("settings", JsonObject().apply { addProperty("mcpp.path", mock) }) - add("workspaceFolders", JsonArray().apply { add(root.path) }) + add("workspaceFolders", JsonArray().apply { add(JsonObject().apply { addProperty("uri", uriOf(root.path)) }) }) + add("initializationOptions", JsonObject().apply { + addProperty("editor", "jetbrains") + add("settings", JsonObject().apply { addProperty("mcpp.path", mock) }) + }) }).get(30, TimeUnit.SECONDS).asJsonObject assertEquals(1, init.getAsJsonArray("projects").size()) + assertEquals(2, init["protocol"].asInt) + // mcpp.toml through LSP: opened, its diagnostics published (0-based, UTF-16), completion answers. + val uri = uriOf(File(root, "mcpp.toml").path) + val published = CompletableFuture() + b.on("textDocument/publishDiagnostics") { _, p -> published.complete(p) } + b.notify("textDocument/didOpen", JsonObject().apply { + add("textDocument", JsonObject().apply { + addProperty("uri", uri); addProperty("languageId", "mcpp-toml"); addProperty("version", 1) + addProperty("text", "[package]\nname = \"名字\"\ncolour = 1\n") + }) + }) + val diagnostic = published.get(30, TimeUnit.SECONDS).asJsonObject.getAsJsonArray("diagnostics")[0].asJsonObject + assertEquals("mcpp.manifest.unknownKey", diagnostic["code"].asString) + assertEquals(2, diagnostic.getAsJsonObject("range").getAsJsonObject("start")["line"].asInt) + val list = b.request("textDocument/completion", JsonObject().apply { + add("textDocument", JsonObject().apply { addProperty("uri", uri) }) + add("position", JsonObject().apply { addProperty("line", 0); addProperty("character", 1) }) + }).get(30, TimeUnit.SECONDS).asJsonObject + assertTrue(list.getAsJsonArray("items").any { it.asJsonObject["label"].asString == "[build]" }) val finished = CompletableFuture() b.on("job/finished") { _, p -> finished.complete(p) } b.request("job/start", JsonObject().apply { addProperty("kind", "build"); addProperty("root", root.path) }).get(30, TimeUnit.SECONDS) @@ -96,6 +118,17 @@ class BackendTest { root.deleteRecursively() } + @Test + fun snippetsAndMarkdownAsCLionShowsThem() { + assertEquals("name = \"1.0.0\"" to 0, io.github.mcppcommunity.mcppclion.editor.expandSnippet("\${1:name} = \"\${2:1.0.0}\"")) + assertEquals("role = source;" to 7, io.github.mcppcommunity.mcppclion.editor.expandSnippet("role = \${3|source,check|};")) + assertEquals("x()" to 2, io.github.mcppcommunity.mcppclion.editor.expandSnippet("x($1)")) + assertEquals("

      [package]

      plane: identity r

      ", + io.github.mcppcommunity.mcppclion.editor.markdownToHtml("**[package]**\n\nplane: `identity` [r](u)")) + assertEquals("file:///C:/a%20b/mcpp.toml", uriOf("C:\\a b\\mcpp.toml")) + assertEquals("C:/a b/mcpp.toml", io.github.mcppcommunity.mcppclion.editor.pathOf("file:///C:/a%20b/mcpp.toml")) + } + @Test fun theGeneratedActionsAreTheTables() { val xml = javaClass.getResource("/META-INF/mcpp-actions.xml")?.readText() diff --git a/mide-plugins/nvim/README.md b/mide-plugins/nvim/README.md index 426b977..6e4a44c 100644 --- a/mide-plugins/nvim/README.md +++ b/mide-plugins/nvim/README.md @@ -8,8 +8,9 @@ graphs, and a statusline component. It is the Neovim adapter of build runs, what a cleanup costs and which commands exist are the backend's answers, the same ones those editors receive. -The plugin registers no language service. Completion, navigation and diagnostics of C++ files come -from mcppls through its own Neovim plugin (below). +Completion, navigation and diagnostics of C++ files come from mcppls through its own Neovim plugin +(below). Those of `mcpp.toml` and `build.mcpp` come from the backend: the session's LSP client is +attached to their buffers (completion, `K`, `gd`, `vim.diagnostic`; see mcpp-ide's `docs/rules.md`). | | | |---|---| @@ -68,8 +69,8 @@ require('mcpp').setup({ `xlings install mcpp-ide`, and starts nothing; `:Mcpp` looks again. **The session.** A buffer whose file lies under an `mcpp.toml` (the nearest one upward, else the -working directory's) starts one backend for the Neovim instance, speaking the protocol through -Neovim's own JSON-RPC client (`vim.lsp.rpc`). It is initialized as the editor `nvim` with Neovim's +working directory's) starts one backend for the Neovim instance, an LSP server that Neovim's own LSP +client (`vim.lsp`) starts and speaks to. It is initialized as the editor `nvim` with Neovim's language (`v:lang`, or `mcpp.ui.language` when that names one), the trust below, that project as its folder and the settings above; the projects of buffers entered later are added to it. The runtime strings and the command table are then read from it. The backend is ended with `shutdown` diff --git a/mide-plugins/nvim/lua/mcpp/init.lua b/mide-plugins/nvim/lua/mcpp/init.lua index 9edcfdb..23d9033 100644 --- a/mide-plugins/nvim/lua/mcpp/init.lua +++ b/mide-plugins/nvim/lua/mcpp/init.lua @@ -3,7 +3,8 @@ -- in a terminal, mcpp's diagnostics, targets, toolchains, libraries, the cache and the graphs, a -- statusline component. Every decision is the backend's, the one mcpp-vscode and mcpp-clion ask. -- --- It registers no language service: C++ files are mcppls's, through its own plugin (mcppls-nvim). +-- C++ files are mcppls's, through its own plugin (mcppls-nvim). mcpp.toml and build.mcpp are the +-- backend's: its LSP client serves their completion, hover, jumps and diagnostics. -- -- require('mcpp').setup({ backend = ..., settings = { ['mcpp.build.target'] = ... }, trusted = true }) -- :Mcpp [subcommand] the backend's command table; no subcommand: the quick menu @@ -62,7 +63,8 @@ end -- A buffer of an mcpp project: the backend starts for its project, or learns of it. A backend that -- could not start is tried again by `:Mcpp`, not by every buffer entered. -local function enter() +local function enter(a) + session.attach(a and a.buf) local root = commands.root() if not root or known[root] then return @@ -122,6 +124,16 @@ function M.setup(opts) end, }) vim.api.nvim_create_autocmd('User', { group = group, pattern = 'McppStatus', callback = on_started }) + -- mcppls attached: it serves build.mcpp now, and the backend leaves it what it answers better. + vim.api.nvim_create_autocmd('LspAttach', { + group = group, + callback = function(a) + local client = vim.lsp.get_client_by_id(a.data and a.data.client_id or -1) + if client and client.name == 'mcppls' then + session.push_settings() + end + end, + }) vim.api.nvim_create_autocmd('VimLeavePre', { group = group, callback = session.stop }) -- The editor regained focus: a `xlings use` or a changed pin happened outside it (§4.2). vim.api.nvim_create_autocmd('FocusGained', { diff --git a/mide-plugins/nvim/lua/mcpp/session.lua b/mide-plugins/nvim/lua/mcpp/session.lua index 23fff2a..01dba6b 100644 --- a/mide-plugins/nvim/lua/mcpp/session.lua +++ b/mide-plugins/nvim/lua/mcpp/session.lua @@ -1,8 +1,10 @@ --- The session with the backend: one `mcpp-ide serve` per Neovim, spoken to through Neovim's own --- JSON-RPC client (vim.lsp.rpc, which frames each message with Content-Length as the protocol --- does). Started for the nearest directory upward that holds an mcpp.toml, initialized as the --- editor `nvim` with the locale, trust, folders and setup()'s settings; then the runtime strings and --- the command table are read from it. Everything else reaches the backend through this module. +-- The session with the backend: one `mcpp-ide serve` per Neovim, an LSP server (protocol 2, mcpp +-- design 2026-10-30 part 2 §8) that Neovim's own LSP client starts and speaks to. Started for the +-- nearest directory upward that holds an mcpp.toml, initialized as the editor `nvim` with the +-- locale, trust, folders and setup()'s settings; then the runtime strings and the command table are +-- read from it. The client serves mcpp.toml and build.mcpp buffers (completion, hover, the jumps, +-- the diagnostics); mcpp-ide's own methods and notifications are under `mcpp-ide/`. Everything else +-- reaches the backend through this module. local flow = require('mcpp.flow') local M = {} @@ -276,8 +278,49 @@ local function not_running() return { code = -32099, message = t('the mcpp-ide backend is not running') } end +-- mcpp-ide's own methods and notifications, beside LSP's. +local NAMESPACE = 'mcpp-ide/' + +-- The notifications the backend sends under its namespace (protocol/protocol.schema.json). +local NOTIFICATIONS = { + 'job/started', 'job/output', 'job/plan', 'job/progress', 'job/diagnostic', 'job/artifact', 'job/record', + 'job/finished', 'test/result', 'test/summary', 'result/changed', 'manifest/changed', 'manifest/diagnostics', + 'project/mcppChanged', 'languageService/changed', 'test/caseResult', +} + +-- A method of Neovim's LSP client: `client:name(...)` since 0.11, `client.name(...)` before. +local methods_take_self = vim.fn.has('nvim-0.11') == 1 +local function invoke(client, name, ...) + if methods_take_self then + return client[name](client, ...) + end + return client[name](...) +end + +-- The session's client: Neovim's LSP client of the backend, as the rest of the plugin asks it. +local function wrap(lsp) + return { + id = lsp.id, + request = function(method, params, handler) + return invoke(lsp, 'request', method, params, handler) + end, + notify = function(method, params) + return invoke(lsp, 'notify', method, params) + end, + is_closing = function() + return invoke(lsp, 'is_stopped') + end, + terminate = function() + invoke(lsp, 'stop', true) + end, + stop = function() + invoke(lsp, 'stop') + end, + } +end + -- A request; `callback(err, result)` on the main loop. A request still unanswered when the backend --- ends is answered with that error (vim.lsp.rpc drops it). +-- ends is answered with that error. function M.request(method, params, callback) local client = state.client if not client or client.is_closing() then @@ -286,7 +329,7 @@ function M.request(method, params, callback) end local entry = { callback = callback } state.pending[entry] = true - local ok = client.request(method, object(params), function(err, result) + local ok = client.request(NAMESPACE .. method, object(params), function(err, result) if state.pending[entry] then state.pending[entry] = nil callback(denull(err), denull(result)) @@ -316,8 +359,15 @@ function M.value(key) end -- settings/update's parameters: setup()'s settings, the trust, the language. +-- Whether mcppls (mcppls-nvim's client) runs: it then serves build.mcpp, and the backend leaves it +-- what it answers better (the unknownSymbol rule, the module names after `import`). +local function mcppls_serves() + local get = vim.lsp.get_clients or vim.lsp.get_active_clients + return #get({ name = 'mcppls' }) > 0 +end + local function settings_params() - return { settings = object(state.options.settings), trusted = trusted(), locale = locale() } + return { settings = object(state.options.settings), trusted = trusted(), locale = locale(), mcpplsServes = mcppls_serves() } end -- The settings changed (setup again, a target chosen): the backend validates and resolves them. @@ -361,18 +411,27 @@ local function report_rejected(resolved) end end -local function initialize() - local info, err = M.call('initialize', { - client = { name = 'Neovim', version = M.nvim_version() }, - editor = 'nvim', - locale = locale(), - trusted = trusted(), - settings = object(state.options.settings), - workspaceFolders = state.folder and { state.folder } or {}, - languageService = { candidates = M.mcppls_candidates() }, - }) +-- The rules layer's buffers: mcpp.toml and build.mcpp. +local function served(buf) + local name = vim.fs.basename(vim.api.nvim_buf_get_name(buf)) + return name == 'mcpp.toml' or name == 'build.mcpp' +end + +-- A buffer of mcpp.toml or build.mcpp joins the session's client (completion, hover, the jumps and +-- the diagnostics of the rules layer). +function M.attach(buf) + buf = (buf == nil or buf == 0) and vim.api.nvim_get_current_buf() or buf + local client = state.client + if not state.ready or not client or client.is_closing() or not vim.api.nvim_buf_is_loaded(buf) or not served(buf) then + return + end + vim.lsp.buf_attach_client(buf, client.id) +end + +-- Runs once the client is initialized: `info` is the backend's initialize answer. +local function initialize(info) if not info then - vim.notify('mcpp-ide: initialize failed: ' .. tostring(err and err.message), vim.log.levels.ERROR) + vim.notify('mcpp-ide: initialize failed', vim.log.levels.ERROR) local client = state.client state.client = nil if client then @@ -408,6 +467,9 @@ local function initialize() end -- A backend that ended while it was asked these has not started. finish_start(state.client ~= nil) + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + M.attach(buf) + end end local function on_exit(client, code, signal) @@ -456,27 +518,56 @@ function M.start(folder, done) end state.starting, state.stopping, state.folder = true, false, folder cmd[#cmd + 1] = 'serve' + local handlers = {} + for _, name in ipairs(NOTIFICATIONS) do + handlers[NAMESPACE .. name] = function(_, params) + params = denull(params) or {} + if JOB_EVENTS[name] then + route(name, params) + end + emit(name, params) + end + end local client - local ok, err = pcall(function() - client = vim.lsp.rpc.start(cmd, { - notification = function(method, params) - params = denull(params) or {} - if JOB_EVENTS[method] then - route(method, params) - end - emit(method, params) - end, - on_exit = function(code, signal) - on_exit(client, code, signal) - end, - }, folder and { cwd = folder } or nil) + local config = { + name = 'mcpp-ide', + cmd = cmd, + cmd_cwd = folder, + root_dir = folder, + workspace_folders = folder and { { uri = vim.uri_from_fname(folder), name = vim.fs.basename(folder) } } or nil, + init_options = { + editor = 'nvim', + locale = locale(), + trusted = trusted(), + settings = object(state.options.settings), + languageService = { candidates = M.mcppls_candidates() }, + mcpplsServes = mcppls_serves(), + }, + handlers = handlers, + on_init = function(_, result) + vim.schedule(function() + flow.run(function() + initialize(denull(result)) + end) + end) + end, + on_exit = function(code, signal) + on_exit(client, code, signal) + end, + } + local ok, id = pcall(function() + if vim.fn.has('nvim-0.11') == 1 then + return vim.lsp.start(config, { attach = false, reuse_client = function() return false end }) + end + return vim.lsp.start_client(config) end) - if not ok then - vim.notify(t('The mcpp-ide backend could not be started: {0}', tostring(err)), vim.log.levels.ERROR) + local lsp = ok and id and vim.lsp.get_client_by_id(id) or nil + if not lsp then + vim.notify(t('The mcpp-ide backend could not be started: {0}', tostring(ok and 'no client' or id)), vim.log.levels.ERROR) return finish_start(false) end + client = wrap(lsp) state.client = client - flow.run(initialize) end -- Inside a flow: the session, started for the current project when it is not running; nil when it @@ -488,22 +579,15 @@ function M.ensure(folder) return ok and M or nil end --- Ends the backend as the protocol asks: shutdown, then exit. Neovim waits briefly for the answer. +-- Ends the backend as the protocol asks: the client's shutdown, then exit. Neovim waits briefly. function M.stop() local client = state.client if not client or client.is_closing() then return end state.stopping = true - local answered = false - client.request('shutdown', vim.empty_dict(), function() - answered = true - end) - vim.wait(1000, function() - return answered - end, 10) - client.notify('exit', vim.empty_dict()) - vim.wait(500, function() + client.stop() + vim.wait(1500, function() return client.is_closing() end, 10) if not client.is_closing() then diff --git a/mide-plugins/nvim/tests/smoke.lua b/mide-plugins/nvim/tests/smoke.lua index 5b06ee8..7fbf43e 100644 --- a/mide-plugins/nvim/tests/smoke.lua +++ b/mide-plugins/nvim/tests/smoke.lua @@ -134,7 +134,7 @@ vim.cmd.edit(vim.fn.fnameescape(root .. '/src/main.cpp')) check('a buffer of the project starts the backend', vim.wait(30000, function() return session.state.ready end, 20), session.state.failed and 'failed' or nil) local info = session.state.info or {} -check('initialize answers as mcpp-ide, protocol 1', (info.server or {}).name == 'mcpp-ide' and info.protocol == 1, vim.inspect(info.server)) +check('initialize answers as mcpp-ide, protocol 2', (info.serverInfo or {}).name == 'mcpp-ide' and info.protocol == 2, vim.inspect(info.serverInfo)) check('the project is the nearest mcpp.toml upward', info.projects and info.projects[1] and vim.fs.normalize(info.projects[1].root) == root, info.projects and info.projects[1] and info.projects[1].root) check('mcpp is the one setup() names', info.mcpp and vim.fs.normalize(info.mcpp.path) == vim.fs.normalize(mcpp), info.mcpp and info.mcpp.path) @@ -216,6 +216,32 @@ check('a broken mcpp.toml gets mcpp\'s problem in the `mcpp.toml` namespace', vim.api.nvim_buf_set_lines(0, 0, 1, false, { '[package]' }) vim.cmd('silent write') os.remove(root .. '/.mock-mcpp-fail') +-- The rules layer through the backend's LSP client (protocol 2): the buffer joined it, a rule is +-- published as the text changes, and hover answers from the snapshot of mcpp's tables. +local function lsp_attached() + local get = vim.lsp.get_clients or vim.lsp.get_active_clients + return #get({ name = 'mcpp-ide', bufnr = toml }) > 0 +end +check('mcpp.toml joins the backend\'s LSP client', vim.wait(5000, lsp_attached, 20)) +vim.api.nvim_buf_set_lines(toml, 1, 1, false, { 'colour = 1' }) +local function rule_seen() + for _, d in ipairs(vim.diagnostic.get(toml)) do + if d.code == 'mcpp.manifest.unknownKey' and d.lnum == 1 and d.col == 0 then + return true + end + end + return false +end +check('a key [package] does not have is published as the text changes', vim.wait(5000, rule_seen, 20), + vim.inspect(vim.diagnostic.get(toml))) +vim.api.nvim_buf_set_lines(toml, 1, 2, false, {}) +local replies = vim.lsp.buf_request_sync(toml, 'textDocument/hover', + { textDocument = { uri = vim.uri_from_bufnr(toml) }, position = { line = 0, character = 2 } }, 5000) or {} +local _, answer = next(replies) +local hovered = answer and answer.result and answer.result.contents and answer.result.contents.value or '' +check('hover on [package] answers from the rules layer', hovered:find('plane', 1, true) ~= nil, hovered) +-- The buffer is the file again; nothing is written (a write would build on save). +vim.bo[toml].modified = false vim.cmd.edit(vim.fn.fnameescape(root .. '/src/main.cpp')) -- ── the backend's views as text ─────────────────────────────────────────────────────────────── diff --git a/mide-plugins/vscode/CHANGELOG.md b/mide-plugins/vscode/CHANGELOG.md index b1e0fb8..763df47 100644 --- a/mide-plugins/vscode/CHANGELOG.md +++ b/mide-plugins/vscode/CHANGELOG.md @@ -13,8 +13,13 @@ sets (mcpp 2026.10.30.1 and later), **Pack**, build on save (`mcpp.build.onSave`), the build's profile and target (`mcpp.build.profile`, `mcpp.build.target`), and cancelling a running job. - A launch without a debugger extension is refused at once; the message offers to install CodeLLDB. -- The build-script API and the `mcpp.toml` schema follow mcpp 2026.10.29.1, the platform plane - included. +- mcpp.toml's and build.mcpp's completion, hover, jumps and diagnostics are the mcpp-ide backend's, + connected through VS Code's language client (`docs/rules.md` of mcpp-ide): every table and key of + mcpp 2026.10.27.1 and the build-script API of 2026.10.29.1, dependency versions from mcpp's + catalogue (`mcpp.toml.indexCompletion`), and diagnostics at the columns VS Code counts (0.9.0 placed + manifest diagnostics one column late). +- Where the backend's runner does not run (macOS x86_64, Windows arm64, Linux armhf) the extension + says so and goes on with build, run, test and the project pages. - Releases are published to Open VSX beside the GitHub release. ## 0.9.0 - 2026-10-20 diff --git a/mide-plugins/vscode/README.md b/mide-plugins/vscode/README.md index 3c3d752..d2a43fb 100644 --- a/mide-plugins/vscode/README.md +++ b/mide-plugins/vscode/README.md @@ -53,8 +53,8 @@ Opening a project never runs `mcpp` on its own and never downloads a toolchain. ## Features - **mcpp CLI and tasks** — build, run, test, clean, the quick menu, toolchains: [docs/commands.md](docs/commands.md). -- **`mcpp.toml` editing** — structural completion, hovers, go-to-definition, seven structural diagnostics, no formatting: [docs/mcpp-toml.md](docs/mcpp-toml.md). -- **`build.mcpp` help** — `build.mcpp` is a `cpp` file that C++ Modules compiles as the build program; this extension adds the `mcpp::…` directive completion, hovers and snippets and the SPEC-007 rule checks no compiler makes: [docs/build-script.md](docs/build-script.md). +- **`mcpp.toml` editing** — completion of tables, keys, values and dependency versions, hovers, go-to-definition, seven structural diagnostics, no formatting, from the mcpp-ide backend's rules layer: [docs/rules.md](../../docs/rules.md). +- **`build.mcpp` help** — `build.mcpp` is a `cpp` file that C++ Modules compiles as the build program; the backend's rules layer adds the `mcpp::…` directive completion, hovers and snippets and the SPEC-007 rule checks no compiler makes: [docs/rules.md](../../docs/rules.md). - **Libraries** — browse the official mcpp-index offline, choose a target project and dependency type, and view available versions and verified example code. Other registry hits have separate previews. See [Libraries](docs/libraries.md). - **Cache views and cleanup** — project artifacts and the shared build cache, previewed before anything is deleted: [docs/cache.md](docs/cache.md). - **C++ Modules** — the other extension's state and actions, in the project view and the status-bar menu, forwarded not reimplemented: [docs/commands.md](docs/commands.md). diff --git a/mide-plugins/vscode/data/buildscript-api.json b/mide-plugins/vscode/data/buildscript-api.json deleted file mode 100644 index 5c6085a..0000000 --- a/mide-plugins/vscode/data/buildscript-api.json +++ /dev/null @@ -1,509 +0,0 @@ -{ - "sourceVersion": "2026.10.29.1", - "sourceCommit": "db3b8159480ce94df2ba7ec20c7f9e0654f97764", - "protocolVersion": 17, - "cacheEpoch": 3, - "roles": [ - "source", - "check", - "object", - "artifact", - "prepare" - ], - "provisions": [ - { - "kind": "tool", - "wire": "tool" - }, - { - "kind": "host-module", - "wire": "host-module" - }, - { - "kind": "dep-dir", - "wire": "dep-dir" - } - ], - "directives": [ - { - "wire": "cxxflag", - "tag": "cxxflag", - "slot": "CxxFlags", - "scope": "PackagePrivate", - "transform": "Verbatim", - "mustExist": false, - "since": 1, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "cflag", - "tag": "cflag", - "slot": "CFlags", - "scope": "PackagePrivate", - "transform": "Verbatim", - "mustExist": false, - "since": 1, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "link-lib", - "tag": "ldflag", - "slot": "LdFlags", - "scope": "LinkGlobal", - "transform": "LibFlag", - "mustExist": false, - "since": 1, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "link-search", - "tag": "ldflag", - "slot": "LdFlags", - "scope": "LinkGlobal", - "transform": "LibSearchPath", - "mustExist": false, - "since": 1, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "cfg", - "tag": "define", - "slot": "Defines", - "scope": "PackagePrivate", - "transform": "DefinePrefix", - "mustExist": false, - "since": 1, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "generated", - "tag": "generated", - "slot": "Generated", - "scope": "SourceSet", - "transform": "Verbatim", - "mustExist": true, - "missingPrefix": "declared generated source", - "missingSuffix": "but it does not exist after the run", - "since": 1, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "source", - "tag": "source", - "slot": "Sources", - "scope": "SourceSet", - "transform": "Verbatim", - "mustExist": true, - "missingPrefix": "selected source", - "missingSuffix": "(mcpp:source=) but no such file exists", - "since": 1, - "rules": [ - "R2.1", - "R3.3", - "R3.6", - "R3.9" - ], - "docsUrl": null - }, - { - "wire": "runner", - "tag": "runner", - "slot": "Runner", - "scope": "RunGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 4, - "rules": [ - "R9.10" - ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#runner-how-the-artifact-is-executed-20268192" - }, - { - "wire": "runner-named", - "tag": "runner-named", - "slot": "NamedRunner", - "scope": "RunGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 6, - "rules": [], - "docsUrl": null - }, - { - "wire": "runner-longlived", - "tag": "runner-longlived", - "slot": "RunnerLongLived", - "scope": "RunGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 6, - "rules": [], - "docsUrl": null - }, - { - "wire": "run-exclusive", - "tag": "run-exclusive", - "slot": "RunExclusive", - "scope": "RunGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 6, - "rules": [], - "docsUrl": null - }, - { - "wire": "link-script", - "tag": "ldflag", - "slot": "LdFlags", - "scope": "LinkGlobal", - "transform": "LinkerScript", - "mustExist": false, - "since": 3, - "rules": [], - "docsUrl": null - }, - { - "wire": "link-flag", - "tag": "ldflag", - "slot": "LdFlags", - "scope": "LinkGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 8, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "include-dir", - "tag": "include-dir", - "slot": "IncludeDirs", - "scope": "PackagePrivate", - "transform": "AbsPath", - "mustExist": false, - "since": 1, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "include-dir-after", - "tag": "include-dir-after", - "slot": "IncludeDirsAfter", - "scope": "PackagePrivate", - "transform": "AbsPath", - "mustExist": false, - "since": 1, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "rerun-if-changed", - "tag": "", - "slot": "RerunFiles", - "scope": "RerunKey", - "transform": "Verbatim", - "mustExist": false, - "since": 1, - "rules": [], - "docsUrl": null - }, - { - "wire": "rerun-if-env-changed", - "tag": "", - "slot": "RerunEnv", - "scope": "RerunKey", - "transform": "Verbatim", - "mustExist": false, - "since": 1, - "rules": [], - "docsUrl": null - }, - { - "wire": "rerun-if-changed-glob", - "tag": "", - "slot": "RerunGlobs", - "scope": "RerunKey", - "transform": "Verbatim", - "mustExist": false, - "since": 2, - "rules": [], - "docsUrl": null - }, - { - "wire": "warning", - "tag": "warning", - "slot": "Warnings", - "scope": "Advisory", - "transform": "Verbatim", - "mustExist": false, - "since": 5, - "rules": [ - "R1.2", - "R2.1", - "R9.4" - ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#warning-succeeding-and-still-being-heard-20268212" - }, - { - "wire": "diagnostic", - "tag": "diagnostic", - "slot": "Diagnostics", - "scope": "Advisory", - "transform": "Verbatim", - "mustExist": false, - "since": 14, - "rules": [], - "docsUrl": null - }, - { - "wire": "action", - "tag": "action", - "slot": "Actions", - "scope": "GraphNode", - "transform": "Verbatim", - "mustExist": false, - "since": 1, - "rules": [ - "R1.1", - "R1.2", - "R1.3", - "R2.1", - "R3.1", - "R3.2", - "R3.3", - "R3.5", - "R3.7", - "R3.8", - "R3.9", - "R5.1", - "R5.3", - "R6.4", - "R9.2", - "R9.10" - ], - "docsUrl": null - }, - { - "wire": "fact", - "tag": "fact", - "slot": "Facts", - "scope": "Claim", - "transform": "Verbatim", - "mustExist": false, - "since": 7, - "rules": [ - "R2.1" - ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" - }, - { - "wire": "floor", - "tag": "floor", - "slot": "Floors", - "scope": "Claim", - "transform": "Verbatim", - "mustExist": false, - "since": 7, - "rules": [ - "R2.1" - ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#the-probe-channel-fact-floor-2026952" - }, - { - "wire": "pack-format", - "tag": "pack-format", - "slot": "PackFormats", - "scope": "Claim", - "transform": "Verbatim", - "mustExist": false, - "since": 9, - "rules": [ - "R9.10" - ], - "docsUrl": null - }, - { - "wire": "windows-subsystem", - "tag": "windows-subsystem", - "slot": "WindowsSubsystem", - "scope": "TargetLink", - "transform": "Verbatim", - "mustExist": false, - "since": 10, - "rules": [], - "docsUrl": null - }, - { - "wire": "windows-entry", - "tag": "windows-entry", - "slot": "WindowsEntry", - "scope": "TargetLink", - "transform": "Verbatim", - "mustExist": false, - "since": 10, - "rules": [], - "docsUrl": null - }, - { - "wire": "deploy", - "tag": "deploy", - "slot": "Deploy", - "scope": "LinkGlobal", - "transform": "Deploy", - "mustExist": false, - "since": 11, - "rules": [ - "R2.1", - "R4.2", - "R9.2", - "R9.5" - ], - "docsUrl": "https://github.com/mcpp-community/mcpp/blob/db3b8159480ce94df2ba7ec20c7f9e0654f97764/docs/30-build-mcpp.md#deploying-what-the-program-generated-deploy-20269123-protocol-11" - }, - { - "wire": "runtime-search-dir", - "tag": "runtime-search-dir", - "slot": "RuntimeSearchDir", - "scope": "LinkGlobal", - "transform": "AbsPath", - "mustExist": false, - "since": 12, - "rules": [ - "R2.1" - ], - "docsUrl": null - }, - { - "wire": "decision", - "tag": "decision", - "slot": "ToolDecisions", - "scope": "Advisory", - "transform": "Verbatim", - "mustExist": false, - "since": 15, - "rules": [ - "R6.5", - "R9.2" - ], - "docsUrl": null - }, - { - "wire": "xpkg-request", - "tag": "xpkg-request", - "slot": "XpkgRequests", - "scope": "Claim", - "transform": "Verbatim", - "mustExist": false, - "since": 15, - "rules": [], - "docsUrl": null - }, - { - "wire": "toolchain", - "tag": "toolchain", - "slot": "ToolchainStatement", - "scope": "Claim", - "transform": "Verbatim", - "mustExist": false, - "since": 15, - "rules": [ - "R9.2", - "R9.9" - ], - "docsUrl": null - }, - { - "wire": "link-start", - "tag": "link-start", - "slot": "LinkStart", - "scope": "LinkGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 16, - "rules": [ - "R9.2" - ], - "docsUrl": null - }, - { - "wire": "link-driver", - "tag": "link-driver", - "slot": "LinkDriver", - "scope": "LinkGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 17, - "rules": [], - "docsUrl": null - }, - { - "wire": "link-input", - "tag": "link-input", - "slot": "LinkInputs", - "scope": "LinkGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 17, - "rules": [], - "docsUrl": null - }, - { - "wire": "artifact-name", - "tag": "artifact-name", - "slot": "ArtifactNames", - "scope": "LinkGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 17, - "rules": [], - "docsUrl": null - }, - { - "wire": "seal", - "tag": "seal", - "slot": "Seal", - "scope": "LinkGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 17, - "rules": [ - "R9.2" - ], - "docsUrl": null - }, - { - "wire": "run-host", - "tag": "run-host", - "slot": "RunHost", - "scope": "RunGlobal", - "transform": "Verbatim", - "mustExist": false, - "since": 17, - "rules": [], - "docsUrl": null - } - ] -} diff --git a/mide-plugins/vscode/data/i18n/zh-cn.json b/mide-plugins/vscode/data/i18n/zh-cn.json index da98d3c..2f7dbd6 100644 --- a/mide-plugins/vscode/data/i18n/zh-cn.json +++ b/mide-plugins/vscode/data/i18n/zh-cn.json @@ -657,5 +657,6 @@ "A feature as a table": "以表形式写出的特性", "Build settings for the targets a selector matches": "选择器匹配的目标的构建设置", "Dependencies for the targets a selector matches": "选择器匹配的目标的依赖", - "Dependencies of one index namespace": "一个索引命名空间的依赖" + "Dependencies of one index namespace": "一个索引命名空间的依赖", + "The mcpp-ide backend does not run on this platform ({0}); build, run, test and the project pages go on without it.": "mcpp-ide 后端不在本平台({0})上运行;构建、运行、测试与项目页面照常可用。" } diff --git a/mide-plugins/vscode/data/toml-schema.json b/mide-plugins/vscode/data/toml-schema.json deleted file mode 100644 index fdbf1b0..0000000 --- a/mide-plugins/vscode/data/toml-schema.json +++ /dev/null @@ -1,871 +0,0 @@ -{ - "sourceVersion": "2026.10.29.1", - "sourceCommit": "db3b8159480ce94df2ba7ec20c7f9e0654f97764", - "sections": [ - { - "header": "[build]", - "name": "build", - "plane": "compile", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration", - "deprecatedBy": null, - "keys": [ - { - "key": "accel", - "type": "string", - "note": "which device backends/architectures this build targets" - }, - { - "key": "allow_host_libs", - "type": "boolean" - }, - { - "key": "bmi_schedule", - "type": "enum", - "values": [ - "auto", - "on", - "off" - ], - "default": "auto", - "note": "module-edge scheduling; \"auto\" currently means off" - }, - { - "key": "build_program_timeout", - "type": "number", - "default": 600, - "note": "seconds a build.mcpp may run; 0 = no limit" - }, - { - "key": "c_standard", - "type": "string", - "default": "c11" - }, - { - "key": "cache", - "type": "enum", - "values": [ - "global", - "local", - "off" - ], - "default": "global" - }, - { - "key": "cflags", - "type": "array" - }, - { - "key": "compile_commands", - "type": "string", - "unmodelled": true - }, - { - "key": "cxx_runtime", - "type": "enum", - "values": [ - "self-contained", - "toolchain-coupled", - "host-coupled" - ], - "default": "self-contained", - "note": "docs/20; static_stdlib is the old spelling" - }, - { - "key": "cxxflags", - "type": "array" - }, - { - "key": "default-profile", - "type": "string", - "note": "profile name, e.g. \"release\"" - }, - { - "key": "defines", - "type": "array" - }, - { - "key": "dependency_linkage", - "type": "enum", - "values": [ - "static", - "shared" - ], - "default": "static" - }, - { - "key": "dialect_cxxflags", - "type": "array", - "since": "2026.9.28.1" - }, - { - "key": "flags", - "type": "array", - "note": "array of { glob, cflags, cxxflags, asmflags, defines } tables", - "unmodelled": true - }, - { - "key": "include_dirs", - "type": "array" - }, - { - "key": "include_dirs_after", - "type": "array" - }, - { - "key": "ios_deployment_target", - "type": "string" - }, - { - "key": "jobs", - "type": "enum", - "values": [ - "auto" - ], - "note": "a positive integer is accepted as well; the type is either", - "unmodelled": true - }, - { - "key": "ldflags", - "type": "array" - }, - { - "key": "macos_deployment_target", - "type": "string" - }, - { - "key": "module_extensions", - "type": "array" - }, - { - "key": "platform-dependencies", - "type": "string", - "note": "names a platform" - }, - { - "key": "private_include_dirs", - "type": "array" - }, - { - "key": "profile", - "type": "string", - "note": "accepted alias of default-profile" - }, - { - "key": "sources", - "type": "array" - }, - { - "key": "static_stdlib", - "type": "boolean", - "legacy": true, - "note": "replaced by [build] cxx_runtime" - }, - { - "key": "std-compat-module", - "type": "string" - }, - { - "key": "std-module", - "type": "string" - }, - { - "key": "std-module-flags", - "type": "array" - }, - { - "key": "target", - "type": "string", - "note": "default target triple when no --target is passed" - } - ] - }, - { - "header": "[build-dependencies]", - "name": "build-dependencies", - "plane": "dependency", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies", - "deprecatedBy": null - }, - { - "header": "[c-abi]", - "name": "c-abi", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[c-abi-absent]", - "name": "c-abi-absent", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[capabilities]", - "name": "capabilities", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[dependencies]", - "name": "dependencies", - "plane": "dependency", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies", - "deprecatedBy": null - }, - { - "header": "[dev-dependencies]", - "name": "dev-dependencies", - "plane": "dependency", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies", - "deprecatedBy": null - }, - { - "header": "[feature-deps]", - "name": "feature-deps", - "plane": "gate", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[feature-xlings]", - "name": "feature-xlings", - "plane": "gate", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[features]", - "name": "features", - "plane": "gate", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#28-features--features", - "deprecatedBy": null - }, - { - "header": "[generated_files]", - "name": "generated_files", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[hooks]", - "name": "hooks", - "plane": "lifecycle", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#216-hooks--project-build-lifecycle-commands", - "deprecatedBy": null - }, - { - "header": "[host]", - "name": "host", - "plane": "platform", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[indices]", - "name": "indices", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[kernel-abi]", - "name": "kernel-abi", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[language]", - "name": "language", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#41-legacy-language-compatibility-layer", - "deprecatedBy": "[package].standard", - "keys": [ - { - "key": "import_std", - "type": "boolean", - "legacy": true, - "note": "replaced by [package].standard and the module scan" - }, - { - "key": "modules", - "type": "boolean", - "legacy": true, - "note": "replaced by the module scan" - }, - { - "key": "standard", - "type": "enum", - "values": [ - "c++20", - "c++23", - "c++26", - "c++2a", - "c++2c", - "gnu++20", - "gnu++23", - "gnu++26", - "c++latest", - "c++fly" - ], - "default": "c++23", - "legacy": true, - "note": "replaced by [package].standard" - } - ] - }, - { - "header": "[lib]", - "name": "lib", - "plane": "artifact", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#24-lib--library-root-module-convention", - "deprecatedBy": null, - "keys": [ - { - "key": "path", - "type": "string" - } - ] - }, - { - "header": "[modules]", - "name": "modules", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null, - "keys": [ - { - "key": "exports", - "type": "array" - }, - { - "key": "sources", - "type": "array" - }, - { - "key": "strict", - "type": "boolean" - } - ] - }, - { - "header": "[pack]", - "name": "pack", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null, - "keys": [ - { - "key": "bundle-project", - "type": "string", - "note": "table of fine-grained overrides", - "unmodelled": true - }, - { - "key": "debug_symbols", - "type": "string" - }, - { - "key": "default_mode", - "type": "enum", - "values": [ - "static", - "bundle-project", - "bundle-all" - ] - }, - { - "key": "exclude", - "type": "array" - }, - { - "key": "include", - "type": "array" - }, - { - "key": "strip", - "type": "boolean" - } - ] - }, - { - "header": "[package]", - "name": "package", - "plane": "identity", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata", - "deprecatedBy": null, - "keys": [ - { - "key": "accelerators", - "type": "array", - "note": "accelerator backends the package supports (docs/04 §2.12b)" - }, - { - "key": "authors", - "type": "array" - }, - { - "key": "c-environment", - "type": "string" - }, - { - "key": "description", - "type": "string" - }, - { - "key": "exclusive", - "type": "array" - }, - { - "key": "license", - "type": "string" - }, - { - "key": "mcpp", - "type": "string", - "since": "2026.9.28.3", - "note": "release floor, only the \">=\" form is accepted (docs/04 §2.1)" - }, - { - "key": "metadata", - "type": "string", - "note": "table keyed by tool name; mcpp keeps it and does not interpret it", - "unmodelled": true - }, - { - "key": "name", - "type": "string" - }, - { - "key": "namespace", - "type": "string" - }, - { - "key": "platforms", - "type": "enum", - "values": [ - "linux", - "macos", - "windows", - "ios", - "android", - "emscripten" - ], - "note": "array of platform names (docs/04 §2.12)" - }, - { - "key": "provides", - "type": "array" - }, - { - "key": "repo", - "type": "string" - }, - { - "key": "requires", - "type": "array" - }, - { - "key": "requires_abi", - "type": "array" - }, - { - "key": "standard", - "type": "enum", - "values": [ - "c++20", - "c++23", - "c++26", - "c++2a", - "c++2c", - "gnu++20", - "gnu++23", - "gnu++26", - "c++latest", - "c++fly" - ], - "default": "c++23", - "note": "c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too" - }, - { - "key": "std-compat-module", - "type": "string" - }, - { - "key": "std-module", - "type": "string" - }, - { - "key": "std-module-flags", - "type": "array" - }, - { - "key": "version", - "type": "string" - } - ] - }, - { - "header": "[platform]", - "name": "platform", - "plane": "platform", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[profile]", - "name": "profile", - "plane": "compile", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles", - "deprecatedBy": null, - "keys": [ - { - "key": "cflags", - "type": "array" - }, - { - "key": "cxxflags", - "type": "array" - }, - { - "key": "debug", - "type": "boolean", - "note": "-g" - }, - { - "key": "dependency_linkage", - "type": "enum", - "values": [ - "static", - "shared" - ] - }, - { - "key": "ldflags", - "type": "array" - }, - { - "key": "lto", - "type": "boolean", - "note": "-flto" - }, - { - "key": "opt", - "type": "enum", - "values": [ - "s", - "z" - ], - "note": "an -O level: a number, or \"s\"/\"z\"", - "unmodelled": true - }, - { - "key": "strip", - "type": "boolean", - "note": "-s at link time" - } - ] - }, - { - "header": "[resources]", - "name": "resources", - "plane": "metadata", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#215-resources--metadata-and-assets-embedded-in-the-artifact-2026871", - "deprecatedBy": null, - "keys": [ - { - "key": "extra-inputs", - "type": "array" - }, - { - "key": "files", - "type": "array" - }, - { - "key": "icon", - "type": "string" - }, - { - "key": "version-info", - "type": "boolean" - } - ] - }, - { - "header": "[runtime]", - "name": "runtime", - "plane": "metadata", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#211-runtime--provider-neutral-runtime-contract", - "deprecatedBy": null - }, - { - "header": "[scan_overrides]", - "name": "scan_overrides", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#283-scan_overridesglob--author-asserted-scan-results", - "deprecatedBy": null - }, - { - "header": "[target]", - "name": "target", - "plane": "condition", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#271-target--platform-conditional-dependencies--flags", - "deprecatedBy": null, - "keys": [ - { - "key": "cxx_runtime", - "type": "enum", - "values": [ - "self-contained", - "toolchain-coupled", - "host-coupled" - ] - }, - { - "key": "linkage", - "type": "enum", - "values": [ - "static", - "shared" - ] - }, - { - "key": "min_api_level", - "type": "number" - }, - { - "key": "platform", - "type": "string", - "unmodelled": true - }, - { - "key": "requires", - "type": "string", - "unmodelled": true - }, - { - "key": "run_under", - "type": "string", - "unmodelled": true - }, - { - "key": "runner", - "type": "array", - "note": "argv template for mcpp run/test on this target" - }, - { - "key": "sysroot", - "type": "string" - }, - { - "key": "toolchain", - "type": "string" - } - ] - }, - { - "header": "[targets]", - "name": "targets", - "plane": "artifact", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets", - "deprecatedBy": null, - "keys": [ - { - "key": "cflags", - "type": "array" - }, - { - "key": "cxxflags", - "type": "array" - }, - { - "key": "defines", - "type": "array" - }, - { - "key": "exports", - "type": "string", - "note": "a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)", - "unmodelled": true - }, - { - "key": "kind", - "type": "enum", - "values": [ - "bin", - "lib", - "shared", - "app" - ], - "since": "2026.9.12.3", - "note": "\"app\" requires mcpp 2026.9.12.3+; library/binary/dylib/so/shlib are accepted aliases" - }, - { - "key": "linkage", - "type": "enum", - "values": [ - "static", - "shared" - ], - "since": "2026.9.15.2" - }, - { - "key": "main", - "type": "string" - }, - { - "key": "required_features", - "type": "array" - }, - { - "key": "soname", - "type": "string" - }, - { - "key": "start", - "type": "string", - "unmodelled": true - }, - { - "key": "windows_auto_export", - "type": "string", - "unmodelled": true - }, - { - "key": "windows_code_page", - "type": "enum", - "values": [ - "utf-8", - "legacy" - ], - "since": "2026.9.26.1" - }, - { - "key": "windows_entry", - "type": "enum", - "values": [ - "main", - "wmain", - "WinMain", - "wWinMain" - ], - "since": "2026.9.12.2" - }, - { - "key": "windows_long_paths", - "type": "string", - "unmodelled": true - }, - { - "key": "windows_subsystem", - "type": "enum", - "values": [ - "console", - "windows" - ], - "since": "2026.9.12.2" - } - ] - }, - { - "header": "[test]", - "name": "test", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#217-test--where-test-programs-are", - "deprecatedBy": null, - "keys": [ - { - "key": "discover", - "type": "array", - "note": "globs whose every match is one test program" - } - ] - }, - { - "header": "[toolchain]", - "name": "toolchain", - "plane": "compile", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#27-toolchain--toolchain-configuration", - "deprecatedBy": null, - "openKeys": true, - "keys": [ - { - "key": "bootstrap", - "type": "string", - "note": "managed spec for the toolchain that builds build programs" - }, - { - "key": "default", - "type": "string", - "note": "a platform key: a managed spec or `{ path = ... }`" - }, - { - "key": "family", - "type": "enum", - "values": [ - "gcc", - "llvm", - "msvc", - "emsdk", - "android-ndk" - ], - "note": "on a `[toolchain.]` entry table; a table named by `path` accepts only \"gcc\" or \"llvm\"" - } - ] - }, - { - "header": "[tools]", - "name": "tools", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md", - "deprecatedBy": null - }, - { - "header": "[workspace]", - "name": "workspace", - "plane": "unclassified", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#218-workspace--tables-a-workspace-shares-202610101", - "deprecatedBy": null - }, - { - "header": "[xlings]", - "name": "xlings", - "plane": "tool", - "doc": "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment", - "deprecatedBy": null - } - ], - "rules": [ - { - "id": "syntax", - "severity": "error" - }, - { - "id": "unknown-section", - "severity": "warning" - }, - { - "id": "unknown-key", - "severity": "warning" - }, - { - "id": "plane-separation", - "severity": "warning" - }, - { - "id": "mcpp-floor", - "severity": "error" - }, - { - "id": "legacy-key", - "severity": "info" - }, - { - "id": "array-table", - "severity": "error" - } - ] -} diff --git a/mide-plugins/vscode/docs/architecture.md b/mide-plugins/vscode/docs/architecture.md index b7f8854..d2e32c5 100644 --- a/mide-plugins/vscode/docs/architecture.md +++ b/mide-plugins/vscode/docs/architecture.md @@ -16,8 +16,8 @@ without an Extension Host, and the parts that cannot are small. | `src/config/` | Settings registry access, validation, the settings panel, presets | `access.ts`, `panel.ts`, `migrate.ts` | | `src/cli/` | mcpp process runner, protocol probe, tasks, toolchain parsing, cache/cleanup plans, artifacts walk, self-check text | `controller.ts` | | `src/projects/` | `mcpp.toml` discovery, the `mcpp.inProject` context key, the manifest summary | no | -| `src/toml/` | Tolerant TOML parser, snapshot reader, structural completion, diagnostics | `providers.ts` | -| `src/buildscript/` | `build.mcpp` API snapshot, known modules, static analysis | `providers.ts` | +| `src/toml/` | The tolerant structure reader the project and library views read a manifest with | no | +| `src/ide/` | The session with the mcpp-ide backend (its language client: mcpp.toml's and build.mcpp's rules layer, [../../../docs/rules.md](../../../docs/rules.md)), jobs, debugging, the Testing view | `session.ts` and the views' adapters | | `src/mcppls/` | The mcppls contract, capability probe, state normaliser, bridge | `stateSource.ts` | | `src/views/` | Tree models as data, the shared tree provider, the three views | `treeProvider.ts`, `projectView.ts`, `cacheView.ts`, `languageServerView.ts` | | `src/i18n/` | Runtime string resolution (`t.ts`) and the pure resolver (`translate.ts`) | `t.ts` | @@ -32,8 +32,8 @@ A module that does not import `vscode` runs under plain `node --test`, cannot to by accident, and is the place to put a decision. The `vscode`-touching modules are exactly: `src/extension.ts`, `src/cli/controller.ts`, `src/config/{access,panel,migrate}.ts`, -`src/i18n/t.ts`, `src/mcppls/stateSource.ts`, `src/toml/providers.ts`, -`src/buildscript/providers.ts`, `src/views/{treeProvider,projectView,cacheView,languageServerView}.ts`. +`src/i18n/t.ts`, `src/mcppls/stateSource.ts`, `src/ide/*` (but `protocol.ts` and `mapping.ts`), +`src/views/{treeProvider,projectView,cacheView,languageServerView}.ts`. Two rules follow from that: @@ -42,9 +42,10 @@ Two rules follow from that: quietly add a sixth level of danger; - a `vscode`-touching module should only translate a decision into API calls, not make it. -CI also asserts the boundary that matters most: the packaged extension contains no -`vscode-languageclient` and no second language client (`test ! -e dist/src/languageClient.js` -plus a `rg` check in `.github/workflows/ci.yml`). +CI also asserts the boundary that matters most: the one language client is the session's, connected +to the mcpp-ide backend for mcpp.toml and build.mcpp, and serves no other C++ source (C++ is C++ +Modules'); it is bundled into `dist/vendor/languageclient.js`, so the package carries no +node_modules (`test/artifacts.test.ts` and `.github/workflows/ci.yml`). ## Single sources of truth @@ -54,8 +55,7 @@ plus a `rg` check in `.github/workflows/ci.yml`). | Setting labels | `package.nls.json` / `package.nls.zh-cn.json` | VS Code, at startup | | Runtime strings (Chinese) | `data/i18n/zh-cn.json` | `src/i18n/t.ts`, `tools/generate-l10n.mjs` | | Runtime strings VS Code reads | `l10n/bundle.l10n*.json` (generated, do not edit) | `vscode.l10n.t` under `mcpp.ui.language: auto` | -| `build.mcpp` API | `data/buildscript-api.json` (generated) | `src/buildscript/api.ts` | -| `mcpp.toml` shape | `data/toml-schema.json` (generated) | `src/toml/schema.ts` | +| `build.mcpp` API, `mcpp.toml` shape | the backend's `spec/buildscript-api.json`, `spec/toml-schema.json` | the backend's rules layer | | Command ids | `src/commands/ids.ts` | the manifest is held to it by `test/artifacts.test.ts` (command list, view ids, colour ids) and `test/commands/ids.test.ts` | The two generated snapshots come from an mcpp checkout and are committed, because the extension @@ -105,7 +105,6 @@ A note on the two directory names, because they look like synonyms and are not: | --- | --- | --- | | Config | `npm run check:config` (`tools/check-config.mjs`) | registry shape; that `package.json` has exactly the registry's keys with the same type, default, enum, scope, bounds and `%key%` references; that `package.nls.json` carries `.title`, `.description` and any `deprecationMessage`, matching the registry text | | Localisation | `npm run check:l10n` (`tools/l10n-check.mjs`) | every `t("…")` literal in `src/**` has a `data/i18n/zh-cn.json` entry; `package.nls.json` and `package.nls.zh-cn.json` have identical key sets; every `%key%` in `package.json` resolves | -| Snapshots | `npm run check:generated` (`tools/check-generators.mjs`) | regenerates `data/buildscript-api.json` and `data/toml-schema.json` and fails on any diff; **skips with a notice** when there is no mcpp checkout | | Docs | `npm run gen:docs` (`tools/generate-settings-docs.mjs`) | regenerates `docs/settings.md`; the stated drift check is `npm run gen:docs` then `git diff --exit-code` | | Tests | `npm test` | runs `npm run check`, compiles, then `node --test "dist/test/**/*.test.js"` | | Extension Host | `npm run test:e2e` | activates the extension against the `test/e2e` fixtures | diff --git a/mide-plugins/vscode/docs/build-script.md b/mide-plugins/vscode/docs/build-script.md deleted file mode 100644 index ee10298..0000000 --- a/mide-plugins/vscode/docs/build-script.md +++ /dev/null @@ -1,135 +0,0 @@ -# Editing `build.mcpp` - -`build.mcpp` is mcpp's build program: C++ that the engine compiles and runs to produce the -build graph. **It is an ordinary `cpp` document** (this extension declares the filename for the -`cpp` language, and C++ Modules associates `.mcpp` with `cpp` too); the module grammar -`source.cpp.mcpp-modules` is injected into `source.cpp`. - -## Who answers what - -C++ Modules (mcppls, 0.0.12 and later) compiles `build.mcpp` as the build program it is: against -the modules it really imports, `mcpp`, `std` and the plugins named in -`[build-dependencies]`, with the toolchain the build uses. Its completion, hover, navigation and -diagnostics are a compiler's. This extension adds what no compiler says: - -| | C++ Modules serves the file | C++ Modules is disabled or switched off here | -| --- | --- | --- | -| SPEC-007 rule checks (six of the seven rules below) | this extension | this extension | -| `unknownSymbol` | off — the compiler's answer replaces it | this extension, import-aware | -| `mcpp::…` directive completion and hover, snippets | this extension | this extension | -| Module names after `import` (completion, hover) | C++ Modules | this extension | - -"Serves" is read without asking anything of the language server: the C++ Modules extension is -installed and enabled, and `mcppls.enable` is not `false` for the workspace. When that changes -(the extension is enabled, disabled, or switched off for the workspace) the open `build.mcpp` -files are re-checked at once. - -Up to 0.6.0 this extension gave `build.mcpp` a language id of its own, `mcpp-build`. VS Code -prefers a filename match over an extension match, so that hid the file from C++ Modules, which -selects `cpp` documents: the build program got a line-by-line text check instead of a compiler, -and `import mcpp.rules.qt;` followed by `mcpp::rules::qt::…` was reported as an unknown name. -A setting written for that language (`"[mcpp-build]": { … }`) no longer applies; write it under -`"[cpp]"`. - -## What this extension provides - -All of it comes from the generated snapshot (`data/buildscript-api.json`) or from -`src/buildscript/modules.ts`; nothing executes a compiler or a build program, so the providers -work in an untrusted workspace. - -### Completion - -Registered for `cpp` documents named `build.mcpp`, in `src/buildscript/providers.ts`. - -- After `mcpp::` (or `mcpp::pre`): directive names, from the 31 entries in the snapshot's - `directives` array, with `-` rewritten to `_` (so the wire name `link-flag` offers - `link_flag`). Kind: function. -- After `import ` or `export import `, only while C++ Modules does not serve the file: the - concrete module names from the known-module table (the `prefix.*` patterns are not offered as - literal text). Kind: module, with the module's description as documentation. - -### Hover - -- On a known module name in an `import`, only while C++ Modules does not serve the file: the - module name and its description (for a `prefix.*` match, the concrete name the user wrote is - shown). -- On `mcpp::`: the wire spelling (`mcpp:=`), `slot`, `scope`, `since protocol`, the - SPEC-007 rules and a link into mcpp's `docs/30-build-mcpp.md`. - -### Known modules — never reported missing - -`KNOWN_MODULES` in `src/buildscript/modules.ts` recognises `std`, `std.compat`, `mcpp`, -`mcpp.core` exactly, and `mcpp.plugins.*`, `mcpp.deps.*`, `mcpp.rules.*`, `mcpp.dist.*`, -`mcpp.tools.*` by prefix. The rule is absolute: **we never report "module not found"**. The -table only *recognises*; it never validates, because a build program's imports are resolved by -mcpp's own module graph, not by clangd. A third-party `mcpp..*` name is deliberately -not claimed: it is indistinguishable from a filename, and a missed hover is cheaper than a -wrong accusation. - -The cost is honest: there is **no symbol-level completion, hover or navigation inside `std` or -`mcpp`** here. Knowing the module name is not the same as knowing its API. - -### Diagnostics - -Seven rules, from `analyseBuildScript` in `src/buildscript/analysis.ts`. `code` is the -diagnostic's `code`; `source` is `mcpp`. - -| Code | What it flags | SPEC-007 | -| --- | --- | --- | -| `mcpp.buildscript.unknownSymbol` | An `mcpp::` that is not a directive, an action role or a typed-API name in the snapshot, and not a namespace the file imports (`import mcpp.rules.qt;` covers `mcpp::rules::…`). Only while C++ Modules does not serve the file | §2 | -| `mcpp.buildscript.roleLiteral` | `role = ""`; use `mcpp::roles::` so an engine that does not know it refuses to compile | R3.6 | -| `mcpp.buildscript.prepareNeedsOutputDir` | A `prepare` action with no `output_dir(…)` | R3.3 | -| `mcpp.buildscript.rpathInLinkFlag` | `-Wl,-rpath` in `link_flag`; use `mcpp::runtime_search_dir(…)` | R4.4 | -| `mcpp.buildscript.rawPathFlag` | `-I…` / `-L…` in `cxxflag` / `cflag` / `link_flag`; use `include_dir`, `include_dir_after`, `link_search` | R2.1 | -| `mcpp.buildscript.shellSyntaxInAction` | `NAME=value cmd` or `cd x &&` inside an action command; use `env(…)` and `cwd(…)` | R3.8 | -| `mcpp.buildscript.actionNeedsRole` | An `mcpp::action` that declares no role, or one of the five roles spelled as an unknown string | R3.2, R3.6 | - -The five roles are `source`, `check`, `object`, `artifact`, `prepare`. Both the typed surface -(`mcpp::action`, `.output_dir(…)`, `.submit()`, `.arg()` / `.command()`) and the frozen raw -payload (`mcpp:action={…}`) are covered. - -`mcpp.buildScript.diagnostics` (default `true`) turns the collection on or off; -`mcpp.buildScript.diagnostics.severity` (`warning` | `info` | `off`, default `warning`) picks -**one** severity for the whole file — the analyser does not rank rules against each other. - -## Limitations, stated plainly - -The analyser is **line-oriented, not a parser**. It has no preprocessor, no macro expansion, no -`#include` graph, no type information, and it does not evaluate a condition that guards a call. -A string built by concatenation or formatting is invisible. The one cross-line heuristic is the -action block: from an `mcpp::action` line forward to `.submit(`, at most 40 lines; a role or -`output_dir` set outside that window is not seen. Positions are exact for the spellings the -rules look for and approximate (the whole string literal) when escapes make the offset -ambiguous. Every rule is conservative on purpose: when a spelling is uncertain it says nothing. - -Comments and string literals are masked before matching, so a rule never fires on a comment, and -the raw-string and block-comment state carries across lines. - -## The snapshot - -`data/buildscript-api.json` this build ships: - -| Field | Value | -| --- | --- | -| `sourceVersion` | `2026.10.11.1` | -| `sourceCommit` | `3fb2d0cc` | -| `protocolVersion` | `15` | -| `cacheEpoch` | `3` | -| `roles` | `source`, `check`, `object`, `artifact`, `prepare` | -| `directives` | 31 entries (wire, tag, slot, scope, transform, `must`, `missingPrefix`, `missingSuffix`, `sinceProtocol`, rules, docs URL) | - -`src/buildscript/api.ts` reads it and exposes `API` and `directive(wire)`. It is generated by -`tools/generate-buildscript-api.mjs` from an mcpp checkout (`MCPP_REPO`, default `../mcpp`) and -committed; `npm run check:generated` fails on drift. - -## Settings that shape this page - -| Key | Default | What it does | -| --- | --- | --- | -| `mcpp.buildScript.intelligence` | `true` | the master switch: off means no completion, hover or snippets for `build.mcpp`, and diagnostics stop too | -| `mcpp.buildScript.diagnostics` | `true` | the seven SPEC-007 diagnostics | -| `mcpp.buildScript.diagnostics.severity` | `warning` | `warning`, `info`, or `off` | -| `mcpp.buildScript.snippets` | `true` | the `mcpp::…` snippet catalogue; independent of completion and hover | -| `mcpp.buildScript.imports.knownModules` | `true` | whether `std`, `std.compat` and `mcpp.*` appear in **import completion**. Hover still recognises them and no import is ever reported missing, whichever way this is set | - -Full descriptions, scopes and "when it applies": [settings.md](settings.md). diff --git a/mide-plugins/vscode/docs/mcpp-toml.md b/mide-plugins/vscode/docs/mcpp-toml.md deleted file mode 100644 index cf2fa0d..0000000 --- a/mide-plugins/vscode/docs/mcpp-toml.md +++ /dev/null @@ -1,134 +0,0 @@ -# Editing `mcpp.toml` - -The file named exactly `mcpp.toml` gets its own language id, `mcpp-toml` (see -`contributes.languages` in `package.json`), and two features: **structural completion** and -**structural diagnostics**. Both are pure text analysis — no `mcpp` process, no language server -— so they keep working in an untrusted workspace, which is what -`capabilities.untrustedWorkspaces.supported: "limited"` promises. - -Formatting is **deliberately not offered**: no `DocumentFormattingEditProvider` or -`DocumentRangeFormattingEditProvider` is registered. This extension does not rewrite a file the -user owns. - -## Completion - -Registered in `src/toml/providers.ts` as a completion provider for `mcpp-toml`, triggered by -`[`. The suggestions are computed by `src/toml/completion.ts` from a tolerant parse of the -lines above the cursor (`contextAt` in `src/toml/parser.ts`). - -| Cursor is | You get | -| --- | --- | -| On a section header | The 25 structural headers from the hand-maintained `SECTION_HEADERS` list | -| In a dependency section | The five dependency templates (`"version"`, `{ path = … }`, `{ git = …, tag = … }`, `{ version = …, features = […] }`, `{ version = …, tools = […] }`) | -| In `[features]` | The four feature templates (`[…]`, `{ defines = […] }`, `{ requires = […] }`, `{ sources = […] }`) | -| In `[generated_files]`, `[capabilities]`, `[xlings.workspace]`, `[tools.overrides]` | That section's writing template | -| Inside `[[…]]` | Nothing, on purpose — array tables are not mcpp's surface | -| In a value position | Nothing: values are not guessed | - -What is **not** offered, in this build: - -- **Key completion** inside a known section, and **enum value completion** (`standard`, `kind`, - `linkage`, profile names, `[target.]` selectors); -- **dependency version completion**. `mcpp.toml.indexCompletion` (default `false`) and - `mcpp.toml.indexCompletionTimeoutSeconds` (default `20`) are declared in the registry, but no - code reads them and nothing invokes `mcpp search`. It is designed, not implemented. - -The section-header list is **not** derived from the snapshot below — it is a separate, -hand-maintained table. The two can drift; the snapshot is what the diagnostics use. - -`mcpp.toml.completion` (default `true`) turns completion off. The old key -`mcpp.tomlCompletion` is a **deprecated alias** of `mcpp.toml.completion`: the current key is what -the extension reads, and the old one is kept so 0.4.x settings keep working. - -## Hover and navigation - -`mcpp.toml.hover` (default `true`) and `mcpp.toml.navigation` (default `true`) each switch off -their provider independently. - -Hovering a **section header** shows its name, plane, whether it is legacy (and what replaces it), -its key count and a link to the manifest reference. Hovering a **key** shows its type, enum -values, default and `since`, plus the `note` and `unmodelled` caveats. Hovering an **enum value** -shows the value and its key's documentation. Everywhere else the hover is empty rather than -guessed — including on the bare opening `[`, which the tolerant parser reads as a key slot. - -Go-to-definition covers three shapes, all of them local to the manifest: - -| From | To | -| --- | --- | -| `workspace = true` in a dependency | the same key in `[workspace.dependencies]` | -| `path = "…"` in a dependency | `[package]` in that directory's `mcpp.toml`, when the file exists | -| an element of `features = […]` | that element's `[features.]` header | - -`path` navigation is limited to dependency tables, so `[lib].path` and an `[indices]` `path` are -ignored. The file system belongs to the caller: `definitionAt` takes a callback that resolves a -relative path to a line index, so the analysis stays pure and testable. - -## Diagnostics - -`analyseManifest` in `src/toml/diagnostics.ts` is a **conservative TOML subset scanner**, not a -validator. It understands section headers, `key = value` lines, line comments, single- and -double-quoted strings, triple-quoted multi-line strings and arrays/inline tables that span -lines. Anything outside that subset is not reported — a missed diagnostic is preferred to a -wrong one. - -Seven rules, each with a stable `code` (the diagnostic's `code`; `source` is `mcpp`): - -| Rule | Code | Default severity | Setting | -| --- | --- | --- | --- | -| Obvious syntax breakage | `mcpp.manifest.syntax` | `error` | `mcpp.toml.diagnostics.syntax` | -| Section not in the snapshot | `mcpp.manifest.unknownSection` | `warning` | `mcpp.toml.diagnostics.unknownSection` | -| Key not in the snapshot for a known section | `mcpp.manifest.unknownKey` | `warning` | `mcpp.toml.diagnostics.unknownKey` | -| Key in the wrong plane (tool as dependency, package as tool) | `mcpp.manifest.planeSeparation` | `warning` | `mcpp.toml.diagnostics.planeSeparation` | -| `[package].mcpp` is not `">="` | `mcpp.manifest.mcppFloor` | `error` | none — fixed | -| Legacy section or key | `mcpp.manifest.legacyKey` | `info` | `mcpp.toml.diagnostics.legacyKeys` | -| Array table `[[…]]` | `mcpp.manifest.arrayTable` | `error` | none — fixed | - -Every severity setting takes `error`, `warning`, `info` or `off`. `mcpp.toml.diagnostics.enabled` -(default `true`) is the master switch: when it is off, the collection is cleared. Diagnostics are -recomputed on open, on change, on close and on any configuration change. - -The legacy markers themselves come from the snapshot: `[language]` is marked `deprecatedBy: -"[package].standard"`, and `build.static_stdlib`, `language.import_std`, `language.modules` and -`language.standard` are marked `legacy`. - -`mcpp.floor` and array tables have no setting yet (see the comment in -`src/toml/diagnostics.ts`): they are always `error`. If the setting surface gains them, the -signature already accepts the fields. - -## The snapshot - -`data/toml-schema.json` is the single source of the section/key/enum vocabulary. This build -ships: - -- `sourceVersion`: `2026.10.1.3` -- `sourceCommit`: `4d81d062` -- 30 sections across the manifest planes, with per-key `type`, `values`, `default`, `since`, - `legacy`, `note`, `unmodelled` and a `doc` URL into mcpp's `docs/04-mcpp-toml.md` -- 7 rules with their default severities - -`src/toml/schema.ts` reads it and exposes `sectionByHeader`, `sectionByName`, `keyOf` and -`schemaSource()`. `sectionProblems()` checks the snapshot's own shape (duplicate header, empty -plane, a `deprecatedBy` that names no known section) and is asserted by the tests. - -It is generated by `tools/generate-toml-schema.mjs` from an mcpp checkout -(`MCPP_REPO`, default `../mcpp`) and committed, so the extension builds without that checkout. -`npm run check:generated` (`tools/check-generators.mjs`) regenerates both snapshots and fails on -any diff; with no checkout present it skips with a notice rather than passing silently. - -## Settings that shape this page - -| Key | Default | Read by this build | -| --- | --- | --- | -| `mcpp.toml.completion` | `true` | yes — `src/extension.ts` | -| `mcpp.toml.diagnostics.enabled` | `true` | yes | -| `mcpp.toml.diagnostics.syntax` | `error` | yes | -| `mcpp.toml.diagnostics.unknownSection` | `warning` | yes | -| `mcpp.toml.diagnostics.unknownKey` | `warning` | yes | -| `mcpp.toml.diagnostics.planeSeparation` | `warning` | yes | -| `mcpp.toml.diagnostics.legacyKeys` | `info` | yes | -| `mcpp.toml.hover` | `true` | yes — switches the hover provider off | -| `mcpp.toml.navigation` | `true` | yes — switches go-to-definition off | -| `mcpp.toml.indexCompletion` | `false` | yes — **off by default**: it runs `mcpp search`, which may use the network | -| `mcpp.toml.indexCompletionTimeoutSeconds` | `20` | yes — the completion promise returns within this, whatever the process does | - -Full descriptions, scopes and "when it applies": [settings.md](settings.md). diff --git a/mide-plugins/vscode/package-lock.json b/mide-plugins/vscode/package-lock.json index a2b06c1..8209f00 100644 --- a/mide-plugins/vscode/package-lock.json +++ b/mide-plugins/vscode/package-lock.json @@ -22,7 +22,8 @@ "mocha": "^11.8.0", "ovsx": "^1.2.0", "playwright": "1.64.1", - "typescript": "^5.9.0" + "typescript": "^5.9.0", + "vscode-languageclient": "9.0.1" }, "engines": { "vscode": "^1.91.0" @@ -6538,6 +6539,79 @@ "url": "https://bevry.me/fund" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json index 078f99a..417a9a5 100644 --- a/mide-plugins/vscode/package.json +++ b/mide-plugins/vscode/package.json @@ -1425,7 +1425,7 @@ }, "scripts": { "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", - "compile": "tsc -p tsconfig.json && node tools/copy-webview-assets.mjs", + "compile": "tsc -p tsconfig.json && node tools/copy-webview-assets.mjs && node tools/bundle-languageclient.mjs", "test": "npm run check && npm run clean && npm run compile && node --test \"dist/test/**/*.test.js\"", "package": "npm run clean && npm run compile && npm run stage:backend && vsce package", "test:e2e": "npm run compile && node dist/test/e2e/runTest.js", @@ -1434,13 +1434,10 @@ "gen:docs": "node tools/generate-settings-docs.mjs", "check:config": "node tools/check-config.mjs", "check:l10n": "node tools/l10n-check.mjs", - "check": "npm run check:config && npm run check:l10n && npm run check:icon && npm run check:generated", - "gen:buildscript": "node tools/generate-buildscript-api.mjs", - "gen:toml": "node tools/generate-toml-schema.mjs", + "check": "npm run check:config && npm run check:l10n && npm run check:icon", "gen:icon": "node tools/generate-activitybar-icon.mjs", "gen:menuicons": "node tools/generate-quick-menu-icons.mjs", "check:icon": "node tools/generate-activitybar-icon.mjs --check && node tools/generate-quick-menu-icons.mjs --check", - "check:generated": "node tools/check-generators.mjs", "test:e2e:one": "node dist/test/e2e/runTest.js", "gen:examples": "node tools/generate-library-examples.mjs", "test:graphs-ui": "npm run compile && node tools/test-graphs-ui.mjs", @@ -1460,6 +1457,7 @@ "@vscode/codicons": "0.0.46-24", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "vscode-languageclient": "9.0.1", "buffer": "^6.0.3", "esbuild": "^0.25.12", "mocha": "^11.8.0", diff --git a/mide-plugins/vscode/src/buildscript/analysis.ts b/mide-plugins/vscode/src/buildscript/analysis.ts deleted file mode 100644 index c2d15e2..0000000 --- a/mide-plugins/vscode/src/buildscript/analysis.ts +++ /dev/null @@ -1,701 +0,0 @@ -/** - * Static analysis for `build.mcpp`, built on the snapshot in `api.ts` and on - * nothing else. No compiler runs, nothing is executed, no workspace is read, so - * this is usable in an untrusted workspace and cannot disagree with the build - * because of a stale compilation database. - * - * ## What this deliberately cannot see - * - * It is **line-oriented, not a parser**. There is no preprocessor, no macro - * expansion, no `#include` graph, no type information, and no evaluation of a - * condition that guards a call. A string built by concatenation or format is - * invisible. Positions are exact for the spellings below and approximate (the - * whole string literal) when escapes make the offset ambiguous. - * - * The one cross-line heuristic is the action block: from a `mcpp::action` line - * forward to `.submit(`, up to 40 lines. A role or `output_dir` set outside that - * window is not seen. - * - * Every rule is kept conservative on purpose: a false positive erodes trust - * faster than a missed hint. When a spelling is uncertain — a role held in a - * variable, a flag built at run time, a payload that does not parse — the rule - * says nothing rather than guessing. - * - * ## Positions - * - * `line` is 1-based, and `startCharacter` / `endCharacter` are 1-based columns - * (the first character of a line is column 1), so a diagnostic prints directly - * as `file:line:column`. - * - * ## Severity - * - * The engine does not distinguish here; the caller's setting picks one severity - * for the whole file (`mcpp.buildScript.diagnostics.severity`). - */ -import { ACTION_ROLES, API } from "./api"; - -export type Severity = "error" | "warning" | "info" | "off"; - -export interface BuildScriptDiagnostic { - code: string; - message: string; - severity: Exclude; - /** 1-based line number. */ - line: number; - /** 1-based column of the first character. */ - startCharacter: number; - /** 1-based column one past the last character. */ - endCharacter: number; -} - -/** The seven rule ids, as language-server code strings. */ -export const DIAGNOSTIC_CODES = { - unknownSymbol: "mcpp.buildscript.unknownSymbol", - roleLiteral: "mcpp.buildscript.roleLiteral", - prepareNeedsOutputDir: "mcpp.buildscript.prepareNeedsOutputDir", - rpathInLinkFlag: "mcpp.buildscript.rpathInLinkFlag", - rawPathFlag: "mcpp.buildscript.rawPathFlag", - shellSyntaxInAction: "mcpp.buildscript.shellSyntaxInAction", - actionNeedsRole: "mcpp.buildscript.actionNeedsRole", -} as const; - -/** - * The typed-API names `docs/30-build-mcpp.md` lists (its `import mcpp;` table, - * the accessor tables, and the `mcpp::action` members). Used only to *not* - * flag a `mcpp::` name: when a name here is wrong the cost is a missed hint, - * never a false one, which is why the list errs towards inclusion. Directive - * wire names are added from the snapshot separately (`link-lib` -> `link_lib`), - * so a directive that reaches the typed surface is known without being listed. - */ -export const TYPED_API_NAMES: readonly string[] = [ - "abi_tool", - "accel", - "action", - "cflag", - "cxx_runtime", - "cxx_stdlib", - "cxxflag", - "decision", - "define", - "dep_bin", - "dep_dir", - "dep_linkage", - "deploy", - "device_sources", - "fact", - "floor", - "generated", - "graph_file", - "has_feature", - "host", - "include_dir", - "include_dir_after", - "link_flag", - "link_lib", - "link_script", - "link_search", - "manifest_dir", - "min_platform_version", - "msvc_crt_linkage", - "msvc_instance_dir", - "ninja_program", - "out_dir", - "pack_debug_symbols_dir", - "pack_format", - "pack_stage_dir", - "pack_strip", - "package_authors", - "package_description", - "package_license", - "package_repo", - "package_version", - "phase", - "pkg_config_libdir", - "plugins", - "profile", - "provision", - "provides_pack_format", - "report", - "rerun_if_changed", - "rerun_if_changed_glob", - "rerun_if_env_changed", - "roles", - "runner", - "runtime_search_dir", - "source", - "stage_dir", - "sysroot_dir", - "target", - "target_arch", - "target_env", - "target_os", - "tool", - "tool_env", - "toolchain", - "toolchain_binutils_dir", - "toolchain_dir", - "toolchain_sysroot", - "tools", - "toolset_identity", - "warning", - "windows_entry", - "windows_subsystem", - "xpkg_dir", - "xpkg_pending", - "xpkg_program", - "xpkg_request", - "xpkg_source", -]; - -/** The five action roles. Constant, so a malformed snapshot cannot disable a rule. */ -const ROLE_NAMES: ReadonlySet = new Set(ACTION_ROLES); - -const KNOWN_SYMBOLS: ReadonlySet = (() => { - const names = new Set(TYPED_API_NAMES); - for (const role of ACTION_ROLES) { - names.add(role); - } - for (const entry of API.directives) { - names.add(entry.wire); - names.add(entry.wire.replace(/-/g, "_")); - } - return names; -})(); - -/** How far past `mcpp::action` a role or `output_dir` may appear. */ -const ACTION_WINDOW = 40; - -const RAWPATH = /(?:^|\s)(-[IL]\S*)/; -const RPATH = "-Wl,-rpath"; -const SHELL_ENV = /(?:^|\s)([A-Za-z_][A-Za-z0-9_]*=\S*)/; -const SHELL_CD = /(?:^|\s)(cd\s+\S+\s*&&)/; -const IMPORT_ACTION = "mcpp:action="; - -interface StringLiteral { - /** The characters the literal denotes. */ - value: string; - /** 0-based column of the opening quote. */ - start: number; - /** 0-based column one past the closing quote. */ - end: number; - /** The source text including quotes, escape sequences intact. */ - raw: string; -} - -interface MaskedLine { - /** The source line with comment and string-literal characters blanked. */ - code: string; - strings: StringLiteral[]; -} - -interface ScanState { - blockComment: boolean; - rawDelim: string | null; -} - -function unescape(raw: string): string { - let out = ""; - for (let i = 0; i < raw.length; i += 1) { - if (raw[i] !== "\\") { - out += raw[i]; - continue; - } - const next = raw[i + 1]; - if (next === "n") out += "\n"; - else if (next === "t") out += "\t"; - else if (next === "r") out += "\r"; - else if (next === undefined) out += "\\"; - else out += next; - i += 1; - } - return out; -} - -/** - * Split one line into real code plus the string literals it contains: every - * character inside a comment or a literal becomes a space in `code`, so a match - * on `code` is a match on code. Indices are preserved. `state` carries the two - * constructs that outlive a line, a block comment and a raw string. - */ -function maskLine(line: string, state: ScanState): MaskedLine { - const out = line.split(""); - const strings: StringLiteral[] = []; - const blank = (from: number, to: number): void => { - for (let i = Math.max(0, from); i < Math.min(to, line.length); i += 1) { - out[i] = " "; - } - }; - - let i = 0; - if (state.rawDelim !== null) { - const terminator = `)${state.rawDelim}"`; - const end = line.indexOf(terminator); - if (end < 0) { - return { code: " ".repeat(line.length), strings }; - } - blank(0, end + terminator.length); - i = end + terminator.length; - state.rawDelim = null; - } - - while (i < line.length) { - if (state.blockComment) { - const end = line.indexOf("*/", i); - if (end < 0) { - blank(i, line.length); - break; - } - blank(i, end + 2); - i = end + 2; - state.blockComment = false; - continue; - } - const pair = line.slice(i, i + 2); - if (pair === "//") { - blank(i, line.length); - break; - } - if (pair === "/*") { - state.blockComment = true; - i += 2; - continue; - } - if (line[i] === "R" && line[i + 1] === '"') { - const open = /^R"([^(\s]{0,16})\(/.exec(line.slice(i)); - if (open !== null) { - const terminator = `)${open[1]}"`; - const end = line.indexOf(terminator, i + open[0].length); - if (end < 0) { - state.rawDelim = open[1]; - blank(i, line.length); - break; - } - blank(i, end + terminator.length); - i = end + terminator.length; - continue; - } - } - if (line[i] === "'" && /[A-Za-z0-9_]/.test(line[i - 1] ?? "")) { - // A `'` right after an identifier character is a digit separator (1'000), - // not the start of a character literal; scanning for a closing quote would - // swallow the rest of the line. - i += 1; - continue; - } - if (line[i] === '"' || line[i] === "'") { - const quote = line[i]; - let j = i + 1; - while (j < line.length) { - if (line[j] === "\\") { - j += 2; - continue; - } - if (line[j] === quote) { - j += 1; - break; - } - j += 1; - } - const stop = Math.min(j, line.length); - strings.push({ - value: unescape(line.slice(i + 1, Math.max(i + 1, stop - 1))), - start: i, - end: stop, - raw: line.slice(i, stop), - }); - blank(i, stop); - i = stop; - continue; - } - i += 1; - } - - return { code: out.join(""), strings }; -} - -/** The narrowest honest span for `needle` inside a literal, or the literal itself. */ -function literalSpan(literal: StringLiteral, needle: string): { start: number; end: number } { - if (needle.length > 0) { - const at = literal.raw.indexOf(needle); - if (at >= 0) { - return { start: literal.start + at, end: literal.start + at + needle.length }; - } - } - return { start: literal.start, end: literal.end }; -} - -interface RoleSite { - text: string; - literal: boolean; - line: number; - start: number; - end: number; -} - -interface ActionBlock { - line: number; - actionStart: number; - actionEnd: number; - role: RoleSite | null; - hasRoleField: boolean; - hasOutputDir: boolean; -} - -/** `mcpp::action` … `.submit()` windows, with the role and `output_dir` they declare. */ -function findActionBlocks(masked: readonly MaskedLine[]): ActionBlock[] { - const blocks: ActionBlock[] = []; - for (let i = 0; i < masked.length; i += 1) { - const token = /\bmcpp::action\b/.exec(masked[i].code); - if (token === null) { - continue; - } - const limit = Math.min(masked.length - 1, i + ACTION_WINDOW); - let end = limit; - for (let j = i; j <= limit; j += 1) { - if (/\.submit\s*\(/.test(masked[j].code)) { - end = j; - break; - } - } - const block: ActionBlock = { - line: i, - actionStart: token.index, - actionEnd: token.index + token[0].length, - role: null, - hasRoleField: false, - hasOutputDir: false, - }; - for (let j = i; j <= end; j += 1) { - const { code, strings } = masked[j]; - if (!block.hasOutputDir && /\.output_dir\s*\(/.test(code)) { - block.hasOutputDir = true; - } - const constant = /mcpp::roles::([A-Za-z_]\w*)/.exec(code); - if (constant !== null && block.role === null) { - block.hasRoleField = true; - block.role = { - text: constant[1], - literal: false, - line: j, - start: constant.index, - end: constant.index + constant[0].length, - }; - } - if (block.role === null) { - const assign = /\brole\s*=/.exec(code); - if (assign !== null) { - block.hasRoleField = true; - const after = assign.index + assign[0].length; - const literal = strings.find( - (entry) => entry.start >= after && code.slice(after, entry.start).trim() === "", - ); - if (literal !== undefined) { - block.role = { - text: literal.value, - literal: true, - line: j, - start: literal.start, - end: literal.end, - }; - } - } - } - } - blocks.push(block); - } - return blocks; -} - -/** - * Every diagnostic the text earns, all at `severity`, sorted by position. - * - * `lines` are the file's lines without their terminators (`text.split(/\r?\n/)`). - */ -export interface AnalysisOptions { - /** - * Whether rule 1 (`unknownSymbol`) runs. Off while C++ Modules (mcppls) serves - * `build.mcpp`: it compiles the file against the modules it really imports, - * so its answer replaces this guess. Default on. - */ - unknownSymbols?: boolean; -} - -/** - * The namespaces under `mcpp::` that the file's own imports bring in: - * `import mcpp.rules.qt;` makes `mcpp::rules::…` a name this snapshot cannot - * judge, so rule 1 leaves it alone. Comments and strings are already masked. - */ -function importedNamespaces(masked: readonly MaskedLine[]): Set { - const namespaces = new Set(); - const imported = /\bimport\s+mcpp\.([A-Za-z_][A-Za-z0-9_]*)/g; - for (const line of masked) { - for (let match = imported.exec(line.code); match !== null; match = imported.exec(line.code)) { - namespaces.add(match[1]); - } - } - return namespaces; -} - -export function analyseBuildScript( - lines: readonly string[], - severity: Exclude, - options: AnalysisOptions = {}, -): BuildScriptDiagnostic[] { - const masked: MaskedLine[] = []; - const state: ScanState = { blockComment: false, rawDelim: null }; - for (const line of lines) { - masked.push(maskLine(line, state)); - } - - const found: BuildScriptDiagnostic[] = []; - const report = (code: string, message: string, line: number, start: number, end: number): void => { - found.push({ - code, - message, - severity, - line: line + 1, - startCharacter: start + 1, - endCharacter: end + 1, - }); - }; - - // 1. `mcpp::` that the engine does not have, and no import of the file brings in. - const imported = options.unknownSymbols === false ? undefined : importedNamespaces(masked); - for (let i = 0; imported !== undefined && i < masked.length; i += 1) { - const symbols = /\bmcpp::([A-Za-z_][A-Za-z0-9_]*)/g; - for (let match = symbols.exec(masked[i].code); match !== null; match = symbols.exec(masked[i].code)) { - if (KNOWN_SYMBOLS.has(match[1]) || imported.has(match[1])) { - continue; - } - report( - DIAGNOSTIC_CODES.unknownSymbol, - `unknown mcpp::${match[1]}: not a directive, action role or typed-API name in the ` + - `mcpp build-script snapshot (SPEC-007 §2; docs/30-build-mcpp.md)`, - i, - match.index, - match.index + match[0].length, - ); - } - } - - // 2. `role = ""` where the engine's constant exists (R3.6). - const blocks = findActionBlocks(masked); - for (let i = 0; i < masked.length; i += 1) { - const assign = /\brole\s*=/g; - for (let match = assign.exec(masked[i].code); match !== null; match = assign.exec(masked[i].code)) { - const after = match.index + match[0].length; - const literal = masked[i].strings.find( - (entry) => entry.start >= after && masked[i].code.slice(after, entry.start).trim() === "", - ); - if (literal === undefined) { - continue; - } - if (ROLE_NAMES.has(literal.value)) { - report( - DIAGNOSTIC_CODES.roleLiteral, - `role is spelled as the string "${literal.value}"; use mcpp::roles::${literal.value} ` + - `so an engine that does not know the role refuses to compile it (SPEC-007 R3.6)`, - i, - literal.start, - literal.end, - ); - } else { - report( - DIAGNOSTIC_CODES.actionNeedsRole, - `unknown action role "${literal.value}"; the five roles are ` + - `${ACTION_ROLES.join(", ")} (SPEC-007 R3.6)`, - i, - literal.start, - literal.end, - ); - } - } - } - - // 3 + 7 (typed form). A block with a role this pass already reported (an unknown - // literal) is not reported twice; a block whose role is a variable is not - // reported at all. - for (const block of blocks) { - if (!block.hasRoleField) { - report( - DIAGNOSTIC_CODES.actionNeedsRole, - "this mcpp::action declares no role; name one of " + - `${ACTION_ROLES.join(", ")} (mcpp::roles::…) — a missing role is read as "source" ` + - `(SPEC-007 R3.2, R3.6)`, - block.line, - block.actionStart, - block.actionEnd, - ); - continue; - } - if (block.role === null || block.role.text !== "prepare" || block.hasOutputDir) { - continue; - } - report( - DIAGNOSTIC_CODES.prepareNeedsOutputDir, - "a prepare action must declare the directory its command fills with output_dir(…): " + - "the engine refuses an action without one (SPEC-007 R3.3)", - block.role.line, - block.role.start, - block.role.end, - ); - } - - // 3 + 6 + 7 (raw `mcpp:action=` payload; the frozen printf surface). A payload - // that does not parse is skipped rather than guessed at, and a role spelled as - // a string is *not* rule 2 here: docs/30 explicitly allows the string on this - // surface. - for (let i = 0; i < masked.length; i += 1) { - for (const literal of masked[i].strings) { - const at = literal.value.indexOf(IMPORT_ACTION); - if (at < 0) { - continue; - } - let payload: unknown; - try { - payload = JSON.parse(literal.value.slice(at + IMPORT_ACTION.length).trim()); - } catch { - continue; - } - if (typeof payload !== "object" || payload === null || Array.isArray(payload)) { - continue; - } - const record = payload as Record; - const role = record.role; - if (typeof role !== "string") { - report( - DIAGNOSTIC_CODES.actionNeedsRole, - `this mcpp:action payload declares no role; the five roles are ` + - `${ACTION_ROLES.join(", ")} (SPEC-007 R3.6)`, - i, - literal.start, - literal.end, - ); - } else if (!ROLE_NAMES.has(role)) { - report( - DIAGNOSTIC_CODES.actionNeedsRole, - `unknown action role "${role}"; the five roles are ${ACTION_ROLES.join(", ")} ` + - `(SPEC-007 R3.6)`, - i, - literal.start, - literal.end, - ); - } else if (role === "prepare" && typeof record.output_dir !== "string") { - report( - DIAGNOSTIC_CODES.prepareNeedsOutputDir, - "a prepare action must declare output_dir: the engine refuses an action without " + - "one (SPEC-007 R3.3)", - i, - literal.start, - literal.end, - ); - } - const command = record.command; - if (Array.isArray(command)) { - for (const entry of command) { - if (typeof entry !== "string") { - continue; - } - const needle = /(?:^|\s)([A-Za-z_][A-Za-z0-9_]*=\S*)/.exec(entry)?.[1] - ?? /(?:^|\s)(cd\s+\S+\s*&&)/.exec(entry)?.[1]; - if (needle !== undefined) { - report( - DIAGNOSTIC_CODES.shellSyntaxInAction, - `the command spells shell syntax ("${needle}"); declare it with env(…) and ` + - `cwd(…) instead (SPEC-007 R3.8)`, - i, - literal.start, - literal.end, - ); - break; - } - } - } - } - } - - // 4 + 5. Flags that R2.1/R4.4 send to directives. - for (let i = 0; i < masked.length; i += 1) { - const { code, strings } = masked[i]; - const isCxxFlagCall = /(?:^|[^\w])(?:cxxflag|cflag)\s*\(/.test(code); - const isLinkFlagCall = /(?:^|[^\w])link_flag\s*\(/.test(code); - for (const literal of strings) { - const value = literal.value; - const isLinkFlag = isLinkFlagCall || value.includes("mcpp:link-flag="); - const isCxxFlag = isCxxFlagCall || value.includes("mcpp:cxxflag=") || value.includes("mcpp:cflag="); - if (isLinkFlag && value.includes(RPATH)) { - report( - DIAGNOSTIC_CODES.rpathInLinkFlag, - "a run path must not be spelled in link_flag; declare the directory with " + - "mcpp::runtime_search_dir(…) (SPEC-007 R4.4)", - i, - literalSpan(literal, RPATH).start, - literalSpan(literal, RPATH).end, - ); - } - if (isLinkFlag || isCxxFlag) { - const token = RAWPATH.exec(value)?.[1]; - if (token !== undefined) { - const span = literalSpan(literal, token); - report( - DIAGNOSTIC_CODES.rawPathFlag, - `spelling "${token}" in a raw flag bypasses the engine's rendering; use ` + - `mcpp::include_dir / mcpp::include_dir_after / mcpp::link_search (SPEC-007 R2.1)`, - i, - span.start, - span.end, - ); - } - } - } - } - - // 6 (typed form). Only strings that are arguments of `.arg()`/`.command()` in - // an action block, so an `env()` value or an unrelated string cannot match. - const actionLines = new Set(); - for (const block of blocks) { - for (let i = block.line; i < masked.length; i += 1) { - actionLines.add(i); - if (/\.submit\s*\(/.test(masked[i].code)) { - break; - } - } - } - for (let i = 0; i < masked.length; i += 1) { - if (!actionLines.has(i) || !/\.(?:arg|command)\s*\(/.test(masked[i].code)) { - continue; - } - for (const literal of masked[i].strings) { - const needle = SHELL_ENV.exec(literal.value)?.[1] ?? SHELL_CD.exec(literal.value)?.[1]; - if (needle === undefined) { - continue; - } - const span = literalSpan(literal, needle); - report( - DIAGNOSTIC_CODES.shellSyntaxInAction, - `the command spells shell syntax ("${needle}"); declare the environment and working ` + - `directory with env(…) and cwd(…) instead (SPEC-007 R3.8)`, - i, - span.start, - span.end, - ); - } - } - - const seen = new Set(); - const unique = found.filter((entry) => { - const key = `${entry.code}:${entry.line}:${entry.startCharacter}:${entry.endCharacter}`; - if (seen.has(key)) { - return false; - } - seen.add(key); - return true; - }); - unique.sort( - (a, b) => - a.line - b.line || - a.startCharacter - b.startCharacter || - (a.code < b.code ? -1 : a.code > b.code ? 1 : 0), - ); - return unique; -} diff --git a/mide-plugins/vscode/src/buildscript/api.ts b/mide-plugins/vscode/src/buildscript/api.ts deleted file mode 100644 index b89be14..0000000 --- a/mide-plugins/vscode/src/buildscript/api.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * The mcpp build-script API snapshot: directives, roles, protocol constants and - * provision kinds, generated from the mcpp checkout by - * `tools/generate-buildscript-api.mjs` into `data/buildscript-api.json`. - * - * Why a snapshot and not the compiler: §3.2.1 of the plugin-optimisation plan - * measured that handing `build.mcpp` to the C++ language service makes *both* - * `import std` and `import mcpp` fail as `module not found` — mcpp deliberately - * keeps the build program out of the compilation database. No language service - * can describe it, so this module is the editor's substitute. It is pure data - * and imports no `vscode`, so the analysis layer can be unit-tested and can - * never start depending on clangd. - * - * `apiProblems()` is the snapshot's own invariant check: `activate()` can log one - * line instead of failing feature by feature when the committed JSON is stale or - * malformed. - */ -import apiJson from "../../data/buildscript-api.json"; - -export interface Directive { - wire: string; - tag: string; - slot: string; - scope: string; - transform: string; - mustExist: boolean; - missingPrefix?: string; - missingSuffix?: string; - since: number; - rules: string[]; - docsUrl?: string | null; -} - -export interface BuildScriptApi { - sourceVersion: string; - sourceCommit: string; - protocolVersion: number; - cacheEpoch: number; - roles: string[]; - provisions: Array<{ kind: string; wire: string }>; - directives: Directive[]; -} - -/** - * The five action roles the engine knows (`mcpp::roles::*`), in the order - * `mcpp::manifest::BuildAction::Role` declares them. The generator reads the - * same list out of `directives.cppm`; keeping the constant here is what lets a - * snapshot whose `roles` drifted be rejected. - */ -export const ACTION_ROLES: readonly string[] = ["source", "check", "object", "artifact", "prepare"]; - -const api = apiJson as unknown as BuildScriptApi; - -export const API: BuildScriptApi = api; - -const DIRECTIVES: readonly Directive[] = api.directives; -const BY_WIRE = new Map(DIRECTIVES.map((entry) => [entry.wire, entry])); - -/** The directive with this wire name (`"link-search"`), or `undefined`. */ -export function directive(wire: string): Directive | undefined { - return BY_WIRE.get(wire); -} - -/** Every directive, in the order the mcpp table declares them. */ -export function directives(): readonly Directive[] { - return DIRECTIVES; -} - -/** The commit/version the snapshot was generated from, for provenance UIs. */ -export function apiSource(): { version: string; commit: string } { - return { version: api.sourceVersion, commit: api.sourceCommit }; -} - -/** - * The snapshot's own invariants. Empty when the table is usable; every string is - * a sentence a log line can carry. Deliberately narrow: it catches a snapshot - * that would make the editor lie (nothing to suggest, two answers for one wire, - * a role list the engine does not have, a protocol number that is not a - * protocol), not cosmetic drift. - */ -export function apiProblems(): string[] { - const problems: string[] = []; - - if (DIRECTIVES.length === 0) { - problems.push("the buildscript-api snapshot has no directives"); - } - - const seen = new Set(); - for (const entry of DIRECTIVES) { - if (seen.has(entry.wire)) { - problems.push(`duplicate directive wire name ${entry.wire}`); - } - seen.add(entry.wire); - } - - const roles = api.roles; - const isTheFiveKnownRoles = - roles.length === ACTION_ROLES.length && ACTION_ROLES.every((role) => roles.includes(role)); - if (!isTheFiveKnownRoles) { - problems.push( - `roles are not the five the engine knows: [${roles.join(", ")}] ` + - `(expected [${ACTION_ROLES.join(", ")}])`, - ); - } - - if (!(api.protocolVersion >= 1)) { - problems.push(`protocolVersion is not a protocol number: ${String(api.protocolVersion)}`); - } - - return problems; -} diff --git a/mide-plugins/vscode/src/buildscript/modules.ts b/mide-plugins/vscode/src/buildscript/modules.ts deleted file mode 100644 index a626743..0000000 --- a/mide-plugins/vscode/src/buildscript/modules.ts +++ /dev/null @@ -1,294 +0,0 @@ -/** - * The modules a `build.mcpp` can legitimately `import`, and a scanner that finds - * the import sites in one. - * - * Since 0.7.0 `build.mcpp` is a `cpp` document and C++ Modules (mcppls) - * compiles it as the build program it is; while it serves the file it explains - * the imports itself, and this table is used only when it does not. The rule is - * absolute — **we never report "module not found"**. This module therefore only - * *recognises*; it never validates, because a build program's imports are - * resolved by mcpp's own module graph (`docs/specs/build-plugins.md` §9, - * `docs/30-build-mcpp.md` "The names of a plugin's modules"), not by clangd. - * - * The layer table of §9 is what the descriptions below name: - * L1 `mcpp.core` the engine interface; `mcpp` is its permanent equivalent spelling; - * L2 `mcpp.plugins.*` the official general-purpose library, provided by `plugins-core`; - * L3 `mcpp.deps.*` / `mcpp.rules.*` / `mcpp.dist.*` / `mcpp.tools.*` official plugins, - * and `mcpp..*` for a third party in namespace `mcpp`. - * - * The third-party form is deliberately *not* claimed by `isKnownModule`: it is - * indistinguishable from a file name (`mcpp.toml`), and the contract here is the - * explicit list (`std`, `std.compat`, `mcpp`, `mcpp.core`, `mcpp.plugins.*`). - * Not claiming a name costs a hover; claiming a wrong one would cost trust. - */ - -/** One `import` site. Positions are 0-based, matching `vscode.Position`. */ -export interface ImportSite { - /** The module name exactly as written, e.g. `mcpp.plugins.tool`. */ - module: string; - /** 0-based line index. */ - line: number; - /** 0-based column of the module name's first character. */ - startCharacter: number; - /** 0-based column one past the module name's last character. */ - endCharacter: number; -} - -export interface KnownModule { - name: string; - description: string; - docsUrl?: string; -} - -const MCPP_DOCS = "https://github.com/mcpp-community/mcpp/blob/main/docs/30-build-mcpp.md"; -const STD_DOCS = `${MCPP_DOCS}#import-std-mcpp-2026821`; -const CORE_DOCS = `${MCPP_DOCS}#the-interfaces-name-mcppcore-protocol-14`; -const PLUGIN_DOCS = `${MCPP_DOCS}#the-names-of-a-plugins-modules`; - -/** - * Exact module names plus `prefix.*` patterns. Order is the order a completion - * list should show: the standard library, the engine, then the libraries built - * on it. `mcpp.plugins.*` is the one the plan names; the other reserved second - * segments are SPEC-007 R9.6's official namespaces. - */ -export const KNOWN_MODULES: readonly KnownModule[] = [ - { - name: "std", - description: - "The C++ standard library module. mcpp stages the same `std` its own build uses, " + - "keyed on toolchain × standard × dialect (`docs/30-build-mcpp.md`, 2026.8.2.1+).", - docsUrl: STD_DOCS, - }, - { - name: "std.compat", - description: - "The C++ standard library plus the C library names in the global namespace; " + - "`import std;` and `import std.compat;` may be used together.", - docsUrl: STD_DOCS, - }, - { - name: "mcpp", - description: - "The mcpp engine interface, bundled in the mcpp binary so it always matches that " + - "engine's protocol. The permanent equivalent spelling of `mcpp.core` (SPEC-007 R9.1).", - docsUrl: CORE_DOCS, - }, - { - name: "mcpp.core", - description: - "The mcpp engine interface (L1). Only grows; a symbol leaves only after six months " + - "of deprecation, and a meaning change is a new symbol (SPEC-007 R9.2).", - docsUrl: CORE_DOCS, - }, - { - name: "mcpp.plugins.*", - description: - "The official general-purpose library (L2), provided by the `plugins-core` feature " + - "and built only on the L1 engine interface, e.g. `mcpp.plugins.tool` (SPEC-007 §9).", - docsUrl: PLUGIN_DOCS, - }, - { - name: "mcpp.deps.*", - description: - "Official dependency-adapter plugins (L3), e.g. a package manager or external build " + - "system brought into the graph (SPEC-007 §0, §9).", - docsUrl: PLUGIN_DOCS, - }, - { - name: "mcpp.rules.*", - description: - "Official rule packages (L3): code generation and device-language toolchains " + - "(SPEC-007 §0, §9).", - docsUrl: PLUGIN_DOCS, - }, - { - name: "mcpp.dist.*", - description: - "Official distribution members (L3): they turn a link artifact into something " + - "installable, e.g. a WiX or APK payload (SPEC-007 §0, §9).", - docsUrl: PLUGIN_DOCS, - }, - { - name: "mcpp.tools.*", - description: - "Official tool namespaces (L3), e.g. `mcpp.tools.island` helpers described in " + - "docs/31 (SPEC-007 §9).", - docsUrl: PLUGIN_DOCS, - }, -]; - -const EXACT_MODULES = new Map( - KNOWN_MODULES.filter((entry) => !entry.name.endsWith(".*")).map((entry) => [entry.name, entry]), -); -const PATTERN_MODULES = KNOWN_MODULES.filter((entry) => entry.name.endsWith(".*")); - -/** A dotted C++ module name (no partitions, no header units). */ -const MODULE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/; - -/** `std`, `std.compat`, `mcpp`, `mcpp.core`, `mcpp.plugins.*` (and the other reserved L3 namespaces). */ -export function isKnownModule(name: string): boolean { - return knownModule(name) !== undefined; -} - -/** - * The description of a known module, or `undefined`. A concrete name that matches - * a `prefix.*` pattern is returned with the concrete name, so a hover can show - * the module the user actually wrote. - */ -export function knownModule(name: string): KnownModule | undefined { - const exact = EXACT_MODULES.get(name); - if (exact !== undefined) { - return exact; - } - for (const pattern of PATTERN_MODULES) { - const prefix = pattern.name.slice(0, -1); // keep the trailing dot - if (name.startsWith(prefix)) { - const rest = name.slice(prefix.length); - if (MODULE_NAME.test(rest)) { - return { name, description: pattern.description, docsUrl: pattern.docsUrl }; - } - } - } - return undefined; -} - -/** Every recognised module (patterns included), for completion lists. */ -export function knownModules(): readonly KnownModule[] { - return KNOWN_MODULES; -} - -/** - * Whitespace/comment/string masking state that has to survive a line boundary. - * `blockComment` and `rawDelim` are the two C++ constructs that do. - */ -interface ScanState { - blockComment: boolean; - rawDelim: string | null; -} - -/** - * Replace every character that is part of a comment or a string literal with a - * space, so a match on the result is a match on real code. Indices are preserved, - * which is what lets positions be reported straight from a match. - */ -function maskLine(line: string, state: ScanState): string { - let out = ""; - let i = 0; - - if (state.rawDelim !== null) { - const terminator = `)${state.rawDelim}"`; - const end = line.indexOf(terminator, i); - if (end < 0) { - return " ".repeat(line.length); - } - out += " ".repeat(end + terminator.length); - i = end + terminator.length; - state.rawDelim = null; - } - - while (i < line.length) { - if (state.blockComment) { - const end = line.indexOf("*/", i); - if (end < 0) { - return out + " ".repeat(line.length - i); - } - out += " ".repeat(end + 2 - i); - i = end + 2; - state.blockComment = false; - continue; - } - const pair = line.slice(i, i + 2); - if (pair === "//") { - return out + " ".repeat(line.length - i); - } - if (pair === "/*") { - state.blockComment = true; - out += " "; - i += 2; - continue; - } - if (line[i] === "R" && line[i + 1] === '"') { - // R"delim( … )delim" — may cross lines. - const open = /^R"([^(\s]{0,16})\(/.exec(line.slice(i)); - if (open !== null) { - const terminator = `)${open[1]}"`; - const end = line.indexOf(terminator, i + open[0].length); - if (end < 0) { - state.rawDelim = open[1]; - return out + " ".repeat(line.length - i); - } - const stop = end + terminator.length; - out += " ".repeat(stop - i); - i = stop; - continue; - } - } - if (line[i] === "'" && /[A-Za-z0-9_]/.test(line[i - 1] ?? "")) { - // A `'` right after an identifier character is a digit separator (1'000), - // not the start of a character literal; scanning for a closing quote would - // swallow the rest of the line. - i += 1; - continue; - } - if (line[i] === '"' || line[i] === "'") { - const quote = line[i]; - let j = i + 1; - while (j < line.length) { - if (line[j] === "\\") { - j += 2; - continue; - } - if (line[j] === quote) { - j += 1; - break; - } - j += 1; - } - out += " ".repeat(Math.min(j, line.length) - i); - i = Math.min(j, line.length); - continue; - } - out += line[i]; - i += 1; - } - return out; -} - -/** - * `import X;` / `export import X;` / `import X.Y.Z;`, with trailing whitespace or - * a comment. The leading class keeps a member call such as `obj.import(...)` or - * an identifier ending in `import` from matching; the trailing lookahead keeps - * `import X` from matching when it is not the whole declaration. - */ -const IMPORT = /(?:^|[\s;{}])import\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*(?=;)/g; - -/** - * Every recognised import in `lines`, in source order. Header units (`import - * ;`), header imports (`import "x.h";`) and partitions (`import :part;`) - * are ignored: they are not module names this extension has anything to say - * about. - * - * Positions are 0-based and cover the module name itself, not the statement — - * that is the range a hover or a semantic highlight wants. - */ -export function scanImports(lines: readonly string[]): ImportSite[] { - const sites: ImportSite[] = []; - const state: ScanState = { blockComment: false, rawDelim: null }; - - for (let line = 0; line < lines.length; line += 1) { - const code = maskLine(lines[line], state); - IMPORT.lastIndex = 0; - for (let match = IMPORT.exec(code); match !== null; match = IMPORT.exec(code)) { - const name = match[1]; - const start = match.index + match[0].lastIndexOf(name); - sites.push({ - module: name, - line, - startCharacter: start, - endCharacter: start + name.length, - }); - } - } - - return sites; -} diff --git a/mide-plugins/vscode/src/buildscript/providers.ts b/mide-plugins/vscode/src/buildscript/providers.ts deleted file mode 100644 index 0e63778..0000000 --- a/mide-plugins/vscode/src/buildscript/providers.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * The one-line integration point for `build.mcpp` intelligence. - * - * `src/extension.ts` stays the only assembly point: it calls this function with - * its real context, a severity reader and a "does C++ Modules serve this - * file" reader, and gets completion, hover and diagnostics for `build.mcpp` - * in return. `build.mcpp` is an ordinary `cpp` document (C++ Modules compiles - * it like any other unit), so the providers register on the `cpp` language with - * a `build.mcpp` filename pattern. The severity reader is the one switch `src/extension.ts` already owns; the four boolean switches - * (`mcpp.buildScript.{intelligence,diagnostics,snippets,imports.knownModules}`) - * are resolved here through `settings.ts`, because that file is the only place - * this extension can be told about them. - * - * So `mcpp.buildScript.intelligence = false` means **no** build.mcpp - * intelligence — completion and hover return nothing, and diagnostics stop — - * rather than merely skipping provider registration. `snippets` and - * `imports.knownModules` each narrow one contribution of that layer and never - * touch another: with `snippets` off, diagnostics and hover stay; with - * `imports.knownModules` off, only the *completion list* of shipped modules - * disappears, while hover keeps recognising `std`/`std.compat`/`mcpp.*` and - * diagnostics never report any import as missing. - * - * While C++ Modules serves the file, what it answers better is left to it: the - * `unknownSymbol` rule (it compiles the file against the modules it really - * imports) and the known-module completion and hover after `import`. The - * SPEC-007 rule checks and the snippets stay: no compiler says those. - * - * This build registers no document-symbol provider for `build.mcpp`, so there is - * nothing there to suppress; if one is added it must consult `intelligence` too. - * - * Everything shown comes from `api.ts` (the generated snapshot), `modules.ts` or - * `snippets.ts`; nothing invokes a compiler, and no provider ever says "module - * not found". The pure modules stay testable and this file stays the only one in - * the slice that touches the `vscode` API. - */ -import type * as vscode from "vscode"; - -import { read } from "../config/access"; -import { analyseBuildScript } from "./analysis"; -import { API, directive } from "./api"; -import { knownModule, knownModules, scanImports } from "./modules"; -import { buildScriptContributions } from "./settings"; -import { snippetsForScope } from "./snippets"; - -// The deliverable for this file is typed against `import type * as vscode`; the -// extension host is the only environment it runs in, so the value is required -// here rather than injected. -const vscodeApi = require("vscode") as typeof vscode; - -const LANGUAGE = "cpp"; -const FILE_NAME = "build.mcpp"; -const SELECTOR: vscode.DocumentFilter = { language: LANGUAGE, pattern: `**/${FILE_NAME}` }; - -/** `build.mcpp` by name, as a `cpp` document. */ -function isBuildScript(document: vscode.TextDocument): boolean { - return document.languageId === LANGUAGE && document.uri.path.split("/").pop() === FILE_NAME; -} - -const VS_SEVERITY: Record<"error" | "warning" | "info", vscode.DiagnosticSeverity> = { - error: vscodeApi.DiagnosticSeverity.Error, - warning: vscodeApi.DiagnosticSeverity.Warning, - info: vscodeApi.DiagnosticSeverity.Information, -}; - -function toVscodeDiagnostics( - document: vscode.TextDocument, - severity: "warning" | "info", - servedByMcppls: boolean, -): vscode.Diagnostic[] { - const options = { unknownSymbols: !servedByMcppls }; - return analyseBuildScript(document.getText().split(/\r?\n/), severity, options).map((entry) => { - const diagnostic = new vscodeApi.Diagnostic( - // The analyser reports 1-based line/column; `vscode.Position` is 0-based. - new vscodeApi.Range( - entry.line - 1, - entry.startCharacter - 1, - entry.line - 1, - entry.endCharacter - 1, - ), - entry.message, - VS_SEVERITY[entry.severity], - ); - diagnostic.code = entry.code; - diagnostic.source = "mcpp"; - return diagnostic; - }); -} - -/** The `mcpp::name` on this line that covers `character`, if any. */ -function symbolAt(line: string, character: number): string | undefined { - const pattern = /mcpp::([A-Za-z_][A-Za-z0-9_]*)/g; - for (let match = pattern.exec(line); match !== null; match = pattern.exec(line)) { - if (character >= match.index && character <= match.index + match[0].length) { - return match[1]; - } - } - return undefined; -} - -function markdown(lines: readonly string[]): vscode.MarkdownString { - return new vscodeApi.MarkdownString(lines.join("\n\n")); -} - -export function registerBuildScriptProviders( - context: { subscriptions: { push(...items: unknown[]): unknown } }, - severity: () => "warning" | "info" | "off", - servedByMcppls: () => boolean, -): void { - const selector: vscode.DocumentSelector = SELECTOR; - const collection = vscodeApi.languages.createDiagnosticCollection("mcpp-build-script"); - const contributions = () => buildScriptContributions(read, severity); - - const refresh = (document: vscode.TextDocument): void => { - if (!isBuildScript(document)) { - return; - } - const { diagnosticSeverity } = contributions(); - if (diagnosticSeverity === undefined) { - collection.delete(document.uri); - return; - } - collection.set(document.uri, toVscodeDiagnostics(document, diagnosticSeverity, servedByMcppls())); - }; - const refreshAll = (): void => { - for (const document of vscodeApi.workspace.textDocuments) { - refresh(document); - } - }; - - const completion = vscodeApi.languages.registerCompletionItemProvider(selector, { - provideCompletionItems(document, position) { - const { intelligence, snippets, knownModules: completesKnownModules } = contributions(); - if (!intelligence) { - return undefined; - } - const before = document.lineAt(position.line).text.slice(0, position.character); - - const afterImport = /\b(?:export\s+)?import\s+([A-Za-z0-9_.]*)$/.exec(before); - if (afterImport !== null) { - // Only the *list* is optional. Recognition itself (`knownModule`, hover, - // the no-diagnostic rule) is unconditional. C++ Modules completes module - // names from what the build can really see, so it answers instead. - if (!completesKnownModules || servedByMcppls()) { - return undefined; - } - return knownModules() - .filter((entry) => !entry.name.endsWith(".*") && entry.name.startsWith(afterImport[1])) - .map((entry) => { - const item = new vscodeApi.CompletionItem( - entry.name, - vscodeApi.CompletionItemKind.Module, - ); - item.detail = "mcpp build-script module"; - item.documentation = markdown([entry.description]); - return item; - }); - } - - const afterScope = /mcpp::([A-Za-z_][A-Za-z0-9_]*)?$/.exec(before); - if (afterScope !== null) { - const typed = afterScope[1] ?? ""; - const names = API.directives.map((entry) => entry.wire.replace(/-/g, "_")); - const items: vscode.CompletionItem[] = names - .filter((name, index) => names.indexOf(name) === index && name.startsWith(typed)) - .map((name) => { - const item = new vscodeApi.CompletionItem(name, vscodeApi.CompletionItemKind.Function); - item.detail = "mcpp build-script API"; - return item; - }); - if (snippets) { - for (const snippet of snippetsForScope(typed)) { - const item = new vscodeApi.CompletionItem( - snippet.label, - vscodeApi.CompletionItemKind.Snippet, - ); - item.filterText = snippet.filterText; - item.detail = snippet.detail; - if (snippet.documentation !== undefined) { - item.documentation = markdown([snippet.documentation]); - } - item.insertText = new vscodeApi.SnippetString(snippet.insertText); - items.push(item); - } - } - return items; - } - - return undefined; - }, - }); - - const hover = vscodeApi.languages.registerHoverProvider(selector, { - provideHover(document, position) { - if (!contributions().intelligence) { - return undefined; - } - const lines = document.getText().split(/\r?\n/); - - // An import's hover is C++ Modules' while it serves the file. - for (const site of servedByMcppls() ? [] : scanImports(lines)) { - if ( - site.line === position.line && - position.character >= site.startCharacter && - position.character <= site.endCharacter - ) { - const info = knownModule(site.module); - if (info !== undefined) { - return new vscodeApi.Hover(markdown([`**${info.name}**`, info.description]), undefined); - } - } - } - - const name = symbolAt(document.lineAt(position.line).text, position.character); - const entry = name === undefined ? undefined : directive(name.replace(/_/g, "-")); - if (entry === undefined) { - return undefined; - } - const details = [ - `**mcpp::${name}**`, - `wire \`mcpp:${entry.wire}=\` · slot \`${entry.slot}\` · scope \`${entry.scope}\` · ` + - `since protocol ${entry.since}`, - ]; - if (entry.rules.length > 0) { - details.push(`SPEC-007: ${entry.rules.join(", ")}`); - } - if (typeof entry.docsUrl === "string") { - details.push(`[docs/30 — build.mcpp](${entry.docsUrl})`); - } - return new vscodeApi.Hover(markdown(details), undefined); - }, - }); - - const subscriptions = [ - collection, - completion, - hover, - vscodeApi.workspace.onDidOpenTextDocument(refresh), - vscodeApi.workspace.onDidChangeTextDocument((event) => refresh(event.document)), - vscodeApi.workspace.onDidCloseTextDocument((document) => collection.delete(document.uri)), - // Whether C++ Modules serves the file can change at any time: installed, - // enabled, or switched off for this workspace. - vscodeApi.extensions.onDidChange(refreshAll), - vscodeApi.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration("mcppls.enable") || event.affectsConfiguration("mcpp.buildScript")) { - refreshAll(); - } - }), - ]; - refreshAll(); - context.subscriptions.push(...subscriptions); -} diff --git a/mide-plugins/vscode/src/buildscript/settings.ts b/mide-plugins/vscode/src/buildscript/settings.ts deleted file mode 100644 index 258cb91..0000000 --- a/mide-plugins/vscode/src/buildscript/settings.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * The `mcpp.buildScript.*` switches, resolved in one place. - * - * `src/extension.ts` is the only assembly point and it already hands this slice - * one thing: the diagnostic severity, read and validated from the registry. It - * cannot grow a second argument for the feature switches, so the providers read - * them here. Keeping the resolution **pure** — a reader in, flags out — is what - * lets a unit test prove that flipping one registry key changes exactly one - * contribution, and that the never-report-a-missing-module rule survives every - * one of them being off (§3.2.1: mcpp keeps `build.mcpp` out of the build - * database, so this extension can never validate an import). - * - * The keys, per `data/config-registry.json`: - * `mcpp.buildScript.intelligence` completion + hover, the master switch - * `mcpp.buildScript.diagnostics` static diagnostics on/off - * `mcpp.buildScript.diagnostics.severity` `warning` | `info` | `off` (passed in) - * `mcpp.buildScript.snippets` snippet completions for the API - * `mcpp.buildScript.imports.knownModules` the known-module list in import completion - */ -import type { Severity } from "./analysis"; - -/** The values the registry allows for `mcpp.buildScript.diagnostics.severity`. */ -export type DiagnosticSeveritySetting = Exclude; - -/** - * The registry accessor shape (`src/config/access.ts`'s `read`, or a test stub). - * A function with an optional `resource` argument satisfies it. - */ -export type SettingsReader = (key: string) => T; - -/** What the `build.mcpp` providers contribute, given the current settings. */ -export interface BuildScriptContributions { - /** Completion and hover for `mcpp::` symbols and known module names. */ - intelligence: boolean; - /** The severity to publish diagnostics at; `undefined` means publish none. */ - diagnosticSeverity?: "warning" | "info"; - /** Snippet completions for the build-script API. */ - snippets: boolean; - /** The known-module list in import completion. */ - knownModules: boolean; -} - -/** - * Resolve the switches. `severity()` is the callback `src/extension.ts` already - * passes; it folds intelligence and diagnostics in, and this function folds them - * again so each key is provably read here as well. - * - * `!== false` rather than a truthiness test throughout: every one of these keys - * defaults to `true`, and a caller (or a test stub) that hands back `undefined` - * must mean "not turned off", not "off". - */ -export function buildScriptContributions( - read: SettingsReader, - severity: () => DiagnosticSeveritySetting, -): BuildScriptContributions { - const intelligence = read("mcpp.buildScript.intelligence") !== false; - const diagnosticsOn = intelligence && read("mcpp.buildScript.diagnostics") !== false; - const level = severity(); - const diagnosticSeverity = - diagnosticsOn && (level === "warning" || level === "info") ? level : undefined; - - return { - intelligence, - ...(diagnosticSeverity === undefined ? {} : { diagnosticSeverity }), - // Snippets and the known-module list live *inside* the intelligence layer: - // their own key only ever narrows what that layer already offers. - snippets: intelligence && read("mcpp.buildScript.snippets") !== false, - knownModules: intelligence && read("mcpp.buildScript.imports.knownModules") !== false, - }; -} diff --git a/mide-plugins/vscode/src/buildscript/snippets.ts b/mide-plugins/vscode/src/buildscript/snippets.ts deleted file mode 100644 index 208c3ff..0000000 --- a/mide-plugins/vscode/src/buildscript/snippets.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Snippet contributions for `build.mcpp` (`mcpp.buildScript.snippets`, §3.2.2). - * - * A snippet is a **starting point, not a signature**: `data/buildscript-api.json` - * records a directive's wire name, tag, slot, scope and SPEC-007 rules, never its - * arity or argument types. So the call-shaped snippets place one editable - * argument and the two `mcpp::action` snippets follow the typed spelling the - * analyser and mcpp's `docs/30-build-mcpp.md` use. Anything more specific would - * be an invented signature, which this extension does not do. - * - * Pure data plus pure filters, so the catalogue is unit-tested and - * `providers.ts` stays the only `vscode`-touching file in the slice. - */ -import { ACTION_ROLES, API } from "./api"; - -export interface BuildScriptSnippet { - /** The text the editor filters the typed prefix against, e.g. `link_script`. */ - filterText: string; - /** Human label; the placeholder is spelled `...` so it never reads as syntax. */ - label: string; - detail: string; - documentation?: string; - /** VS Code snippet syntax (`${1:…}`, `${2|a,b|}`). */ - insertText: string; -} - -/** The one placeholder a directive snippet places: no arity is known. */ -const VALUE_PLACEHOLDER = "${1:value}"; - -/** `mcpp::("…")` for every directive the snapshot has, except `action`. */ -export function directiveCallSnippets(): BuildScriptSnippet[] { - const seen = new Set(); - const snippets: BuildScriptSnippet[] = []; - - for (const entry of API.directives) { - const name = entry.wire.replace(/-/g, "_"); - // `mcpp::action` is a declaration, not a call; the block snippets below are - // its shape. A duplicate typed name would be a snapshot bug, but skipping it - // keeps the list a set either way. - if (name === "action" || seen.has(name)) { - continue; - } - seen.add(name); - - const documentation: string[] = []; - if (entry.rules.length > 0) { - documentation.push(`SPEC-007 ${entry.rules.join(", ")}`); - } - if (typeof entry.docsUrl === "string") { - documentation.push(`[docs/30 — build.mcpp](${entry.docsUrl})`); - } - - const snippet: BuildScriptSnippet = { - filterText: name, - label: `${name}(...)`, - detail: `mcpp:${entry.wire}= · slot ${entry.slot}`, - insertText: `${name}("${VALUE_PLACEHOLDER}")`, - }; - if (documentation.length > 0) { - snippet.documentation = documentation.join(" · "); - } - snippets.push(snippet); - } - - return snippets; -} - -/** - * The two action blocks worth spelling out: a general one with the five roles as - * a choice, and a `prepare` one, which is the only role whose `output_dir` the - * engine *requires* (SPEC-007 R3.3, the analyser's `prepareNeedsOutputDir`). - */ -export function actionSnippets(): BuildScriptSnippet[] { - const roles = ACTION_ROLES.join(","); - return [ - { - filterText: "action", - label: "action ... (typed action block)", - detail: "mcpp::action with id, role, description, arg/input/output and submit", - documentation: `The five roles (SPEC-007 R3.6): ${ACTION_ROLES.join(", ")}.`, - insertText: [ - `mcpp::action \${1:handle};`, - `\${1:handle}.id = "\${2:id}";`, - `\${1:handle}.role = mcpp::roles::\${3|${roles}|};`, - `\${1:handle}.description = "\${4:description}";`, - `\${1:handle}.arg("\${5:argument}")`, - ` .input("\${6:input}")`, - ` .output("\${7:output}")`, - ` .submit();`, - ].join("\n"), - }, - { - filterText: "action", - label: "action ... (prepare block)", - detail: "mcpp::action for a prepare role; output_dir is required (SPEC-007 R3.3)", - documentation: "A prepare action must declare output_dir; the engine refuses one without it.", - insertText: [ - `mcpp::action \${1:handle};`, - `\${1:handle}.id = "\${2:id}";`, - `\${1:handle}.role = mcpp::roles::prepare;`, - `\${1:handle}.output_dir("\${3:dir}");`, - `\${1:handle}.command("\${4:command}").submit();`, - ].join("\n"), - }, - ]; -} - -/** The whole catalogue, action blocks first. */ -export function buildScriptSnippets(): BuildScriptSnippet[] { - return [...actionSnippets(), ...directiveCallSnippets()]; -} - -/** The snippets whose typed name starts with `typed` (the text after `mcpp::`). */ -export function snippetsForScope(typed: string): BuildScriptSnippet[] { - return buildScriptSnippets().filter((snippet) => snippet.filterText.startsWith(typed)); -} diff --git a/mide-plugins/vscode/src/cli/process.ts b/mide-plugins/vscode/src/cli/process.ts index 78eb175..a51a845 100644 --- a/mide-plugins/vscode/src/cli/process.ts +++ b/mide-plugins/vscode/src/cli/process.ts @@ -61,30 +61,34 @@ export function quoteWindowsArgument(argument: string): string { * payload's runner and image, or a program). The output reaches the same parsers it reached when * the extension ran the programs itself. Any other program still runs here (`runProcess`). */ -let mcppExecutor: ProcessRunner | undefined; -let mcpplsExecutor: ((args: string[], cwd?: string, options?: ProcessRunOptions) => Promise) | undefined; +// An executor answers `undefined` where the backend does not run (the declared degraded mode, +// design 2026-10-30 part 2 §8 F15): the program then runs here, as it did before the backend. +type McppExecutor = (executable: string, args: string[], cwd?: string, options?: ProcessRunOptions) => Promise; +type McpplsExecutor = (args: string[], cwd?: string, options?: ProcessRunOptions) => Promise; +let mcppExecutor: McppExecutor | undefined; +let mcpplsExecutor: McpplsExecutor | undefined; -export function useMcppExecutor(executor: ProcessRunner | undefined): void { +export function useMcppExecutor(executor: McppExecutor | undefined): void { mcppExecutor = executor; } -export function useMcpplsExecutor(executor: typeof mcpplsExecutor): void { +export function useMcpplsExecutor(executor: McpplsExecutor | undefined): void { mcpplsExecutor = executor; } -/** Runs mcpp: through the backend when one is installed, else as `executable`. No trust check. */ -export function runMcppCommand( +/** Runs mcpp: through the backend when one is installed and runs, else as `executable`. No trust check. */ +export async function runMcppCommand( executable: string, args: string[], cwd?: string, options: ProcessRunOptions = {}, ): Promise { - return mcppExecutor !== undefined ? mcppExecutor(executable, args, cwd, options) : runProcess(executable, args, cwd, options); + return (await mcppExecutor?.(executable, args, cwd, options)) ?? runProcess(executable, args, cwd, options); } -/** Runs mcppls: through the backend when one is installed, else `mcppls` from PATH. */ -export function runMcppls(args: string[], cwd?: string, options: ProcessRunOptions = {}): Promise { - return mcpplsExecutor !== undefined ? mcpplsExecutor(args, cwd, options) : runProcess("mcppls", args, cwd, options); +/** Runs mcppls: through the backend when one is installed and runs, else `mcppls` from PATH. */ +export async function runMcppls(args: string[], cwd?: string, options: ProcessRunOptions = {}): Promise { + return (await mcpplsExecutor?.(args, cwd, options)) ?? runProcess("mcppls", args, cwd, options); } export async function runProcess( diff --git a/mide-plugins/vscode/src/cli/search.ts b/mide-plugins/vscode/src/cli/search.ts deleted file mode 100644 index 8cf21ef..0000000 --- a/mide-plugins/vscode/src/cli/search.ts +++ /dev/null @@ -1,175 +0,0 @@ -// `mcpp search` 的适配层(依赖版本补全,方案默认关)。 -// -// **这是全项目唯一解析「人类输出」的地方。** `mcpp search` 没有机读格式 -// (`--all-versions` 也只改人类列表的详细程度,不是 JSON),所以这里只能按 -// 实测行形状解析,并且必须防御性地失败:看不懂的行一律跳过,绝不抛异常、 -// 绝不半猜。mcpp 一旦提供机读格式就替换本模块(方案 §3.3.2 已登记)。 -// -// 纯字符串函数:不启动进程、不读盘、不依赖 vscode。执行 `mcpp`、超时、联网 -// 判定与会话缓存都由调用方负责(`mcpp.toml.indexCompletion*`)。 -// -// 实测形状(mcpp 2026.9.30.2): -// 兼容索引行 ` compat:zlib A compression library (1.3.2)` -// --all-versions 多版本 ` mcpplibs:aarch64-virt-rt … (0.2.1, 0.2.0, 0.1.1, ...)` -// 没有版本的行(如纯 xim 工具)没有尾括号,直接跳过。 - -export interface PackageVersion { - version: string; - summary?: string; -} - -/** semver 近似:数字点分,可带 `-`/`+` 后缀(`2026.07.09` 这类日期版本也算)。 */ -const VERSION = /^\d+(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/; -/** 行尾的括号组:实测版本都写在这里。 */ -const TRAILING_PARENS = /\(([^()]*)\)\s*$/; -/** 有些终端会给输出上色;颜色码不属于内容。 */ -const ANSI = /\u001b\[[0-9;]*m/g; - -function isVersionToken(text: string): boolean { - return VERSION.test(text); -} - -/** 去掉包标识后的描述文本;没有描述时返回 undefined。 */ -function summaryOf(head: string): string | undefined { - const match = /^\S+\s+(.*)$/.exec(head.trim()); - const summary = match?.[1]?.trim() ?? ""; - return summary === "" ? undefined : summary; -} - -/** 简单 semver 比较:数字段按数值比,缺段视为更小,后缀按字典序。 */ -function compareVersions(a: string, b: string): number { - const left = a.split(/[.+-]/); - const right = b.split(/[.+-]/); - const length = Math.max(left.length, right.length); - for (let index = 0; index < length; index += 1) { - const l = left[index]; - const r = right[index]; - if (l === undefined) { - return -1; - } - if (r === undefined) { - return 1; - } - const ln = /^\d+$/.test(l) ? Number(l) : undefined; - const rn = /^\d+$/.test(r) ? Number(r) : undefined; - if (ln !== undefined && rn !== undefined) { - if (ln !== rn) { - return ln < rn ? -1 : 1; - } - continue; - } - const order = l.localeCompare(r); - if (order !== 0) { - return order < 0 ? -1 : 1; - } - } - return 0; -} - -/** - * 从 `mcpp search --all-versions` 的 stdout 提取候选版本, - * 去重后按新→旧排序。空输入、只有旁白、或形状不认识时返回空数组。 - */ -export function parseSearchOutput(stdout: string): PackageVersion[] { - if (typeof stdout !== "string" || stdout === "") { - return []; - } - const byVersion = new Map(); - for (const rawLine of stdout.split(/\r?\n/)) { - const line = rawLine.replace(ANSI, "").trim(); - if (line === "") { - continue; - } - const match = TRAILING_PARENS.exec(line); - if (match === null) { - continue; - } - const versions = match[1] - .split(",") - .map((part) => part.trim()) - .filter(isVersionToken); - if (versions.length === 0) { - continue; - } - const summary = summaryOf(line.slice(0, match.index)); - for (const version of versions) { - if (byVersion.has(version)) { - continue; - } - const entry: PackageVersion = { version }; - if (summary !== undefined) { - entry.summary = summary; - } - byVersion.set(version, entry); - } - } - return [...byVersion.values()].sort((a, b) => compareVersions(b.version, a.version)); -} - -/** `mcpp` 的 argv(不含可执行文件名)。 */ -export function searchArguments(name: string): string[] { - return ["search", name, "--all-versions"]; -} - -/** - * 是否执行索引查询:显式开启、工作区受信任、且未离线。 - * `name` 留给将来的 deny-list;当前判定只是三个开关的合取。 - */ -export function shouldSearch( - name: string, - options: { enabled: boolean; offline: boolean; trusted: boolean }, -): boolean { - return options.enabled && options.trusted && !options.offline; -} - -/** `mcpp.toml.indexCompletionTimeoutSeconds` 的注册表默认值。 */ -const DEFAULT_INDEX_TIMEOUT_SECONDS = 20; - -/** 一次索引查询的完整描述:argv 与硬超时。 */ -export interface IndexSearchRequest { - name: string; - args: string[]; - timeoutMs: number; -} - -/** - * 把设置解析成"要跑什么",或者 `undefined` 表示不跑。 - * - * 纯函数:进程、会话缓存、vscode 判定都在调用方(`src/toml/providers.ts`)。 - * 超时从 `mcpp.toml.indexCompletionTimeoutSeconds` 来,至少 1 秒;非有限值 - * 回落到注册表默认的 20 秒。这样"绝不超过设置的超时"有一个可单测的落点。 - */ -export function indexCompletionRequest( - name: string, - options: { enabled: boolean; trusted: boolean; offline: boolean; timeoutSeconds: number }, -): IndexSearchRequest | undefined { - if (!shouldSearch(name, options)) { - return undefined; - } - const seconds = Number.isFinite(options.timeoutSeconds) - ? Math.max(1, Math.floor(options.timeoutSeconds)) - : DEFAULT_INDEX_TIMEOUT_SECONDS; - return { name, args: searchArguments(name), timeoutMs: seconds * 1000 }; -} - -/** - * 等 `work`,但最多等 `timeoutMs`;超时得到 `undefined`。 - * - * 调用方自己的进程超时(`execFile` 的 `timeout`)才是第一道闸;这是第二道: - * 即便进程卡住不退出,补全也必须在超时后返回,绝不把编辑器挂住。 - */ -export async function withDeadline(work: Promise, timeoutMs: number): Promise { - let timer: ReturnType | undefined; - try { - return await Promise.race([ - work, - new Promise((resolve) => { - timer = setTimeout(() => resolve(undefined), timeoutMs); - }), - ]); - } finally { - if (timer !== undefined) { - clearTimeout(timer); - } - } -} diff --git a/mide-plugins/vscode/src/extension.ts b/mide-plugins/vscode/src/extension.ts index 2f2facc..161b6d1 100644 --- a/mide-plugins/vscode/src/extension.ts +++ b/mide-plugins/vscode/src/extension.ts @@ -22,12 +22,9 @@ import { describeState } from "./mcppls/state"; import { languageServerEnabled, languageServerInstalled, - languageServerServes, languageServerVersion, readLanguageServerState, } from "./mcppls/stateSource"; -import { registerBuildScriptProviders } from "./buildscript/providers"; -import { registerTomlProviders } from "./toml/providers"; import { buildModuleSetupPlan, executeModuleSetup, @@ -295,7 +292,9 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi cliController.refreshStatus(); }; // 0.9.0's one seam for running mcpp now runs it through the backend; the parsers are unchanged. + // Where the backend does not run (the declared degraded mode, part 2 §8 F15), mcpp runs here. useMcppExecutor(async (_executable, args, cwd, options = {}) => { + if (session.degraded) return undefined; try { const answer = await session.request<{ exitCode: number; stdout: string; stderr: string; timedOut: boolean }>("mcpp/exec", { args, @@ -309,6 +308,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi } }); useMcpplsExecutor(async (args, cwd, options = {}) => { + if (session.degraded) return undefined; try { const answer = await session.request<{ exitCode: number; stdout: string; stderr: string; timedOut: boolean }>("languageService/exec", { args, @@ -380,28 +380,9 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi languageService, }); - // Text analysis only: no mcpp process, so both stay available in a restricted - // workspace, which is exactly what the `limited` capability promises. - registerTomlProviders( - extensionContext, - () => read("mcpp.toml.completion"), - () => ({ - syntax: read("mcpp.toml.diagnostics.syntax"), - unknownSection: read("mcpp.toml.diagnostics.unknownSection"), - unknownKey: read("mcpp.toml.diagnostics.unknownKey"), - planeSeparation: read("mcpp.toml.diagnostics.planeSeparation"), - legacyKeys: read("mcpp.toml.diagnostics.legacyKeys"), - }), - () => read("mcpp.toml.diagnostics.enabled"), - () => read("mcpp.toml.hover"), - () => read("mcpp.toml.navigation"), - ); - registerBuildScriptProviders(extensionContext, () => { - if (!read("mcpp.buildScript.intelligence") || !read("mcpp.buildScript.diagnostics")) { - return "off"; - } - return read<"warning" | "info" | "off">("mcpp.buildScript.diagnostics.severity"); - }, languageServerServes); + // mcpp.toml's and build.mcpp's completion, hover, jumps and diagnostics are the backend's, which + // the language client of `session` connects (design 2026-10-30 part 2 §8): text analysis that runs + // nothing, so they serve a restricted workspace too, as the `limited` capability promises. extensionContext.subscriptions.push( output, diff --git a/mide-plugins/vscode/src/ide/backend/client.ts b/mide-plugins/vscode/src/ide/backend/client.ts deleted file mode 100644 index daec36f..0000000 --- a/mide-plugins/vscode/src/ide/backend/client.ts +++ /dev/null @@ -1,164 +0,0 @@ -// The backend's wire, as the adapter speaks it: `mcpp-ide serve` started as a child process, -// JSON-RPC 2.0 framed as the LSP base protocol on its standard streams. No editor API here, so the -// unit tests and the panel tests drive a real backend through the same code. -import { spawn, type ChildProcess } from "node:child_process"; -import { EventEmitter } from "node:events"; - -export interface RpcError { - code: number; - message: string; - data?: unknown; -} - -export class BackendError extends Error { - constructor(public readonly method: string, public readonly error: RpcError) { - super(error.message); - } -} - -type Pending = { resolve: (v: unknown) => void; reject: (e: unknown) => void; method: string }; - -/** Splits a byte stream into the bodies of Content-Length frames. */ -export class FrameReader { - private buffer = Buffer.alloc(0); - - feed(chunk: Buffer): string[] { - this.buffer = Buffer.concat([this.buffer, chunk]); - const out: string[] = []; - for (;;) { - const headerEnd = this.buffer.indexOf("\r\n\r\n"); - if (headerEnd < 0) break; - const headers = this.buffer.subarray(0, headerEnd).toString("ascii"); - const match = /content-length:\s*(\d+)/i.exec(headers); - if (!match) { - this.buffer = this.buffer.subarray(headerEnd + 4); - continue; - } - const length = Number(match[1]); - if (this.buffer.length < headerEnd + 4 + length) break; - out.push(this.buffer.subarray(headerEnd + 4, headerEnd + 4 + length).toString("utf8")); - this.buffer = this.buffer.subarray(headerEnd + 4 + length); - } - return out; - } -} - -export function encode(message: unknown): Buffer { - const body = Buffer.from(JSON.stringify(message), "utf8"); - return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii"), body]); -} - -export interface BackendOptions { - program: string; - args?: string[]; - cwd?: string; - env?: NodeJS.ProcessEnv; - /** Lines the backend writes to its standard error (its log). */ - log?: (line: string) => void; -} - -/** - * One backend process. `request` sends a method and resolves with its result; `notifications` - * emits each notification by method name (and "*" with the method first). `exited` resolves when - * the process ends. - */ -export class Backend { - readonly notifications = new EventEmitter(); - readonly exited: Promise; - private readonly child: ChildProcess; - private readonly pending = new Map(); - private readonly reader = new FrameReader(); - private nextId = 1; - private alive = true; - - constructor(options: BackendOptions) { - this.notifications.setMaxListeners(0); - this.child = spawn(options.program, options.args ?? ["serve"], { - cwd: options.cwd, - env: options.env ?? process.env, - stdio: ["pipe", "pipe", "pipe"], - windowsHide: true, - }); - this.child.stdout!.on("data", (chunk: Buffer) => { - for (const body of this.reader.feed(chunk)) this.dispatch(body); - }); - let errText = ""; - this.child.stderr!.on("data", (chunk: Buffer) => { - errText += chunk.toString("utf8"); - let nl: number; - while ((nl = errText.indexOf("\n")) >= 0) { - options.log?.(errText.slice(0, nl)); - errText = errText.slice(nl + 1); - } - }); - this.exited = new Promise((resolve) => { - const done = (code: number | null) => { - if (!this.alive) return; - this.alive = false; - for (const p of this.pending.values()) p.reject(new BackendError(p.method, { code: -32099, message: "the mcpp-ide backend ended" })); - this.pending.clear(); - resolve(code); - }; - this.child.on("exit", (code) => done(code)); - this.child.on("error", (e) => { - options.log?.(`mcpp-ide could not start: ${e.message}`); - done(null); - }); - }); - } - - get running(): boolean { - return this.alive; - } - - request(method: string, params: unknown = {}): Promise { - if (!this.alive) return Promise.reject(new BackendError(method, { code: -32099, message: "the mcpp-ide backend is not running" })); - const id = this.nextId++; - return new Promise((resolve, reject) => { - this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject, method }); - this.child.stdin!.write(encode({ jsonrpc: "2.0", id, method, params })); - }); - } - - notify(method: string, params: unknown = {}): void { - if (this.alive) this.child.stdin!.write(encode({ jsonrpc: "2.0", method, params })); - } - - cancel(id: number): void { - this.notify("$/cancelRequest", { id }); - } - - async stop(): Promise { - if (!this.alive) return; - try { - await Promise.race([this.request("shutdown"), new Promise((r) => setTimeout(r, 2000))]); - } catch { - // ended already - } - this.notify("exit"); - this.child.stdin!.end(); - const ended = await Promise.race([this.exited.then(() => true), new Promise((r) => setTimeout(() => r(false), 3000))]); - if (!ended) this.child.kill(); - } - - private dispatch(body: string): void { - let message: { id?: number; method?: string; params?: unknown; result?: unknown; error?: RpcError }; - try { - message = JSON.parse(body); - } catch { - return; - } - if (message.id !== undefined && message.id !== null && (message.result !== undefined || message.error !== undefined)) { - const p = this.pending.get(message.id); - if (!p) return; - this.pending.delete(message.id); - if (message.error) p.reject(new BackendError(p.method, message.error)); - else p.resolve(message.result); - return; - } - if (message.method) { - this.notifications.emit(message.method, message.params); - this.notifications.emit("*", message.method, message.params); - } - } -} diff --git a/mide-plugins/vscode/src/ide/backend/locate.ts b/mide-plugins/vscode/src/ide/backend/locate.ts index 9638e12..3fd0cf2 100644 --- a/mide-plugins/vscode/src/ide/backend/locate.ts +++ b/mide-plugins/vscode/src/ide/backend/locate.ts @@ -91,3 +91,21 @@ export function locateBackend(options: { const found = onPath("mcpp-ide", env, platform); return found ? { program: found, args: [], source: "PATH" } : undefined; } + +/** + * The extension carries the backend, but okrun runs on neither this system nor PATH (macOS x86_64, + * Windows arm64, Linux armhf): the backend does not run here, and the extension goes on in its + * declared degraded mode (design 2026-10-30 part 2 §8, F15). + */ +export function backendUnavailableHere(options: { + extensionRoot: string; + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; + arch?: string; +}): boolean { + const platform = options.platform ?? process.platform; + const arch = options.arch ?? process.arch; + if (!isFile(path.join(options.extensionRoot, "backend", "mcpp-ide.okb"))) return false; + if (isFile(path.join(options.extensionRoot, "backend", "okrun", okrunDir(platform, arch), executable("okrun", platform)))) return false; + return onPath("okrun", options.env ?? process.env, platform) === undefined; +} diff --git a/mide-plugins/vscode/src/ide/session.ts b/mide-plugins/vscode/src/ide/session.ts index 5a4bdc9..bf142e5 100644 --- a/mide-plugins/vscode/src/ide/session.ts +++ b/mide-plugins/vscode/src/ide/session.ts @@ -1,20 +1,52 @@ /** * The session with the mcpp-ide backend (`mcpp-ide serve`): started at activation, given the * editor's settings, trust, language and folders, kept current as they change, and restarted (once, - * then on request) when it ends. The views' data adapters and the debugging, testing and packing - * flows reach the backend through `session.request`; the strings are this extension's own (`t`). + * then on request) when it ends. The backend is an LSP server (design 2026-10-30 part 2 §8): VS + * Code's language client starts it and connects to it, which brings mcpp.toml's and build.mcpp's + * completion, hover, jumps and diagnostics; the views' data adapters and the debugging, testing and + * packing flows reach mcpp-ide's own methods through `session.request`; the strings are this + * extension's own (`t`). */ import * as vscode from "vscode"; +import type * as lc from "vscode-languageclient/node"; import { t } from "../i18n/t"; -import { Backend, BackendError } from "./backend/client"; -import { locateBackend, type Located } from "./backend/locate"; +import { languageServerServes } from "../mcppls/stateSource"; +import { backendUnavailableHere, locateBackend, okrunDir, type Located } from "./backend/locate"; import { mcpplsCandidates } from "./backend/mcppls"; import { MCPPLS_EXTENSION } from "./mapping"; import type { CommandEntry, InitializeResult, LanguageServiceStatus } from "./protocol"; +// The language client, bundled into dist/vendor (tools/bundle-languageclient.mjs): the VSIX carries +// no node_modules. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const lsp = require("../../vendor/languageclient") as typeof lc; + +/** mcpp-ide's own methods and notifications are under `mcpp-ide/` beside LSP's. */ +const NAMESPACE = "mcpp-ide/"; + +export interface RpcError { + code: number; + message: string; + data?: unknown; +} + +export class BackendError extends Error { + constructor(public readonly method: string, public readonly error: RpcError) { + super(error.message); + } +} + +/** The documents the backend's rules layer reads: mcpp.toml, and build.mcpp (a `cpp` document). */ +export const DOCUMENT_SELECTOR = [ + { scheme: "file", language: "mcpp-toml" }, + { scheme: "file", language: "cpp", pattern: "**/build.mcpp" }, +]; + export class Session implements vscode.Disposable { - private backend: Backend | undefined; + private client: lc.LanguageClient | undefined; + /** The notifications the current client delivers to `listeners`. */ + private subscribed = new Set(); private located: Located | undefined; private initResult: InitializeResult | undefined; private commandTable: CommandEntry[] = []; @@ -37,17 +69,18 @@ export class Session implements vscode.Disposable { // The editor regained focus: a `xlings use` or a changed pin happened outside it (§4.2), or a // server started or stopped (§5.2). vscode.window.onDidChangeWindowState((state) => { - if (!state.focused || !this.backend?.running) return; - void this.backend.request("project/refresh", {}).then(() => this.onDidRefresh.fire(), () => undefined); + if (!state.focused || !this.running) return; + void this.request("project/refresh", {}).then(() => this.onDidRefresh.fire(), () => undefined); void this.refreshLanguageService(); }), vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("mcpp.ide.backendPath")) { void this.restart(); return; } - if (e.affectsConfiguration("mcpp")) void this.pushSettings(); + if (e.affectsConfiguration("mcpp") || e.affectsConfiguration("mcppls.enable")) void this.pushSettings(); }), vscode.workspace.onDidGrantWorkspaceTrust(() => void this.pushSettings()), vscode.workspace.onDidChangeWorkspaceFolders(() => void this.restart()), - vscode.extensions.onDidChange(() => void this.refreshLanguageService()), + // Whether C++ Modules serves build.mcpp: installed, enabled, or switched off for the workspace. + vscode.extensions.onDidChange(() => { void this.refreshLanguageService(); void this.pushSettings(); }), this.onDidChangeLanguageService, ); // mcppls answered differently, or a job ended (a build changes what the server serves). @@ -68,12 +101,12 @@ export class Session implements vscode.Disposable { * time, and none while the backend is not running (nothing is started to answer it). */ refreshLanguageService(root?: string): Promise { - if (!this.backend?.running) return Promise.resolve(this.languageServiceNow); + if (!this.running) return Promise.resolve(this.languageServiceNow); if (this.languageServiceAsking) return this.languageServiceAsking; const params: Record = { candidates: this.mcpplsCandidates() }; const folder = root ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (folder !== undefined) params.root = folder; - this.languageServiceAsking = this.backend + this.languageServiceAsking = this .request("languageService/status", params) .then((status) => { this.languageServiceNow = status; @@ -121,9 +154,30 @@ export class Session implements vscode.Disposable { on(method: string, fn: (params: any) => void): vscode.Disposable { if (!this.listeners.has(method)) this.listeners.set(method, new Set()); this.listeners.get(method)!.add(fn); + if (this.client) this.subscribe(this.client, method); return new vscode.Disposable(() => this.listeners.get(method)?.delete(fn)); } + private subscribe(client: lc.LanguageClient, method: string): void { + if (this.subscribed.has(method)) return; + this.subscribed.add(method); + client.onNotification(NAMESPACE + method, (params: unknown) => { + for (const fn of this.listeners.get(method) ?? []) { + try { fn(params); } catch (e) { this.output.appendLine(`mcpp: ${method}: ${String(e)}`); } + } + }); + } + + /** What the backend is told at the start and on each change. */ + private sessionOptions(): Record { + return { + locale: vscode.env.language, + trusted: vscode.workspace.isTrusted, + settings: this.settings(), + mcpplsServes: languageServerServes(), + }; + } + async start(): Promise { if (this.starting) return this.starting; this.starting = this.doStart().finally(() => { this.starting = undefined; }); @@ -133,6 +187,15 @@ export class Session implements vscode.Disposable { private async doStart(): Promise { const configured = vscode.workspace.getConfiguration().get("mcpp.ide.backendPath") ?? ""; this.located = locateBackend({ configured: vscode.workspace.isTrusted ? configured : "", extensionRoot: this.context.extensionPath }); + if (!this.located && backendUnavailableHere({ extensionRoot: this.context.extensionPath })) { + // The declared degraded mode: build, run, test and the project pages, without the backend. + this.degradedNow = true; + this.onDidDegrade.fire(); + const message = t("The mcpp-ide backend does not run on this platform ({0}); build, run, test and the project pages go on without it.", okrunDir()); + this.output.appendLine(`mcpp: ${message}`); + void vscode.window.showWarningMessage(`mcpp: ${message}`); + return false; + } if (!this.located) { const message = t("The mcpp-ide backend was not found. Install it with `xlings install mcpp-ide`, or set mcpp.ide.backendPath."); this.output.appendLine(`mcpp: ${message}`); @@ -143,31 +206,38 @@ export class Session implements vscode.Disposable { return false; } this.output.appendLine(`mcpp-ide: starting ${[this.located.program, ...this.located.args].join(" ")} (${this.located.source})`); - const backend = new Backend({ - program: this.located.program, + const client: lc.LanguageClient = new lsp.LanguageClient("mcpp-ide", "mcpp-ide", { + command: this.located.program, args: [...this.located.args, "serve"], - log: (line) => this.output.appendLine(`[backend] ${line}`), - }); - this.backend = backend; - backend.notifications.on("*", (method: string, params: unknown) => { - for (const fn of this.listeners.get(method) ?? []) { - try { fn(params); } catch (e) { this.output.appendLine(`mcpp: ${method}: ${String(e)}`); } - } - for (const fn of this.listeners.get("*") ?? []) fn({ method, params }); - }); - void backend.exited.then((code) => this.onExit(backend, code)); - try { - const folders = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath); - this.initResult = await backend.request("initialize", { - client: { name: "VS Code", version: vscode.version }, + transport: lsp.TransportKind.stdio, + }, { + documentSelector: DOCUMENT_SELECTOR, + initializationOptions: { editor: "vscode", - locale: vscode.env.language, - trusted: vscode.workspace.isTrusted, - settings: this.settings(), - workspaceFolders: folders, + ...this.sessionOptions(), languageService: { candidates: this.mcpplsCandidates() }, - }); - this.commandTable = (await backend.request<{ commands: CommandEntry[] }>("commands/list", { editor: "vscode" })).commands; + }, + // The backend's log (its standard error) and the client's own lines share the mcpp channel. + outputChannel: this.output, + revealOutputChannelOn: lsp.RevealOutputChannelOn.Never, + diagnosticCollectionName: "mcpp", + initializationFailedHandler: () => false, + // The restarts are §3.1's, not the client's: 1, 4 and 16 seconds, then the degraded mode. + errorHandler: { + error: () => ({ action: lsp.ErrorAction.Continue }), + closed: () => { + this.onExit(client); + return { action: lsp.CloseAction.DoNotRestart, handled: true }; + }, + }, + }); + this.client = client; + this.subscribed = new Set(); + for (const method of this.listeners.keys()) this.subscribe(client, method); + try { + await client.start(); + this.initResult = client.initializeResult as unknown as InitializeResult; + this.commandTable = (await this.request<{ commands: CommandEntry[] }>("commands/list", { editor: "vscode" })).commands; if (this.initResult.languageService) { this.languageServiceNow = this.initResult.languageService; this.onDidChangeLanguageService.fire(this.initResult.languageService); @@ -187,10 +257,10 @@ export class Session implements vscode.Disposable { * declared degraded mode (build, run, test and the project pages) with two actions: start the * backend again, and export the IDE report. */ - private onExit(backend: Backend, code: number | null): void { - if (backend !== this.backend) return; - this.backend = undefined; - this.output.appendLine(`mcpp-ide: the backend ended (${code ?? "signal"})`); + private onExit(client: lc.LanguageClient): void { + if (client !== this.client) return; + this.client = undefined; + this.output.appendLine("mcpp-ide: the backend ended"); const now = Date.now(); this.exits = [...this.exits.filter((at) => now - at < 3 * 60_000), now]; const delays = [1000, 4000, 16000]; @@ -214,37 +284,53 @@ export class Session implements vscode.Disposable { } async restart(): Promise { - const old = this.backend; - this.backend = undefined; - if (old) await old.stop(); + const old = this.client; + this.client = undefined; + if (old) await stopClient(old); this.exits = []; this.degradedNow = false; await this.start(); } async request(method: string, params: unknown = {}): Promise { - if (this.degradedNow && !this.backend?.running) { + if (this.degradedNow && !this.running) { throw new BackendError(method, { code: -32099, message: t("the mcpp-ide backend is not running") }); } - if (!this.backend?.running) { + if (!this.running && !this.starting) { if (!(await this.start())) throw new BackendError(method, { code: -32099, message: t("the mcpp-ide backend is not running") }); } - return this.backend!.request(method, params); + const client = this.client; + if (!client) throw new BackendError(method, { code: -32099, message: t("the mcpp-ide backend is not running") }); + try { + return await client.sendRequest(NAMESPACE + method, params); + } catch (e) { + if (e instanceof lsp.ResponseError) throw new BackendError(method, { code: e.code, message: e.message, data: e.data }); + throw e; + } } async pushSettings(): Promise { - if (!this.backend?.running) return; - await this.backend.request("settings/update", { settings: this.settings(), trusted: vscode.workspace.isTrusted, locale: vscode.env.language }); + if (!this.running) return; + await this.request("settings/update", this.sessionOptions()); } /** Whether a backend is running now (nothing is started to answer this). */ get running(): boolean { - return this.backend?.running === true; + return this.client?.isRunning() === true; } dispose(): void { this.onDidRefresh.dispose(); this.disposables.forEach((d) => d.dispose()); - void this.backend?.stop(); + if (this.client) void stopClient(this.client); + } +} + +/** Shutdown and exit, or the process ended, within a few seconds either way. */ +async function stopClient(client: lc.LanguageClient): Promise { + try { + await client.stop(3000); + } catch { + // it ended already } } diff --git a/mide-plugins/vscode/src/toml/completion.ts b/mide-plugins/vscode/src/toml/completion.ts deleted file mode 100644 index 6525aa3..0000000 --- a/mide-plugins/vscode/src/toml/completion.ts +++ /dev/null @@ -1,539 +0,0 @@ -// mcpp.toml 的代码补全查询层(schema 驱动版)。 -// -// 范围:段头结构建议 + 已知段的键/枚举值补全 + 开放词汇段的写法模板。每条建议 -// 携带显式替换范围。段/键/枚举来自 `data/toml-schema.json`(src/toml/schema.ts), -// 与诊断共用同一份快照:快照没有的段不会出现在段头表里,快照标记 `openKeys` -// 的段(键由用户自选)不出键建议。 -// -// 依赖版本补全(`mcpp.toml.indexCompletion`,默认关):本模块只负责 -// 「光标是不是在依赖版本值位置」与「把候选变成建议」两件纯事;执行 -// `mcpp search`、超时、会话缓存、信任/离线判定都在 src/toml/providers.ts, -// 解析人类输出在 src/cli/search.ts。本模块不依赖 vscode API:`t()` 只在 -// `auto` 路径上按需加载编辑器 API,纯单测环境下退化为英文 key。 - -import type { PackageVersion } from "../cli/search"; -import { t } from "../i18n/t"; -import { - contextAt, - parseMcppToml, - resolveSection, - type ReplaceRange, -} from "./parser"; -import { SCHEMA, sectionByName, type TomlKey, type TomlSection } from "./schema"; - -export type McppTomlSuggestionKind = "section" | "template" | "version"; - -export interface McppTomlSuggestion { - label: string; - kind: McppTomlSuggestionKind; - detail: string; - documentation?: string; - /** 插入文本;含 $1 等 snippet 占位符。缺省时插入 label。 */ - insertSnippet?: string; - /** 替换范围(光标所在行的起止列)。 */ - range: ReplaceRange; -} - -export interface SectionHeaderSpec { - group: string; - label: string; - /** snippet 形式的段头(含 ${1:...} 占位)。 */ - header: string; - /** - * 明细文案。存的是英文 key 的**取用函数**,每个补全请求才 `t()` 一次,所以 - * 改 `mcpp.ui.language` 无需重载窗口,且本模块在无编辑器的单测里也能加载。 - */ - detail: () => string; -} - -// 段头明细表:只补充「写法」(label/snippet/detail),不再是段清单本身。 -// 段清单来自 schema;这里按 group 名匹配,schema 新出现的段用 plane 兜底。 -// 出处:mcpp 文档 02/03/05/06 与 src/manifest/toml.cppm 的段清单(契约测试用 -// 真实 mcpp 逐段验证)。 -export const SECTION_HEADERS: readonly SectionHeaderSpec[] = [ - { group: "package", label: "[package]", header: "[package]", detail: () => t("Package metadata") }, - { group: "lib", label: "[lib]", header: "[lib]", detail: () => t("Library root-module convention") }, - { group: "build", label: "[build]", header: "[build]", detail: () => t("Build configuration") }, - { group: "generated_files", label: "[generated_files]", header: "[generated_files]", detail: () => t("Generated files (path → content)") }, - { group: "dependencies", label: "[dependencies]", header: "[dependencies]", detail: () => t("Runtime dependencies") }, - { group: "dev-dependencies", label: "[dev-dependencies]", header: "[dev-dependencies]", detail: () => t("Development/test dependencies") }, - { group: "build-dependencies", label: "[build-dependencies]", header: "[build-dependencies]", detail: () => t("Build-time dependencies (pulled at build time only, invisible at run time)") }, - { group: "workspace", label: "[workspace]", header: "[workspace]", detail: () => t("Workspace member declarations") }, - { group: "workspace.dependencies", label: "[workspace.dependencies]", header: "[workspace.dependencies]", detail: () => t("Declare dependency versions centrally; members inherit them with workspace = true") }, - { group: "features", label: "[features]", header: "[features]", detail: () => t("Feature definitions") }, - { group: "feature-deps", label: "[feature-deps.]", header: "[feature-deps.${1:name}]", detail: () => t("Optional dependencies pulled in by a feature") }, - { group: "capabilities", label: "[capabilities]", header: "[capabilities]", detail: () => t("Capability bindings (provider selection)") }, - { group: "targets", label: "[targets.]", header: "[targets.${1:name}]", detail: () => t("Build targets") }, - { group: "profile", label: "[profile.]", header: "[profile.${1:name}]", detail: () => t("Build profiles") }, - { group: "runtime", label: "[runtime]", header: "[runtime]", detail: () => t("Host runtime capabilities") }, - { group: "resources", label: "[resources]", header: "[resources]", detail: () => t("Metadata and assets compiled into the product (PE targets only)") }, - { group: "toolchain", label: "[toolchain]", header: "[toolchain]", detail: () => t("Compiler toolchain shorthand") }, - { group: "xlings", label: "[xlings]", header: "[xlings]", detail: () => t("Build environment (supplied by xlings)") }, - { group: "xlings.workspace", label: "[xlings.workspace]", header: "[xlings.workspace]", detail: () => t("Pin tool versions") }, - { group: "target", label: "[target.]", header: "[target.${1:x86_64-linux-gnu}]", detail: () => t("Configuration per target triple") }, - { group: "dependencies.", label: "[dependencies.]", header: "[dependencies.${1:mcpplibs}]", detail: () => t("Dependencies of one index namespace") }, - { group: "features.", label: "[features.]", header: "[features.${1:name}]", detail: () => t("A feature as a table") }, - { group: "target.cfg.build", label: "[target.'cfg(...)'.build]", header: "[target.'cfg(${1:os = \"linux\"})'.build]", detail: () => t("Build settings for the targets a selector matches") }, - { group: "target.cfg.dependencies", label: "[target.'cfg(...)'.dependencies]", header: "[target.'cfg(${1:os = \"linux\"})'.dependencies]", detail: () => t("Dependencies for the targets a selector matches") }, - { group: "pack", label: "[pack]", header: "[pack]", detail: () => t("mcpp pack packaging configuration") }, - { group: "pack.bundle-project", label: "[pack.bundle-project]", header: "[pack.bundle-project]", detail: () => t("Fine-tuning of the vendored filtering policy") }, - { group: "indices", label: "[indices]", header: "[indices]", detail: () => t("Project-level index redirection") }, - { group: "tools.overrides", label: "[tools.overrides]", header: "[tools.overrides]", detail: () => t("Host tool binary overrides") }, - { group: "language", label: "[language]", header: "[language]", detail: () => t("Legacy compatibility field; new projects should use [package].standard") }, -]; - -/** 依赖类段(键位置给依赖写法模板)。 */ -const DEPENDENCY_GROUPS: ReadonlySet = new Set([ - "dependencies", - "dev-dependencies", - "build-dependencies", - "workspace.dependencies", - "feature-deps", -]); - -interface TemplateSpec { - label: string; - /** 同 `SectionHeaderSpec.detail`:取用时才 `t()`,语言切换无需重载。 */ - detail: () => string; - documentation?: () => string; - insertSnippet: string; -} - -const DEPENDENCY_TEMPLATES: readonly TemplateSpec[] = [ - { - label: 'name = "version"', - detail: () => t("SemVer version dependency"), - documentation: () => t("Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported."), - insertSnippet: '${1:name} = "${2:1.0.0}"', - }, - { - label: "name = { path = ... }", - detail: () => t("Path dependency (local development)"), - insertSnippet: '${1:name} = { path = "${2:../mylib}" }', - }, - { - label: "name = { git = ..., tag = ... }", - detail: () => t("Git dependency (one of tag / branch / rev)"), - insertSnippet: '${1:name} = { git = "${2:https://github.com/user/repo.git}", tag = "${3:v1.0.0}" }', - }, - { - label: "name = { version = ..., features = [...] }", - detail: () => t("Long dep spec: request a feature of that dependency"), - insertSnippet: '${1:name} = { version = "${2:1.0}", features = ["${3:feature}"] }', - }, - { - label: "name = { version = ..., tools = [...] }", - detail: () => t("Host tools produced by the dependency (must be a bin target of that package)"), - insertSnippet: '${1:name} = { version = "${2:1.0}", tools = ["${3:protoc}"] }', - }, -]; - -const FEATURE_TEMPLATES: readonly TemplateSpec[] = [ - { label: "name = [...]", detail: () => t("Array shorthand: implies the feature only"), insertSnippet: "${1:name} = [${2}]" }, - { label: "name = { defines = [...] }", detail: () => t("Table form: contributes the package's own macros when activated"), insertSnippet: '${1:name} = { defines = ["${2:MACRO}"] }' }, - { label: "name = { requires = [...] }", detail: () => t("Table form: requires a capability"), insertSnippet: '${1:name} = { requires = ["${2:blas}"] }' }, - { label: "name = { sources = [...] }", detail: () => t("Table form: feature-gated source globs"), insertSnippet: '${1:name} = { sources = ["${2:src/simd/**}"] }' }, -]; - -const GENERATED_FILE_TEMPLATES: readonly TemplateSpec[] = [ - { - label: '"path" = "content"', - detail: () => t("Generated file (relative path → content, part of the fingerprint)"), - insertSnippet: '"${1:src/gen/wrap.cppm}" = """\n${2:}\n"""', - }, -]; - -const CAPABILITY_TEMPLATES: readonly TemplateSpec[] = [ - { - label: 'capability = "provider"', - detail: () => t("Capability binding (equivalent to --cap)"), - insertSnippet: '${1:blas} = "${2:compat.openblas}"', - }, -]; - -const XLINGS_WORKSPACE_TEMPLATES: readonly TemplateSpec[] = [ - { label: 'tool = "version"', detail: () => t("Pin the xlings tool version"), insertSnippet: '${1:node} = "${2:24.19.0}"' }, -]; - -const TOOLS_OVERRIDES_TEMPLATES: readonly TemplateSpec[] = [ - { - label: '"pkg:tool" = "path"', - detail: () => t("Override a host tool with an existing binary (skips the build)"), - insertSnippet: '"${1:compat.protobuf:protoc}" = "${2:/usr/bin/protoc}"', - }, -]; - -const TEMPLATES_BY_GROUP: Record = { - "features": FEATURE_TEMPLATES, - "generated_files": GENERATED_FILE_TEMPLATES, - "capabilities": CAPABILITY_TEMPLATES, - "xlings.workspace": XLINGS_WORKSPACE_TEMPLATES, - "tools.overrides": TOOLS_OVERRIDES_TEMPLATES, -}; - -/** 手写明细按 group 索引;schema 段只借它取 label/snippet/detail。 */ -const HEADER_SPEC_BY_GROUP: ReadonlyMap = new Map( - SECTION_HEADERS.map((spec) => [spec.group, spec]), -); - -interface HeaderEntry { - label: string; - header: string; - detail: string; - documentation?: string; -} - -/** - * 段头清单:以 schema 为准(§3.3.2)。schema 为空(快照未生成)时才退回手写 - * 清单,保证补全在缺数据时仍可用。手写明细按段名匹配以保留既有 label/snippet/ - * 文案;schema 新收录的段用平面名兜底。 - */ -function headerEntries(): HeaderEntry[] { - if (SCHEMA.sections.length === 0) { - return SECTION_HEADERS.map((spec) => ({ - label: spec.label, - header: spec.header, - detail: spec.detail(), - })); - } - // The snapshot describes the schema mcpp validates; the curated list also covers - // tables mcpp accepts but the snapshot does not model (for example - // `[workspace.dependencies]`). Merge them, preferring the curated wording, so - // schema-driven detail never costs a writable table its suggestion. - const entries: HeaderEntry[] = SCHEMA.sections.map((section) => { - // A table written only under a selector (`[target..abi]`) is offered with the - // selector to fill in. - if ((section as { selector?: boolean }).selector === true) { - const tail = section.header.slice(section.header.indexOf("") + "".length); - return { - label: `[target.'cfg(...)'${tail}`, - header: `[target.'cfg(\${1:os = "linux"})'${tail}`, - detail: `plane: ${section.plane}`, - }; - } - const spec = HEADER_SPEC_BY_GROUP.get(section.name); - const entry: HeaderEntry = { - label: spec?.label ?? section.header, - header: spec?.header ?? section.header, - detail: spec?.detail() ?? `plane: ${section.plane}`, - }; - if (section.deprecatedBy) { - entry.documentation = `Deprecated; use \`${section.deprecatedBy}\`.`; - } - return entry; - }); - const suggested = new Set(entries.map((entry) => entry.header)); - for (const spec of SECTION_HEADERS) { - if (suggested.has(spec.header)) { - continue; - } - entries.push({ label: spec.label, header: spec.header, detail: spec.detail() }); - } - return entries; -} - -function headerSuggestions(range: ReplaceRange): McppTomlSuggestion[] { - return headerEntries().map((entry) => { - const suggestion: McppTomlSuggestion = { - label: entry.label, - kind: "section", - detail: entry.detail, - insertSnippet: entry.header, - range, - }; - if (entry.documentation !== undefined) { - suggestion.documentation = entry.documentation; - } - return suggestion; - }); -} - -function templateSuggestions(templates: readonly TemplateSpec[], range: ReplaceRange): McppTomlSuggestion[] { - return templates.map((template) => ({ - label: template.label, - kind: "template", - detail: template.detail(), - documentation: template.documentation?.(), - insertSnippet: template.insertSnippet, - range, - })); -} - -/** 类型的占位值:与 §3.3.2 的键补全约定一致。 */ -function keyPlaceholder(key: TomlKey): string { - switch (key.type) { - case "boolean": - return "true"; - case "number": - return "0"; - case "array": - return "[]"; - case "table": - return "{ }"; - case "enum": - return JSON.stringify(key.values?.[0] ?? ""); - default: - return '""'; - } -} - -/** `detail` 展示类型;枚举展开取值,有默认值再补默认值。 */ -function keyDetail(key: TomlKey): string { - let detail = key.type; - if (key.values !== undefined && key.values.length > 0) { - detail += `: ${key.values.join(" | ")}`; - } - if (key.default !== undefined) { - detail += ` (default: ${JSON.stringify(key.default)})`; - } - return detail; -} - -function keyDocumentation(key: TomlKey): string | undefined { - const lines: string[] = []; - if (key.note !== undefined && key.note !== "") { - lines.push(key.note); - } - if (key.since !== undefined) { - lines.push(`Since ${key.since}.`); - } - if (key.legacy === true) { - lines.push("Legacy key."); - } - return lines.length === 0 ? undefined : lines.join(" "); -} - -function keySuggestions( - section: TomlSection, - used: ReadonlySet, - range: ReplaceRange, -): McppTomlSuggestion[] { - const keys = section.keys ?? []; - return keys - .filter((key) => !used.has(key.key)) - .map((key) => { - const suggestion: McppTomlSuggestion = { - label: key.key, - kind: "template", - detail: keyDetail(key), - insertSnippet: `${key.key} = ${keyPlaceholder(key)}`, - range, - }; - const documentation = keyDocumentation(key); - if (documentation !== undefined) { - suggestion.documentation = documentation; - } - return suggestion; - }); -} - -/** - * 同段中光标之前已出现的顶层键。用容错解析器遍历节点树:`[targets.]` 这类 - * 行表按 parser 的组归属聚合,因此同组的不同行表会互相剔除已用键。 - */ -function keysUsedBefore(lines: readonly string[], line: number, group: string): Set { - const used = new Set(); - let current: string | undefined; - for (const node of parseMcppToml(lines).nodes) { - if (node.type === "section") { - const resolution = resolveSection(node.segments.map((segment) => segment.name)); - current = resolution.kind === "known" ? resolution.group : undefined; - continue; - } - if (current !== group || node.range.startLine >= line) { - continue; - } - const first = node.keyPath[0]; - if (first !== undefined) { - used.add(first.name); - } - } - return used; -} - -/** 值位置的枚举键:点分键按完整路径查,找不到退回首段(与诊断一致)。 */ -function enumKeyOf(section: TomlSection | undefined, keyPath: readonly string[]): TomlKey | undefined { - if (section?.keys === undefined || keyPath.length === 0) { - return undefined; - } - const joined = keyPath.join("."); - const key = - section.keys.find((entry) => entry.key === joined) ?? - section.keys.find((entry) => entry.key === keyPath[0]); - return key?.type === "enum" ? key : undefined; -} - -function enumValueSuggestions( - key: TomlKey, - insideString: boolean, - range: ReplaceRange, -): McppTomlSuggestion[] { - return (key.values ?? []).map((value) => ({ - label: value, - kind: "template", - detail: `value of ${key.key}`, - // 字符串内只替换内容;裸值位置补上引号,枚举值在 manifest 里都是字符串。 - insertSnippet: insideString ? value : JSON.stringify(value), - range, - })); -} - -/** - * 计算 mcpp.toml 在指定位置的补全建议(段头 + 键 + 枚举值 + 写法模板)。 - */ -export function computeMcppTomlCompletions( - lines: readonly string[], - line: number, - character: number, -): McppTomlSuggestion[] { - const context = contextAt(lines, line, character); - - if (context.kind === "section-header") { - // mcpp manifest 不使用 TOML 数组表([[...]]);[[ 内不提供建议, - // 避免把用户意图的数组表悄悄替换成普通段 [x](未知段会被 mcpp 静默忽略)。 - if (context.isArray) { - return []; - } - // parser 的替换范围从段名 token 开始;段头建议插入的是完整 "[xxx]", - // 需要把范围扩展到本行的 "[",避免留下 "[["。仅当 "[" 是行内首个 - // 非空白字符时才扩展(section-header 上下文正常都满足,防御奇怪输入)。 - const lineText = (lines[line] ?? "").replace(/\r$/, ""); - const bracket = lineText.indexOf("["); - const firstNonWs = lineText.search(/\S/); - const range = bracket >= 0 && bracket === firstNonWs - ? { startCharacter: bracket, endCharacter: context.replaceRange.endCharacter } - : context.replaceRange; - return headerSuggestions(range); - } - - if (context.kind === "key") { - const { section, containerPath, replaceRange } = context; - // 文档顶部(尚无段头):提示段头。未知段:不提供建议 - // (附录 A:不支持包自定义 toml 键)。 - if (section.kind === "top") { - return headerSuggestions(replaceRange); - } - if (section.kind !== "known" || containerPath.length > 0) { - return []; - } - if (DEPENDENCY_GROUPS.has(section.group)) { - return templateSuggestions(DEPENDENCY_TEMPLATES, replaceRange); - } - const schemaSection = sectionByName(section.group); - // openKeys 段的键由用户自选(如 [toolchain] 的平台名),不能给固定词表。 - if (schemaSection?.openKeys !== true && schemaSection?.keys !== undefined) { - const used = keysUsedBefore(lines, line, section.group); - return keySuggestions(schemaSection, used, replaceRange); - } - const templates = TEMPLATES_BY_GROUP[section.group]; - return templates === undefined ? [] : templateSuggestions(templates, replaceRange); - } - - // 值位置:只有已知枚举键才出候选,其余自由格式值不瞎猜。 - if (context.section.kind === "known") { - const schemaSection = sectionByName(context.section.group); - const key = enumKeyOf(schemaSection, context.keyPath); - if (key !== undefined) { - return enumValueSuggestions(key, context.insideString, context.replaceRange); - } - } - return []; -} - -/** 光标所在的依赖版本值位置:属于哪个依赖、替换范围、是否已在字符串内。 */ -export interface DependencyVersionContext { - /** 依赖名(值所属的键,如 `zlib`)。 */ - name: string; - /** 光标已在引号内(只替换内容,不再补引号)。 */ - insideString: boolean; - /** 替换范围:简写覆盖 `= ` 之后的值,长式在字符串内只覆盖已输入内容。 */ - range: ReplaceRange; -} - -/** - * 判断光标是否落在依赖的**版本值**上(方案 §3.3.2 的 `toml.indexCompletion` - * 位置)。只认两种写法: - * - * `zlib = "1.2|"` 简写版本 - * `zlib = { version = "1.2|" }` 长式 dep spec 的 version 字段 - * - * `git` / `path` / `features` 的值不是版本,返回 undefined,绝不据此查询索引。 - */ -export function dependencyVersionContextAt( - lines: readonly string[], - line: number, - character: number, -): DependencyVersionContext | undefined { - const context = contextAt(lines, line, character); - if (context.kind !== "value" || context.section.kind !== "known") { - return undefined; - } - if (!DEPENDENCY_GROUPS.has(context.section.group) || context.keyPath.length === 0) { - return undefined; - } - const last = context.keyPath[context.keyPath.length - 1]; - if (context.keyPath.length > 1 && last !== "version") { - return undefined; - } - // A version is a string, or a value not typed yet. An inline table, an array - // or a bare non-version token is a different dep-spec field; offering versions - // there would replace the wrong thing. - const kind = context.valueKind; - if (kind !== undefined && kind !== "string" && kind !== "unknown") { - return undefined; - } - return { - name: context.keyPath[0], - insideString: context.insideString, - range: context.replaceRange, - }; -} - -/** 把索引候选变成补全建议;字符串内只替换内容,裸值位置补上引号。 */ -export function indexVersionSuggestions( - context: DependencyVersionContext, - versions: readonly PackageVersion[], -): McppTomlSuggestion[] { - return versions.map((entry) => { - const suggestion: McppTomlSuggestion = { - label: entry.version, - kind: "version", - detail: - entry.summary === undefined ? `version of ${context.name}` : `${context.name}: ${entry.summary}`, - insertSnippet: context.insideString ? entry.version : JSON.stringify(entry.version), - range: context.range, - }; - return suggestion; - }); -} - -/** 取索引候选:按依赖名查询,失败与超时都由调用方降级为空数组。 */ -export type IndexVersionResolver = (name: string) => Promise; - -/** - * 静态补全 + 依赖版本补全的合并入口。静态层有建议就返回它;否则若光标在依赖 - * 版本值上且调用方给了 resolver,就询问索引。resolver 抛异常时静默降级为 - * 「无候选」,绝不冒泡成错误。 - */ -export async function computeMcppTomlCompletionsWithIndex( - lines: readonly string[], - line: number, - character: number, - resolveIndex?: IndexVersionResolver, -): Promise { - const direct = computeMcppTomlCompletions(lines, line, character); - if (direct.length > 0 || resolveIndex === undefined) { - return direct; - } - const context = dependencyVersionContextAt(lines, line, character); - if (context === undefined) { - return direct; - } - let versions: readonly PackageVersion[]; - try { - versions = await resolveIndex(context.name); - } catch { - return direct; - } - return indexVersionSuggestions(context, versions); -} diff --git a/mide-plugins/vscode/src/toml/diagnostics.ts b/mide-plugins/vscode/src/toml/diagnostics.ts deleted file mode 100644 index eec85ad..0000000 --- a/mide-plugins/vscode/src/toml/diagnostics.ts +++ /dev/null @@ -1,634 +0,0 @@ -// mcpp.toml 的纯文本诊断层。 -// -// 输入是文件的物理行 + 严重度配置,输出是 1 基行列(列按 UTF-16 码元计数, -// 与 VS Code 的 Diagnostic 一致)的诊断列表。本模块不依赖 vscode API, -// 可在 node --test 下直接测试。 -// -// 它**不是**一个 TOML 校验器:只理解一个保守子集——段头、`key = value`、 -// 行注释、单/双引号字符串、三引号多行字符串,以及可跨行的数组/内联表—— -// 目的是抓住明显写坏的行与 mcpp 专属的平面/废弃规则,而不是重实现 TOML。 -// 子集之外的一切一律不报(宁可漏报,不可误报)。下面每个规则都注明它停在哪里。 -// -// 规则与严重度(默认值见 data/toml-schema.json 的 rules): -// 1 syntax 明显的语法破坏 -// 2 unknown-section 段不在 schema 中 -// 3 unknown-key 已知段中的未知键 -// 4 plane-separation 工具当依赖 / 包当工具 -// 5 mcpp-floor [package].mcpp 不是 ">=" 形式 -// 6 legacy-key legacy 段/键 -// 7 array-table 数组表 [[...]](mcpp 只接受少数几处,见下) - -import { SCHEMA, sectionByName, type TomlKey, type TomlSection } from "./schema"; - -export type Severity = "error" | "warning" | "info" | "off"; - -export interface TomlDiagnostic { - code: string; - message: string; - severity: Exclude; - /** 1 基行号。 */ - line: number; - /** 1 基起始列(含)。 */ - startCharacter: number; - /** 1 基结束列(不含)。 */ - endCharacter: number; - relatedKey?: string; -} - -export interface DiagnosticSettings { - syntax: Severity; - unknownSection: Severity; - unknownKey: Severity; - planeSeparation: Severity; - legacyKeys: Severity; -} - -/** - * `mcppFloor` 与 `arrayTable` 在设置面上还没有独立开关(§4 的 registry 尚未 - * 收录),因此默认是 error;一旦设置面补齐,传入同名字段即可覆盖,无需改签名。 - */ -type ExtendedSettings = DiagnosticSettings & { - mcppFloor?: Severity; - arrayTable?: Severity; -}; - -/** 稳定的诊断码。补全/快速修复按它分发。 */ -export const DIAGNOSTIC_CODES = { - syntax: "mcpp.manifest.syntax", - unknownSection: "mcpp.manifest.unknownSection", - unknownKey: "mcpp.manifest.unknownKey", - planeSeparation: "mcpp.manifest.planeSeparation", - mcppFloor: "mcpp.manifest.mcppFloor", - legacyKey: "mcpp.manifest.legacyKey", - arrayTable: "mcpp.manifest.arrayTable", -} as const; - -type RuleName = keyof typeof DIAGNOSTIC_CODES; - -// ── TOML 子集的扫描状态 ───────────────────────────────────────────────────── -// mode: 当前是否在一个未闭合的三引号字符串里。 -// depth: 未闭合的 `[` / `{` 层数(数组与内联表可跨行)。 -interface ParseState { - mode: "normal" | "ml-double" | "ml-single"; - depth: number; -} - -/** text[i] 之前的反斜杠是否为奇数个(三引号串里的 \"\"\" 不结束)。 */ -function isEscaped(text: string, index: number): boolean { - let backslashes = 0; - for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) { - backslashes += 1; - } - return backslashes % 2 === 1; -} - -/** - * 扫描一行,更新跨行状态;返回该行中「真正的注释起始 `#`」的下标(没有则 -1)。 - * 引号内的 `#` 不是注释。这是本模块对 TOML 词法理解的边界:不处理转义还原、 - * 不处理 \uXXXX、不看值是否合法。 - */ -function scan(text: string, state: ParseState): number { - let i = 0; - while (i < text.length) { - if (state.mode === "ml-double") { - if (text.startsWith('"""', i) && !isEscaped(text, i)) { - state.mode = "normal"; - i += 3; - continue; - } - i += 1; - continue; - } - if (state.mode === "ml-single") { - if (text.startsWith("'''", i)) { - state.mode = "normal"; - i += 3; - continue; - } - i += 1; - continue; - } - - const ch = text[i]; - if (ch === "#") { - return i; - } - if (ch === '"') { - if (text.startsWith('"""', i)) { - state.mode = "ml-double"; - i += 3; - continue; - } - i += 1; - while (i < text.length) { - if (text[i] === "\\") { - i += 2; - continue; - } - if (text[i] === '"') { - i += 1; - break; - } - i += 1; - } - continue; - } - if (ch === "'") { - if (text.startsWith("'''", i)) { - state.mode = "ml-single"; - i += 3; - continue; - } - i += 1; - while (i < text.length) { - if (text[i] === "'") { - i += 1; - break; - } - i += 1; - } - continue; - } - if (ch === "[" || ch === "{") { - state.depth += 1; - i += 1; - continue; - } - if (ch === "]" || ch === "}") { - if (state.depth > 0) state.depth -= 1; - i += 1; - continue; - } - i += 1; - } - return -1; -} - -/** 去掉行尾注释(引号内的 `#` 保留),返回可解析的代码片段。 */ -function codeBeforeComment(text: string): string { - const stop = scan(text, { mode: "normal", depth: 0 }); - return stop < 0 ? text : text.slice(0, stop); -} - -/** 在引号之外查找 needle 的下标;找不到返回 -1。 */ -function indexOfUnquoted(text: string, needle: string): number { - let i = 0; - while (i < text.length) { - const ch = text[i]; - if (ch === '"' || ch === "'") { - const delim = ch.repeat(3); - if (!text.startsWith(delim, i)) { - i += 1; - while (i < text.length) { - if (ch === '"' && text[i] === "\\") { - i += 2; - continue; - } - if (text[i] === ch) { - i += 1; - break; - } - i += 1; - } - continue; - } - i += 3; - while (i < text.length) { - if (text.startsWith(delim, i) && !isEscaped(text, i)) { - i += 3; - break; - } - i += 1; - } - continue; - } - if (text.startsWith(needle, i)) { - return i; - } - i += 1; - } - return -1; -} - -/** 按点拆分段路径/点分键,引号内的点不拆。保留引号。 */ -function splitDots(text: string): string[] { - const parts: string[] = []; - let current = ""; - let quote: string | null = null; - for (let i = 0; i < text.length; i += 1) { - const ch = text[i]; - if (quote !== null) { - current += ch; - if (ch === quote) { - quote = null; - } - continue; - } - if (ch === '"' || ch === "'") { - quote = ch; - current += ch; - continue; - } - if (ch === ".") { - parts.push(current); - current = ""; - continue; - } - current += ch; - } - parts.push(current); - return parts; -} - -function isBareKey(text: string): boolean { - return /^[A-Za-z0-9_-]+$/.test(text); -} - -function isQuotedKey(text: string): boolean { - if (text.length < 2) { - return false; - } - const first = text[0]; - const last = text[text.length - 1]; - return (first === '"' && last === '"') || (first === "'" && last === "'"); -} - -/** `a.b` / `"a b"` / `c."d.e"` 是合法键;其余交给语法规则。 */ -function isValidKey(text: string): boolean { - const parts = splitDots(text); - return parts.length > 0 && parts.every((part) => isBareKey(part) || isQuotedKey(part)); -} - -/** 去掉一层引号;不是带引号的字符串则返回 undefined(不做转义还原)。 */ -function unquote(text: string): string | undefined { - if (text.length >= 2) { - const first = text[0]; - const last = text[text.length - 1]; - if ((first === '"' && last === '"') || (first === "'" && last === "'")) { - return text.slice(1, -1); - } - } - return undefined; -} - -// ── schema 侧的段解析 ─────────────────────────────────────────────────────── - -/** 精确匹配,失败时逐级去掉尾部段(`[target.'cfg(x)'.dependencies]` → `target`)。 */ -function lookupSection(name: string): TomlSection | undefined { - const exact = sectionByName(name); - if (exact !== undefined) { - return exact; - } - const parts = splitDots(name); - for (let i = parts.length - 1; i >= 1; i -= 1) { - const candidate = sectionByName(parts.slice(0, i).join(".")); - if (candidate !== undefined) { - return candidate; - } - } - return undefined; -} - -/** - * 键规则要用的段:`[targets.]`、`[profile.]`、`[target.]` 这三种 - * 「行表」继承基段的键表,更深一层(`[package.metadata.]`、 - * `[target..build]`、`[runtime.]`、`[toolchain.]`)的键 - * 属于另一套词表,不做未知键检查。 - */ -const ROW_KEY_SECTIONS: ReadonlySet = new Set(["targets", "profile", "target"]); - -function keySectionOf(headerName: string): TomlSection | undefined { - const exact = sectionByName(headerName); - if (exact !== undefined) { - return exact; - } - const parts = splitDots(headerName); - if (parts.length === 2 && ROW_KEY_SECTIONS.has(parts[0])) { - return sectionByName(parts[0]); - } - return undefined; -} - -/** 键在段里的元数据;点分键(`metadata.demo`)按首段查找。 */ -function keyInfoOf(section: TomlSection | undefined, key: string): TomlKey | undefined { - if (section?.keys === undefined) { - return undefined; - } - const parts = splitDots(key); - const direct = section.keys.find((entry) => entry.key === key); - if (direct !== undefined) { - return direct; - } - if (parts.length > 1) { - return section.keys.find((entry) => entry.key === parts[0]); - } - return undefined; -} - -// ── mcpp 专属规则用的判定 ─────────────────────────────────────────────────── - -const DEPENDENCY_TABLES: ReadonlySet = new Set([ - "dependencies", - "dev-dependencies", - "build-dependencies", -]); - -function lastSegment(name: string): string { - const parts = splitDots(name); - return parts[parts.length - 1]; -} - -function isDependencySection(name: string): boolean { - return DEPENDENCY_TABLES.has(lastSegment(name)); -} - -/** `[xlings]`、`[xlings.workspace]` 及其条件形式 `[target..xlings…]`。 */ -function isXlingsSection(name: string): boolean { - return splitDots(name).includes("xlings"); -} - -/** 工具载荷名:`xim:` / `xpm:` / `xlings:` / `xvm:` 前缀。 */ -function isToolPayloadName(key: string): boolean { - const bare = unquote(key) ?? key; - return /^(xim|xpm|xlings|xvm):/i.test(bare); -} - -/** mcpp 包身份形如 `namespace.name`(至少一个点),不是工具载荷。 */ -function isMcppPackageName(key: string): boolean { - const bare = unquote(key) ?? key; - if (isToolPayloadName(bare)) { - return false; - } - return /^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)+$/.test(bare); -} - -/** - * 值是否陈述了一个版本而不是一个路径。`"1.0"` / `{ version = "1.0" }` 是; - * `{ path = "../x" }`、`""`(平台键对象)、`{ linux = "" }` 不是。 - */ -function declaresVersion(valueText: string): boolean { - const text = valueText.trim(); - if (text.startsWith("{")) { - return /(^|[,{\s])version\s*=/.test(text) && !/(^|[,{\s])path\s*=/.test(text); - } - const bare = unquote(text); - if (bare === undefined || bare === "") { - return false; - } - return !bare.includes("/") && !bare.startsWith("."); -} - -/** - * mcpp 允许的数组表(`[[...]]`)。这份名单来自 toml.cppm 的 - * `kAllowedArraysOfTables`:除这些之外,任何 `[[...]]` 都是 mcpp 不接受的写法。 - * - * 注意:任务书写的是「`[[name]]`,mcpp 不使用」,但 mcpp 明确接受 - * `[[build.flags]]`、`[[features..flags]]`、`[[runtime.requirements]]` 等 - * 少数数组表,把它们报错就是误报——而本层的第一原则是不误报。 - */ -const ALLOWED_ARRAY_TABLES: ReadonlyArray> = [ - ["build", "flags"], - ["build", "sources"], - ["features", "*", "flags"], - ["runtime", "requirements"], - ["runtime", "artifacts"], - ["runtime", "deploy"], - ["target", "*", "build", "flags"], - ["target", "*", "build", "sources"], - ["xlings", "deps"], -]; - -/** The schema's own arrays of tables (`[[platform.link.inputs]]`, `[[runtime.artifacts]]`). */ -const SCHEMA_ARRAY_TABLES: ReadonlyArray> = SCHEMA.sections - .filter((section) => section.header.startsWith("[[")) - .map((section) => splitDots(section.name)); - -function isAllowedArrayTable(name: string): boolean { - const parts = splitDots(name); - return [...ALLOWED_ARRAY_TABLES, ...SCHEMA_ARRAY_TABLES].some( - (pattern) => - pattern.length === parts.length && pattern.every((part, index) => part === "*" || part === parts[index]), - ); -} - -/** docs/04 §2.1:只有 `>=` 形式的下界被接受。 */ -function isMcppFloor(valueText: string): boolean { - const bare = unquote(valueText.trim()); - return bare !== undefined && /^>=\d+(\.\d+)+$/.test(bare); -} - -// ── 入口 ──────────────────────────────────────────────────────────────────── - -/** - * 分析一份 mcpp.toml。`lines` 是文件的物理行(含或不含 `\r` 都可以); - * 返回按 (line, startCharacter, code) 排序的稳定诊断列表。 - */ -export function analyseManifest( - lines: readonly string[], - settings: DiagnosticSettings, -): TomlDiagnostic[] { - const extended = settings as ExtendedSettings; - const severity: Record = { - syntax: settings.syntax, - unknownSection: settings.unknownSection, - unknownKey: settings.unknownKey, - planeSeparation: settings.planeSeparation, - mcppFloor: extended.mcppFloor ?? "error", - legacyKey: settings.legacyKeys, - arrayTable: extended.arrayTable ?? "error", - }; - - const diagnostics: TomlDiagnostic[] = []; - - function push( - rule: RuleName, - message: string, - line: number, - startCharacter: number, - endCharacter: number, - relatedKey?: string, - ): void { - const level = severity[rule]; - if (level === "off") { - return; - } - const diagnostic: TomlDiagnostic = { - code: DIAGNOSTIC_CODES[rule], - message, - severity: level, - line, - startCharacter, - endCharacter, - }; - if (relatedKey !== undefined) { - diagnostic.relatedKey = relatedKey; - } - diagnostics.push(diagnostic); - } - - const state: ParseState = { mode: "normal", depth: 0 }; - /** 当前段头的原始名字(未解析到 schema 的时刻也保留,用于平面规则)。 */ - let headerName: string | undefined; - /** 当前段解析到的 schema 段(键规则用;未知段为 undefined)。 */ - let section: TomlSection | undefined; - - for (let index = 0; index < lines.length; index += 1) { - const raw = lines[index].endsWith("\r") ? lines[index].slice(0, -1) : lines[index]; - const lineNumber = index + 1; - - // 多行字符串/数组的续行:整行属于上一个结构,不产生诊断。 - if (state.mode !== "normal" || state.depth > 0) { - scan(raw, state); - continue; - } - - const code = codeBeforeComment(raw); - const text = code.trim(); - if (text === "") { - scan(raw, state); - continue; - } - // 行首空白宽度。 - const lead = code.length - code.trimStart().length; - const lineStart = lead + 1; - const lineEnd = lead + text.length + 1; - - // ── 段头 ── - if (text.startsWith("[")) { - const arrayHeader = /^\[\[(.+?)\]\]$/.exec(text); - const plainHeader = /^\[(.+?)\]$/.exec(text); - if (arrayHeader === null && plainHeader === null) { - // `[` / `[foo` / `[foo] bar`:不是合法段头,也不是 key = value。 - push("syntax", `not a [section] header: ${text}`, lineNumber, lineStart, lineEnd); - scan(raw, state); - continue; - } - - const isArray = arrayHeader !== null; - const name = ((isArray ? arrayHeader : plainHeader)?.[1] ?? "").trim(); - headerName = name; - section = keySectionOf(name); - - if (isArray) { - // 规则 7:mcpp 只接受少数数组表;其余是用户写错的 `[[...]]`。 - if (!isAllowedArrayTable(name)) { - push( - "arrayTable", - `[[${name}]] is an array of tables, which mcpp does not use here`, - lineNumber, - lineStart, - lineEnd, - name, - ); - } - // 规则 2 明确忽略数组表。 - } else { - // 规则 2:未知段(允许点分名逐级回退到已知前缀段)。 - if (lookupSection(name) === undefined) { - push("unknownSection", `unknown section [${name}]`, lineNumber, lineStart, lineEnd, name); - } else if (section?.deprecatedBy) { - // 规则 6:段级 legacy。 - push( - "legacyKey", - `[${name}] is deprecated; use ${section.deprecatedBy}`, - lineNumber, - lineStart, - lineEnd, - section.deprecatedBy, - ); - } - } - scan(raw, state); - continue; - } - - // ── 键值行 ── - const equals = indexOfUnquoted(text, "="); - if (equals <= 0) { - push("syntax", `not a [section] header or key = value: ${text}`, lineNumber, lineStart, lineEnd); - scan(raw, state); - continue; - } - const key = text.slice(0, equals).trim(); - const value = text.slice(equals + 1).trim(); - const keyStart = lineStart; - const keyEnd = keyStart + key.length; - - if (!isValidKey(key)) { - push("syntax", `invalid key: ${key}`, lineNumber, keyStart, keyEnd); - scan(raw, state); - continue; - } - if (value === "") { - push("syntax", `key ${key} has no value`, lineNumber, keyStart, keyEnd, key); - scan(raw, state); - continue; - } - - const info = keyInfoOf(section, key); - - // 规则 3:已知段 + 有键表 + 不在表中(且该段不是自选键的段)。 - if (section?.keys !== undefined && section.openKeys !== true && info === undefined) { - const label = section.header; - push("unknownKey", `unknown key ${key} in ${label}`, lineNumber, keyStart, keyEnd, key); - } - - // 规则 6:键级 legacy(段本身已 deprecated 时只在段头报一次)。 - if (info?.legacy === true && !section?.deprecatedBy) { - const replacement = info.note ?? "no replacement documented"; - push( - "legacyKey", - `key ${key} is deprecated; ${replacement}`, - lineNumber, - keyStart, - keyEnd, - key, - ); - } - - // 规则 5:[package].mcpp 必须是 ">="。 - if (section?.name === "package" && (splitDots(key)[0] === "mcpp") && !isMcppFloor(value)) { - push( - "mcppFloor", - `[package] mcpp must be a release floor like ">=2026.9.28.3"`, - lineNumber, - keyStart, - keyEnd, - "mcpp", - ); - } - - // 规则 4:平面分离。 - const bareKey = unquote(key) ?? key; - if (isDependencySection(headerName ?? "") && isToolPayloadName(bareKey)) { - push( - "planeSeparation", - `${bareKey} is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]`, - lineNumber, - keyStart, - keyEnd, - bareKey, - ); - } else if (isXlingsSection(headerName ?? "") && isMcppPackageName(bareKey) && declaresVersion(value)) { - push( - "planeSeparation", - `${bareKey} is an mcpp package, not a tool payload; declare it in [dependencies]`, - lineNumber, - keyStart, - keyEnd, - bareKey, - ); - } - - scan(raw, state); - } - - diagnostics.sort( - (a, b) => - a.line - b.line || - a.startCharacter - b.startCharacter || - a.code.localeCompare(b.code) || - a.message.localeCompare(b.message), - ); - return diagnostics; -} diff --git a/mide-plugins/vscode/src/toml/hover.ts b/mide-plugins/vscode/src/toml/hover.ts deleted file mode 100644 index 1342499..0000000 --- a/mide-plugins/vscode/src/toml/hover.ts +++ /dev/null @@ -1,206 +0,0 @@ -// mcpp.toml 的悬停查询层。 -// -// 输入是文件的物理行 + 0 基光标,输出纯数据 HoverInfo;provider 负责渲染成 -// MarkdownString(本模块不依赖 vscode API)。上下文来自 parser 的 contextAt, -// 与补全共用同一套容错解析,不另写 TOML 解析器。 -// -// 覆盖三类位置:段头、键、枚举键的值。其余位置一律 undefined——宁可不显示, -// 也不猜;任何畸形输入都不抛异常。 - -import { - contextAt, - resolveSection, - type ReplaceRange, - type TomlCursorContext, -} from "./parser"; -import { sectionByName, type TomlKey, type TomlSection } from "./schema"; - -export interface HoverInfo { - title: string; - body: string; - documentation?: string; -} - -function clamp(value: number, min: number, max: number): number { - if (!Number.isFinite(value)) { - return min; - } - return Math.min(Math.max(Math.trunc(value), min), max); -} - -/** 取光标所在行(越界与 CRLF 都容错)。 */ -function lineTextAt(lines: readonly string[], line: number): string { - if (!Array.isArray(lines) || lines.length === 0) { - return ""; - } - const index = clamp(line, 0, lines.length - 1); - return (lines[index] ?? "").replace(/\r$/, ""); -} - -/** 替换范围对应的原文(越界钳制)。 */ -function tokenAt(lineText: string, range: ReplaceRange): string { - const start = clamp(range.startCharacter, 0, lineText.length); - const end = clamp(range.endCharacter, start, lineText.length); - return lineText.slice(start, end); -} - -function unquote(text: string): string { - if (text.length >= 2) { - const first = text[0]; - const last = text[text.length - 1]; - if ((first === '"' && last === '"') || (first === "'" && last === "'")) { - return text.slice(1, -1); - } - } - return text; -} - -/** - * 段头路径 → schema 段:先精确匹配,再让 parser 的组映射处理参数化段 - * (`[targets.app]` → `targets`),最后逐级去掉尾部(与诊断的 lookupSection - * 同策略),使 `[features.a]` 这类行表也能落到基段。 - */ -function sectionForHeader(segments: readonly string[]): TomlSection | undefined { - if (segments.length === 0) { - return undefined; - } - const exact = sectionByName(segments.join(".")); - if (exact !== undefined) { - return exact; - } - const resolution = resolveSection(segments); - if (resolution.kind === "known") { - // parser 认识的组就是权威归属:schema 没收录它(如 [xlings.workspace])时 - // 不显示,而不是把键张冠李戴到基段上。 - return sectionByName(resolution.group); - } - for (let length = segments.length - 1; length >= 1; length -= 1) { - const found = sectionByName(segments.slice(0, length).join(".")); - if (found !== undefined) { - return found; - } - } - return undefined; -} - -function sectionHover(section: TomlSection): HoverInfo { - const lines: string[] = [`plane: \`${section.plane}\``]; - if (section.deprecatedBy) { - lines.push(`legacy: replaced by \`${section.deprecatedBy}\``); - } - const keyCount = section.keys?.length ?? 0; - lines.push( - section.keys === undefined - ? "keys: none declared in the snapshot" - : `keys: ${keyCount}${section.openKeys === true ? " (open: the user picks the names)" : ""}`, - ); - const info: HoverInfo = { title: section.header, body: lines.join("\n\n") }; - if (section.doc !== undefined) { - info.documentation = section.doc; - } - return info; -} - -function keyHover(key: TomlKey, section: TomlSection | undefined): HoverInfo { - const lines: string[] = [`type: \`${key.type}\``]; - if (key.values !== undefined && key.values.length > 0) { - lines.push(`values: ${key.values.map((value) => `\`${value}\``).join(" | ")}`); - } - if (key.default !== undefined) { - lines.push(`default: \`${JSON.stringify(key.default)}\``); - } - if (key.since !== undefined) { - lines.push(`since: ${key.since}`); - } - if (key.note !== undefined && key.note !== "") { - lines.push(key.note); - } - if (key.legacy === true) { - lines.push("legacy: this key is deprecated"); - } - if (key.unmodelled === true) { - lines.push("unmodelled: the type is inferred from the docs, not read from mcpp"); - } - const info: HoverInfo = { title: key.key, body: lines.join("\n\n") }; - if (section?.doc !== undefined) { - info.documentation = section.doc; - } - return info; -} - -function enumValueHover(key: TomlKey, value: string, section: TomlSection | undefined): HoverInfo { - const lines: string[] = [`enum value of \`${key.key}\``]; - if (key.values !== undefined && key.values.length > 0) { - lines.push(`one of: ${key.values.map((entry) => `\`${entry}\``).join(" | ")}`); - } - if (key.default !== undefined) { - lines.push(`default: \`${JSON.stringify(key.default)}\``); - } - if (key.note !== undefined && key.note !== "") { - lines.push(key.note); - } - const info: HoverInfo = { title: value === "" ? key.key : value, body: lines.join("\n\n") }; - if (section?.doc !== undefined) { - info.documentation = section.doc; - } - return info; -} - -/** - * 光标下的悬停内容;没有可显示的内容时返回 undefined。永不抛异常。 - */ -export function hoverAt( - lines: readonly string[], - line: number, - character: number, -): HoverInfo | undefined { - try { - const context: TomlCursorContext = contextAt(lines, line, character); - const text = lineTextAt(lines, line); - - if (context.kind === "section-header") { - const segments = [...context.segments]; - const token = tokenAt(text, context.replaceRange).trim(); - // 光标落在某个段路径 token 内时,context.segments 是它之前的段。 - if (token !== "") { - segments.push(unquote(token)); - } - const section = sectionForHeader(segments); - return section === undefined ? undefined : sectionHover(section); - } - - if (context.kind === "key") { - if (context.section.kind !== "known" || context.containerPath.length > 0) { - return undefined; - } - const section = sectionByName(context.section.group); - if (section?.keys === undefined) { - return undefined; - } - // 点分键的根段是 schema 里的键(`metadata.demo` → `metadata`)。 - const token = unquote(tokenAt(text, context.replaceRange).trim()); - const name = context.keyPrefix.length > 0 ? context.keyPrefix[0] : token; - const key = section.keys.find((entry) => entry.key === name); - return key === undefined ? undefined : keyHover(key, section); - } - - // 值位置:只有枚举键的值有可展示的语义。 - if (context.section.kind !== "known") { - return undefined; - } - const section = sectionByName(context.section.group); - if (section?.keys === undefined || context.keyPath.length === 0) { - return undefined; - } - const key = - section.keys.find((entry) => entry.key === context.keyPath.join(".")) ?? - section.keys.find((entry) => entry.key === context.keyPath[0]); - if (key?.type !== "enum") { - return undefined; - } - const value = unquote(tokenAt(text, context.replaceRange)).trim(); - return enumValueHover(key, value, section); - } catch { - return undefined; - } -} diff --git a/mide-plugins/vscode/src/toml/navigation.ts b/mide-plugins/vscode/src/toml/navigation.ts deleted file mode 100644 index aa0b7f8..0000000 --- a/mide-plugins/vscode/src/toml/navigation.ts +++ /dev/null @@ -1,306 +0,0 @@ -// mcpp.toml 的跳转查询层:只处理 manifest 内部的三种局部跳转。 -// -// workspace = true → [workspace.dependencies] 里的同名依赖键 -// path = "../x" → 那个 mcpp.toml 的 [package] 段头(行号由调用方解析) -// features = ["a"] → [features.a] 段头(光标所在的那个元素) -// -// 复用 parser 的节点树(parseMcppToml)定位光标下的键/值/数组元素;跨文件的 -// 文件系统语义由 `resolveManifest` 回调注入,本模块保持纯函数、不依赖 vscode, -// 也不自己读盘。任何畸形输入都不抛异常。 - -import { - parseMcppToml, - resolveSection, - type TomlDocument, - type TomlKeyValueNode, - type TomlSectionNode, - type TomlValueNode, -} from "./parser"; - -export interface ManifestLocation { - line: number; - startCharacter: number; - endCharacter: number; -} - -/** 依赖类段组:三种跳转都只在这些段里有意义(`[lib] path`、`[indices] path` 不是)。 */ -const DEPENDENCY_GROUPS: ReadonlySet = new Set([ - "dependencies", - "dev-dependencies", - "build-dependencies", - "workspace.dependencies", - "feature-deps", -]); - -const PACKAGE_HEADER = "[package]"; - -function clamp(value: number, min: number, max: number): number { - if (!Number.isFinite(value)) { - return min; - } - return Math.min(Math.max(Math.trunc(value), min), max); -} - -/** 光标是否落在范围内(端点含尾,与 parser 的判定一致)。 */ -function contains( - range: { startLine: number; startCharacter: number; endLine: number; endCharacter: number }, - line: number, - character: number, -): boolean { - if (line < range.startLine || line > range.endLine) { - return false; - } - if (line === range.startLine && character < range.startCharacter) { - return false; - } - if (line === range.endLine && character > range.endCharacter) { - return false; - } - return true; -} - -/** 光标所在候选:一条键值语句,外加它所属的顶层依赖键(内联表内的条目才有)。 */ -interface Candidate { - node: TomlKeyValueNode; - /** 顶层语句的点分键(`compat.zlib = { workspace = true }` → "compat.zlib")。 */ - ownerKey?: string; - sectionSegments: string[]; -} - -/** 内联表/数组可以嵌套:把所有可达的键值语句摊平,附带顶层依赖键。 */ -function collectNested( - value: TomlValueNode | undefined, - ownerKey: string, - sectionSegments: string[], - out: Candidate[], -): void { - if (value === undefined) { - return; - } - if (value.kind === "inlineTable" && value.entries !== undefined) { - for (const entry of value.entries) { - out.push({ node: entry, ownerKey, sectionSegments }); - collectNested(entry.value, ownerKey, sectionSegments, out); - } - return; - } - if (value.kind === "array" && value.elements !== undefined) { - for (const element of value.elements) { - collectNested(element, ownerKey, sectionSegments, out); - } - } -} - -function candidatesOf(doc: TomlDocument): Candidate[] { - const out: Candidate[] = []; - let sectionSegments: string[] = []; - for (const node of doc.nodes) { - if (node.type === "section") { - sectionSegments = node.segments.map((segment) => segment.name); - continue; - } - out.push({ node, sectionSegments }); - const key = node.keyPath.map((segment) => segment.name).join("."); - collectNested(node.value, key, sectionSegments, out); - } - return out; -} - -function groupOf(sectionSegments: readonly string[]): string | undefined { - if (sectionSegments.length === 0) { - return undefined; - } - const resolution = resolveSection(sectionSegments); - return resolution.kind === "known" ? resolution.group : undefined; -} - -/** 依赖名:内联表条目取顶层语句的键;`[dependencies.foo]` 取段名去掉首段。 */ -function dependencyName(candidate: Candidate): string { - if (candidate.ownerKey !== undefined && candidate.ownerKey !== "") { - return candidate.ownerKey; - } - if (candidate.sectionSegments.length >= 2) { - return candidate.sectionSegments.slice(1).join("."); - } - return ""; -} - -function locationOfKey(node: TomlKeyValueNode): ManifestLocation | undefined { - const first = node.keyPath[0]; - const last = node.keyPath[node.keyPath.length - 1]; - if (first === undefined || last === undefined) { - return undefined; - } - // 点分键(`compat.zlib`)整体选中,不只选第一段。 - return { - line: first.range.startLine, - startCharacter: first.range.startCharacter, - endCharacter: last.range.endCharacter, - }; -} - -function locationOfSection(node: TomlSectionNode): ManifestLocation { - return { - line: node.line, - startCharacter: node.range.startCharacter, - endCharacter: node.range.endCharacter, - }; -} - -/** `[workspace.dependencies]` 里名字匹配的那条键值语句。 */ -function findWorkspaceDependency( - doc: TomlDocument, - dependency: string, -): TomlKeyValueNode | undefined { - if (dependency === "") { - return undefined; - } - let inWorkspaceDependencies = false; - for (const node of doc.nodes) { - if (node.type === "section") { - inWorkspaceDependencies = groupOf(node.segments.map((segment) => segment.name)) === "workspace.dependencies"; - continue; - } - if (inWorkspaceDependencies && node.keyPath.map((segment) => segment.name).join(".") === dependency) { - return node; - } - } - return undefined; -} - -/** `[features.]` 段头(数组表不算)。 */ -function findFeatureSection(doc: TomlDocument, name: string): TomlSectionNode | undefined { - for (const node of doc.nodes) { - if (node.type !== "section" || node.isArray || node.segments.length !== 2) { - continue; - } - if (node.segments[0].name === "features" && node.segments[1].name === name) { - return node; - } - } - return undefined; -} - -function isRelativePath(value: string): boolean { - const text = value.trim(); - if (text === "") { - return false; - } - if (text.startsWith("/") || text.startsWith("\\") || text.startsWith("~")) { - return false; - } - if (/^[A-Za-z]:[\\/]/.test(text)) { - return false; - } - if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) { - return false; - } - return true; -} - -function safeResolve( - resolveManifest: (relative: string) => number | undefined, - relative: string, -): number | undefined { - try { - const value = resolveManifest(relative); - if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { - return undefined; - } - return Math.trunc(value); - } catch { - return undefined; - } -} - -function locate( - candidate: Candidate, - line: number, - character: number, - doc: TomlDocument, - resolveManifest: (relative: string) => number | undefined, -): ManifestLocation | undefined { - const { node, sectionSegments } = candidate; - const onKey = node.keyPath.some((segment) => contains(segment.range, line, character)); - const onValue = node.value !== undefined && contains(node.value.range, line, character); - if (!onKey && !onValue) { - return undefined; - } - - const keyName = node.keyPath[node.keyPath.length - 1]?.name ?? ""; - const group = groupOf(sectionSegments); - const inDependencies = group !== undefined && DEPENDENCY_GROUPS.has(group); - - if (keyName === "workspace") { - const isTrue = node.value?.kind === "boolean" && node.value.text === "true"; - if (!isTrue || !inDependencies) { - return undefined; - } - const dependency = dependencyName(candidate); - const target = findWorkspaceDependency(doc, dependency); - return target === undefined ? undefined : locationOfKey(target); - } - - if (keyName === "path") { - if (!inDependencies || node.value?.kind !== "string") { - return undefined; - } - const relative = node.value.text ?? ""; - if (!isRelativePath(relative)) { - return undefined; - } - const resolved = safeResolve(resolveManifest, relative); - if (resolved === undefined) { - return undefined; - } - // 只有行号是可知的(另一个文件的内容不在本函数手里),所以选中该行的 - // [package] 字面量;缩进可能使起点略有偏差,位置仍落在段头行上。 - return { line: resolved, startCharacter: 0, endCharacter: PACKAGE_HEADER.length }; - } - - if (keyName === "features") { - if (node.value?.kind !== "array" || node.value.elements === undefined) { - return undefined; - } - const element = node.value.elements.find((entry) => contains(entry.range, line, character)); - if (element === undefined || element.kind !== "string") { - return undefined; - } - const name = (element.text ?? "").trim(); - if (name === "") { - return undefined; - } - const target = findFeatureSection(doc, name); - return target === undefined ? undefined : locationOfSection(target); - } - - return undefined; -} - -/** - * 光标位置的跳转目标;不在三种形态上时返回 undefined。永不抛异常。 - * - * `resolveManifest` 把相对路径解析为「那个 mcpp.toml 里 [package] 段头的行号 - * (0 基)」,不存在时返回 undefined——文件系统由调用方负责。 - */ -export function definitionAt( - lines: readonly string[], - line: number, - character: number, - resolveManifest: (relative: string) => number | undefined = () => undefined, -): ManifestLocation | undefined { - try { - const cursorLine = clamp(line, 0, Number.MAX_SAFE_INTEGER); - const cursorCharacter = clamp(character, 0, Number.MAX_SAFE_INTEGER); - const doc = parseMcppToml(lines); - for (const candidate of candidatesOf(doc)) { - const found = locate(candidate, cursorLine, cursorCharacter, doc, resolveManifest); - if (found !== undefined) { - return found; - } - } - return undefined; - } catch { - return undefined; - } -} diff --git a/mide-plugins/vscode/src/toml/parser.ts b/mide-plugins/vscode/src/toml/parser.ts index 4535992..25a249e 100644 --- a/mide-plugins/vscode/src/toml/parser.ts +++ b/mide-plugins/vscode/src/toml/parser.ts @@ -1,14 +1,13 @@ -// mcpp.toml 的容错解析层:为代码补全提供结构化信息。 -// 设计目标是容错而非校验——用户在编辑器里输入到一半(未闭合的 `[`、字符串、 -// 内联表)时不抛异常,而是把已识别的结构连同精确的 0 基行列范围返回。 -// 本模块不依赖 vscode API,可在 node --test 下直接测试;extension.ts 负责 -// 把这里的纯数据上下文映射为 CompletionItem。 +// mcpp.toml read tolerantly, for the views that list what a manifest declares (its dependencies, +// its targets): the structure an edit in progress already has, with exact 0-based ranges, never an +// exception for a half-typed `[`, string, array or inline table. The rules layer -- completion, +// hover, the jumps, the diagnostics -- is the mcpp-ide backend's (design 2026-10-30 part 2 §8), whose +// scanner answers the same structure (its tests/rules goldens were written by this one). // -// 覆盖的 TOML 子集:段头(含 [[...]] 数组表、单/双引号段)、裸键/引号键/ -// 点分键、字符串(含三引号多行串)、整数、布尔、数组与内联表(可嵌套、 -// 可跨行)、行注释。CRLF 行尾在进入解析前剥除,列号按剥除后的文本计算。 - -import { SCHEMA } from "./schema"; +// The subset read: table headers (`[[...]]` arrays of tables, quoted segments), bare, quoted and +// dotted keys, strings (`"""` and `'''` multi-line ones too), integers, booleans, arrays and inline +// tables (nested, across lines), and comments. A CRLF line end is dropped before the scan; columns +// count the text without it. /** 0 基行列范围,end 为开区间(与 VS Code Range 同构)。 */ export interface TomlRange { @@ -75,167 +74,11 @@ export interface TomlDocument { nodes: TomlNode[]; } -/** 段归属语义:top = 文档顶部尚无段头;unknown = 未识别的自定义段;known = 已知段组。 */ -export type SectionResolution = - | { kind: "top" } - | { kind: "unknown"; segments: string[] } - | { kind: "known"; group: string }; - -/** 当前 token 的替换范围(与光标同行的起止列),供补全做显式 range。 */ -export interface ReplaceRange { - startCharacter: number; - endCharacter: number; -} - -/** 光标的结构化上下文(纯数据)。 */ -export type TomlCursorContext = - | { - kind: "section-header"; - /** 光标所在 token 之前已解析出的段路径段(去引号)。 */ - segments: string[]; - isArray: boolean; - replaceRange: ReplaceRange; - } - | { - kind: "key"; - section: SectionResolution; - /** 容器路径:顶层语句为 [];内联表内为从语句根键开始的路径(如 simd = { flags = [ { … 深处为 ["simd","flags"])。 */ - containerPath: string[]; - /** 同一行点分键中光标 token 之前已输入的段(如 capi.la| → ["capi"])。 */ - keyPrefix: string[]; - replaceRange: ReplaceRange; - } - | { - kind: "value"; - section: SectionResolution; - /** 所属键的完整路径(含内联表/数组下钻)。 */ - keyPath: string[]; - /** 光标处值的种类;值尚未开始或不可判定时为 undefined。 */ - valueKind: TomlValueKind | undefined; - insideString: boolean; - /** 所在字符串未闭合(不在字符串内时为 false)。 */ - stringOpen: boolean; - replaceRange: ReplaceRange; - }; - -/** 精确匹配的已知段(与 mcpp manifest 层一致)。 */ -const KNOWN_SECTIONS: ReadonlySet = new Set([ - "package", - "lib", - "build", - "generated_files", - "dependencies", - "dev-dependencies", - "build-dependencies", - "features", - "indices", - "capabilities", - "runtime", - "resources", - "toolchain", - "xlings", - "xlings.workspace", - "xlings.envs", - "workspace", - "workspace.dependencies", - "pack", - "pack.bundle-project", - "language", - "tools.overrides", -]); - -/** 参数化段基组:. 归入 (dependencies 系支持命名空间再嵌套)。 */ -const PARAMETERIZED_BASES: ReadonlySet = new Set([ - "targets", - "profile", - "feature-deps", - "dependencies", - "dev-dependencies", -]); - -/** The schema's section names (the rules layer's reading of a table, design 2026-10-30 part 2 §8). */ -const SCHEMA_SECTIONS: ReadonlySet = new Set(SCHEMA.sections.map((section) => section.name)); - -/** A platform's tables that a target selector overrides (`[target..link]` is `[platform.link]`). */ -const PLATFORM_OVERRIDES: ReadonlySet = new Set(["carrier", "layers", "link", "runners", "tools"]); - -/** Tables whose sub-tables are named entries: `[targets.]`, `[platform.build.]`. */ -const NAMED_ENTRY_BASES: ReadonlySet = new Set([ - ...PARAMETERIZED_BASES, - "build-dependencies", - "features", - "platform", -]); - -function resolveGroup(segments: readonly string[]): string | undefined { - const joined = segments.join("."); - if (KNOWN_SECTIONS.has(joined) || SCHEMA_SECTIONS.has(joined)) { - return joined; - } - const head = segments[0]; - if (head === "target") { - // [target.] 或 [target..<子表>];sel 是三元组或去引号后的 - // cfg 表达式,本身不含未引号点,因此子表部分从第三段开始。A platform's table under a - // selector is the platform's. - if (segments.length <= 2) { - return "target"; - } - const rest = segments.slice(2); - return resolveGroup(PLATFORM_OVERRIDES.has(rest[0]) ? ["platform", ...rest] : rest); - } - if (head === "workspace" && segments[1] === "dependencies" && segments.length >= 3) { - return "workspace.dependencies"; - } - if (head === "workspace" && segments.length >= 2) { - // `[workspace.
      ]` is the table it mirrors. - return resolveGroup(segments.slice(1)); - } - if (segments.length >= 2 && segments[segments.length - 1] === "requires_abi") { - return "requires_abi"; - } - if (head === "platform" && segments[1] === "hosted" && segments[2] === "layers") { - return "platform.layers"; - } - if (head === "runtime" && segments.length === 2) { - // [runtime.""]:带点 capability 名的显式 provider 子表。 - return "runtime.capability"; - } - if (NAMED_ENTRY_BASES.has(head) && segments.length >= 2) { - // The longest leading run that names a section: `[platform.build.'cfg(...)']` is - // `platform.build`, `[targets.app]` is `targets`. - for (let length = segments.length - 1; length >= 1; length -= 1) { - const name = segments.slice(0, length).join("."); - if (SCHEMA_SECTIONS.has(name) || KNOWN_SECTIONS.has(name)) { - return name; - } - } - return head; - } - return undefined; -} - -/** - * 把解析出的段路径规范化为语义组。空 segments 表示文档顶部(尚无段头), - * 未识别的段返回 unknown,由调用方区分这两种情况。 - */ -export function resolveSection(segments: readonly string[]): SectionResolution { - if (segments.length === 0) { - return { kind: "top" }; - } - const group = resolveGroup(segments); - return group === undefined - ? { kind: "unknown", segments: [...segments] } - : { kind: "known", group }; -} - interface Pos { line: number; col: number; } -/** 内部控制流信号:光标上下文已捕获,提前结束扫描。解析本身永不抛异常。 */ -const CONTEXT_FOUND = Symbol("mcppTomlContextFound"); - function isBareKeyChar(ch: string): boolean { return ( (ch >= "a" && ch <= "z") || @@ -251,28 +94,14 @@ class Scanner { private readonly lines: string[]; private line = 0; private col = 0; - private readonly cursor: Pos | undefined; - /** 光标上下文捕获结果。 */ - context: TomlCursorContext | undefined; - /** 扫描过程中最近经过的段头,用于给键/值上下文标注段归属。 */ - private currentSection: TomlSectionNode | undefined; - - constructor(lines: readonly string[], cursor?: Pos) { - this.lines = lines - .map((text) => (text.endsWith("\r") ? text.slice(0, -1) : text)); + + constructor(lines: readonly string[]) { + this.lines = lines.map((text) => (text.endsWith("\r") ? text.slice(0, -1) : text)); if (this.lines.length === 0) { this.lines.push(""); } - if (cursor !== undefined) { - // 越界坐标钳制到文档内,保证补全在任意光标位置都能得到上下文。 - const line = Math.min(Math.max(0, cursor.line), this.lines.length - 1); - const col = Math.min(Math.max(0, cursor.col), this.lines[line].length); - this.cursor = { line, col }; - } } - // ---- 基础游标操作 ---- - private pos(): Pos { return { line: this.line, col: this.col }; } @@ -312,10 +141,6 @@ class Scanner { return a.line === b.line && a.col === b.col; } - private cmpPos(a: Pos, b: Pos): number { - return a.line - b.line || a.col - b.col; - } - private skipInlineWs(): void { while (this.peek() === " " || this.peek() === "\t") { this.col += 1; @@ -346,12 +171,7 @@ class Scanner { } private rangeFrom(start: Pos): TomlRange { - return { - startLine: start.line, - startCharacter: start.col, - endLine: this.line, - endCharacter: this.col, - }; + return { startLine: start.line, startCharacter: start.col, endLine: this.line, endCharacter: this.col }; } private sliceText(a: Pos, b: Pos): string { @@ -375,137 +195,10 @@ class Scanner { return parts.join("\n"); } - // ---- 光标上下文捕获 ---- - - private cursorReached(): boolean { - const cursor = this.cursor; - return cursor !== undefined && this.cmpPos(this.pos(), cursor) >= 0; - } - - /** 光标是否落在范围内(端点含尾:补全时光标常贴在 token 末尾)。 */ - private cursorWithin(range: TomlRange): boolean { - const cursor = this.cursor; - if (cursor === undefined) { - return false; - } - if (cursor.line < range.startLine || cursor.line > range.endLine) { - return false; - } - if (cursor.line === range.startLine && cursor.col < range.startCharacter) { - return false; - } - if (cursor.line === range.endLine && cursor.col > range.endCharacter) { - return false; - } - return true; - } - - /** 在光标行上向左/右扩展 token(遇 stop 字符停),用于字符串内容等无预扫描范围的替换区间。 */ - private expandOnLine(stop: (ch: string) => boolean): ReplaceRange { - const cursor = this.cursor; - if (cursor === undefined) { - return { startCharacter: 0, endCharacter: 0 }; - } - const text = this.lines[cursor.line] ?? ""; - const at = Math.min(cursor.col, text.length); - let start = at; - let end = at; - while (start > 0 && !stop(text[start - 1])) { - start -= 1; - } - while (end < text.length && !stop(text[end])) { - end += 1; - } - return { startCharacter: start, endCharacter: end }; - } - - private emptyReplaceRange(): ReplaceRange { - const cursor = this.cursor; - const col = cursor?.col ?? 0; - return { startCharacter: col, endCharacter: col }; - } - - private static tokenReplaceRange(range: TomlRange): ReplaceRange { - // 键/段 token 保证单行(引号键跨行即视为未闭合而截断在行尾)。 - return { startCharacter: range.startCharacter, endCharacter: range.endCharacter }; - } - - /** 单行范围转替换范围;跨行值(数组/多行串)返回 undefined,由调用方回退。 */ - private static singleLineReplaceRange(range: TomlRange): ReplaceRange | undefined { - if (range.startLine !== range.endLine) { - return undefined; - } - return { startCharacter: range.startCharacter, endCharacter: range.endCharacter }; - } - - private sectionResolution(): SectionResolution { - if (this.currentSection === undefined) { - return { kind: "top" }; - } - return resolveSection(this.currentSection.segments.map((segment) => segment.name)); - } - - private capture(context: TomlCursorContext): never { - this.context = context; - throw CONTEXT_FOUND; - } - - private captureKey(containerPath: readonly string[], keyPrefix: readonly string[], token?: TomlRange): never { - this.capture({ - kind: "key", - section: this.sectionResolution(), - containerPath: [...containerPath], - keyPrefix: [...keyPrefix], - replaceRange: token !== undefined ? Scanner.tokenReplaceRange(token) : this.emptyReplaceRange(), - }); - } - - private captureValue( - keyPath: readonly string[], - valueKind: TomlValueKind | undefined, - options?: { insideString?: boolean; stringOpen?: boolean; replaceRange?: ReplaceRange }, - ): never { - this.capture({ - kind: "value", - section: this.sectionResolution(), - keyPath: [...keyPath], - valueKind, - insideString: options?.insideString ?? false, - stringOpen: options?.stringOpen ?? false, - replaceRange: options?.replaceRange ?? this.emptyReplaceRange(), - }); - } - - private captureHeader(segments: readonly TomlKeySegment[], isArray: boolean, token?: TomlRange): never { - this.capture({ - kind: "section-header", - segments: segments.map((segment) => segment.name), - isArray, - replaceRange: token !== undefined ? Scanner.tokenReplaceRange(token) : this.emptyReplaceRange(), - }); - } - - /** 扫描到文档末尾仍未捕获光标上下文时的兜底:当前段内的顶层键位置。 */ - finalContext(): TomlCursorContext { - return { - kind: "key", - section: this.sectionResolution(), - containerPath: [], - keyPrefix: [], - replaceRange: this.emptyReplaceRange(), - }; - } - - // ---- 结构扫描 ---- - scan(): TomlNode[] { const nodes: TomlNode[] = []; while (!this.eof()) { this.skipTrivia(); - // 光标落在语句之间的空白/注释里:视为顶层键位置(新语句的起点)。 - if (this.cursorReached()) { - this.captureKey([], []); - } if (this.eof()) { break; } @@ -513,7 +206,7 @@ class Scanner { if (this.peek() === "[") { nodes.push(this.scanHeader(start)); } else { - const keyValue = this.scanKeyValue([]); + const keyValue = this.scanKeyValue(); if (keyValue !== undefined) { nodes.push(keyValue); } @@ -538,17 +231,10 @@ class Scanner { let open = true; for (;;) { this.skipInlineWs(); - // 光标在段之间的空白/点号附近(如 `[targets.` 之后)。 - if (this.cursorReached()) { - this.captureHeader(segments, isArray); - } const segment = this.scanKeySegment(); if (segment === undefined) { break; } - if (this.cursorWithin(segment.range)) { - this.captureHeader(segments, isArray, segment.range); - } segments.push(segment); this.skipInlineWs(); if (this.peek() === ".") { @@ -558,9 +244,6 @@ class Scanner { break; } this.skipInlineWs(); - if (this.cursorReached()) { - this.captureHeader(segments, isArray); - } if (this.peek() === "]") { this.advance(); if (isArray && this.peek() === "]") { @@ -575,16 +258,7 @@ class Scanner { this.col += 1; } } - const node: TomlSectionNode = { - type: "section", - segments, - isArray, - open, - range: this.rangeFrom(start), - line: start.line, - }; - this.currentSection = node; - return node; + return { type: "section", segments, isArray, open, range: this.rangeFrom(start), line: start.line }; } /** 单个键/段路径段:裸键或单/双引号键(引号键跨行即按未闭合截断)。 */ @@ -606,11 +280,7 @@ class Scanner { if (this.peek() === quote) { this.advance(); } - return { - name: this.sliceText(contentStart, contentEnd), - quoted: true, - range: this.rangeFrom(start), - }; + return { name: this.sliceText(contentStart, contentEnd), quoted: true, range: this.rangeFrom(start) }; } if (!isBareKeyChar(quote)) { return undefined; @@ -618,33 +288,19 @@ class Scanner { while (isBareKeyChar(this.peek())) { this.col += 1; } - return { - name: this.sliceText(start, this.pos()), - quoted: false, - range: this.rangeFrom(start), - }; + return { name: this.sliceText(start, this.pos()), quoted: false, range: this.rangeFrom(start) }; } - /** - * 键值对:点分键 [= 值]。containerPath 是内联表递归时下钻的键路径 - * (顶层语句为 [])。无法识别出键也没有值时返回 undefined。 - */ - private scanKeyValue(containerPath: readonly string[]): TomlKeyValueNode | undefined { + /** 键值对:点分键 [= 值]。无法识别出键也没有值时返回 undefined。 */ + private scanKeyValue(): TomlKeyValueNode | undefined { const start = this.pos(); const segments: TomlKeySegment[] = []; for (;;) { this.skipInlineWs(); - // 光标在键槽位的空白处(如 `{ ` 之后、键未开始时)。 - if (this.cursorReached()) { - this.captureKey(containerPath, segments.map((segment) => segment.name)); - } const segment = this.scanKeySegment(); if (segment === undefined) { break; } - if (this.cursorWithin(segment.range)) { - this.captureKey(containerPath, segments.map((segment) => segment.name), segment.range); - } segments.push(segment); this.skipInlineWs(); if (this.peek() === ".") { @@ -654,32 +310,13 @@ class Scanner { break; } this.skipInlineWs(); - // 光标在键与 = 之间(如 `name |`):仍算键位置。 - if (this.cursorReached()) { - this.captureKey(containerPath, segments.map((segment) => segment.name)); - } - const keyNames = segments.map((segment) => segment.name); let value: TomlValueNode | undefined; if (this.peek() === "=") { this.advance(); this.skipInlineWs(); - const valuePath = containerPath.concat(keyNames); - // 光标在 = 之后、值未开始(如 `kind = |`)。 - if (this.cursorReached()) { - // 光标后方同行还有值 token(光标恰在 token 首字符、或在 = 与 token - // 之间的空白上)时,替换范围要覆盖整个 token,否则补全插入后原文残留。 - if (!this.atEol() && this.peek() !== "#") { - // 光标落在 token 内时由 scanValue 捕获。 - const ahead = this.scanValue(valuePath); - this.captureValue(valuePath, ahead.kind, { - replaceRange: Scanner.singleLineReplaceRange(ahead.range) ?? this.emptyReplaceRange(), - }); - } - this.captureValue(valuePath, undefined); - } // 值必须在同行开始(TOML 本就如此);行尾没有值则按缺失处理。 if (!this.atEol() && this.peek() !== "#") { - value = this.scanValue(valuePath); + value = this.scanValue(); } } if (segments.length === 0 && value === undefined) { @@ -688,22 +325,22 @@ class Scanner { return { type: "keyValue", keyPath: segments, range: this.rangeFrom(start), value }; } - private scanValue(keyPath: readonly string[]): TomlValueNode { + private scanValue(): TomlValueNode { const ch = this.peek(); if (ch === '"' || ch === "'") { - return this.scanStringValue(keyPath); + return this.scanStringValue(); } if (ch === "[") { - return this.scanArrayValue(keyPath); + return this.scanArrayValue(); } if (ch === "{") { - return this.scanInlineTableValue(keyPath); + return this.scanInlineTableValue(); } - return this.scanBareValue(keyPath); + return this.scanBareValue(); } /** 字符串:单/双引号单行串与三引号多行串;未闭合时 open = true。 */ - private scanStringValue(keyPath: readonly string[]): TomlValueNode { + private scanStringValue(): TomlValueNode { const start = this.pos(); const quote = this.peek() as '"' | "'"; this.advance(); @@ -714,25 +351,20 @@ class Scanner { if (this.peek() === quote) { this.advance(); if (this.peek() === quote) { - // 三引号多行串。 multiline = true; this.advance(); contentStart = this.pos(); } else { - // 空字符串 "" / ''。 open = false; } } - let closeStart: Pos | undefined; if (open) { while (!this.eof()) { if (!multiline && this.atEol()) { - // 单行串跨行 → 未闭合。 break; } const ch = this.peek(); if (ch === "\\" && quote === '"') { - // 双引号串内的转义:跳过下一个字符(\" 不结束串)。 this.advance(); if (!this.eof() && (multiline || !this.atEol())) { this.advance(); @@ -743,7 +375,6 @@ class Scanner { if (multiline) { if (this.peekAt(1) === quote && this.peekAt(2) === quote) { contentEnd = this.pos(); - closeStart = this.pos(); this.advance(); this.advance(); this.advance(); @@ -754,7 +385,6 @@ class Scanner { continue; } contentEnd = this.pos(); - closeStart = this.pos(); this.advance(); open = false; break; @@ -765,7 +395,7 @@ class Scanner { contentEnd = this.pos(); } } - const node: TomlValueNode = { + return { kind: "string", range: this.rangeFrom(start), open, @@ -779,46 +409,16 @@ class Scanner { endCharacter: contentEnd.col, }, }; - if (this.cursorWithin(node.range)) { - const cursor = this.cursor; - if (cursor !== undefined) { - const afterOpen = this.cmpPos(cursor, contentStart) >= 0; - const beforeClose = closeStart === undefined || this.cmpPos(cursor, closeStart) <= 0; - const insideString = afterOpen && beforeClose; - // 字符串内容的替换范围:扩展到本行引号边界,并钳制在内容范围内。 - let replaceRange = this.emptyReplaceRange(); - if (insideString) { - const expanded = this.expandOnLine((ch) => ch === quote); - const minCol = contentStart.line === cursor.line ? contentStart.col : 0; - const maxCol = - contentEnd.line === cursor.line ? contentEnd.col : this.lines[cursor.line]?.length ?? 0; - replaceRange = { - startCharacter: Math.max(expanded.startCharacter, minCol), - endCharacter: Math.min(Math.max(expanded.endCharacter, minCol), maxCol), - }; - } - this.captureValue(keyPath, "string", { - insideString, - stringOpen: open && afterOpen, - replaceRange, - }); - } - } - return node; } /** 数组:可跨行,元素递归解析;缺 ] 时 open = true。 */ - private scanArrayValue(keyPath: readonly string[]): TomlValueNode { + private scanArrayValue(): TomlValueNode { const start = this.pos(); this.advance(); // [ const elements: TomlValueNode[] = []; let open = true; for (;;) { this.skipTrivia(); - // 光标在元素槽位(如 `sources = [ ` 之后)。 - if (this.cursorReached()) { - this.captureValue(keyPath, undefined); - } if (this.eof()) { break; } @@ -833,9 +433,8 @@ class Scanner { continue; } const elementStart = this.pos(); - elements.push(this.scanValue(keyPath)); + elements.push(this.scanValue()); if (this.samePos(this.pos(), elementStart)) { - // 进度保护。 this.advance(); } } @@ -843,17 +442,13 @@ class Scanner { } /** 内联表:可嵌套、可跨行,条目是完整键值对;缺 } 时 open = true。 */ - private scanInlineTableValue(keyPath: readonly string[]): TomlValueNode { + private scanInlineTableValue(): TomlValueNode { const start = this.pos(); this.advance(); // { const entries: TomlKeyValueNode[] = []; let open = true; for (;;) { this.skipTrivia(); - // 光标在条目键槽位(如 `simd = { flags = [ { ` 深处)。 - if (this.cursorReached()) { - this.captureKey(keyPath, []); - } if (this.eof()) { break; } @@ -868,12 +463,11 @@ class Scanner { continue; } const entryStart = this.pos(); - const entry = this.scanKeyValue(keyPath); + const entry = this.scanKeyValue(); if (entry !== undefined) { entries.push(entry); } if (this.samePos(this.pos(), entryStart)) { - // 进度保护。 this.advance(); } } @@ -881,7 +475,7 @@ class Scanner { } /** 裸值 token:布尔、整数,其余归为 unknown(容错,不校验合法性)。 */ - private scanBareValue(keyPath: readonly string[]): TomlValueNode { + private scanBareValue(): TomlValueNode { const start = this.pos(); let end = this.pos(); while (!this.atEol()) { @@ -901,39 +495,12 @@ class Scanner { } else if (/^[+-]?\d[\d_]*$/.test(text)) { kind = "integer"; } - const range: TomlRange = { - startLine: start.line, - startCharacter: start.col, - endLine: end.line, - endCharacter: end.col, - }; - if (this.cursorWithin(range)) { - this.captureValue(keyPath, kind, { replaceRange: Scanner.tokenReplaceRange(range) }); - } + const range: TomlRange = { startLine: start.line, startCharacter: start.col, endLine: end.line, endCharacter: end.col }; return { kind, range, open: false, text }; } } /** 容错解析整个文档,产出带 0 基行列范围的节点树。永不抛异常。 */ export function parseMcppToml(lines: readonly string[]): TomlDocument { - const scanner = new Scanner(lines); - return { nodes: scanner.scan() }; -} - -/** - * 计算光标的结构化上下文。line / character 均为 0 基(character 按剥除 - * \r 后的文本计算);越界坐标会被钳制到文档内。永不抛异常。 - */ -export function contextAt(lines: readonly string[], line: number, character: number): TomlCursorContext { - const scanner = new Scanner(lines, { line, col: character }); - try { - scanner.scan(); - } catch (error) { - if (error === CONTEXT_FOUND && scanner.context !== undefined) { - return scanner.context; - } - throw error; - } - // 扫描结束仍未命中:光标在文档末尾的空白处,视为当前段内的顶层键位置。 - return scanner.finalContext(); + return { nodes: new Scanner(lines).scan() }; } diff --git a/mide-plugins/vscode/src/toml/providers.ts b/mide-plugins/vscode/src/toml/providers.ts deleted file mode 100644 index d474ecc..0000000 --- a/mide-plugins/vscode/src/toml/providers.ts +++ /dev/null @@ -1,284 +0,0 @@ -/** - * `mcpp.toml` editing: structural completion plus manifest diagnostics. - * - * Both are pure text analysis — no mcpp process, no language server — so they - * keep working in a restricted workspace, which is what - * `capabilities.untrustedWorkspaces: limited` promises. - * - * The one exception is dependency-version completion - * (`mcpp.toml.indexCompletion`, off by default): it runs `mcpp search`, which may - * use the network. Everything about *that* decision lives here — the two - * settings, workspace trust, the timeout and the session cache — while - * `./completion` stays pure and `../cli/search` stays the one place that parses - * mcpp's human-readable output. - */ - -import * as vscode from "vscode"; - -import { runMcpp } from "../cli/process"; -import { - indexCompletionRequest, - parseSearchOutput, - withDeadline, - type PackageVersion, -} from "../cli/search"; -import { read } from "../config/access"; -import { t } from "../i18n/t"; - -import { computeMcppTomlCompletionsWithIndex } from "./completion"; -import { analyseManifest, type DiagnosticSettings, type Severity } from "./diagnostics"; -import { hoverAt } from "./hover"; -import { definitionAt } from "./navigation"; - -export const MCPP_TOML_LANGUAGE = "mcpp-toml"; - -/** - * The session cache §3.3.2 asks for: at most one `mcpp search - * --all-versions` per package per session. Empty results are cached too — a - * package the index does not know must not be queried on every keystroke. - */ -const indexVersions = new Map(); - -let indexNoticeShown = false; - -/** The plan's one-time notice: this feature runs mcpp and may use the network. */ -function noticeIndexQuery(): void { - if (indexNoticeShown) { - return; - } - indexNoticeShown = true; - void vscode.window.showInformationMessage( - t("Completing dependency versions runs `mcpp search`, which may use the network."), - ); -} - -/** - * Versions for one dependency, or none. Every failure mode — the setting off, an - * untrusted workspace, a non-zero exit, a timeout, output the parser does not - * recognise — degrades to an empty list; nothing here surfaces an error. - */ -async function resolveIndexVersions( - document: vscode.TextDocument, - name: string, -): Promise { - const request = indexCompletionRequest(name, { - enabled: read("mcpp.toml.indexCompletion"), - trusted: vscode.workspace.isTrusted, - // VS Code exposes no offline flag and this extension never probes the - // network, so "offline" is discovered the only honest way: the query fails - // and degrades to no candidates. The term stays in `shouldSearch`'s contract - // (and its tests) so a future signal can fill it in without a shape change. - offline: false, - timeoutSeconds: read("mcpp.toml.indexCompletionTimeoutSeconds"), - }); - if (request === undefined) { - return []; - } - - // The cache is consulted *after* the switches, so turning the setting off (or - // losing trust) stops suggestions immediately instead of serving a hit. - const cached = indexVersions.get(name); - if (cached !== undefined) { - return cached; - } - noticeIndexQuery(); - - const folder = vscode.workspace.getWorkspaceFolder(document.uri); - const cwd = folder?.uri.fsPath ?? vscode.Uri.joinPath(document.uri, "..").fsPath; - const executable = (read("mcpp.path") ?? "").trim() || "mcpp"; - // The process gets the setting's timeout and the promise a short grace period, - // so a process that refuses to die still cannot hold the editor past it. - const result = await withDeadline( - runMcpp(vscode.workspace.isTrusted, executable, request.args, cwd, { timeoutMs: request.timeoutMs }), - request.timeoutMs + 250, - ); - const versions = - result === undefined || result.exitCode !== 0 ? [] : parseSearchOutput(result.stdout); - indexVersions.set(name, versions); - return versions; -} - -function linesOf(document: vscode.TextDocument): string[] { - const lines: string[] = []; - for (let line = 0; line < document.lineCount; line += 1) { - lines.push(document.lineAt(line).text); - } - return lines; -} - -/** - * The line index of `[package]` in a neighbouring manifest, for `path = "…"`. - * The file system belongs to the caller, which is why navigation takes a callback. - */ -function packageHeaderLine(uri: vscode.Uri, relative: string): number | undefined { - try { - const target = vscode.Uri.joinPath(uri, "..", relative, "mcpp.toml"); - const bytes = require("node:fs").readFileSync(target.fsPath, "utf8") as string; - const index = bytes.split(/\r?\n/).findIndex((line) => /^\s*\[\s*package\s*\]\s*$/.test(line)); - return index === -1 ? undefined : index; - } catch { - return undefined; - } -} - -function completionItemKind(kind: "section" | "template" | "version"): vscode.CompletionItemKind { - switch (kind) { - case "section": - return vscode.CompletionItemKind.Folder; - case "version": - return vscode.CompletionItemKind.Value; - default: - return vscode.CompletionItemKind.Snippet; - } -} - -function severityOf(value: Severity | undefined, fallback: Severity): Severity { - return value === "error" || value === "warning" || value === "info" || value === "off" ? value : fallback; -} - -export function registerTomlProviders( - context: vscode.ExtensionContext, - /** `mcpp.toml.completion`: structure suggestions on or off. */ - completionEnabled: () => boolean, - settings: () => DiagnosticSettings, - /** `mcpp.toml.diagnostics.enabled`. */ - enabled: () => boolean, - /** `mcpp.toml.hover` and `mcpp.toml.navigation`, which are independent switches. */ - hoverEnabled: () => boolean = () => true, - navigationEnabled: () => boolean = () => true, -): void { - context.subscriptions.push( - vscode.languages.registerCompletionItemProvider( - { language: MCPP_TOML_LANGUAGE }, - { - async provideCompletionItems(document, position) { - if (!completionEnabled()) { - return undefined; - } - const lines: string[] = []; - for (let line = 0; line <= position.line; line += 1) { - lines.push(document.lineAt(line).text); - } - const suggestions = await computeMcppTomlCompletionsWithIndex( - lines, - position.line, - position.character, - (name) => resolveIndexVersions(document, name), - ); - return suggestions.map((suggestion) => { - const item = new vscode.CompletionItem(suggestion.label, completionItemKind(suggestion.kind)); - item.detail = suggestion.detail; - if (suggestion.documentation !== undefined) { - item.documentation = new vscode.MarkdownString(suggestion.documentation); - } - if (suggestion.insertSnippet !== undefined) { - item.insertText = new vscode.SnippetString(suggestion.insertSnippet); - } - item.range = new vscode.Range( - position.line, - suggestion.range.startCharacter, - position.line, - suggestion.range.endCharacter, - ); - return item; - }); - }, - }, - // `"` so a dependency version value is discoverable without Ctrl+Space; - // every other trigger position falls straight through to no suggestions. - "[", - '"', - ), - ); - - context.subscriptions.push( - vscode.languages.registerHoverProvider({ language: MCPP_TOML_LANGUAGE }, { - provideHover(document, position) { - if (!hoverEnabled()) { - return undefined; - } - const info = hoverAt(linesOf(document), position.line, position.character); - if (info === undefined) { - return undefined; - } - const contents = [new vscode.MarkdownString(`**${info.title}**`), new vscode.MarkdownString(info.body)]; - if (info.documentation !== undefined) { - contents.push(new vscode.MarkdownString(`[${t("Manifest reference")}](${info.documentation})`)); - } - return new vscode.Hover(contents); - }, - }), - vscode.languages.registerDefinitionProvider({ language: MCPP_TOML_LANGUAGE }, { - provideDefinition(document, position) { - if (!navigationEnabled()) { - return undefined; - } - const location = definitionAt(linesOf(document), position.line, position.character, (relative) => - packageHeaderLine(document.uri, relative), - ); - if (location === undefined) { - return undefined; - } - return new vscode.Location(document.uri, new vscode.Position(location.line, location.startCharacter)); - }, - }), - ); - - const collection = vscode.languages.createDiagnosticCollection("mcpp-manifest"); - context.subscriptions.push(collection); - - const refresh = (document: vscode.TextDocument): void => { - if (document.languageId !== MCPP_TOML_LANGUAGE) { - return; - } - if (!enabled()) { - collection.delete(document.uri); - return; - } - const lines: string[] = []; - for (let line = 0; line < document.lineCount; line += 1) { - lines.push(document.lineAt(line).text); - } - const diagnostics = analyseManifest(lines, settings()).map((entry) => { - const range = new vscode.Range( - Math.max(0, entry.line - 1), - entry.startCharacter, - Math.max(0, entry.line - 1), - entry.endCharacter, - ); - const diagnostic = new vscode.Diagnostic(range, entry.message, severityFor(entry.severity)); - diagnostic.code = entry.code; - diagnostic.source = "mcpp"; - return diagnostic; - }); - collection.set(document.uri, diagnostics); - }; - - for (const document of vscode.workspace.textDocuments) { - refresh(document); - } - context.subscriptions.push( - vscode.workspace.onDidOpenTextDocument(refresh), - vscode.workspace.onDidChangeTextDocument((event) => refresh(event.document)), - vscode.workspace.onDidCloseTextDocument((document) => collection.delete(document.uri)), - vscode.workspace.onDidChangeConfiguration(() => { - for (const document of vscode.workspace.textDocuments) { - refresh(document); - } - }), - ); -} - -function severityFor(severity: "error" | "warning" | "info"): vscode.DiagnosticSeverity { - switch (severity) { - case "error": - return vscode.DiagnosticSeverity.Error; - case "warning": - return vscode.DiagnosticSeverity.Warning; - default: - return vscode.DiagnosticSeverity.Information; - } -} - -/** The registry keys that shape manifest diagnostics, read in one place. */ -export { severityOf }; diff --git a/mide-plugins/vscode/src/toml/schema.ts b/mide-plugins/vscode/src/toml/schema.ts deleted file mode 100644 index 8c6e60a..0000000 --- a/mide-plugins/vscode/src/toml/schema.ts +++ /dev/null @@ -1,125 +0,0 @@ -// mcpp.toml 快照的读取层。 -// -// 数据来自 `tools/generate-toml-schema.mjs` 生成的 `data/toml-schema.json` -// (贡献者期从 mcpp 仓库生成,运行期只读快照,不新增依赖)。本模块是纯数据 -// 访问,不依赖 vscode API:补全、悬停、诊断都从这里取段/键/枚举。 -// -// 快照里没有的段不会出现在这里,因此 mcpp 停止接受的段会自动消失——这正是 -// "段列表必须来自 toml.cppm" 的落点。 - -import raw from "../../data/toml-schema.json"; - -export interface TomlKey { - key: string; - type: "string" | "boolean" | "number" | "array" | "enum" | "table"; - values?: string[]; - default?: unknown; - since?: string; - legacy?: boolean; - note?: string; - /** 类型是推断而非读出来的:不要据此做严格的类型校验。 */ - unmodelled?: boolean; -} - -export interface TomlSection { - header: string; - name: string; - plane: string; - doc?: string; - deprecatedBy?: string | null; - keys?: TomlKey[]; - /** - * 该段的键由用户自选而非固定词表(如 `[toolchain]` 的平台名与 `bootstrap`), - * 诊断层不对它做未知键检查。生成器只对这类段写 true。 - */ - openKeys?: boolean; -} - -export interface TomlSchema { - sourceVersion: string; - sourceCommit: string; - sections: TomlSection[]; - rules: Array<{ id: string; severity: string }>; -} - -export const SCHEMA: TomlSchema = raw as unknown as TomlSchema; - -/** `[package]` / `package` → `package`;数组表 `[[x]]` 不是段,返回空串。 */ -function normaliseSectionName(headerOrName: string): string { - const text = headerOrName.trim(); - if (text.startsWith("[[")) { - return ""; - } - if (text.startsWith("[") && text.endsWith("]")) { - return text.slice(1, -1).trim(); - } - return text; -} - -/** 按段头(`"[package]"` 或 `"package"`)查找。数组表不匹配任何段。 */ -export function sectionByHeader(header: string): TomlSection | undefined { - const name = normaliseSectionName(header); - return name === "" ? undefined : sectionByName(name); -} - -/** 按段名精确查找。参数化段(`[targets.]`)由调用方用前缀匹配处理。 */ -export function sectionByName(name: string): TomlSection | undefined { - const wanted = normaliseSectionName(name); - if (wanted === "") { - return undefined; - } - return SCHEMA.sections.find((section) => section.name === wanted); -} - -/** 段内某个键的元数据;段不存在、段没有键表或键不在表中都返回 undefined。 */ -export function keyOf(sectionName: string, key: string): TomlKey | undefined { - const section = sectionByName(sectionName); - return section?.keys?.find((entry) => entry.key === key); -} - -/** - * `deprecatedBy` 形如 `[package].standard` / `[package]` / `package.standard`, - * 取出它指向的段名。 - */ -function deprecatedSectionName(reference: string): string { - const text = reference.trim(); - const bracketed = /^\[([^\]]+)\]/.exec(text); - if (bracketed !== null) { - return bracketed[1].split(".")[0].trim(); - } - return text.split(".")[0].trim(); -} - -/** - * 快照自身的结构问题:重复段头、空平面、以及指向不存在段的 deprecatedBy。 - * 生成器保证当前快照为空数组;测试把它当门禁。 - */ -export function sectionProblems(): string[] { - const problems: string[] = []; - const seen = new Set(); - for (const section of SCHEMA.sections) { - if (seen.has(section.header)) { - problems.push(`duplicate section header ${section.header}`); - } - seen.add(section.header); - - if (section.plane.trim() === "") { - problems.push(`section ${section.header} has an empty plane`); - } - - if (section.deprecatedBy) { - const target = deprecatedSectionName(section.deprecatedBy); - if (target === "" || sectionByName(target) === undefined) { - problems.push( - `${section.header} is deprecated by ${section.deprecatedBy}, which names no known section`, - ); - } - } - } - return problems; -} - -/** 快照来自哪个 mcpp 版本与提交(环境自检与诊断输出用)。 */ -export function schemaSource(): { version: string; commit: string } { - return { version: SCHEMA.sourceVersion, commit: SCHEMA.sourceCommit }; -} diff --git a/mide-plugins/vscode/test/architecture.test.ts b/mide-plugins/vscode/test/architecture.test.ts index 4e636c0..a5cc8f6 100644 --- a/mide-plugins/vscode/test/architecture.test.ts +++ b/mide-plugins/vscode/test/architecture.test.ts @@ -24,7 +24,6 @@ const PURE_MODULES = [ "src/cli/artifacts.ts", "src/cli/process.ts", "src/cli/protocol.ts", - "src/cli/search.ts", "src/cli/tasks.ts", "src/cli/newProject.ts", "src/cli/toolchain.ts", @@ -49,12 +48,7 @@ const PURE_MODULES = [ "src/graphs/panelHtml.ts", "src/cli/moduleGraph.ts", "src/webview/readOnlyDocuments.ts", - "src/toml/completion.ts", - "src/toml/diagnostics.ts", "src/toml/parser.ts", - "src/toml/schema.ts", - "src/toml/hover.ts", - "src/toml/navigation.ts", "src/views/models.ts", "src/cache/cacheState.ts", "src/library/indexModel.ts", @@ -63,11 +57,6 @@ const PURE_MODULES = [ "src/library/detailHtml.ts", "src/cache/cachePanelHtml.ts", "src/views/viewPolicy.ts", - "src/buildscript/analysis.ts", - "src/buildscript/api.ts", - "src/buildscript/modules.ts", - "src/buildscript/settings.ts", - "src/buildscript/snippets.ts", "src/util/format.ts", "src/util/log.ts", "src/util/text.ts", diff --git a/mide-plugins/vscode/test/artifacts.test.ts b/mide-plugins/vscode/test/artifacts.test.ts index d1e2bb3..1bb1771 100644 --- a/mide-plugins/vscode/test/artifacts.test.ts +++ b/mide-plugins/vscode/test/artifacts.test.ts @@ -12,6 +12,7 @@ interface PackageManifest { icon?: string; dependencies?: Record; devDependencies?: Record; + scripts?: Record; repository?: { url?: string }; homepage?: string; bugs?: { url?: string }; @@ -106,8 +107,11 @@ test("declares mcpp-language-server as the C++ modules language service", () => assert.match(command.title ?? "", /^%command\.[^%]+\.title%$/, `command ${command.command} title must be an nls key`); assert.equal(command.category, "%category%"); } + // The language client connects the backend (mcpp.toml, build.mcpp); it is bundled into dist/vendor, + // so the package carries no node_modules and the manifest names no runtime dependency. assert.equal(manifest.dependencies?.["vscode-languageclient"], undefined); - assert.equal(manifest.devDependencies?.["vscode-languageclient"], undefined); + assert.equal(manifest.devDependencies?.["vscode-languageclient"], "9.0.1"); + assert.match(manifest.scripts?.compile ?? "", /node tools\/bundle-languageclient\.mjs/); assert.deepEqual(manifest.contributes?.configurationDefaults?.["files.associations"], { "*.ccm": "cpp", "*.cppm": "cpp", @@ -270,25 +274,33 @@ test("every webview host holds its document through the kit", () => { assert.deepEqual(offenders, [], "a webview nonce is minted outside src/webview/document.ts"); }); -test("no module drives a second language client", () => { - // The same rule the `package` job enforces, but runnable here: the CI grep used - // to match the bare name, and the compiled comments that explain why this - // extension does *not* use a language client made it fail on a clean build. +test("the one language client is the backend's, and it serves no C++ source", () => { + // C++ is C++ Modules' (mcppls): a second client over C++ sources would answer every request + // twice. The backend's client (src/ide/session.ts) reads mcpp.toml and build.mcpp alone. const offenders: string[] = []; for (const [file, text] of sourceFiles()) { + if (file === path.join("src", "ide", "session.ts")) { + continue; + } const code = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); - if (/from\s+["']vscode-languageclient["']|require\(["']vscode-languageclient["']\)/.test(code)) { + if (/vscode-languageclient|vendor\/languageclient/.test(code)) { offenders.push(file); } } - assert.deepEqual(offenders, [], "a language client is imported again"); + assert.deepEqual(offenders, [], "a language client is driven outside the session"); + const session = readFileSync(path.join(root, "src", "ide", "session.ts"), "utf8"); + const selector = /export const DOCUMENT_SELECTOR = \[([\s\S]*?)\];/.exec(session)?.[1] ?? ""; + assert.match(selector, /language: "mcpp-toml"/); + for (const entry of selector.split("\n").filter((line) => line.includes('language: "cpp"'))) { + assert.match(entry, /pattern: "\*\*\/build\.mcpp"/, `a C++ selector beyond build.mcpp: ${entry.trim()}`); + } }); test("the committed snapshots record a clone-independent commit", () => { // `git rev-parse --short` shortens to the shortest unique prefix *for that // clone*, so the same mcpp produced a 7-character hash in a fresh CI checkout // and 8 in a developer's — and the drift gate went red on an unchanged API. - for (const file of ["data/buildscript-api.json", "data/toml-schema.json"]) { + for (const file of ["../../spec/buildscript-api.json", "../../spec/toml-schema.json"]) { const snapshot = JSON.parse(readFileSync(path.join(root, file), "utf8")) as { sourceVersion?: string; sourceCommit?: string; @@ -677,10 +689,10 @@ test("CI runs the gates, the package checks, the drift check and every e2e varia assert.match(workflow, /npm run test:parity/); assert.match(workflow, /npm run package/); assert.match(workflow, /xvfb-run -a npm run test:e2e:one/); - // The manifest is spec/'s, and the snapshots are mcpp's. + // The manifest is spec/'s; the snapshots of mcpp are the backend's (spec/), read by its rules layer. assert.match(workflow, /mcpp-ide-dev -- gen vscode\n/); - assert.match(workflow, /repository: mcpp-community\/mcpp/); - assert.match(workflow, /MCPP_REPO: \$\{\{ github.workspace \}\}\/\.mcpp-source/); + // The declared degraded mode runs in the Extension Host too (part 2 §8, F15). + assert.match(workflow, /MCPP_E2E_NO_BACKEND: "1"/); assert.match(workflow, /npm run test:graphs-ui/); assert.match(workflow, /sunrisepeak.mcpp-language-server/); assert.match(workflow, /tools\/ci-vscode-cli\.mjs/); diff --git a/mide-plugins/vscode/test/buildscript/analysis.test.ts b/mide-plugins/vscode/test/buildscript/analysis.test.ts deleted file mode 100644 index 96358cd..0000000 --- a/mide-plugins/vscode/test/buildscript/analysis.test.ts +++ /dev/null @@ -1,233 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { DIAGNOSTIC_CODES, analyseBuildScript } from "../../src/buildscript/analysis"; - -function codes(lines: readonly string[]): string[] { - return analyseBuildScript(lines, "warning").map((entry) => entry.code); -} - -test("a clean, realistic build script earns nothing", () => { - const lines = [ - "import std;", - "import mcpp;", - "", - "int main() {", - ' const std::string out = std::string(mcpp::out_dir()) + "/foo.pb.cc";', - ' mcpp::cxxflag("-DHAVE_BANNER=1");', - ' mcpp::link_lib("m");', - ' mcpp::link_search("vendor/lib");', - ' mcpp::link_flag("-Wl,--version-script=gen.map");', - ' mcpp::define("HAVE_FEATURE");', - ' mcpp::generated("src/gen.cpp");', - ' mcpp::include_dir("vendor/include");', - ' mcpp::runtime_search_dir("lib");', - ' mcpp::rerun_if_changed("config.h");', - ' mcpp::rerun_if_env_changed("USE_FAST");', - " mcpp::action a;", - ' a.id = "protoc:foo";', - " a.role = mcpp::roles::source;", - ' a.arg(mcpp::dep_bin("protobuf", "protoc"))', - ' .arg("--cpp_out=gen")', - ' .output("gen/foo.pb.cc")', - " .submit();", - " return 0;", - "}", - ]; - assert.deepEqual(analyseBuildScript(lines, "warning"), []); -}); - -test("rule 1 flags an unknown mcpp:: name and only that", () => { - assert.deepEqual(codes(["int main() {", ' mcpp::cxxflagk("-O2");', "}"]), [ - DIAGNOSTIC_CODES.unknownSymbol, - ]); - assert.deepEqual( - codes([ - ' mcpp::cxxflag("-O2");', - " mcpp::roles::prepare;", - ' mcpp::warning("x");', - ' mcpp::xpkg_source("a", "b");', - ' mcpp::toolchain("k", "v");', - ' mcpp::has_feature("f");', - ' mcpp::pack_format("dir");', - ' mcpp::windows_subsystem("app", "windows");', - ]), - [], - ); -}); - -test("rule 1 ignores mcpp:: inside strings and comments", () => { - assert.deepEqual( - codes([ - ' const char* s = "mcpp::bogus";', - " // mcpp::bogus();", - " /* mcpp::bogus(); */", - " const char* t = R\"(mcpp::bogus;)\";", - ]), - [], - ); -}); - -test("rule 1 reports 1-based line and column, at the requested severity", () => { - const diagnostics = analyseBuildScript(["mcpp::bogus();"], "info"); - assert.equal(diagnostics.length, 1); - assert.deepEqual( - { - code: diagnostics[0].code, - line: diagnostics[0].line, - startCharacter: diagnostics[0].startCharacter, - endCharacter: diagnostics[0].endCharacter, - severity: diagnostics[0].severity, - }, - { - code: DIAGNOSTIC_CODES.unknownSymbol, - line: 1, - startCharacter: 1, - endCharacter: 12, - severity: "info", - }, - ); -}); - -test("rule 2 flags a role spelled as a string literal", () => { - assert.deepEqual(codes(["mcpp::action a;", 'a.role = "source";', "a.submit();"]), [ - DIAGNOSTIC_CODES.roleLiteral, - ]); - assert.deepEqual(codes(["mcpp::action a;", "a.role = mcpp::roles::source;", "a.submit();"]), []); - // The frozen printf surface is allowed to spell the string (docs/30). - assert.deepEqual(codes(['printf("mcpp:action={\\"role\\":\\"source\\"}");']), []); -}); - -test("rule 3 flags a prepare action with no output_dir", () => { - assert.deepEqual( - codes([ - "mcpp::action a;", - "a.role = mcpp::roles::prepare;", - 'a.arg("install");', - "a.submit();", - ]), - [DIAGNOSTIC_CODES.prepareNeedsOutputDir], - ); - assert.deepEqual( - codes([ - "mcpp::action a;", - "a.role = mcpp::roles::prepare;", - "a.output_dir(prefix.c_str());", - "a.submit();", - ]), - [], - ); - // The raw payload carries the same obligation. - assert.deepEqual(codes(['printf("mcpp:action={\\"role\\":\\"prepare\\"}");']), [ - DIAGNOSTIC_CODES.prepareNeedsOutputDir, - ]); -}); - -test("rule 4 flags a run path spelled in link_flag", () => { - assert.deepEqual(codes(['mcpp::link_flag("-Wl,-rpath,$ORIGIN/../lib");']), [ - DIAGNOSTIC_CODES.rpathInLinkFlag, - ]); - assert.deepEqual( - codes([ - 'mcpp::link_flag("-Wl,--version-script=x.map");', - 'mcpp::runtime_search_dir("lib");', - ]), - [], - ); -}); - -test("rule 5 flags -I and -L spelled in raw flags", () => { - assert.deepEqual(codes(['mcpp::cxxflag("-Ivendor/include");']), [DIAGNOSTIC_CODES.rawPathFlag]); - assert.deepEqual(codes(['mcpp::link_flag("-Lvendor/lib");']), [DIAGNOSTIC_CODES.rawPathFlag]); - assert.deepEqual( - codes(['mcpp::include_dir("vendor/include");', 'mcpp::link_search("vendor/lib");']), - [], - ); -}); - -test("rule 6 flags shell syntax in an action command", () => { - assert.deepEqual( - codes([ - "mcpp::action a;", - "a.role = mcpp::roles::source;", - 'a.arg("CFLAGS=-O2").arg("make");', - "a.submit();", - ]), - [DIAGNOSTIC_CODES.shellSyntaxInAction], - ); - assert.deepEqual( - codes([ - "mcpp::action a;", - "a.role = mcpp::roles::source;", - 'a.arg("cd tools && make");', - "a.submit();", - ]), - [DIAGNOSTIC_CODES.shellSyntaxInAction], - ); - assert.deepEqual( - codes([ - "mcpp::action a;", - "a.role = mcpp::roles::source;", - 'a.env("GEN_MODE", "release").cwd("tools");', - 'a.arg("--out=build");', - "a.submit();", - ]), - [], - ); -}); - -test("rule 7 flags a missing or unknown action role", () => { - assert.deepEqual(codes(["mcpp::action a;", 'a.arg("gen");', "a.submit();"]), [ - DIAGNOSTIC_CODES.actionNeedsRole, - ]); - assert.deepEqual(codes(["mcpp::action a;", 'a.role = "banana";', "a.submit();"]), [ - DIAGNOSTIC_CODES.actionNeedsRole, - ]); - assert.deepEqual(codes(["mcpp::action a;", "a.role = mcpp::roles::object;", "a.submit();"]), []); -}); - -test("each rule id is the SPEC-007 code the plan names", () => { - assert.deepEqual(Object.values(DIAGNOSTIC_CODES), [ - "mcpp.buildscript.unknownSymbol", - "mcpp.buildscript.roleLiteral", - "mcpp.buildscript.prepareNeedsOutputDir", - "mcpp.buildscript.rpathInLinkFlag", - "mcpp.buildscript.rawPathFlag", - "mcpp.buildscript.shellSyntaxInAction", - "mcpp.buildscript.actionNeedsRole", - ]); -}); - -test("rule 1 leaves a namespace the file imports to the module that brings it", () => { - // The qt-demo shape: `import mcpp.rules.qt;` and then `mcpp::rules::qt::…`. - const lines = [ - "import std;", - "import mcpp;", - "import mcpp.rules.qt;", - "", - "int main() {", - " auto qt = mcpp::rules::qt::options{};", - " mcpp::rules::qt::apply(qt);", - " mcpp::nope();", - "}", - ]; - assert.deepEqual(codes(lines), [DIAGNOSTIC_CODES.unknownSymbol]); - assert.match(analyseBuildScript(lines, "warning")[0].message, /mcpp::nope/); -}); - -test("rule 1 does not count an import that is commented out", () => { - const lines = ["// import mcpp.rules.qt;", "int main() { mcpp::rules::qt::apply(); }"]; - assert.deepEqual(codes(lines), [DIAGNOSTIC_CODES.unknownSymbol]); -}); - -test("with rule 1 off, the SPEC-007 rule checks still run", () => { - const lines = [ - "import mcpp;", - "int main() {", - " mcpp::nope();", - ' mcpp::link_flag("-Wl,-rpath,/opt/lib");', - "}", - ]; - const found = analyseBuildScript(lines, "warning", { unknownSymbols: false }).map((entry) => entry.code); - assert.deepEqual(found, [DIAGNOSTIC_CODES.rpathInLinkFlag]); -}); diff --git a/mide-plugins/vscode/test/buildscript/api.test.ts b/mide-plugins/vscode/test/buildscript/api.test.ts deleted file mode 100644 index 82b8551..0000000 --- a/mide-plugins/vscode/test/buildscript/api.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; -import test from "node:test"; - -import apiJson from "../../data/buildscript-api.json"; -import { - ACTION_ROLES, - API, - apiProblems, - apiSource, - directive, - directives, -} from "../../src/buildscript/api"; - -// Compiled layout: /dist/test/buildscript/*.js. -const repositoryRoot = path.resolve(__dirname, "..", "..", ".."); - -/** The mcpp checkout to compare against, when one is reachable. */ -function findMcppRepo(): string | undefined { - const candidates = [ - process.env.MCPP_REPO, - path.resolve(process.cwd(), "..", "mcpp"), - path.resolve(repositoryRoot, "..", "mcpp"), - ].filter((candidate): candidate is string => typeof candidate === "string"); - return candidates.find((candidate) => - fs.existsSync(path.join(candidate, "modules", "buildmcpp", "src", "directives.cppm")), - ); -} - -const mcppRepo = findMcppRepo(); -const skipReason = - mcppRepo === undefined - ? "no mcpp checkout found (set MCPP_REPO to compare against the source)" - : false; - -test("the snapshot loads with a usable shape", () => { - assert.equal(typeof API.sourceVersion, "string"); - assert.ok(API.sourceVersion.length > 0); - assert.equal(typeof API.sourceCommit, "string"); - assert.ok(API.protocolVersion >= 1); - assert.ok(API.cacheEpoch >= 1); - // mcpp 2026.10.29.1 answers 37 directives (31 when 0.9.0 was generated). - assert.equal(API.directives.length, 37); - assert.equal(directives().length, apiJson.directives.length); -}); - -test("the snapshot's own invariants hold", () => { - assert.deepEqual(apiProblems(), []); -}); - -test("every role the engine knows is present", () => { - assert.equal(API.roles.length, ACTION_ROLES.length); - for (const role of ACTION_ROLES) { - assert.ok(API.roles.includes(role), `missing role ${role}`); - } -}); - -test("directive() resolves wire names and only wire names", () => { - const cxxflag = directive("cxxflag"); - assert.ok(cxxflag); - assert.equal(cxxflag.slot, "CxxFlags"); - assert.equal(cxxflag.scope, "PackagePrivate"); - assert.equal(cxxflag.transform, "Verbatim"); - assert.equal(cxxflag.mustExist, false); - assert.ok(cxxflag.since >= 1); - assert.ok(Array.isArray(cxxflag.rules)); - assert.equal(directive("cxx_flag"), undefined); - assert.equal(directive("no-such-directive"), undefined); -}); - -test("apiSource() reports the generated provenance", () => { - assert.deepEqual(apiSource(), { - version: API.sourceVersion, - commit: API.sourceCommit, - }); -}); - -test("provisions carry the kind and the wire name", () => { - assert.ok(API.provisions.length >= 3); - const tool = API.provisions.find((entry) => entry.wire === "tool"); - assert.ok(tool); - assert.equal(tool.kind, "tool"); - for (const entry of API.provisions) { - assert.equal(typeof entry.kind, "string"); - assert.ok(entry.wire.length > 0); - } -}); - -test( - "the table size and every wire name match the mcpp source", - { skip: skipReason }, - () => { - assert.ok(mcppRepo); - const source = fs.readFileSync( - path.join(mcppRepo, "modules", "buildmcpp", "src", "directives.cppm"), - "utf8", - ); - const declared = /inline constexpr\s+std::array\s+kTable\{\{/.exec(source); - assert.ok(declared, "the declared kTable size is missing"); - - const open = source.indexOf("kTable{{"); - const body = source.slice(open, source.indexOf("}};", open)); - const rows = body - .split(/\r?\n/) - .filter((line) => /Slot::\w+\s*,\s*Scope::\w+\s*,\s*Transform::\w+/.test(line)); - - assert.equal(Number(declared[1]), directives().length); - assert.equal(rows.length, directives().length); - for (const entry of directives()) { - assert.ok(body.includes(`"${entry.wire}"`), `source has no row for ${entry.wire}`); - } - }, -); diff --git a/mide-plugins/vscode/test/buildscript/modules.test.ts b/mide-plugins/vscode/test/buildscript/modules.test.ts deleted file mode 100644 index 0df8cff..0000000 --- a/mide-plugins/vscode/test/buildscript/modules.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - isKnownModule, - knownModule, - knownModules, - scanImports, -} from "../../src/buildscript/modules"; - -test("scans plain, exported and dotted imports", () => { - const sites = scanImports([ - "import std;", - "export import mcpp;", - "import mcpp.plugins.tool; // trailing comment", - ]); - assert.deepEqual( - sites.map((site) => site.module), - ["std", "mcpp", "mcpp.plugins.tool"], - ); - assert.deepEqual( - sites.map((site) => site.line), - [0, 1, 2], - ); -}); - -test("reports the module name's own span, not the statement's", () => { - const [site] = scanImports(["import mcpp.core; "]); - assert.equal(site.module, "mcpp.core"); - assert.equal(site.startCharacter, 9); - assert.equal(site.endCharacter, 18); -}); - -test("finds two imports on one line", () => { - const sites = scanImports(["import a; import b.c;"]); - assert.deepEqual( - sites.map((site) => site.module), - ["a", "b.c"], - ); -}); - -test("ignores import inside strings, line comments and block comments", () => { - assert.deepEqual( - scanImports([ - "// import fake;", - 'const char* s = "import fake;";', - "/* import fake; */", - "/*", - "import fake;", - "*/", - 'const char* t = R"(import fake;)";', - "important(); // import fake;", - ]), - [], - ); -}); - -test("ignores header units, header imports and partitions", () => { - assert.deepEqual(scanImports(['import ;', 'import "vector.h";', "import :part;"]), []); -}); - -test("a comment after a real import does not hide it", () => { - const sites = scanImports(["import std; // import fake;"]); - assert.deepEqual( - sites.map((site) => site.module), - ["std"], - ); -}); - -test("recognises the standard library, the engine and mcpp plugins", () => { - for (const name of [ - "std", - "std.compat", - "mcpp", - "mcpp.core", - "mcpp.plugins.tool", - "mcpp.plugins.a.b", - "mcpp.deps.vcpkg", - "mcpp.rules.qt", - ]) { - assert.equal(isKnownModule(name), true, `${name} should be known`); - } -}); - -test("does not claim names that are not modules", () => { - for (const name of [ - "mcpp.toml", - "mcppx", - "stdlib", - "std.compat.x", - "mcpp.plugins", - "mcpp.plugins.", - "mcpp.core.x", - "", - ]) { - assert.equal(isKnownModule(name), false, `${name} should not be known`); - } -}); - -test("knownModule() describes a wildcard match with the queried name", () => { - const plugin = knownModule("mcpp.plugins.tool"); - assert.ok(plugin); - assert.equal(plugin.name, "mcpp.plugins.tool"); - assert.ok(plugin.description.length > 0); - assert.ok(plugin.docsUrl?.startsWith("https://github.com/mcpp-community/mcpp")); - assert.equal(knownModule("mcpp.toml"), undefined); - assert.equal(knownModule("nope"), undefined); -}); - -test("knownModules() lists the exact names a build program can import", () => { - const names = knownModules().map((entry) => entry.name); - for (const name of ["std", "std.compat", "mcpp", "mcpp.core", "mcpp.plugins.*"]) { - assert.ok(names.includes(name), `missing ${name}`); - } - for (const entry of knownModules()) { - assert.ok(entry.description.length > 0, `${entry.name} has no description`); - } -}); diff --git a/mide-plugins/vscode/test/buildscript/settings.test.ts b/mide-plugins/vscode/test/buildscript/settings.test.ts deleted file mode 100644 index 6d122a3..0000000 --- a/mide-plugins/vscode/test/buildscript/settings.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { analyseBuildScript } from "../../src/buildscript/analysis"; -import { isKnownModule } from "../../src/buildscript/modules"; -import { buildScriptContributions } from "../../src/buildscript/settings"; - -/** - * The four boolean keys `settings.ts` resolves, with the registry's defaults. - * A stub reader records what was asked for, so a wiring change that stops - * reading a key fails the first test instead of silently keeping a default. - */ -const DEFAULTS: Readonly> = { - "mcpp.buildScript.intelligence": true, - "mcpp.buildScript.diagnostics": true, - "mcpp.buildScript.snippets": true, - "mcpp.buildScript.imports.knownModules": true, -}; - -function reader(overrides: Record = {}): { - read: (key: string) => T; - keys: string[]; -} { - const keys: string[] = []; - const values: Record = { ...DEFAULTS, ...overrides }; - return { - keys, - read: (key: string): T => { - keys.push(key); - return values[key] as T; - }, - }; -} - -test("all four switches are resolved, each from its own registry key", () => { - const { read, keys } = reader(); - const contributions = buildScriptContributions(read, () => "warning"); - assert.deepEqual(contributions, { - intelligence: true, - diagnosticSeverity: "warning", - snippets: true, - knownModules: true, - }); - // The four literal keys matter: `test/config/wiring.test.ts` scans the sources - // for exactly this accessor shape to prove each declared setting is read. - assert.deepEqual(keys.sort(), [ - "mcpp.buildScript.diagnostics", - "mcpp.buildScript.imports.knownModules", - "mcpp.buildScript.intelligence", - "mcpp.buildScript.snippets", - ]); -}); - -test("mcpp.buildScript.snippets=false drops snippets and nothing else", () => { - const contributions = buildScriptContributions( - reader({ "mcpp.buildScript.snippets": false }).read, - () => "warning", - ); - assert.equal(contributions.snippets, false); - assert.equal(contributions.intelligence, true, "snippets must not turn the whole layer off"); - assert.equal(contributions.diagnosticSeverity, "warning", "diagnostics must stay"); - assert.equal(contributions.knownModules, true, "import completion must stay"); -}); - -test("mcpp.buildScript.imports.knownModules=false drops only the completion list", () => { - const contributions = buildScriptContributions( - reader({ "mcpp.buildScript.imports.knownModules": false }).read, - () => "warning", - ); - assert.equal(contributions.knownModules, false); - assert.equal(contributions.intelligence, true); - assert.equal(contributions.snippets, true); - assert.equal(contributions.diagnosticSeverity, "warning"); -}); - -test("mcpp.buildScript.diagnostics=false stops diagnostics but keeps completion and hover", () => { - const contributions = buildScriptContributions( - reader({ "mcpp.buildScript.diagnostics": false }).read, - () => "warning", - ); - assert.equal(contributions.diagnosticSeverity, undefined); - assert.equal(contributions.intelligence, true); - assert.equal(contributions.snippets, true); - assert.equal(contributions.knownModules, true); -}); - -test("mcpp.buildScript.intelligence=false leaves no build.mcpp intelligence at all", () => { - const contributions = buildScriptContributions( - reader({ "mcpp.buildScript.intelligence": false }).read, - () => "off", - ); - assert.equal(contributions.intelligence, false); - assert.equal(contributions.diagnosticSeverity, undefined); - assert.equal(contributions.snippets, false); - assert.equal(contributions.knownModules, false); -}); - -test("a severity of off publishes nothing, and a valid severity is passed through", () => { - const off = buildScriptContributions(reader().read, () => "off"); - assert.equal(off.diagnosticSeverity, undefined); - assert.equal(off.intelligence, true, "severity=off must not disable completion or hover"); - - const info = buildScriptContributions(reader().read, () => "info"); - assert.equal(info.diagnosticSeverity, "info"); -}); - -test("known modules stay recognised and are never reported missing with every switch off", () => { - const { read } = reader({ - "mcpp.buildScript.intelligence": false, - "mcpp.buildScript.diagnostics": false, - "mcpp.buildScript.snippets": false, - "mcpp.buildScript.imports.knownModules": false, - }); - const contributions = buildScriptContributions(read, () => "off"); - assert.equal(contributions.diagnosticSeverity, undefined, "no diagnostics with everything off"); - - // §3.2.1: mcpp keeps `build.mcpp` out of the compilation database, so this - // extension can never validate an import. The "never report std/std.compat/ - // mcpp.* as missing" rule is therefore absolute and lives in the analyser and - // the recognition table, not behind `mcpp.buildScript.imports.knownModules` - // (that key removes the *completion list* only). The two `mcpp.toml. - // indexCompletion*` switches act on a different language and cannot reach - // this path, so "every switch off" here is the five `mcpp.buildScript.*` keys. - const lines = [ - "import std;", - "import std.compat;", - "import mcpp;", - "import mcpp.core;", - "import mcpp.plugins.tool;", - "import mcpp.deps.something;", - ]; - assert.deepEqual(analyseBuildScript(lines, "warning"), []); - for (const name of ["std", "std.compat", "mcpp", "mcpp.core", "mcpp.plugins.tool"]) { - assert.equal(isKnownModule(name), true, `${name} stopped being recognised`); - } -}); diff --git a/mide-plugins/vscode/test/buildscript/snippets.test.ts b/mide-plugins/vscode/test/buildscript/snippets.test.ts deleted file mode 100644 index d99bbcf..0000000 --- a/mide-plugins/vscode/test/buildscript/snippets.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { ACTION_ROLES, API } from "../../src/buildscript/api"; -import { - actionSnippets, - buildScriptSnippets, - directiveCallSnippets, - snippetsForScope, -} from "../../src/buildscript/snippets"; - -test("every snapshot directive but action has exactly one call snippet", () => { - const expected = new Set( - API.directives.map((entry) => entry.wire.replace(/-/g, "_")).filter((name) => name !== "action"), - ); - const snippets = directiveCallSnippets(); - assert.equal(snippets.length, expected.size); - assert.deepEqual( - [...new Set(snippets.map((snippet) => snippet.filterText))].sort(), - [...expected].sort(), - ); - // A declaration is not a call: `mcpp::action` must not get an `action(...)` stub. - assert.ok(!snippets.some((snippet) => snippet.filterText === "action")); -}); - -test("a directive snippet is the typed spelling with one editable argument", () => { - const snippet = directiveCallSnippets().find((entry) => entry.filterText === "link_script"); - assert.ok(snippet !== undefined, "link_script disappeared from the snippet list"); - assert.equal(snippet.label, "link_script(...)"); - assert.equal(snippet.insertText, 'link_script("${1:value}")'); - assert.match(snippet.detail, /mcpp:link-script=/); -}); - -test("the action snippets carry a role, and the prepare one carries output_dir", () => { - const snippets = actionSnippets(); - assert.equal(snippets.length, 2); - for (const snippet of snippets) { - assert.equal(snippet.filterText, "action"); - assert.match(snippet.insertText, /\.submit\(\);/); - assert.match(snippet.insertText, /mcpp::roles::/); - } - const prepare = snippets.find((snippet) => snippet.label.includes("prepare")); - assert.ok(prepare !== undefined); - assert.match(prepare.insertText, /mcpp::roles::prepare/); - assert.match(prepare.insertText, /\.output_dir\("/); - assert.match(prepare.detail, /R3\.3/); - - const general = snippets.find((snippet) => snippet.label.includes("typed")); - assert.ok(general !== undefined); - // The role choice lists the engine's five roles. - for (const role of ACTION_ROLES) { - assert.ok(general.insertText.includes(role), `${role} is missing from the role choice`); - } -}); - -test("snippetsForScope narrows on the typed mcpp:: prefix", () => { - const all = buildScriptSnippets(); - assert.equal(snippetsForScope("").length, all.length); - const link = snippetsForScope("link_"); - assert.ok(link.length > 0); - assert.ok(link.every((snippet) => snippet.filterText.startsWith("link_"))); - assert.ok(!link.some((snippet) => snippet.filterText === "action")); - assert.equal(snippetsForScope("action").length, actionSnippets().length); - assert.deepEqual(snippetsForScope("zzz_not_a_directive"), []); -}); - -test("every snippet is insertable: it has a placeholder and a non-empty body", () => { - for (const snippet of buildScriptSnippets()) { - assert.ok(snippet.insertText.length > 0, `${snippet.label} has no insert text`); - assert.match(snippet.insertText, /\$\{1[:|]/, `${snippet.label} has no first placeholder`); - assert.ok(snippet.label.length > 0); - assert.ok(snippet.detail.length > 0); - } -}); diff --git a/mide-plugins/vscode/test/cli/search.test.ts b/mide-plugins/vscode/test/cli/search.test.ts deleted file mode 100644 index 6e3605d..0000000 --- a/mide-plugins/vscode/test/cli/search.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - indexCompletionRequest, - parseSearchOutput, - searchArguments, - shouldSearch, - withDeadline, -} from "../../src/cli/search"; - -/** - * A capture of `mcpp search z --all-versions` from mcpp 2026.9.30.2: single - * versions end the line with `(x.y.z)`, `--all-versions` lines list several - * comma-separated versions (with a trailing `...`), and xim-only entries carry - * no version at all. - */ -const REAL_CAPTURE = [ - " compat:gzip-hpp gzip-hpp — header-only gzip/deflate compression wrappers over zlib (0.1.0)", - " compat:libpng PNG reference library — portable PNG encode/decode (depends on zlib) (1.6.43)", - " compat:zlib A compression library (1.3.2)", - " freedesktop:wayland-protocols-unstable wayland-protocols unstable — the zwp_*/zxdg_* protocols still in flux (1.49.1, 1.49)", - " mcpplibs:aarch64-virt-rt Board support for QEMU's aarch64 virt machine (0.2.1, 0.2.0, 0.1.1, ...)", - " scode:zlib A Massively Spiffy Yet Delicately Unobtrusive Compression Library", - " xim:zlib A massively spiffy yet delicately unobtrusive compression library", -].join("\n"); - -test("parses a real capture newest-first and skips narration and unversioned lines", () => { - const versions = parseSearchOutput(REAL_CAPTURE); - assert.deepEqual( - versions.map((entry) => entry.version), - ["1.49.1", "1.49", "1.6.43", "1.3.2", "0.2.1", "0.2.0", "0.1.1", "0.1.0"], - ); - // `...` (the "more versions exist" marker) must not become a version. - assert.ok(versions.every((entry) => /^\d/.test(entry.version))); - assert.equal(versions.find((entry) => entry.version === "1.3.2")?.summary, "A compression library"); - assert.equal(versions.find((entry) => entry.version === "0.1.0")?.summary, "gzip-hpp — header-only gzip/deflate compression wrappers over zlib"); -}); - -test("accepts leading narration around the package list", () => { - const withNarration = [ - "Refreshing package index ...", - "", - REAL_CAPTURE, - "", - "note: run mcpp search --all-versions for the full list", - ].join("\n"); - assert.equal(parseSearchOutput(withNarration).length, parseSearchOutput(REAL_CAPTURE).length); -}); - -test("de-duplicates repeated versions across lines", () => { - const output = [ - " compat:zlib A compression library (1.3.2)", - " compat:zlib A compression library (1.3.2)", - " compat:zlib A compression library (1.2.9)", - ].join("\n"); - assert.deepEqual(parseSearchOutput(output).map((entry) => entry.version), ["1.3.2", "1.2.9"]); -}); - -test("orders versions semver-ish, not lexically", () => { - const output = [ - " a:one A library (1.9.0)", - " a:one A library (1.10.0)", - " a:one A library (1.9)", - ].join("\n"); - assert.deepEqual(parseSearchOutput(output).map((entry) => entry.version), ["1.10.0", "1.9.0", "1.9"]); -}); - -test("returns nothing for empty, whitespace-only and narration-only input", () => { - assert.deepEqual(parseSearchOutput(""), []); - assert.deepEqual(parseSearchOutput(" \n\n "), []); - const narration = [ - "Refreshing package index ...", - "note: 0 packages matched", - "warning: using the cached index", - " some:thing looks like a package but has no version", - ].join("\n"); - assert.deepEqual(parseSearchOutput(narration), []); - // 括号里不是版本号也不行。 - assert.deepEqual(parseSearchOutput(" a:b description (not a version)"), []); -}); - -test("tolerates carriage returns and terminal colours", () => { - const output = " compat:zlib A compression library (1.3.2)\r\n\u001b[32m compat:zlib A compression library (1.2.9)\u001b[0m"; - assert.deepEqual(parseSearchOutput(output).map((entry) => entry.version), ["1.3.2", "1.2.9"]); -}); - -test("searchArguments asks for every version", () => { - assert.deepEqual(searchArguments("zlib"), ["search", "zlib", "--all-versions"]); - assert.deepEqual(searchArguments("compat.zlib"), ["search", "compat.zlib", "--all-versions"]); -}); - -test("shouldSearch is enabled && trusted && !offline", () => { - for (const enabled of [true, false]) { - for (const trusted of [true, false]) { - for (const offline of [true, false]) { - assert.equal( - shouldSearch("zlib", { enabled, trusted, offline }), - enabled && trusted && !offline, - `enabled=${enabled} trusted=${trusted} offline=${offline}`, - ); - } - } - } - // 唯独"全开"才查询:这也是方案里默认关(enabled=false)的落点。 - assert.equal(shouldSearch("zlib", { enabled: true, trusted: true, offline: false }), true); - assert.equal(shouldSearch("zlib", { enabled: true, trusted: false, offline: false }), false); - assert.equal(shouldSearch("zlib", { enabled: true, trusted: true, offline: true }), false); - assert.equal(shouldSearch("zlib", { enabled: false, trusted: true, offline: false }), false); -}); - -test("indexCompletionRequest carries the switches into argv and the timeout", () => { - const on = { enabled: true, trusted: true, offline: false, timeoutSeconds: 5 }; - assert.deepEqual(indexCompletionRequest("zlib", on), { - name: "zlib", - args: ["search", "zlib", "--all-versions"], - timeoutMs: 5000, - }); - - // The default is off, and every one of the three switches alone withholds the - // request — no argv, no process. - assert.equal(indexCompletionRequest("zlib", { ...on, enabled: false }), undefined); - assert.equal(indexCompletionRequest("zlib", { ...on, trusted: false }), undefined); - assert.equal(indexCompletionRequest("zlib", { ...on, offline: true }), undefined); - - // The registry's minimum is 1 second; a hand-edited non-number falls back to - // the declared default of 20 seconds rather than to "no timeout". - assert.equal(indexCompletionRequest("zlib", { ...on, timeoutSeconds: 0 })?.timeoutMs, 1000); - assert.equal(indexCompletionRequest("zlib", { ...on, timeoutSeconds: -3 })?.timeoutMs, 1000); - assert.equal(indexCompletionRequest("zlib", { ...on, timeoutSeconds: 20 })?.timeoutMs, 20_000); - assert.equal( - indexCompletionRequest("zlib", { ...on, timeoutSeconds: Number.NaN })?.timeoutMs, - 20_000, - ); -}); - -test("withDeadline returns the work's value and gives up when work never settles", async () => { - assert.equal(await withDeadline(Promise.resolve("done"), 1000), "done"); - const neverSettles = new Promise(() => undefined); - assert.equal(await withDeadline(neverSettles, 20), undefined); -}); diff --git a/mide-plugins/vscode/test/config/wiring.test.ts b/mide-plugins/vscode/test/config/wiring.test.ts index e5ca7f1..371e43c 100644 --- a/mide-plugins/vscode/test/config/wiring.test.ts +++ b/mide-plugins/vscode/test/config/wiring.test.ts @@ -50,6 +50,87 @@ const READ_INDIRECTLY: ReadonlyArray<{ key: string; file: string; evidence: stri file: "src/ide/session.ts", evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", }, + // The rules layer of mcpp.toml and build.mcpp is the backend's (design 2026-10-30 part 2 §8). + { + key: "mcpp.toml.completion", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.hover", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.navigation", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.diagnostics.enabled", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.diagnostics.syntax", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.diagnostics.unknownSection", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.diagnostics.unknownKey", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.diagnostics.planeSeparation", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.diagnostics.legacyKeys", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.indexCompletion", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.toml.indexCompletionTimeoutSeconds", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.buildScript.intelligence", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.buildScript.diagnostics", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.buildScript.diagnostics.severity", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.buildScript.imports.knownModules", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, + { + key: "mcpp.buildScript.snippets", + file: "src/ide/session.ts", + evidence: "for (const key of Object.keys(props)) out[key] = config.get(key);", + }, { key: "mcpp.task.buildArgs", file: "src/cli/tasks.ts", diff --git a/mide-plugins/vscode/test/e2e/runTest.ts b/mide-plugins/vscode/test/e2e/runTest.ts index cef4a64..8def9ef 100644 --- a/mide-plugins/vscode/test/e2e/runTest.ts +++ b/mide-plugins/vscode/test/e2e/runTest.ts @@ -1,4 +1,4 @@ -import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -97,6 +97,7 @@ async function runVariant(repositoryRoot: string, fixtureRoot: string, variant: MCPP_E2E_LOG: mcppLogPath, MCPP_E2E_MCPPLS_LOG: mcpplsLogPath, MCPP_E2E_STUB: variant, + MCPP_E2E_NO_BACKEND: process.env.MCPP_E2E_NO_BACKEND ?? "", }, }); } finally { @@ -117,6 +118,22 @@ async function main(): Promise { throw new Error(`MCPP_E2E_STUB=${requested} is not one of ${STUB_VARIANTS.join(", ")}`); } + // MCPP_E2E_NO_BACKEND=1: the bundle is carried, and its runners are not -- the platform okrun does + // not cover -- so the extension runs in its declared degraded mode (part 2 §8, F15). + if (process.env.MCPP_E2E_NO_BACKEND === "1") { + const runners = join(repositoryRoot, "backend", "okrun"), hidden = `${runners}.hidden`; + if (!existsSync(join(repositoryRoot, "backend", "mcpp-ide.okb")) || process.env.MCPP_IDE_BACKEND) { + throw new Error("the degraded run needs the staged bundle (npm run stage:backend) and no MCPP_IDE_BACKEND"); + } + if (existsSync(runners)) renameSync(runners, hidden); + try { + console.log("e2e: running the extension host without its backend"); + await runVariant(repositoryRoot, fixtureRoot, "full"); + } finally { + if (existsSync(hidden)) renameSync(hidden, runners); + } + return; + } for (const variant of variants) { console.log(`e2e: running the extension host against the "${variant}" mcppls stub`); await runVariant(repositoryRoot, fixtureRoot, variant); diff --git a/mide-plugins/vscode/test/e2e/suite/index.ts b/mide-plugins/vscode/test/e2e/suite/index.ts index 392d38b..c9df139 100644 --- a/mide-plugins/vscode/test/e2e/suite/index.ts +++ b/mide-plugins/vscode/test/e2e/suite/index.ts @@ -3,7 +3,8 @@ import path from "node:path"; export function run(): Promise { const mocha = new Mocha({ ui: "tdd", color: true, timeout: 30_000 }); - mocha.addFile(path.resolve(__dirname, "extension.e2e.js")); + // MCPP_E2E_NO_BACKEND: the declared degraded mode, without the backend; else the whole suite. + mocha.addFile(path.resolve(__dirname, process.env.MCPP_E2E_NO_BACKEND === "1" ? "nobackend.e2e.js" : "extension.e2e.js")); return new Promise((resolvePromise, reject) => { mocha.run((failures) => { if (failures === 0) { diff --git a/mide-plugins/vscode/test/e2e/suite/nobackend.e2e.ts b/mide-plugins/vscode/test/e2e/suite/nobackend.e2e.ts new file mode 100644 index 0000000..c5918f9 --- /dev/null +++ b/mide-plugins/vscode/test/e2e/suite/nobackend.e2e.ts @@ -0,0 +1,43 @@ +// The declared degraded mode (mcpp design 2026-10-30 part 2 §8, F15): the extension carries the +// backend, and okrun runs on neither this system nor PATH, as on macOS x86_64, Windows arm64 or Linux +// armhf. The extension says so, does not try to start the backend again, and build, run, test and +// the project pages go on: a build runs mcpp, and mcpp's queries run here rather than through the +// backend. +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import * as vscode from "vscode"; + +async function waitFor(check: () => boolean, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (check()) return true; + await new Promise((r) => setTimeout(r, 100)); + } + return check(); +} + +suite("mcpp extension without its backend (the declared degraded mode)", () => { + test("the extension activates in the degraded mode, and says so", async () => { + const extension = vscode.extensions.getExtension("mcpp-community.mcpp-vscode"); + assert.ok(extension); + const api = (await extension.activate()) as { session: { degraded: boolean; running: boolean; request(method: string): Promise } }; + assert.ok(await waitFor(() => api.session.degraded, 20_000), "the session is degraded"); + assert.equal(api.session.running, false); + await assert.rejects(api.session.request("selfCheck"), /not running/); + }); + + test("a build runs mcpp, and mcpp's queries run without the backend", async () => { + const folder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(folder); + const fakeMcpp = process.env.MCPP_E2E_FAKE_MCPP, mcppLog = process.env.MCPP_E2E_LOG; + assert.ok(fakeMcpp && mcppLog); + await vscode.workspace.getConfiguration("mcpp", folder.uri).update("path", fakeMcpp, vscode.ConfigurationTarget.Workspace); + void vscode.commands.executeCommand("mcpp.build"); + const lines = () => (existsSync(mcppLog) ? readFileSync(mcppLog, "utf8").split(/\r?\n/) : []); + assert.ok(await waitFor(() => lines().includes("build"), 20_000), `mcpp build did not run: ${lines().join(",")}`); + // The self-check asks mcpp for its version and protocol (0.9.0's parsers, mcpp run here). + void vscode.commands.executeCommand("mcpp.selfCheck"); + assert.ok(await waitFor(() => lines().some((l) => l.startsWith("--version") || l.startsWith("--protocol-version")), 20_000), + `no query reached mcpp: ${lines().join(",")}`); + }); +}); diff --git a/mide-plugins/vscode/test/i18n/hardcoded.test.ts b/mide-plugins/vscode/test/i18n/hardcoded.test.ts index 82d9539..c1ffcc5 100644 --- a/mide-plugins/vscode/test/i18n/hardcoded.test.ts +++ b/mide-plugins/vscode/test/i18n/hardcoded.test.ts @@ -66,7 +66,7 @@ test("the scan still inspects every source file", () => { for (const expected of [ path.join("src", "cli", "controller.ts"), path.join("src", "extension.ts"), - path.join("src", "toml", "completion.ts"), + path.join("src", "toml", "parser.ts"), path.join("src", "views", "languageServerView.ts"), ]) { assert.ok(files.includes(path.join(process.cwd(), expected)), `the scan skipped ${expected}`); diff --git a/mide-plugins/vscode/test/ide/client.test.ts b/mide-plugins/vscode/test/ide/client.test.ts index e6fc912..9c92431 100644 --- a/mide-plugins/vscode/test/ide/client.test.ts +++ b/mide-plugins/vscode/test/ide/client.test.ts @@ -1,52 +1,102 @@ -// The wire against the real backend: frames split anywhere reassemble; `initialize` over the mock -// mcpp finds the project; a build's notifications arrive and end in job/finished. +// The wire against the real backend, through the JSON-RPC library VS Code's language client is built +// on: `initialize` in LSP's shape over the mock mcpp finds the project; a build's notifications +// arrive under `mcpp-ide/` and end in job/finished; mcpp.toml opened has its diagnostics published, +// and completion and hover answer for it (design 2026-10-30 part 2 §8). import * as assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { pathToFileURL } from "node:url"; +import * as path from "node:path"; import { test } from "node:test"; +import { createMessageConnection, StreamMessageReader, StreamMessageWriter, type MessageConnection } from "vscode-jsonrpc/node"; + import { runProcess, useMcppExecutor } from "../../src/cli/process"; -import { Backend, encode, FrameReader } from "../../src/ide/backend/client"; import { built, project } from "./harness"; -test("frames reassemble whatever the chunking", () => { - const all = Buffer.concat([Buffer.from("Junk: 1\r\n\r\n"), encode({ a: 1 }), encode({ b: "é" })]); - const r = new FrameReader(); - const bodies: string[] = []; - for (let i = 0; i < all.length; i++) bodies.push(...r.feed(all.subarray(i, i + 1))); - assert.deepEqual(bodies.map((b) => JSON.parse(b)), [{ a: 1 }, { b: "é" }]); -}); +interface Session { + connection: MessageConnection; + request(method: string, params?: unknown): Promise; + stop(): Promise; +} + +/** `mcpp-ide serve`, initialized for a workspace folder over the mock mcpp. */ +async function start(backend: string, mock: string, root: string): Promise<{ session: Session; init: any }> { + const child = spawn(backend, ["serve"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); + const connection = createMessageConnection(new StreamMessageReader(child.stdout!), new StreamMessageWriter(child.stdin!)); + connection.listen(); + const ended = new Promise((resolve) => child.on("exit", () => resolve())); + const session: Session = { + connection, + request: (method: string, params: unknown = {}) => connection.sendRequest(`mcpp-ide/${method}`, params) as Promise, + async stop() { + await connection.sendRequest("shutdown"); + connection.sendNotification("exit"); + await ended; + connection.dispose(); + }, + }; + const init = await connection.sendRequest("initialize", { + processId: process.pid, + clientInfo: { name: "test" }, + workspaceFolders: [{ uri: pathToFileURL(root).href, name: "app" }], + capabilities: {}, + initializationOptions: { editor: "vscode", settings: { "mcpp.path": mock } }, + }); + connection.sendNotification("initialized", {}); + return { session, init }; +} test("a session over the mock mcpp", async (t) => { const backend = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); if (!backend || !mock) { t.skip("build the backend first (mcpp build)"); return; } const root = project(); - const b = new Backend({ program: backend }); - const init = await b.request<{ projects: { root: string }[]; mcpp: { contract: boolean } }>("initialize", { - editor: "vscode", settings: { "mcpp.path": mock }, workspaceFolders: [root], - }); + const { session, init } = await start(backend, mock, root); assert.equal(init.projects.length, 1); assert.equal(init.mcpp.contract, true); + assert.equal(init.protocol, 2); + assert.equal(init.capabilities.textDocumentSync.change, 1); const seen: string[] = []; - b.notifications.on("*", (method: string) => seen.push(method)); - const finished = new Promise((resolve) => b.notifications.on("job/finished", resolve)); - const { jobId } = await b.request<{ jobId: string }>("job/start", { kind: "build", root }); + for (const m of ["job/started", "job/plan", "job/progress", "job/artifact"]) + session.connection.onNotification(`mcpp-ide/${m}`, () => seen.push(m)); + const finished = new Promise((resolve) => session.connection.onNotification("mcpp-ide/job/finished", resolve)); + const { jobId } = await session.request<{ jobId: string }>("job/start", { kind: "build", root }); assert.match(jobId, /^job-/); const done = (await finished) as { status: string }; assert.equal(done.status, "ok"); for (const m of ["job/started", "job/plan", "job/progress", "job/artifact"]) assert.ok(seen.includes(m), m); - await assert.rejects(b.request("no/such/method"), /unknown method/); - await b.stop(); - assert.equal(b.running, false); + await assert.rejects(session.request("no/such/method"), /unknown method/); + await session.stop(); +}); + +test("mcpp.toml through LSP: diagnostics published, completion, hover", async (t) => { + const backend = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); + if (!backend || !mock) { t.skip("build the backend first (mcpp build)"); return; } + const root = project(); + const { session } = await start(backend, mock, root); + const uri = pathToFileURL(path.join(root, "mcpp.toml")).href; + const published = new Promise((resolve) => session.connection.onNotification("textDocument/publishDiagnostics", resolve)); + session.connection.sendNotification("textDocument/didOpen", { + textDocument: { uri, languageId: "mcpp-toml", version: 1, text: "[package]\nname = \"名字\"\ncolour = 1\n[build]\nbmi_schedule = \"\"\n" }, + }); + const diagnostics = await published; + assert.equal(diagnostics.uri, uri); + assert.deepEqual(diagnostics.diagnostics.map((d: any) => [d.code, d.range.start.line, d.range.start.character]), + [["mcpp.manifest.unknownKey", 2, 0]]); + const list = await session.connection.sendRequest("textDocument/completion", { textDocument: { uri }, position: { line: 4, character: 16 } }); + assert.deepEqual(list.items.map((i: any) => i.label).sort(), ["auto", "off", "on"]); + const hover = await session.connection.sendRequest("textDocument/hover", { textDocument: { uri }, position: { line: 4, character: 2 } }); + assert.match(hover.contents.value, /bmi_schedule/); + await session.stop(); }); test("mcpp runs through the backend, and the extension's parsers read what it printed", async (t) => { const backend = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); if (!backend || !mock) { t.skip("build the backend first (mcpp build)"); return; } const root = project(); - const b = new Backend({ program: backend }); - await b.request("initialize", { editor: "vscode", settings: { "mcpp.path": mock }, workspaceFolders: [root] }); + const { session } = await start(backend, mock, root); // 0.9.0's seam (`runProcess`) handed to the backend, as the extension does at activation. useMcppExecutor(async (_exe, args, cwd, options = {}) => { - const r = await b.request<{ exitCode: number; stdout: string; stderr: string }>("mcpp/exec", { + const r = await session.request<{ exitCode: number; stdout: string; stderr: string }>("mcpp/exec", { args, ...(cwd ? { cwd } : {}), ...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}), }); return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr }; @@ -55,12 +105,12 @@ test("mcpp runs through the backend, and the extension's parsers read what it pr const version = await runProcess("mcpp", ["--version"], root); assert.equal(version.exitCode, 0); assert.match(version.stdout, /^mcpp \d/); - const files = await b.request<{ manifest: string; lock: string | null; sourceFiles: number }>("project/files", { root }); + const files = await session.request<{ manifest: string; lock: string | null; sourceFiles: number }>("project/files", { root }); assert.match(files.manifest, /^\[package\]/); assert.equal(files.lock, null); assert.equal(files.sourceFiles, 0); } finally { useMcppExecutor(undefined); - await b.stop(); + await session.stop(); } }); diff --git a/mide-plugins/vscode/test/ide/locate.test.ts b/mide-plugins/vscode/test/ide/locate.test.ts index 4e66554..d6a2bce 100644 --- a/mide-plugins/vscode/test/ide/locate.test.ts +++ b/mide-plugins/vscode/test/ide/locate.test.ts @@ -5,7 +5,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { test } from "node:test"; -import { locateBackend, okrunDir, onPath } from "../../src/ide/backend/locate"; +import { backendUnavailableHere, locateBackend, okrunDir, onPath } from "../../src/ide/backend/locate"; import { mcpplsCandidates } from "../../src/ide/backend/mcppls"; // PATH is this host's (its separator, its suffix), so these use the host's own system. @@ -38,6 +38,13 @@ test("the bundle runs under the okrun carried for this system, else okrun on PAT // No okrun at all: nothing in the package starts. const none = locateBackend({ extensionRoot: ext, platform: host, arch: "arm64", env: { PATH: "" }, home: "/nonexistent" }); assert.equal(none, undefined); + // That is the declared degraded mode (part 2 §8, F15): the bundle is carried, no runner runs it here. + assert.equal(backendUnavailableHere({ extensionRoot: ext, platform: host, arch: "arm64", env: { PATH: "" } }), true); + assert.equal(backendUnavailableHere({ extensionRoot: ext, platform: host, arch: "x64", env: { PATH: "" } }), false); + assert.equal(backendUnavailableHere({ extensionRoot: ext, platform: host, arch: "arm64", env: { PATH: bin } }), false); + // Without the bundle the backend is not installed, which is another message. + const bare = fs.mkdtempSync(path.join(os.tmpdir(), "mcppide-bare-")); + assert.equal(backendUnavailableHere({ extensionRoot: bare, platform: host, arch: "arm64", env: { PATH: "" } }), false); assert.equal(okrunDir("linux", "x64"), "linux-x86_64"); assert.equal(okrunDir("win32", "x64"), "windows-x86_64"); assert.equal(okrunDir("darwin", "arm64"), "macos-aarch64"); diff --git a/mide-plugins/vscode/test/toml/completion.test.ts b/mide-plugins/vscode/test/toml/completion.test.ts deleted file mode 100644 index 0974d31..0000000 --- a/mide-plugins/vscode/test/toml/completion.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - computeMcppTomlCompletions, - computeMcppTomlCompletionsWithIndex, - dependencyVersionContextAt, - indexVersionSuggestions, - type McppTomlSuggestion, -} from "../../src/toml/completion"; -import { SCHEMA } from "../../src/toml/schema"; - -function labels(suggestions: McppTomlSuggestion[]): string[] { - return suggestions.map((suggestion) => suggestion.label); -} - -test("suggests section headers on a partial bracket line", () => { - const suggestions = computeMcppTomlCompletions(["[dep"], 0, 4); - assert.ok(suggestions.length > 0); - assert.ok(suggestions.every((suggestion) => suggestion.kind === "section")); - assert.ok(labels(suggestions).includes("[dependencies]")); - assert.ok(labels(suggestions).includes("[build-dependencies]")); - assert.ok(labels(suggestions).includes("[workspace]")); - assert.ok(labels(suggestions).includes("[indices]")); - // 每条建议都带显式替换范围(覆盖已输入的 "[dep")。 - for (const suggestion of suggestions) { - assert.deepEqual(suggestion.range, { startCharacter: 0, endCharacter: 4 }); - } - // 参数化段插入 snippet。 - const targets = suggestions.find((suggestion) => suggestion.label === "[targets.]"); - assert.equal(targets?.insertSnippet, "[targets.${1:name}]"); -}); - -test("a section mcpp removed is never suggested, but an unmodelled one still is", () => { - // 快照描述 mcpp 真正校验的 schema,是段与键的主要来源;手写清单只用来补上 - // mcpp 接受、快照尚未建模的段(例如 [workspace.dependencies]), - // 所以「已移除」必须消失,「未建模」必须保留。 - const suggestions = labels(computeMcppTomlCompletions(["[xl"], 0, 3)); - assert.ok(!suggestions.includes("[xlings.envs]"), "a removed section came back"); - assert.ok(suggestions.includes("[xlings]")); - assert.ok(suggestions.includes("[xlings.workspace]"), "an unmodelled but accepted section disappeared"); -}); - -test("the header list is the snapshot with the curated labels and snippets", () => { - const suggestions = computeMcppTomlCompletions(["[hooks"], 0, 6); - // 快照的段都在(参数化段用清单里的写法),加上手写清单里快照没有的段。 - const names = labels(suggestions); - assert.ok(suggestions.length >= SCHEMA.sections.length); - assert.ok(names.includes("[workspace.dependencies]"), "an unmodelled but accepted section is missing"); - // 快照新增、手写清单没有的段也要出现。 - assert.ok(names.includes("[hooks]")); - assert.ok(names.includes("[modules]")); - assert.ok(names.includes("[c-abi]")); - // 参数化段保留手写 snippet。 - assert.equal( - suggestions.find((suggestion) => suggestion.label === "[targets.]")?.insertSnippet, - "[targets.${1:name}]", - ); - // 废弃段带一条 documentation 指向替代写法。 - const language = suggestions.find((suggestion) => suggestion.label === "[language]"); - assert.match(language?.documentation ?? "", /\[package\]\.standard/); -}); - -test("offers nothing inside [[...]] array-table headers", () => { - // mcpp manifest 不使用 TOML 数组表([[...]]):[[ 内不出建议, - // 避免把用户意图的数组表悄悄替换成普通段 [x]。 - assert.deepEqual(computeMcppTomlCompletions(["[[dep"], 0, 5), []); - assert.deepEqual(computeMcppTomlCompletions(["[[dependencies]"], 0, 3), []); -}); - -test("suggests section headers at the top of the document", () => { - const suggestions = computeMcppTomlCompletions([""], 0, 0); - assert.ok(suggestions.length > 0); - assert.ok(suggestions.every((suggestion) => suggestion.kind === "section")); -}); - -test("offers nothing in unknown sections", () => { - // 附录 A:不支持包自定义 toml 键;未知段不提供任何建议。 - assert.deepEqual(computeMcppTomlCompletions(["[mytool]", ""], 1, 0), []); - assert.deepEqual(computeMcppTomlCompletions(["[mytool]", "key = "], 1, 6), []); -}); - -test("suggests schema keys inside a known section, skipping those already used", () => { - const suggestions = computeMcppTomlCompletions(["[package]", 'name = "x"', ""], 2, 0); - assert.ok(suggestions.length > 0); - assert.ok(suggestions.every((suggestion) => suggestion.kind === "template")); - const names = labels(suggestions); - assert.ok(!names.includes("name"), "an already-used key must not be offered again"); - assert.ok(names.includes("standard")); - assert.ok(names.includes("version")); - - const standard = suggestions.find((suggestion) => suggestion.label === "standard"); - assert.ok(standard); - assert.equal(standard.insertSnippet, 'standard = "c++20"'); - assert.match(standard.detail, /enum/); - assert.match(standard.detail, /c\+\+20/); - assert.match(standard.detail, /default/); - assert.deepEqual(standard.range, { startCharacter: 0, endCharacter: 0 }); - - // 已用键按段归属剔除:上一段的 name 不得影响 [build]。 - const build = computeMcppTomlCompletions(["[package]", 'name = "x"', "", "[build]", ""], 4, 0); - const buildNames = labels(build); - assert.ok(buildNames.includes("sources")); - assert.ok(!buildNames.includes("name")); -}); - -test("suggests enum values in the value position of a known enum key", () => { - const bare = computeMcppTomlCompletions(["[package]", "standard = "], 1, 11); - assert.deepEqual(labels(bare).slice(0, 3), ["c++20", "c++23", "c++26"]); - assert.equal(bare[0].insertSnippet, '"c++20"'); - for (const suggestion of bare) { - assert.deepEqual(suggestion.range, { startCharacter: 11, endCharacter: 11 }); - } - - // 光标已在字符串里:只替换内容,不再补引号。 - const inside = computeMcppTomlCompletions(["[package]", 'standard = "c++2'], 1, 17); - assert.equal(inside[0].insertSnippet, "c++20"); - assert.deepEqual(inside[0].range, { startCharacter: 12, endCharacter: 16 }); - - // [targets.] 行表继承基段的枚举键。 - const targets = computeMcppTomlCompletions(["[targets.app]", "kind = "], 1, 7); - assert.deepEqual(labels(targets), ["bin", "lib", "shared", "app"]); -}); - -test("offers no key suggestions in sections whose keys the user picks", () => { - // [toolchain] 的 openKeys: true:平台名由用户自选,给出固定词表就是错的。 - assert.deepEqual(computeMcppTomlCompletions(["[toolchain]", ""], 1, 0), []); - assert.deepEqual(computeMcppTomlCompletions(["[toolchain]", "gcc = "], 1, 6), []); -}); - -test("suggests dependency writing templates", () => { - const suggestions = computeMcppTomlCompletions(["[dependencies]", ""], 1, 0); - assert.ok(suggestions.length > 0); - assert.ok(suggestions.every((suggestion) => suggestion.kind === "template")); - const names = labels(suggestions); - assert.ok(names.includes('name = "version"')); - assert.ok(names.includes("name = { git = ..., tag = ... }")); - assert.ok(names.includes("name = { version = ..., tools = [...] }")); - assert.deepEqual(suggestions[0].range, { startCharacter: 0, endCharacter: 0 }); -}); - -test("suggests dependency templates in conditional dependency sections", () => { - const suggestions = computeMcppTomlCompletions(["[target.'cfg(windows)'.dependencies]", ""], 1, 0); - assert.ok(labels(suggestions).includes('name = "version"')); -}); - -test("suggests dependency templates in build-dependencies", () => { - const suggestions = computeMcppTomlCompletions(["[build-dependencies]", ""], 1, 0); - assert.ok(labels(suggestions).includes('name = "version"')); - assert.ok(suggestions.every((suggestion) => suggestion.kind === "template")); -}); - -test("suggests templates in free-key sections", () => { - const features = computeMcppTomlCompletions(["[features]", ""], 1, 0); - assert.ok(features.every((suggestion) => suggestion.kind === "template")); - assert.ok(labels(features).includes("name = { defines = [...] }")); - - const capabilities = computeMcppTomlCompletions(["[capabilities]", ""], 1, 0); - assert.ok(labels(capabilities).includes('capability = "provider"')); - - const generated = computeMcppTomlCompletions(["[generated_files]", ""], 1, 0); - assert.ok(labels(generated).includes('"path" = "content"')); -}); - -test("offers nothing at value positions", () => { - // 版本候选等动态数据层落地前,值位置不出建议。 - assert.deepEqual(computeMcppTomlCompletions(["[dependencies]", 'zlib = "'], 1, 8), []); - assert.deepEqual(computeMcppTomlCompletions(["[package]", 'name = "'], 1, 7), []); -}); - -test("offers nothing inside nested inline tables", () => { - // containerPath 非空(内联表深处)不出建议。 - assert.deepEqual(computeMcppTomlCompletions(["[features]", "simd = { flags = [ { "], 1, 21), []); -}); - -test("replacement range covers a partially typed key", () => { - const suggestions = computeMcppTomlCompletions(["[dependencies]", "na"], 1, 2); - assert.ok(suggestions.length > 0); - for (const suggestion of suggestions) { - assert.deepEqual(suggestion.range, { startCharacter: 0, endCharacter: 2 }); - } -}); - -// ── 依赖版本补全(mcpp.toml.indexCompletion,默认关)──────────────────────── -// 纯文本层只回答两件事:光标是不是在依赖的「版本值」上,候选怎么变成建议。 -// 执行 `mcpp search`、超时、信任与缓存都在 src/toml/providers.ts。 - -test("locates a dependency version value in both spellings", () => { - assert.deepEqual(dependencyVersionContextAt(["[dependencies]", 'zlib = "'], 1, 8), { - name: "zlib", - insideString: true, - range: { startCharacter: 8, endCharacter: 8 }, - }); - - // 长式 dep spec 的 version 字段。 - const long = dependencyVersionContextAt(["[dependencies]", 'zlib = { version = "1.'], 1, 26); - assert.equal(long?.name, "zlib"); - assert.equal(long?.insideString, true); - - // 值还没开始输入:不算字符串内,插入时要补引号。 - const bare = dependencyVersionContextAt(["[dependencies]", "zlib = "], 1, 7); - assert.equal(bare?.name, "zlib"); - assert.equal(bare?.insideString, false); - assert.deepEqual(bare?.range, { startCharacter: 7, endCharacter: 7 }); - - // 每个依赖段都算。 - for (const header of ["[dev-dependencies]", "[build-dependencies]", "[workspace.dependencies]", "[feature-deps.foo]"]) { - assert.equal( - dependencyVersionContextAt([header, 'zlib = "'], 1, 8)?.name, - "zlib", - header, - ); - } -}); - -test("a git/path value, a non-dependency key or a key position is never a version", () => { - assert.equal(dependencyVersionContextAt(["[dependencies]", 'zlib = { git = "htt'], 1, 21), undefined); - assert.equal(dependencyVersionContextAt(["[dependencies]", 'zlib = { path = "..'], 1, 20), undefined); - assert.equal(dependencyVersionContextAt(["[package]", 'name = "'], 1, 7), undefined); - assert.equal(dependencyVersionContextAt(["[dependencies]", "zl"], 1, 2), undefined); - assert.equal(dependencyVersionContextAt(["[not-a-section]", 'zlib = "'], 1, 8), undefined); - // 值本身是内联表(dep spec 的容器):在容器里是键位置,不是版本位置; - // 已经写死的布尔/整数也不是版本。 - assert.equal(dependencyVersionContextAt(["[dependencies]", "zlib = { "], 1, 9), undefined); - assert.equal(dependencyVersionContextAt(["[dependencies]", "zlib = true"], 1, 11), undefined); -}); - -test("index suggestions replace the value, quoting it only outside a string", () => { - const context = dependencyVersionContextAt(["[dependencies]", 'zlib = "'], 1, 8); - assert.ok(context !== undefined); - const suggestions = indexVersionSuggestions(context, [ - { version: "1.3.2", summary: "A compression library" }, - { version: "1.2.9" }, - ]); - assert.deepEqual(labels(suggestions), ["1.3.2", "1.2.9"]); - assert.ok(suggestions.every((suggestion) => suggestion.kind === "version")); - assert.equal(suggestions[0].insertSnippet, "1.3.2"); - assert.deepEqual(suggestions[0].range, context.range); - assert.match(suggestions[0].detail, /zlib: A compression library/); - - const bare = dependencyVersionContextAt(["[dependencies]", "zlib = "], 1, 7); - assert.ok(bare !== undefined); - assert.equal(indexVersionSuggestions(bare, [{ version: "1.3.2" }])[0].insertSnippet, '"1.3.2"'); -}); - -test("the async layer asks the index exactly once, only in a version position", async () => { - const asked: string[] = []; - const resolve = async (name: string): Promise> => { - asked.push(name); - return [{ version: "1.3.2" }]; - }; - - const versions = await computeMcppTomlCompletionsWithIndex( - ["[dependencies]", 'zlib = "'], - 1, - 8, - resolve, - ); - assert.deepEqual(asked, ["zlib"]); - assert.deepEqual(labels(versions), ["1.3.2"]); - - asked.length = 0; - assert.deepEqual( - await computeMcppTomlCompletionsWithIndex(["[dependencies]", 'zlib = { git = "htt'], 1, 21, resolve), - [], - ); - assert.deepEqual(asked, [], "a git value must not reach the index"); - - // 静态层有建议(枚举值)时索引不被询问。 - asked.length = 0; - const enumValues = await computeMcppTomlCompletionsWithIndex(["[package]", "standard = "], 1, 11, resolve); - assert.deepEqual(labels(enumValues).slice(0, 3), ["c++20", "c++23", "c++26"]); - assert.deepEqual(asked, []); -}); - -test("no resolver (the setting off) means no version suggestions", async () => { - assert.deepEqual(await computeMcppTomlCompletionsWithIndex(["[dependencies]", 'zlib = "'], 1, 8), []); -}); - -test("a failing resolver degrades to no suggestions, never an error", async () => { - const versions = await computeMcppTomlCompletionsWithIndex( - ["[dependencies]", 'zlib = "'], - 1, - 8, - async () => { - throw new Error("mcpp search blew up"); - }, - ); - assert.deepEqual(versions, []); -}); - -test("a resolver that returns nothing leaves the value position empty", async () => { - const versions = await computeMcppTomlCompletionsWithIndex( - ["[dependencies]", 'zlib = "'], - 1, - 8, - async () => [], - ); - assert.deepEqual(versions, []); -}); diff --git a/mide-plugins/vscode/test/toml/contract.test.ts b/mide-plugins/vscode/test/toml/contract.test.ts deleted file mode 100644 index d0ffa9d..0000000 --- a/mide-plugins/vscode/test/toml/contract.test.ts +++ /dev/null @@ -1,231 +0,0 @@ -// mcpp.toml 契约测试:用真实 mcpp 二进制验证扩展手写的 manifest 语义 -// (段头清单、模板键、条件段规则)与权威 parser 一致。 -// -// 没有 mcpp 的环境整个文件 skip。mcpp 调用每次 60s 超时,全程串行。 - -import assert from "node:assert/strict"; -import { spawn, spawnSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import test from "node:test"; - -import { SECTION_HEADERS, type SectionHeaderSpec } from "../../src/toml/completion"; - -/** 探测 mcpp 是否可用;不可用则全部跳过。 */ -function detectMcpp(): string | undefined { - try { - const result = spawnSync("mcpp", ["--version"], { timeout: 10_000 }); - if (result.error !== undefined || result.status !== 0) { - return undefined; - } - return String(result.stdout).trim(); - } catch { - return undefined; - } -} - -const mcppVersion = detectMcpp(); -const skipReason = mcppVersion === undefined ? "未检测到 mcpp 二进制,跳过契约测试" : false; -if (skipReason !== false) { - console.log(skipReason); -} else { - console.log(`契约测试使用 ${mcppVersion}`); -} - -const MCPP_TIMEOUT_MS = 60_000; - -interface McppRun { - code: number; - output: string; -} - -/** 在指定工程目录串行执行 `mcpp build`,汇总 stdout+stderr。 */ -function runMcppBuild(projectDir: string): Promise { - return new Promise((resolve, reject) => { - const child = spawn("mcpp", ["build"], { cwd: projectDir, timeout: MCPP_TIMEOUT_MS }); - let output = ""; - child.stdout.on("data", (chunk: Buffer) => { output += chunk.toString(); }); - child.stderr.on("data", (chunk: Buffer) => { output += chunk.toString(); }); - child.on("error", reject); - child.on("close", (code, signal) => { - if (signal !== null) { - reject(new Error(`mcpp build 被信号 ${signal} 终止(疑似超时):\n${output}`)); - return; - } - resolve({ code: code ?? -1, output }); - }); - }); -} - -/** 每次新建唯一临时工程目录,注册到清理列表。 */ -const tempDirs: string[] = []; -function makeProject(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mcpp-contract-")); - tempDirs.push(dir); - fs.mkdirSync(path.join(dir, "src"), { recursive: true }); - fs.writeFileSync(path.join(dir, "src", "main.cpp"), "int main() { return 0; }\n"); - return dir; -} - -process.on("exit", () => { - for (const dir of tempDirs) { - fs.rmSync(dir, { recursive: true, force: true }); - } -}); - -const BASE_MANIFEST = '[package]\nname = "probe"\nversion = "0.1.0"\n'; - -/** 写 manifest(基线 + 附加内容)与额外卖文件,然后跑 mcpp build。 */ -function buildWith(extraManifest: string, files: Record = {}): Promise { - const dir = makeProject(); - fs.writeFileSync(path.join(dir, "mcpp.toml"), `${BASE_MANIFEST}\n${extraManifest}`); - for (const [relative, content] of Object.entries(files)) { - const target = path.join(dir, relative); - fs.mkdirSync(path.dirname(target), { recursive: true }); - fs.writeFileSync(target, content); - } - return runMcppBuild(dir); -} - -/** 核心断言:构建成功且输出无 unsupported / error(大小写不敏感)。 */ -function assertClean(run: McppRun, what: string): void { - assert.equal(run.code, 0, `${what}:mcpp build 退出码 ${run.code}\n${run.output}`); - assert.ok( - !/unsupported|error/i.test(run.output), - `${what}:输出含 unsupported/error 诊断\n${run.output}`, - ); -} - -/** - * 把段头注册表条目变成可构建的具体 manifest 片段。 - * snippet 占位符取默认值(如 [targets.${1:name}] → [targets.name])。 - */ -function fixtureFor(entry: SectionHeaderSpec): { manifest: string; files: Record } { - const header = entry.header.replace(/\$\{\d+:([^}]*)\}/g, "$1"); - assert.ok(!header.includes("$"), `段头 ${entry.label} 的占位符未能全部展开`); - if (entry.group === "package") { - // [package] 永远在基线工程里,直接验证基线本身。 - return { manifest: "", files: {} }; - } - if (entry.group === "targets") { - // [targets.] 需要 kind;kind = "lib" 需要 src/.cppm 消除 lib-root 警告。 - return { - manifest: `${header}\nkind = "lib"\n`, - files: { "src/name.cppm": "export module name;\n" }, - }; - } - return { manifest: `${header}\n`, files: {} }; -} - -test("段头注册表形态:数量与关键段齐全(防止测试与实现脱节)", { skip: skipReason }, () => { - assert.ok(SECTION_HEADERS.length >= 20, `段头注册表只有 ${SECTION_HEADERS.length} 条(期望 ≥ 20)`); - const groups = new Set(SECTION_HEADERS.map((entry) => entry.group)); - for (const key of ["package", "dependencies", "build-dependencies", "workspace", "pack"]) { - assert.ok(groups.has(key), `段头注册表缺少关键段 ${key}`); - } - const labels = SECTION_HEADERS.map((entry) => entry.label); - assert.equal(new Set(labels).size, labels.length, "段头注册表存在重复 label"); -}); - -test("段头清单契约:每个注册段 mcpp 都接受、无诊断", { skip: skipReason, timeout: 600_000 }, async (t) => { - for (const entry of SECTION_HEADERS) { - await t.test(entry.label, async () => { - const fixture = fixtureFor(entry); - const run = await buildWith(fixture.manifest, fixture.files); - assertClean(run, `段 ${entry.label}`); - }); - } -}); - -test("[indices] 带 path 条目被接受(项目级索引重定向)", { skip: skipReason, timeout: 120_000 }, async () => { - const run = await buildWith('[indices]\nmyidx = { path = "/tmp/mcpp-contract-nonexistent-index" }\n'); - assertClean(run, "[indices] 索引重定向"); -}); - -// 依赖 spec 的 12 个键:与 src/toml/completion.ts 的 DEPENDENCY_TEMPLATES -// 保持同步(模板未逐一列出键名,此处按 mcpp manifest schema 硬编码)。 -// 注意:features/backend/tools/host-module/reexport 不是「锚定键」——单独出现 -// 时 mcpp 会把内联表当成嵌套依赖表报错,必须搭配 version/path/git/workspace -// 之一;tag/branch/rev 则必须搭配 git。写在 [feature-deps.] 下只解析、 -// 不下载(feature 未激活),适合断言「键被接受、无诊断」。 -const DEP_SPEC_CASES: ReadonlyArray = [ - ["version", 'dep = { version = "1.0.0" }'], - ["path", 'dep = { path = "../x" }'], - ["git+tag", 'dep = { git = "https://example.com/r.git", tag = "v1" }'], - ["git+branch", 'dep = { git = "https://example.com/r.git", branch = "main" }'], - ["git+rev", 'dep = { git = "https://example.com/r.git", rev = "abc123" }'], - ["features", 'dep = { version = "1.0.0", features = ["f"] }'], - ["backend", 'dep = { version = "1.0.0", backend = "cmake" }'], - ["tools", 'dep = { version = "1.0.0", tools = ["t"] }'], - ["host-module", 'dep = { version = "1.0.0", host-module = true }'], - ["reexport", 'dep = { version = "1.0.0", reexport = true }'], - ["workspace", "dep = { workspace = true }"], -]; - -test("模板键契约:[feature-deps] 下依赖 spec 各键被接受", { skip: skipReason, timeout: 600_000 }, async (t) => { - for (const [name, spec] of DEP_SPEC_CASES) { - await t.test(name, async () => { - // `workspace = true` names the [workspace.dependencies] of the workspace that lists this - // package; mcpp resolves it even for a feature that is off, so the template stands in one. - const workspace = name === "workspace" - ? '[workspace]\nmembers = ["."]\n[workspace.dependencies]\ndep = "1.0.0"\n\n' - : ""; - const run = await buildWith(`${workspace}[feature-deps.f1]\n${spec}\n`); - assertClean(run, `[feature-deps.f1] 依赖 spec 键 ${name}`); - }); - } -}); - -test("模板键契约:[features] 表形式与数组简写被接受", { skip: skipReason, timeout: 120_000 }, async () => { - const run = await buildWith([ - "[features]", - 'f1 = { defines = ["X"], implies = [], sources = ["src/**"] }', - 'f2 = ["f1"]', - 'f3 = { requires = ["blas"] }', - "", - ].join("\n")); - assertClean(run, "[features] 表形式键"); -}); - -// 其余写法模板的完整实例(依赖/feature 之外的模板条目)。capabilities 的 -// provider 绑定在「无包 require 该能力」时不解析,静默通过;generated_files -// 的条目会在源 glob 展开前写入工程树,这里同时验证生成机制生效(编译成功 -// 即说明模块文件被正常纳入构建)。 -test("模板实例契约:capabilities / xlings.workspace / tools.overrides / generated_files", { skip: skipReason, timeout: 120_000 }, async () => { - const run = await buildWith( - [ - "[capabilities]", - 'blas = "compat.openblas"', - "", - "[xlings.workspace]", - 'node = "24.19.0"', - "", - "", - "[tools.overrides]", - '"compat.protobuf:protoc" = "/usr/bin/protoc"', - "", - "[generated_files]", - '"src/gen/wrap.cppm" = """', - "export module wrap;", - '"""', - "", - ].join("\n"), - ); - assertClean(run, "模板实例(capabilities/xlings.workspace/tools.overrides/generated_files)"); -}); - -test("条件段规则:[target.'cfg(windows)'.build] 接受 build inputs", { skip: skipReason, timeout: 120_000 }, async () => { - const run = await buildWith("[target.'cfg(windows)'.build]\ndefines = [\"A=1\"]\n"); - assertClean(run, "条件段 build inputs"); -}); - -test("条件段规则:反向断言——cache 在条件段必须报 unsupported", { skip: skipReason, timeout: 120_000 }, async () => { - // 钉住「条件段只接受 build inputs」:cache 属于 profile 设置,必须告警。 - const run = await buildWith("[target.'cfg(windows)'.build]\ncache = \"off\"\n"); - assert.equal(run.code, 0, `条件段 cache:mcpp build 退出码 ${run.code}\n${run.output}`); - assert.ok( - /unsupported key 'cache'/i.test(run.output), - `条件段 cache:期望 unsupported-key 警告\n${run.output}`, - ); -}); diff --git a/mide-plugins/vscode/test/toml/diagnostics.test.ts b/mide-plugins/vscode/test/toml/diagnostics.test.ts deleted file mode 100644 index 4705aeb..0000000 --- a/mide-plugins/vscode/test/toml/diagnostics.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - DIAGNOSTIC_CODES, - analyseManifest, - type DiagnosticSettings, -} from "../../src/toml/diagnostics"; - -function settings(overrides: Partial = {}): DiagnosticSettings { - return { - syntax: "error", - unknownSection: "warning", - unknownKey: "warning", - planeSeparation: "warning", - legacyKeys: "info", - ...overrides, - }; -} - -function codes(lines: readonly string[], overrides: Partial = {}): string[] { - return analyseManifest(lines, settings(overrides)).map((diagnostic) => diagnostic.code); -} - -// ── rule 1: syntax ────────────────────────────────────────────────────────── - -test("rule 1: a line that is neither a header nor key = value is a syntax error", () => { - const found = analyseManifest(["[package]", 'name = "x"', "this is broken"], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].code, DIAGNOSTIC_CODES.syntax); - assert.equal(found[0].severity, "error"); - assert.equal(found[0].line, 3); - assert.deepEqual([found[0].startCharacter, found[0].endCharacter], [1, 15]); -}); - -test("rule 1: a key without a value and a malformed header are syntax errors", () => { - assert.deepEqual(codes(["[package]", "name ="]), [DIAGNOSTIC_CODES.syntax]); - assert.deepEqual(codes(["[package", 'name = "x"']), [DIAGNOSTIC_CODES.syntax]); - assert.deepEqual(codes(["[package]", "= 1"]), [DIAGNOSTIC_CODES.syntax]); -}); - -test("rule 1: headers, comments, continuations and quoted # are not syntax errors", () => { - const lines = [ - "# a comment", - "", - "[package]", - 'description = """', - "a # inside a string", - "and a = sign", - '"""', - "", - "[build]", - "sources = [", - ' "src/**/*.cppm", # a real comment', - ' "src/**",', - "]", - 'cxxflags = ["-DA#B"]', - ]; - assert.deepEqual(codes(lines), []); -}); - -// ── rule 2: unknown section ───────────────────────────────────────────────── - -test("rule 2: a section the schema does not know is reported", () => { - const found = analyseManifest(["[packag]", 'name = "x"'], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].code, DIAGNOSTIC_CODES.unknownSection); - assert.equal(found[0].line, 1); - assert.equal(found[0].relatedKey, "packag"); -}); - -test("rule 2: a dotted conditional name matches its prefix section", () => { - assert.deepEqual( - codes(["[package]", 'name = "x"', "[target.'cfg(os = \"linux\")'.dependencies]", 'foo = "1"']), - [], - ); - assert.deepEqual(codes(["[targets.app]", 'kind = "bin"']), []); - assert.deepEqual(codes(["[package]", 'name = "x"', "[package.metadata.demo]", 'anything = 1']), []); -}); - -// ── rule 3: unknown key ───────────────────────────────────────────────────── - -test("rule 3: a key absent from a section's key table is reported", () => { - const found = analyseManifest(["[package]", 'nmae = "x"'], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].code, DIAGNOSTIC_CODES.unknownKey); - assert.equal(found[0].relatedKey, "nmae"); - assert.deepEqual(codes(["[package]", 'name = "x"', 'standard = "c++23"']), []); -}); - -test("rule 3: row tables inherit their base section's key table", () => { - assert.deepEqual(codes(["[targets.app]", 'kind = "bin"', "bogus = 1"]), [DIAGNOSTIC_CODES.unknownKey]); - assert.deepEqual(codes(["[profile.dist]", "opt = 3"]), []); -}); - -test("rule 3: deeper sub-tables have their own vocabulary and are not checked", () => { - assert.deepEqual(codes(["[runtime.\"display.present\"]", 'provider = "acme@2.0"']), []); - assert.deepEqual(codes(["[toolchain.linux]", 'path = "/opt"', 'family = "gcc"']), []); - assert.deepEqual(codes(["[target.windows.build]", 'dialect_cxxflags = ["-DX"]']), []); -}); - -// ── rule 4: plane separation ──────────────────────────────────────────────── - -test("rule 4: a tool payload inside a dependency table is a plane mistake", () => { - const found = analyseManifest(["[dependencies]", '"xim:ninja" = "1.0"'], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].code, DIAGNOSTIC_CODES.planeSeparation); - assert.match(found[0].message, /xim:ninja/); - assert.match(found[0].message, /\[xlings\]/); -}); - -test("rule 4: an mcpp package inside [xlings] is a plane mistake", () => { - assert.deepEqual(codes(["[xlings.workspace]", 'mcpplibs.cmdline = "1.0"']), [ - DIAGNOSTIC_CODES.planeSeparation, - ]); -}); - -test("rule 4: the correct plane in each table is not reported", () => { - assert.deepEqual(codes(["[dependencies]", 'lua = "5.4.7"']), []); - assert.deepEqual(codes(["[dependencies]", 'foo = { path = "../foo" }']), []); - assert.deepEqual(codes(["[xlings.workspace]", '"xim:ninja" = "1.0"']), []); - assert.deepEqual(codes(["[xlings.workspace]", 'mcpplibs.cmdline = { path = "../cmdline" }']), []); -}); - -// ── rule 5: [package].mcpp floor ──────────────────────────────────────────── - -test("rule 5: [package].mcpp must be a >= floor", () => { - const found = analyseManifest(["[package]", 'mcpp = "2026.9.28.3"'], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].code, DIAGNOSTIC_CODES.mcppFloor); - assert.equal(found[0].relatedKey, "mcpp"); -}); - -test("rule 5: the >= form and a package without the key are fine", () => { - assert.deepEqual(codes(["[package]", 'mcpp = ">=2026.9.28.3"']), []); - assert.deepEqual(codes(["[package]", 'name = "x"']), []); - // A key of the same name in another section is not this rule. - assert.deepEqual(codes(["[build]", "accel = 1"]), []); -}); - -// ── rule 6: legacy section or key ─────────────────────────────────────────── - -test("rule 6: a deprecated section names its replacement", () => { - const found = analyseManifest(["[language]", 'standard = "c++20"'], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].code, DIAGNOSTIC_CODES.legacyKey); - assert.match(found[0].message, /\[package\]\.standard/); -}); - -test("rule 6: a legacy key names its replacement", () => { - const found = analyseManifest(["[build]", "static_stdlib = true"], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].code, DIAGNOSTIC_CODES.legacyKey); - assert.match(found[0].message, /cxx_runtime/); - assert.deepEqual(codes(["[package]", 'standard = "c++23"']), []); -}); - -// ── rule 7: array tables ──────────────────────────────────────────────────── - -test("rule 7: an array table mcpp does not use is reported", () => { - const found = analyseManifest(["[[targets.app]]", 'kind = "bin"'], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].code, DIAGNOSTIC_CODES.arrayTable); - assert.equal(found[0].line, 1); -}); - -test("rule 7: the array tables mcpp accepts are not reported", () => { - assert.deepEqual(codes(["[[build.flags]]", 'glob = "src/**"']), []); - assert.deepEqual(codes(["[[features.simd.flags]]", 'glob = "src/**"']), []); - assert.deepEqual(codes(["[targets.app]", 'kind = "bin"']), []); -}); - -// ── severity and quoting ──────────────────────────────────────────────────── - -test("severity off silences its rule and leaves the others alone", () => { - const lines = ["[packag]", 'name = "x"', "this is broken"]; - assert.deepEqual(codes(lines), [DIAGNOSTIC_CODES.unknownSection, DIAGNOSTIC_CODES.syntax]); - assert.deepEqual(codes(lines, { syntax: "off" }), [DIAGNOSTIC_CODES.unknownSection]); - assert.deepEqual(codes(lines, { unknownSection: "off" }), [DIAGNOSTIC_CODES.syntax]); - assert.deepEqual(codes(["[language]", 'standard = "c++20"'], { legacyKeys: "off" }), []); - assert.deepEqual(codes(["[dependencies]", '"xim:ninja" = "1.0"'], { planeSeparation: "off" }), []); - assert.deepEqual(codes(["[package]", 'nmae = "x"'], { unknownKey: "off" }), []); -}); - -test("a # inside a string is not a comment, and later lines are still checked", () => { - assert.deepEqual( - codes(["[package]", 'description = "a # not a comment"', "license = 'MIT' # trailing"]), - [], - ); - const found = analyseManifest(["[package]", 'description = "#"', "still broken"], settings()); - assert.equal(found.length, 1); - assert.equal(found[0].line, 3); -}); - -test("a multi-line string hides its contents from every rule", () => { - const lines = ["[package]", 'description = """', "not a key = value", "[[not a table]]", '"""']; - assert.deepEqual(codes(lines), []); -}); - -test("positions are 1-based and cover the offending token", () => { - const header = analyseManifest([" [packag]"], settings()); - assert.deepEqual( - header.map((diagnostic) => [diagnostic.line, diagnostic.startCharacter, diagnostic.endCharacter]), - [[1, 3, 11]], - ); - const key = analyseManifest(["[package]", ' nmae = "x"'], settings()); - assert.deepEqual( - key.map((diagnostic) => [diagnostic.line, diagnostic.startCharacter, diagnostic.endCharacter]), - [[2, 3, 7]], - ); -}); - -test("a clean manifest produces no diagnostics", () => { - const lines = [ - "[package]", - 'name = "greeter"', - 'version = "0.1.0"', - 'standard = "c++23"', - 'mcpp = ">=2026.9.28.3"', - "", - "[build]", - 'sources = ["src/**/*.cppm", "src/**/*.cpp"]', - 'bmi_schedule = "on"', - "", - "[targets.greeter]", - 'kind = "bin"', - "", - "[dependencies]", - 'fmt = "11.0.2"', - "", - "[xlings.workspace]", - '"xim:ninja" = "1.12.1"', - ]; - assert.deepEqual(codes(lines), []); -}); diff --git a/mide-plugins/vscode/test/toml/hover.test.ts b/mide-plugins/vscode/test/toml/hover.test.ts deleted file mode 100644 index 14e62c8..0000000 --- a/mide-plugins/vscode/test/toml/hover.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { hoverAt } from "../../src/toml/hover"; - -test("hovers a section header with its plane, key count and doc link", () => { - const info = hoverAt(["[package]", 'name = "x"'], 0, 3); - assert.ok(info); - assert.equal(info.title, "[package]"); - assert.match(info.body, /plane: `identity`/); - assert.match(info.body, /keys: 20/); - assert.match(info.documentation ?? "", /04-mcpp-toml\.md#21-package--package-metadata$/); -}); - -test("hovers a legacy section with what replaces it", () => { - const info = hoverAt(["[language]"], 0, 2); - assert.ok(info); - assert.equal(info.title, "[language]"); - assert.match(info.body, /legacy: replaced by `\[package\]\.standard`/); -}); - -test("hovers a parameterized row header on its base section", () => { - const info = hoverAt(["[targets.app]", 'kind = "lib"'], 0, 4); - assert.ok(info); - assert.equal(info.title, "[targets]"); - assert.match(info.body, /plane: `artifact`/); - assert.match(info.documentation ?? "", /#22-targetsname--build-targets$/); -}); - -test("hovers a key with type, enum values, default and since", () => { - const standard = hoverAt(["[package]", "standard = c++23"], 1, 3); - assert.ok(standard); - assert.equal(standard.title, "standard"); - assert.match(standard.body, /type: `enum`/); - assert.match(standard.body, /c\+\+23/); - assert.match(standard.body, /default: `"c\+\+23"`/); - - const mcpp = hoverAt(["[package]", 'mcpp = ">=2026.9.28.3"'], 1, 1); - assert.ok(mcpp); - assert.match(mcpp.body, /since: 2026\.9\.28\.3/); - assert.match(mcpp.body, /release floor/); -}); - -test("hovers an unmodelled key with the inference caveat", () => { - const info = hoverAt(["[build]", "flags = []"], 1, 2); - assert.ok(info); - assert.equal(info.title, "flags"); - assert.match(info.body, /unmodelled: the type is inferred/); -}); - -test("hovers an enum value with the value and its key's documentation", () => { - const info = hoverAt(["[package]", "standard = c++23"], 1, 12); - assert.ok(info); - assert.equal(info.title, "c++23"); - assert.match(info.body, /enum value of `standard`/); - assert.match(info.documentation ?? "", /04-mcpp-toml\.md#21/); -}); - -test("offers nothing outside a section, a known key or an enum value", () => { - // 段在快照里没有键表:依赖名不是 schema 键。 - assert.equal(hoverAt(["[dependencies]", 'zlib = "1.0"'], 1, 10), undefined); - // 未知段:不猜。 - assert.equal(hoverAt(["[mytool]", "x = 1"], 1, 1), undefined); - // 非枚举键的值。 - assert.equal(hoverAt(["[package]", 'name = "x"'], 1, 10), undefined); - // 空文档与越界光标。 - assert.equal(hoverAt([], 5, 5), undefined); -}); - -test("never throws on malformed input", () => { - const inputs: string[][] = [ - [""], - ["["], - ["[", "]]", "= = ="], - ["[package", "name"], - ["[[x]]", "{"], - ["[package]", 'name = "unterminated'], - ]; - for (const lines of inputs) { - assert.doesNotThrow(() => hoverAt(lines, 0, 99)); - assert.doesNotThrow(() => hoverAt(lines, 99, 99)); - } -}); diff --git a/mide-plugins/vscode/test/toml/navigation.test.ts b/mide-plugins/vscode/test/toml/navigation.test.ts deleted file mode 100644 index e328ca3..0000000 --- a/mide-plugins/vscode/test/toml/navigation.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { definitionAt } from "../../src/toml/navigation"; - -test("workspace = true jumps to the same key in [workspace.dependencies]", () => { - const lines = [ - "[dependencies]", - "compat.zlib = { workspace = true }", - "", - "[workspace.dependencies]", - 'compat.zlib = "1.3.2"', - ]; - // 光标在值 true 上,也在键 workspace 上:都应落到同名依赖键。 - assert.deepEqual(definitionAt(lines, 1, 30), { - line: 4, - startCharacter: 0, - endCharacter: "compat.zlib".length, - }); - assert.deepEqual(definitionAt(lines, 1, 20), { - line: 4, - startCharacter: 0, - endCharacter: "compat.zlib".length, - }); -}); - -test("workspace = true also works in a table-per-dependency section", () => { - const lines = ["[dependencies.foo]", "workspace = true", "", "[workspace.dependencies]", 'foo = "1"']; - assert.deepEqual(definitionAt(lines, 1, 3), { line: 4, startCharacter: 0, endCharacter: 3 }); - - const inline = ["[dependencies]", "foo = { workspace = true }", "", "[workspace.dependencies]", 'foo = "1"']; - assert.deepEqual(definitionAt(inline, 1, 20), { line: 4, startCharacter: 0, endCharacter: 3 }); -}); - -test("workspace = true without a matching workspace dependency jumps nowhere", () => { - assert.equal( - definitionAt(["[dependencies]", "foo = { workspace = true }"], 1, 20), - undefined, - ); -}); - -test("a relative path jumps to the resolved manifest's [package] header", () => { - const lines = ["[dependencies]", 'mylib = { path = "../mylib" }']; - const seen: string[] = []; - const found = definitionAt(lines, 1, 20, (relative) => { - seen.push(relative); - return 7; - }); - assert.deepEqual(seen, ["../mylib"]); - assert.deepEqual(found, { line: 7, startCharacter: 0, endCharacter: "[package]".length }); -}); - -test("a path that the callback cannot resolve jumps nowhere", () => { - const lines = ["[dependencies]", 'mylib = { path = "../mylib" }']; - assert.equal(definitionAt(lines, 1, 20, () => undefined), undefined); - // 回调可省略:纯函数在主 manifest 内没有任何目标可指。 - assert.equal(definitionAt(lines, 1, 20), undefined); -}); - -test("absolute paths and non-dependency path keys are left alone", () => { - const absolute = ["[dependencies]", 'mylib = { path = "/opt/mylib" }']; - assert.equal(definitionAt(absolute, 1, 20, () => 3), undefined); - - const lib = ["[lib]", 'path = "src/x.cppm"']; - assert.equal(definitionAt(lib, 1, 3, () => 3), undefined); -}); - -test("features = [...] jumps to the header of the element under the cursor", () => { - const lines = [ - "[dependencies]", - 'fmt = { features = ["simd", "gpu"] }', - "", - "[features.simd]", - "defines = []", - ]; - assert.deepEqual(definitionAt(lines, 1, 21), { - line: 3, - startCharacter: 0, - endCharacter: "[features.simd]".length, - }); - // 光标在 "gpu" 上,但没有 [features.gpu]。 - assert.equal(definitionAt(lines, 1, 31), undefined); -}); - -test("ordinary positions have no definition", () => { - assert.equal(definitionAt(["[package]", 'name = "x"'], 1, 10), undefined); - assert.equal(definitionAt(["[dependencies]", 'zlib = "1.0"'], 1, 10), undefined); - assert.equal(definitionAt([], 3, 3), undefined); -}); - -test("never throws on malformed input", () => { - const inputs: string[][] = [ - [""], - ["["], - ["[", "]]", "= = ="], - ["[dependencies]", 'foo = { path = "../'], - ["[[x]]", "{"], - ]; - for (const lines of inputs) { - assert.doesNotThrow(() => definitionAt(lines, 0, 99, () => 0)); - assert.doesNotThrow(() => definitionAt(lines, 99, 99)); - } -}); diff --git a/mide-plugins/vscode/test/toml/parser.test.ts b/mide-plugins/vscode/test/toml/parser.test.ts index ddb5e79..2633a30 100644 --- a/mide-plugins/vscode/test/toml/parser.test.ts +++ b/mide-plugins/vscode/test/toml/parser.test.ts @@ -2,9 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { - contextAt, parseMcppToml, - resolveSection, type TomlKeyValueNode, type TomlSectionNode, } from "../../src/toml/parser"; @@ -193,252 +191,7 @@ test("无法识别的垃圾行不抛错、不中断后续解析", () => { assert.deepEqual(section.segments.map((s) => s.name), ["package"]); }); -// ---- resolveSection 段归属语义 ---- - -test("resolveSection 精确匹配已知段", () => { - assert.deepEqual(resolveSection(["package"]), { kind: "known", group: "package" }); - assert.deepEqual(resolveSection(["build"]), { kind: "known", group: "build" }); - assert.deepEqual(resolveSection(["workspace", "dependencies"]), { kind: "known", group: "workspace.dependencies" }); - assert.deepEqual(resolveSection(["pack", "bundle-project"]), { kind: "known", group: "pack.bundle-project" }); - assert.deepEqual(resolveSection(["tools", "overrides"]), { kind: "known", group: "tools.overrides" }); - assert.deepEqual(resolveSection(["xlings", "envs"]), { kind: "known", group: "xlings.envs" }); -}); - -test("resolveSection 参数化段归入基组", () => { - assert.deepEqual(resolveSection(["targets", "myapp"]), { kind: "known", group: "targets" }); - assert.deepEqual(resolveSection(["profile", "release"]), { kind: "known", group: "profile" }); - assert.deepEqual(resolveSection(["feature-deps", "simd"]), { kind: "known", group: "feature-deps" }); - assert.deepEqual(resolveSection(["dependencies", "capi"]), { kind: "known", group: "dependencies" }); - assert.deepEqual(resolveSection(["dependencies", "capi", "lua"]), { kind: "known", group: "dependencies" }); - assert.deepEqual(resolveSection(["dev-dependencies", "ns"]), { kind: "known", group: "dev-dependencies" }); - assert.deepEqual(resolveSection(["workspace", "dependencies", "ns"]), { kind: "known", group: "workspace.dependencies" }); -}); - -test("resolveSection target 选择器与子表递归", () => { - assert.deepEqual(resolveSection(["target"]), { kind: "known", group: "target" }); - assert.deepEqual(resolveSection(["target", "x86_64-linux-gnu"]), { kind: "known", group: "target" }); - assert.deepEqual(resolveSection(["target", "cfg(windows)", "build"]), { kind: "known", group: "build" }); - assert.deepEqual(resolveSection(["target", "x", "feature-deps", "codegen"]), { kind: "known", group: "feature-deps" }); - assert.deepEqual(resolveSection(["target", "x", "dependencies", "capi"]), { kind: "known", group: "dependencies" }); -}); - -test("resolveSection runtime capability 子表", () => { - assert.deepEqual(resolveSection(["runtime"]), { kind: "known", group: "runtime" }); - assert.deepEqual(resolveSection(["runtime", "opengl.glx.driver"]), { kind: "known", group: "runtime.capability" }); -}); - -test("resolveSection 区分未知段与文档顶部", () => { - assert.deepEqual(resolveSection(["custom"]), { kind: "unknown", segments: ["custom"] }); - assert.deepEqual(resolveSection(["build", "flags"]), { kind: "unknown", segments: ["build", "flags"] }); - assert.deepEqual(resolveSection([]), { kind: "top" }); -}); - -// ---- contextAt 光标上下文 ---- - -test("contextAt 未闭合段头内:段头上下文与替换范围", () => { - const context = contextAt(["[tar"], 0, 4); - assert.equal(context.kind, "section-header"); - if (context.kind !== "section-header") return; - assert.deepEqual(context.segments, []); - assert.equal(context.isArray, false); - assert.deepEqual(context.replaceRange, { startCharacter: 1, endCharacter: 4 }); -}); - -test("contextAt 段头点号之后:已解析段作为前缀", () => { - const context = contextAt(["[targets."], 0, 9); - assert.equal(context.kind, "section-header"); - if (context.kind !== "section-header") return; - assert.deepEqual(context.segments, ["targets"]); - assert.deepEqual(context.replaceRange, { startCharacter: 9, endCharacter: 9 }); -}); - -test("contextAt 段头中间段的替换范围覆盖整个 token", () => { - const context = contextAt(["[targets.myapp]"], 0, 11); - assert.equal(context.kind, "section-header"); - if (context.kind !== "section-header") return; - assert.deepEqual(context.segments, ["targets"]); - assert.deepEqual(context.replaceRange, { startCharacter: 9, endCharacter: 14 }); -}); - -test("contextAt 数组表段头刚输入 [[ 时", () => { - const context = contextAt(["[["], 0, 2); - assert.equal(context.kind, "section-header"); - if (context.kind !== "section-header") return; - assert.equal(context.isArray, true); - assert.deepEqual(context.segments, []); -}); - -test("contextAt 已知段内空行:键位置并携带段归属", () => { - const context = contextAt(["[package]", ""], 1, 0); - assert.equal(context.kind, "key"); - if (context.kind !== "key") return; - assert.deepEqual(context.section, { kind: "known", group: "package" }); - assert.deepEqual(context.containerPath, []); - assert.deepEqual(context.keyPrefix, []); - assert.deepEqual(context.replaceRange, { startCharacter: 0, endCharacter: 0 }); -}); - -test("contextAt 区分文档顶部与未知段", () => { - const top = contextAt([""], 0, 0); - assert.equal(top.kind, "key"); - if (top.kind === "key") { - assert.deepEqual(top.section, { kind: "top" }); - } - - const unknown = contextAt(["[custom]", ""], 1, 0); - assert.equal(unknown.kind, "key"); - if (unknown.kind === "key") { - assert.deepEqual(unknown.section, { kind: "unknown", segments: ["custom"] }); - } -}); - -test("contextAt 点分键中间:keyPrefix 与替换范围", () => { - const context = contextAt(["capi.la"], 0, 7); - assert.equal(context.kind, "key"); - if (context.kind !== "key") return; - assert.deepEqual(context.containerPath, []); - assert.deepEqual(context.keyPrefix, ["capi"]); - assert.deepEqual(context.replaceRange, { startCharacter: 5, endCharacter: 7 }); -}); - -test("contextAt 字符串值内部:insideString 与替换范围", () => { - const context = contextAt(['kind = "bin"'], 0, 9); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.deepEqual(context.keyPath, ["kind"]); - assert.equal(context.valueKind, "string"); - assert.equal(context.insideString, true); - assert.equal(context.stringOpen, false); - assert.deepEqual(context.replaceRange, { startCharacter: 8, endCharacter: 11 }); -}); - -test("contextAt 未闭合字符串内:stringOpen 为 true", () => { - const context = contextAt(['name = "hel'], 0, 11); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.deepEqual(context.keyPath, ["name"]); - assert.equal(context.valueKind, "string"); - assert.equal(context.insideString, true); - assert.equal(context.stringOpen, true); - assert.deepEqual(context.replaceRange, { startCharacter: 8, endCharacter: 11 }); -}); - -test("contextAt 等号后值未开始:valueKind 为 undefined", () => { - const context = contextAt(["kind = "], 0, 7); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.deepEqual(context.keyPath, ["kind"]); - assert.equal(context.valueKind, undefined); - assert.equal(context.insideString, false); - assert.deepEqual(context.replaceRange, { startCharacter: 7, endCharacter: 7 }); -}); - -test("contextAt 数组元素槽位", () => { - const context = contextAt(["sources = [ "], 0, 12); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.deepEqual(context.keyPath, ["sources"]); - assert.equal(context.valueKind, undefined); - assert.deepEqual(context.replaceRange, { startCharacter: 12, endCharacter: 12 }); -}); - -test("contextAt 深度下钻:features 值 → flags 数组 → 内联表条目的键位置", () => { - const context = contextAt(["[features]", "simd = { flags = [ { "], 1, 21); - assert.equal(context.kind, "key"); - if (context.kind !== "key") return; - assert.deepEqual(context.section, { kind: "known", group: "features" }); - assert.deepEqual(context.containerPath, ["simd", "flags"]); - assert.deepEqual(context.keyPrefix, []); - assert.deepEqual(context.replaceRange, { startCharacter: 21, endCharacter: 21 }); -}); - -test("contextAt 内联表条目的字符串值:keyPath 含下钻路径", () => { - const context = contextAt(['dep = { version = "1.0" }'], 0, 20); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.deepEqual(context.keyPath, ["dep", "version"]); - assert.equal(context.valueKind, "string"); - assert.equal(context.insideString, true); - assert.deepEqual(context.replaceRange, { startCharacter: 19, endCharacter: 22 }); -}); - -test("contextAt 数组内内联表条目的值:keyPath 穿过数组", () => { - const context = contextAt(['flags = [ { glob = "src/**" } ]'], 0, 23); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.deepEqual(context.keyPath, ["flags", "glob"]); - assert.equal(context.insideString, true); - assert.deepEqual(context.replaceRange, { startCharacter: 20, endCharacter: 26 }); -}); - -test("contextAt 布尔 token 中间:valueKind 为 boolean 且替换范围覆盖 token", () => { - const context = contextAt(["debug = true"], 0, 9); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.equal(context.valueKind, "boolean"); - assert.deepEqual(context.replaceRange, { startCharacter: 8, endCharacter: 12 }); -}); - -test("contextAt 兼容 CRLF 行尾", () => { - const context = contextAt(["[package]\r", 'name = "demo"\r'], 1, 9); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.deepEqual(context.section, { kind: "known", group: "package" }); - assert.equal(context.insideString, true); - assert.deepEqual(context.replaceRange, { startCharacter: 8, endCharacter: 12 }); -}); - -test("contextAt 越界坐标被钳制而不抛错", () => { - const context = contextAt(["[package]"], 99, 99); - assert.equal(context.kind, "key"); - if (context.kind !== "key") return; - assert.deepEqual(context.section, { kind: "known", group: "package" }); -}); - -// ---- 回归:值槽位替换范围 / 引号停止符 / 裸值停止字符 ---- - -test("contextAt 光标在 = 与值 token 之间的空白上:替换整个 token", () => { - const context = contextAt(["cmdline = true"], 0, 9); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.deepEqual(context.keyPath, ["cmdline"]); - assert.equal(context.valueKind, "boolean"); - assert.deepEqual(context.replaceRange, { startCharacter: 10, endCharacter: 14 }); -}); - -test("contextAt 光标恰在值 token 首字符:替换范围覆盖整个 token", () => { - const context = contextAt(["cmdline = true"], 0, 10); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.equal(context.valueKind, "boolean"); - assert.deepEqual(context.replaceRange, { startCharacter: 10, endCharacter: 14 }); -}); - -test("contextAt 光标在字符串 token 前的空白上:替换范围覆盖整个带引号 token", () => { - const context = contextAt(['name = "demo"'], 0, 6); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.equal(context.valueKind, "string"); - assert.equal(context.insideString, false); - assert.deepEqual(context.replaceRange, { startCharacter: 7, endCharacter: 13 }); -}); - -test("contextAt 单引号字符串内的双引号不截断替换范围", () => { - const context = contextAt(["s = 'a\"b'"], 0, 7); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.equal(context.valueKind, "string"); - assert.equal(context.insideString, true); - assert.deepEqual(context.replaceRange, { startCharacter: 5, endCharacter: 8 }); -}); - -test("contextAt 双引号字符串内的单引号不截断替换范围", () => { - const context = contextAt(["s = \"a'b\""], 0, 7); - assert.equal(context.kind, "value"); - if (context.kind !== "value") return; - assert.equal(context.insideString, true); - assert.deepEqual(context.replaceRange, { startCharacter: 5, endCharacter: 8 }); -}); +// ---- 回归:裸值停止字符 ---- test("裸值不停在 [ / { 之外的后续结构:x = 1 [dep", () => { const document = parseMcppToml(["x = 1 [dep"]); diff --git a/mide-plugins/vscode/test/toml/schema.test.ts b/mide-plugins/vscode/test/toml/schema.test.ts deleted file mode 100644 index 7736bd4..0000000 --- a/mide-plugins/vscode/test/toml/schema.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - SCHEMA, - keyOf, - schemaSource, - sectionByHeader, - sectionByName, - sectionProblems, -} from "../../src/toml/schema"; - -test("the generated schema loads and every section header matches its name", () => { - assert.ok(SCHEMA.sections.length > 0, "the snapshot is empty"); - for (const section of SCHEMA.sections) { - assert.equal(section.header, `[${section.name}]`, section.name); - } -}); - -test("the snapshot is internally consistent", () => { - assert.deepEqual(sectionProblems(), []); -}); - -test("the snapshot records which mcpp produced it", () => { - const source = schemaSource(); - assert.match(source.version, /^\d+(\.\d+)+$/, source.version); - assert.notEqual(source.commit, ""); - assert.equal(source.version, SCHEMA.sourceVersion); - assert.equal(source.commit, SCHEMA.sourceCommit); -}); - -test("section lookup accepts both the header and the bare name", () => { - const byHeader = sectionByHeader("[package]"); - assert.ok(byHeader); - assert.equal(byHeader.name, "package"); - assert.equal(sectionByHeader("package"), byHeader); - assert.equal(sectionByName("build")?.header, "[build]"); - // An array table is not a section. - assert.equal(sectionByHeader("[[package]]"), undefined); - assert.equal(sectionByName("does-not-exist"), undefined); -}); - -test("planes come from SPEC-004 §2 and legacy sections name their replacement", () => { - assert.equal(sectionByName("package")?.plane, "identity"); - assert.equal(sectionByName("lib")?.plane, "artifact"); - assert.equal(sectionByName("targets")?.plane, "artifact"); - assert.equal(sectionByName("build")?.plane, "compile"); - assert.equal(sectionByName("profile")?.plane, "compile"); - assert.equal(sectionByName("dependencies")?.plane, "dependency"); - assert.equal(sectionByName("xlings")?.plane, "tool"); - assert.equal(sectionByName("features")?.plane, "gate"); - assert.equal(sectionByName("target")?.plane, "condition"); - assert.equal(sectionByName("runtime")?.plane, "metadata"); - assert.equal(sectionByName("hooks")?.plane, "lifecycle"); - assert.equal(sectionByName("language")?.deprecatedBy, "[package].standard"); -}); - -test("the snapshot carries a doc anchor for sections docs/04 documents", () => { - assert.equal( - sectionByName("package")?.doc, - "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata", - ); - assert.match(sectionByName("build")?.doc ?? "", /#23-build--build-configuration$/); -}); - -test("enums carry the values the docs list", () => { - const standard = keyOf("package", "standard"); - assert.equal(standard?.type, "enum"); - assert.equal(standard?.default, "c++23"); - for (const value of ["c++20", "c++23", "c++26"]) { - assert.ok(standard?.values?.includes(value), value); - } - assert.deepEqual(keyOf("targets", "kind")?.values, ["bin", "lib", "shared", "app"]); - assert.deepEqual(keyOf("build", "bmi_schedule")?.values, ["auto", "on", "off"]); - assert.deepEqual(keyOf("build", "cache")?.values, ["global", "local", "off"]); - assert.deepEqual(keyOf("profile", "opt")?.values, ["s", "z"]); - assert.ok(keyOf("toolchain", "family")?.values?.includes("gcc")); - assert.equal(keyOf("language", "standard")?.legacy, true); - assert.equal(keyOf("build", "static_stdlib")?.legacy, true); - assert.equal(keyOf("package", "does-not-exist"), undefined); -}); - -test("a section that carries a key table carries a non-empty, typed one", () => { - const types = new Set(["string", "boolean", "number", "array", "enum"]); - for (const section of SCHEMA.sections) { - if (section.keys === undefined) { - continue; - } - assert.ok(section.keys.length > 0, section.header); - for (const key of section.keys) { - assert.notEqual(key.key, "", section.header); - assert.ok(types.has(key.type), `${section.header} ${key.key} has type ${key.type}`); - if (key.type === "enum") { - assert.ok((key.values?.length ?? 0) > 0, `${section.header} ${key.key} has no values`); - } - } - } -}); diff --git a/mide-plugins/vscode/tools/bundle-languageclient.mjs b/mide-plugins/vscode/tools/bundle-languageclient.mjs new file mode 100644 index 0000000..a1c866d --- /dev/null +++ b/mide-plugins/vscode/tools/bundle-languageclient.mjs @@ -0,0 +1,20 @@ +// vscode-languageclient, bundled into dist/vendor/languageclient.js: the extension connects to the +// mcpp-ide backend as to an LSP server (design 2026-10-30 part 2 §8), and the VSIX carries no +// node_modules. `vscode` stays the host's. +import { copyFileSync } from "node:fs"; +import { build } from "esbuild"; + +await build({ + entryPoints: ["node_modules/vscode-languageclient/lib/node/main.js"], + bundle: true, + platform: "node", + format: "cjs", + target: "node18", + external: ["vscode"], + outfile: "dist/vendor/languageclient.js", + logLevel: "warning", + minify: true, + keepNames: true, + legalComments: "inline", +}); +copyFileSync("node_modules/vscode-languageclient/License.txt", "dist/vendor/LANGUAGECLIENT-LICENSE"); diff --git a/mide-plugins/vscode/tools/check-generators.mjs b/mide-plugins/vscode/tools/check-generators.mjs deleted file mode 100644 index aa66e79..0000000 --- a/mide-plugins/vscode/tools/check-generators.mjs +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env node -/** - * Drift gate for the generated snapshots. - * - * `data/buildscript-api.json` and `data/toml-schema.json` are generated from the - * mcpp checkout and committed, because the extension must work without that - * checkout present. That only stays honest if something notices when the source - * moves, so CI regenerates them here and fails on any difference. - * - * **What "difference" means: the data, not the provenance.** The two files record - * which mcpp they came from (`sourceVersion`, `sourceCommit`, and doc links built - * from the commit). Every mcpp commit moves those, so comparing them literally - * made this gate red for reasons no one in this repository caused — and a gate - * that is always red is not a gate. `canonical()` below blanks that provenance and - * compares the rest, so a red run means the API or the schema really moved. - * - * Without a checkout (`MCPP_REPO`, else `../mcpp`) this **skips with a notice** - * rather than passing silently: a green run that checked nothing is worse than a - * yellow one. - */ -import { execFileSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const checkout = process.env.MCPP_REPO ?? path.resolve(root, "..", "mcpp"); - -if (!fs.existsSync(path.join(checkout, "mcpp.toml"))) { - console.log(`check-generators: skipped — no mcpp checkout at ${checkout} (set MCPP_REPO to enable)`); - process.exit(0); -} - -const targets = [ - { name: "buildscript API", script: "tools/generate-buildscript-api.mjs", file: "data/buildscript-api.json" }, - { name: "mcpp.toml schema", script: "tools/generate-toml-schema.mjs", file: "data/toml-schema.json" }, -]; - -/** - * One JSON document with its **provenance** replaced by placeholders. - * - * `sourceVersion` and `sourceCommit` are facts about the checkout the snapshot was - * generated from, and any string containing the commit (the documentation links) - * is derived from it. Two clones of the same mcpp agree on everything else. - */ -function canonical(text) { - const value = JSON.parse(text); - const commit = typeof value.sourceCommit === "string" ? value.sourceCommit : ""; - const scrub = (node) => { - if (typeof node === "string") { - return commit.length === 0 ? node : node.split(commit).join(""); - } - if (Array.isArray(node)) { - return node.map(scrub); - } - if (node !== null && typeof node === "object") { - const out = {}; - for (const [key, child] of Object.entries(node)) { - out[key] = key === "sourceVersion" || key === "sourceCommit" ? "" : scrub(child); - } - return out; - } - return node; - }; - return JSON.stringify(scrub(value)); -} - -let drifted = 0; -for (const target of targets) { - const before = fs.readFileSync(path.join(root, target.file), "utf8"); - try { - execFileSync(process.execPath, [target.script], { cwd: root, env: { ...process.env, MCPP_REPO: checkout }, stdio: "pipe" }); - } catch (error) { - console.error(`error: ${target.script} failed; the snapshot cannot be refreshed`); - console.error(String(error.stderr ?? error.message)); - drifted += 1; - continue; - } - const after = fs.readFileSync(path.join(root, target.file), "utf8"); - if (canonical(before) !== canonical(after)) { - console.error( - `error: ${target.file} is out of date — the mcpp it was generated from now answers differently; ` + - `run \`npm run gen:…\` and commit the result`, - ); - drifted += 1; - } else { - console.log(`check-generators: ${target.name} is current`); - } -} - -if (drifted > 0) { - process.exit(1); -} -console.log("check-generators: ok"); diff --git a/mide-plugins/vscode/tools/generate-buildscript-api.mjs b/mide-plugins/vscode/tools/generate-buildscript-api.mjs deleted file mode 100644 index 461dd2c..0000000 --- a/mide-plugins/vscode/tools/generate-buildscript-api.mjs +++ /dev/null @@ -1,374 +0,0 @@ -#!/usr/bin/env node -/** - * `data/buildscript-api.json` <- the mcpp checkout. - * - * Spec: `.agents/docs/archive/2026-10-02-plugin-optimisation-plan.md` §3.2.3. `build.mcpp` - * is deliberately kept away from clangd (§3.2.1 measured that handing it to the C++ - * language service makes both `import std` and `import mcpp` fail), so the editor - * intelligence for `mcpp::…` names comes from this snapshot instead: the directive - * table, the five action roles, the protocol/cache constants, the provision kinds, - * the SPEC-007 rule ids that mention each wire name, and the docs anchor when a - * `docs/30-build-mcpp.md` heading spells the wire name in backticks. - * - * Everything here is derived from the mcpp sources; no row is hardcoded. The - * declared `std::array` size is checked against the number of rows actually - * parsed, and every source is mandatory — a generator that quietly writes a partial - * or empty table is worse than one that fails, because an empty table turns every - * `mcpp::` name into "unknown" in the editor. - * - * Run: node tools/generate-buildscript-api.mjs - * Env: MCPP_REPO path to the mcpp checkout (default: ../mcpp beside this repo) - * Exit: 0 on success; 1 on any unreadable source, parse failure or count mismatch. - * Nothing is written on failure. - * - * Output is deterministic: fixed key order, source order for arrays (the directive - * table's own order, the declared role order, the provision table's order), no - * timestamps. `sourceCommit` is part of the data on purpose. - */ -import { execFileSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const mcppRepo = path.resolve(process.env.MCPP_REPO ?? path.join(root, "..", "mcpp")); -const outFile = path.join(root, "data", "buildscript-api.json"); - -/** The upstream repository, for `docsUrl` links into the pinned revision. */ -const MCPP_URL = "https://github.com/mcpp-community/mcpp"; -/** The docs file directive anchors are drawn from. */ -const DOCS_FILE = "docs/30-build-mcpp.md"; -/** The spec that owns the rule ids. */ -const SPEC_FILE = "docs/specs/build-plugins.md"; - -function fail(message) { - console.error(`generate-buildscript-api: error: ${message}`); - process.exit(1); -} - -function readSource(relative) { - const file = path.join(mcppRepo, relative); - if (!fs.existsSync(file)) { - fail(`${relative}: not found under ${mcppRepo} (set MCPP_REPO to the mcpp checkout)`); - } - const text = fs.readFileSync(file, "utf8"); - if (text.length === 0) { - fail(`${relative}: empty`); - } - return text; -} - -/** C++ string-literal body -> the characters it denotes (enough for these tables). */ -function unescapeCpp(value) { - let out = ""; - for (let i = 0; i < value.length; i += 1) { - if (value[i] !== "\\") { - out += value[i]; - continue; - } - const next = value[i + 1]; - if (next === "n") out += "\n"; - else if (next === "t") out += "\t"; - else if (next === "r") out += "\r"; - else if (next === "0") out += "\0"; - else if (next === undefined) out += "\\"; - else out += next; - i += 1; - } - return out; -} - -function requireInt(text, label, pattern, where) { - const match = pattern.exec(text); - if (match === null) { - fail(`${where}: could not find ${label}`); - } - return Number.parseInt(match[1], 10); -} - -// ── `[package] version` from mcpp.toml ────────────────────────────────────── - -function parseSourceVersion(text) { - const lines = text.split(/\r?\n/); - let inPackage = false; - for (const line of lines) { - const header = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line); - if (header !== null) { - inPackage = header[1].trim() === "package"; - continue; - } - if (!inPackage) continue; - const version = /^\s*version\s*=\s*"([^"]+)"/.exec(line); - if (version !== null) return version[1]; - } - fail("mcpp.toml: no [package] version"); - return ""; -} - -function sourceCommit() { - try { - // The **full** hash, never `--short`: git shortens to the shortest unique - // prefix *for this clone*, so a fresh CI checkout (7 characters) and a - // developer's clone with more objects (8) produced two different files for - // the same mcpp — the drift gate went red on an unchanged API. - return execFileSync("git", ["rev-parse", "HEAD"], { - cwd: mcppRepo, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } catch { - return "unknown"; - } -} - -// ── `modules/buildmcpp/src/directives.cppm` ───────────────────────────────── - -/** - * One row of `kTable`. The comments between rows are ignored; a line that looks - * like a row but does not parse makes the count fall short, which is a failure. - */ -const DIRECTIVE_ROW = - /^\s*\{\s*"((?:[^"\\]|\\.)*)"\s*,\s*"((?:[^"\\]|\\.)*)"\s*,\s*Slot::([A-Za-z_]\w*)\s*,\s*Scope::([A-Za-z_]\w*)\s*,\s*Transform::([A-Za-z_]\w*)\s*,\s*(true|false)\s*,\s*"((?:[^"\\]|\\.)*)"\s*,\s*"((?:[^"\\]|\\.)*)"\s*,\s*(\d+)\s*\}\s*,?\s*$/; - -function parseDirectives(text) { - const declared = requireInt( - text, - "the kTable array size", - /inline constexpr\s+std::array\s+kTable\{\{/, - "directives.cppm", - ); - const start = text.indexOf("inline constexpr std::array role.trim()) - .filter((role) => role.length > 0); - if (roles.length !== 5) { - fail(`directives.cppm: expected 5 roles, found ${roles.length}`); - } - return roles; -} - -// ── `modules/buildmcpp/src/program_protocol.cppm` ─────────────────────────── - -function parseProtocol(text) { - return { - protocolVersion: requireInt( - text, - "kProtocolVersion", - /kProtocolVersion\s*=\s*(\d+)/, - "program_protocol.cppm", - ), - cacheEpoch: requireInt(text, "kCacheEpoch", /kCacheEpoch\s*=\s*(\d+)/, "program_protocol.cppm"), - }; -} - -// ── `modules/buildmcpp/src/provisions.cppm` ───────────────────────────────── - -const PROVISION_ROW = /^\s*\{\s*Kind::([A-Za-z_]\w*)\s*,\s*"([^"]*)"\s*,/; - -/** `HostModule` -> `host-module`; the wire string is part of the same table. */ -function kebab(name) { - return name - .replace(/([a-z0-9])([A-Z])/g, "$1-$2") - .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2") - .toLowerCase(); -} - -function parseProvisions(text) { - const start = text.indexOf("inline constexpr Def kTable[]"); - const end = text.indexOf("};", start); - if (start < 0 || end < 0) { - fail("provisions.cppm: could not delimit the kTable block"); - } - const rows = []; - for (const line of text.slice(start, end).split(/\r?\n/)) { - const match = PROVISION_ROW.exec(line); - if (match === null) continue; - rows.push({ kind: kebab(match[1]), wire: match[2] }); - } - if (rows.length === 0) { - fail("provisions.cppm: kTable parsed to zero rows"); - } - return rows; -} - -// ── `docs/specs/build-plugins.md` rule ids ────────────────────────────────── - -/** - * A rule is `- **R2.1** …` up to the next rule or the next heading; R2.1's own - * block contains the directive/`C++ 接口`/wire table, so a wire name mentioned - * only in that table still credits R2.1. - */ -function parseRuleBlocks(text) { - const blocks = []; - let current; - for (const line of text.split(/\r?\n/)) { - const rule = /^-\s+\*\*(R\d+(?:\.\d+)?)\*\*/.exec(line); - if (rule !== null) { - current = { id: rule[1], text: line }; - blocks.push(current); - continue; - } - if (/^#{1,6}\s/.test(line)) { - current = undefined; - continue; - } - if (current !== undefined) current.text += `\n${line}`; - } - return blocks; -} - -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function rulesForWire(blocks, wire) { - const pattern = new RegExp(`(? `${MCPP_URL}/blob/${revision}/${DOCS_FILE}#${anchor}`; - -const table = directives.map((row) => { - const entry = { - wire: row.wire, - tag: row.tag, - slot: row.slot, - scope: row.scope, - transform: row.transform, - mustExist: row.mustExist, - }; - // Empty strings are omitted: an optional field that is absent reads the same as - // one that is empty, and keeping them out makes the snapshot diffable. - if (row.missingPrefix.length > 0) entry.missingPrefix = row.missingPrefix; - if (row.missingSuffix.length > 0) entry.missingSuffix = row.missingSuffix; - entry.since = row.since; - entry.rules = rulesForWire(ruleBlocks, row.wire); - entry.docsUrl = anchors.has(row.wire) ? docsUrl(anchors.get(row.wire)) : null; - return entry; -}); - -const api = { - sourceVersion: version, - sourceCommit: commit, - protocolVersion, - cacheEpoch, - roles, - provisions, - directives: table, -}; - -fs.mkdirSync(path.dirname(outFile), { recursive: true }); -fs.writeFileSync(outFile, `${JSON.stringify(api, null, 2)}\n`); - -const withRules = table.filter((entry) => entry.rules.length > 0).length; -const withDocs = table.filter((entry) => entry.docsUrl !== null).length; -console.log( - `generate-buildscript-api: mcpp ${version} @ ${commit} (protocol ${protocolVersion}, ` + - `cache epoch ${cacheEpoch})`, -); -console.log( - ` ${table.length} directives (${withRules} with rule ids, ${withDocs} with docs anchors), ` + - `${roles.length} roles, ${provisions.length} provisions`, -); -console.log(` wrote ${path.relative(root, outFile)}`); diff --git a/mide-plugins/vscode/tools/generate-toml-schema.mjs b/mide-plugins/vscode/tools/generate-toml-schema.mjs deleted file mode 100644 index 59226dd..0000000 --- a/mide-plugins/vscode/tools/generate-toml-schema.mjs +++ /dev/null @@ -1,581 +0,0 @@ -#!/usr/bin/env node -// Generate data/toml-schema.json from an mcpp checkout. -// -// Sources (all read-only): -// docs/specs/manifest-semantics.md — SPEC-004 §2 plane table: which plane each -// section belongs to. -// docs/04-mcpp-toml.md — field reference; its section headings give -// the `doc` anchor for each section. -// modules/manifest/src/toml.cppm — the authoritative list of accepted section -// names (the string literals the parser -// compares against) plus the key lists it -// checks (`kKnownPackageKeys`, ...). A -// section mcpp stops accepting therefore -// disappears from the schema. -// modules/manifest/src/key_registry.cppm — the [build] keys, once the parser -// reads them from the registry. -// mcpp.toml — [package] version -> sourceVersion. -// git rev-parse --short HEAD — sourceCommit (or "unknown"). -// -// The checkout is `$MCPP_REPO`, defaulting to `../mcpp` next to this repository. -// -// The script fails loudly (exit 1) whenever a source cannot be read or yields -// nothing: it must never write an empty or partial schema. The output is -// deterministic — the same checkout produces byte-identical bytes (arrays are -// sorted, no timestamps, no environment data). -// -// ───────────────────────────────────────────────────────────────────────────── -// HAND-MAINTAINED, AND THE ONLY HAND-MAINTAINED PARTS -// -// Everything below the "HAND-MAINTAINED" banner is written by hand rather than -// parsed, because the mcpp checkout does not spell these out as data: -// -// * PLANE_LABELS — the Chinese plane names of SPEC-004 §2 mapped to the -// English vocabulary. An unknown label is a hard error, so a rewording of -// the table is noticed instead of silently producing "unclassified". -// * LEGACY_SECTIONS — sections docs/04 explicitly marks as a compatibility -// layer, and what replaces them. -// * OPEN_KEYS_SECTIONS — sections whose keys are user-chosen names rather -// than a closed vocabulary (the diagnostics layer must not report those as -// unknown keys). -// * KEY_INFO — the enum vocabulary the docs describe but the source does not -// spell out as a table (standard, kind, bmi_schedule, cache, cxx_runtime, -// linkage, the Windows keys, the platform vocabulary, profile knobs, -// toolchain family) plus a type for every key of a section that carries a -// key table. Keys whose type cannot be expressed by the five-value -// type vocabulary carry `unmodelled: true`. -// -// The *section list* is never hand-maintained: it comes from toml.cppm alone. -// ───────────────────────────────────────────────────────────────────────────── - -import { execFileSync } from "node:child_process"; -import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = resolve(HERE, ".."); -const MCPP_REPO = resolve(process.env.MCPP_REPO ?? join(REPO_ROOT, "..", "mcpp")); -const OUT_FILE = join(REPO_ROOT, "data", "toml-schema.json"); - -const DOC_BASE = "https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md"; -const SPEC_FILE = "docs/specs/manifest-semantics.md"; -const FIELD_DOC_FILE = "docs/04-mcpp-toml.md"; -const PARSER_FILE = "modules/manifest/src/toml.cppm"; -const REGISTRY_FILE = "modules/manifest/src/key_registry.cppm"; - -function fail(message) { - process.stderr.write(`generate-toml-schema: ${message}\n`); - process.exit(1); -} - -function readSource(relPath) { - const abs = join(MCPP_REPO, relPath); - try { - return readFileSync(abs, "utf8"); - } catch (error) { - fail(`cannot read ${abs}: ${error?.message ?? error}`); - } -} - -// ─── read the mcpp [package] version ──────────────────────────────────────── - -function readPackageVersion(tomlText) { - let section = ""; - for (const rawLine of tomlText.split(/\r?\n/)) { - const line = rawLine.trim(); - if (line === "" || line.startsWith("#")) continue; - const header = /^\[([^\]]+)\]$/.exec(line); - if (header) { - section = header[1].trim(); - continue; - } - if (section !== "package") continue; - const assignment = /^version\s*=\s*"([^"]*)"\s*(?:#.*)?$/.exec(line); - if (assignment) return assignment[1]; - } - return undefined; -} - -function readSourceCommit() { - try { - // The **full** hash, never `--short`: git shortens to the shortest unique - // prefix *for this clone*, so a fresh CI checkout (7 characters) and a - // developer's clone with more objects (8) produced two different files for - // the same mcpp — the drift gate went red on an unchanged API. - return execFileSync("git", ["rev-parse", "HEAD"], { - cwd: MCPP_REPO, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } catch { - return "unknown"; - } -} - -// ─── HAND-MAINTAINED: plane vocabulary ────────────────────────────────────── -// SPEC-004 §2 is written in Chinese; the schema uses an English vocabulary. -// Every row of the table must resolve here, otherwise the script exits 1. -const PLANE_LABELS = new Map([ - ["身份", "identity"], - ["产物", "artifact"], - ["编译", "compile"], - ["库依赖", "dependency"], - ["工具与环境", "tool"], - ["门", "gate"], - ["条件", "condition"], - ["产物元数据", "metadata"], - ["生命周期", "lifecycle"], - ["平台", "platform"], -]); - -// Sections the plane table does not name. The table is the spec's own closure -// claim ("a section MUST fall in one of these planes") but the parser reads -// more sections than the table lists, so the gap is surfaced as an explicit -// plane rather than invented away. A later revision of SPEC-004 can move a -// section out of this plane simply by adding it to §2. -const UNCLASSIFIED_PLANE = "unclassified"; - -// ─── HAND-MAINTAINED: legacy sections ─────────────────────────────────────── -// docs/04 §4.1: `[language]` is the old configuration, `[package].standard` is -// authoritative when both are present. -const LEGACY_SECTIONS = new Map([["language", "[package].standard"]]); - -// ─── HAND-MAINTAINED: sections whose keys are user-chosen names ───────────── -// `[toolchain]` is a platform -> spec map (`[toolchain] linux = "gcc@16"`) plus -// `bootstrap`, so its keys cannot be a closed list; the diagnostics layer skips -// unknown-key checking for these sections. -const OPEN_KEYS_SECTIONS = new Set(["toolchain"]); - -// ─── HAND-MAINTAINED: enum vocabulary and key types ───────────────────────── -// C++ standards, docs/04 §2.1 (`c++2a`/`c++2c` are normalized aliases; -// `gnu++NN`, `c++latest` and the experimental `c++fly` are documented too). -const CXX_STANDARDS = [ - "c++20", - "c++23", - "c++26", - "c++2a", - "c++2c", - "gnu++20", - "gnu++23", - "gnu++26", - "c++latest", - "c++fly", -]; - -// docs/04 §2.12: the platform vocabulary is fixed by mcpp. -const PLATFORMS = ["linux", "macos", "windows", "ios", "android", "emscripten"]; - -// docs/20 §"the toolchain": families of a managed spec. A `[toolchain]` entry -// *named by path* accepts only "gcc" or "llvm" (toml.cppm read_local_toolchain). -const TOOLCHAIN_FAMILIES = ["gcc", "llvm", "msvc", "emsdk", "android-ndk"]; - -// Keys are grouped by the section that owns them. For the four sections whose -// key list is generated from toml.cppm (package, targets, build, target), this -// table supplies the type/enum metadata for keys the parser names; a key the -// parser grows that is not listed here is emitted with `unmodelled: true`. -const KEY_INFO = { - package: { - accelerators: { type: "array", note: "accelerator backends the package supports (docs/04 §2.12b)" }, - authors: { type: "array" }, - "c-environment": { type: "string" }, - description: { type: "string" }, - exclusive: { type: "array" }, - license: { type: "string" }, - mcpp: { - type: "string", - since: "2026.9.28.3", - note: 'release floor, only the ">=" form is accepted (docs/04 §2.1)', - }, - metadata: { - type: "string", - unmodelled: true, - note: "table keyed by tool name; mcpp keeps it and does not interpret it", - }, - name: { type: "string" }, - namespace: { type: "string" }, - platforms: { type: "enum", values: PLATFORMS, note: "array of platform names (docs/04 §2.12)" }, - provides: { type: "array" }, - repo: { type: "string" }, - requires: { type: "array" }, - requires_abi: { type: "array" }, - standard: { - type: "enum", - values: CXX_STANDARDS, - default: "c++23", - note: "c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too", - }, - "std-compat-module": { type: "string" }, - "std-module": { type: "string" }, - "std-module-flags": { type: "array" }, - version: { type: "string" }, - }, - targets: { - cflags: { type: "array" }, - cxxflags: { type: "array" }, - defines: { type: "array" }, - exports: { - type: "string", - unmodelled: true, - note: "a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)", - }, - kind: { - type: "enum", - values: ["bin", "lib", "shared", "app"], - since: "2026.9.12.3", - note: '"app" requires mcpp 2026.9.12.3+; library/binary/dylib/so/shlib are accepted aliases', - }, - linkage: { type: "enum", values: ["static", "shared"], since: "2026.9.15.2" }, - main: { type: "string" }, - required_features: { type: "array" }, - soname: { type: "string" }, - windows_code_page: { type: "enum", values: ["utf-8", "legacy"], since: "2026.9.26.1" }, - windows_entry: { type: "enum", values: ["main", "wmain", "WinMain", "wWinMain"], since: "2026.9.12.2" }, - windows_subsystem: { type: "enum", values: ["console", "windows"], since: "2026.9.12.2" }, - }, - build: { - accel: { type: "string", note: "which device backends/architectures this build targets" }, - allow_host_libs: { type: "boolean" }, - bmi_schedule: { - type: "enum", - values: ["auto", "on", "off"], - default: "auto", - note: 'module-edge scheduling; "auto" currently means off', - }, - build_program_timeout: { type: "number", default: 600, note: "seconds a build.mcpp may run; 0 = no limit" }, - c_standard: { type: "string", default: "c11" }, - cache: { type: "enum", values: ["global", "local", "off"], default: "global" }, - cflags: { type: "array" }, - cxxflags: { type: "array" }, - cxx_runtime: { - type: "enum", - values: ["self-contained", "toolchain-coupled", "host-coupled"], - default: "self-contained", - note: "docs/20; static_stdlib is the old spelling", - }, - "default-profile": { type: "string", note: 'profile name, e.g. "release"' }, - defines: { type: "array" }, - dependency_linkage: { type: "enum", values: ["static", "shared"], default: "static" }, - dialect_cxxflags: { type: "array", since: "2026.9.28.1" }, - flags: { type: "array", unmodelled: true, note: "array of { glob, cflags, cxxflags, asmflags, defines } tables" }, - include_dirs: { type: "array" }, - include_dirs_after: { type: "array" }, - private_include_dirs: { type: "array" }, - ios_deployment_target: { type: "string" }, - jobs: { - type: "enum", - values: ["auto"], - unmodelled: true, - note: "a positive integer is accepted as well; the type is either", - }, - ldflags: { type: "array" }, - macos_deployment_target: { type: "string" }, - module_extensions: { type: "array" }, - "platform-dependencies": { type: "string", note: "names a platform" }, - profile: { type: "string", note: "accepted alias of default-profile" }, - sources: { type: "array" }, - static_stdlib: { type: "boolean", legacy: true, note: "replaced by [build] cxx_runtime" }, - target: { type: "string", note: "default target triple when no --target is passed" }, - "std-compat-module": { type: "string" }, - "std-module": { type: "string" }, - "std-module-flags": { type: "array" }, - }, - target: { - cxx_runtime: { type: "enum", values: ["self-contained", "toolchain-coupled", "host-coupled"] }, - linkage: { type: "enum", values: ["static", "shared"] }, - min_api_level: { type: "number" }, - runner: { type: "array", note: "argv template for mcpp run/test on this target" }, - sysroot: { type: "string" }, - toolchain: { type: "string" }, - }, - // Sections whose key list is not generated from a toml.cppm array, so the - // keys themselves are hand-maintained here as well. - lib: { - path: { type: "string" }, - }, - test: { - discover: { type: "array", note: "globs whose every match is one test program" }, - }, - language: { - import_std: { type: "boolean", legacy: true, note: "replaced by [package].standard and the module scan" }, - modules: { type: "boolean", legacy: true, note: "replaced by the module scan" }, - standard: { type: "enum", values: CXX_STANDARDS, default: "c++23", legacy: true, note: "replaced by [package].standard" }, - }, - profile: { - cflags: { type: "array" }, - cxxflags: { type: "array" }, - debug: { type: "boolean", note: "-g" }, - dependency_linkage: { type: "enum", values: ["static", "shared"] }, - ldflags: { type: "array" }, - lto: { type: "boolean", note: "-flto" }, - // docs/04 §2.9 spells the key `opt` (a number, or the string "s"/"z"). - opt: { - type: "enum", - values: ["s", "z"], - unmodelled: true, - note: 'an -O level: a number, or "s"/"z"', - }, - strip: { type: "boolean", note: "-s at link time" }, - }, - resources: { - "extra-inputs": { type: "array" }, - files: { type: "array" }, - icon: { type: "string" }, - "version-info": { type: "boolean" }, - }, - modules: { - exports: { type: "array" }, - sources: { type: "array" }, - strict: { type: "boolean" }, - }, - pack: { - "bundle-project": { type: "string", unmodelled: true, note: "table of fine-grained overrides" }, - debug_symbols: { type: "string" }, - default_mode: { type: "enum", values: ["static", "bundle-project", "bundle-all"] }, - exclude: { type: "array" }, - include: { type: "array" }, - strip: { type: "boolean" }, - }, - toolchain: { - bootstrap: { type: "string", note: "managed spec for the toolchain that builds build programs" }, - default: { type: "string", note: "a platform key: a managed spec or `{ path = ... }`" }, - family: { - type: "enum", - values: TOOLCHAIN_FAMILIES, - note: 'on a `[toolchain.]` entry table; a table named by `path` accepts only "gcc" or "llvm"', - }, - }, -}; - -// toml.cppm arrays that are the authoritative key list of a section; the array -// declarations are read by name so a renamed list is a hard error rather than a -// silently empty key table. -const GENERATED_KEY_LISTS = { - package: ["kKnownPackageKeys"], - targets: ["kKnownTargetKeys"], - build: ["kKnownBuildKeys"], - target: ["kKnownTargetScalars", "kKnownTargetArrays"], -}; - -// The manifest rules and their default severities. Ids match the diagnostics -// layer's rule names (src/toml/diagnostics.ts). -const RULES = [ - { id: "syntax", severity: "error" }, - { id: "unknown-section", severity: "warning" }, - { id: "unknown-key", severity: "warning" }, - { id: "plane-separation", severity: "warning" }, - { id: "mcpp-floor", severity: "error" }, - { id: "legacy-key", severity: "info" }, - { id: "array-table", severity: "error" }, -]; - -// ─── parse SPEC-004 §2 (planes) ───────────────────────────────────────────── - -function parsePlaneTable(specText) { - const lines = specText.split(/\r?\n/); - const start = lines.findIndex((line) => /^##\s+2\.\s/.test(line)); - if (start < 0) fail(`${SPEC_FILE}: no "## 2." plane section`); - - const planes = new Map(); - let rows = 0; - for (let i = start + 1; i < lines.length; i += 1) { - const line = lines[i]; - if (/^##\s/.test(line)) break; - if (!line.trimStart().startsWith("|")) continue; - const cells = line.split("|").map((cell) => cell.trim()); - // ["", plane, sections, meaning, ""] - if (cells.length < 4) continue; - const label = cells[1]; - if (label === "" || label === "平面" || /^-+$/.test(label)) continue; - const plane = PLANE_LABELS.get(label); - if (!plane) fail(`${SPEC_FILE}: plane table row "${label}" has no English mapping`); - const tokenPattern = /`\[([^\]]+)\]`/g; - let match; - let named = 0; - while ((match = tokenPattern.exec(cells[2])) !== null) { - const base = match[1].split(".")[0].trim(); - if (base === "") continue; - planes.set(base, plane); - named += 1; - } - if (named === 0) fail(`${SPEC_FILE}: plane table row "${label}" names no section`); - rows += 1; - } - if (rows === 0) fail(`${SPEC_FILE}: plane table yielded no rows`); - return planes; -} - -// ─── parse docs/04 (doc anchors) ──────────────────────────────────────────── - -// GitHub's heading slug: lowercase, drop everything that is not a letter, -// number, space, underscore or hyphen, then each space becomes a hyphen. -// (Verified against an in-tree link: `dependency_linkage` — static … → -// #dependency_linkage--static-or-shared-is-the-consumers-decision.) -function headingSlug(text) { - return text - .toLowerCase() - .replace(/[^\p{L}\p{N} _-]/gu, "") - .replace(/ /g, "-"); -} - -function parseDocAnchors(docText) { - const anchors = new Map(); - for (const line of docText.split(/\r?\n/)) { - const heading = /^#{2,6}\s+(.*)$/.exec(line); - if (!heading) continue; - const title = heading[1].trim(); - const slug = headingSlug(title); - const tokenPattern = /`\[([^\]]+)\]`/g; - let match; - while ((match = tokenPattern.exec(title)) !== null) { - const raw = match[1].trim(); - const base = raw.split(".")[0].trim(); - if (base === "") continue; - // A heading whose bracket is exactly the section name is preferred over - // one that only mentions it (e.g. `[build]` over `[build] cache`). - const exact = raw === base; - const existing = anchors.get(base); - if (existing === undefined || (exact && !existing.exact)) { - anchors.set(base, { slug, exact }); - } - } - } - if (anchors.size === 0) fail(`${FIELD_DOC_FILE}: no section headings yielded a doc anchor`); - return anchors; -} - -// ─── parse toml.cppm (sections and key lists) ─────────────────────────────── - -function extractTopLevelSections(parserText) { - const names = new Set(); - const pattern = - /(?:doc->(?:get|get_table|get_string|get_bool|get_int|get_string_array|get_int_array|contains)\("([^"]+)"\)|(?:load_deps|read_deps|assign_dep|load_nested_dep_table|load_selector_dep_table)\("([^"]+)")/g; - let match; - while ((match = pattern.exec(parserText)) !== null) { - const path = match[1] ?? match[2]; - const base = path.split(".")[0].trim(); - if (base !== "") names.add(base); - } - if (names.size === 0) fail(`${PARSER_FILE}: no accepted section names found`); - return names; -} - -function extractStringArray(parserText, arrayName) { - const block = new RegExp( - `(?:static\\s+)?constexpr\\s+std::string_view\\s+${arrayName}\\s*\\[\\]\\s*=\\s*\\{([\\s\\S]*?)\\};`, - ).exec(parserText); - if (!block) { - // mcpp 2026.10.11+ states the [build] keys once, in the key registry, and the - // parser reads them from there: `const auto kKnownBuildKeys = registry_keys("build");`. - const fromRegistry = new RegExp( - `\\b${arrayName}\\s*=\\s*registry_keys\\(\\s*"([^"]+)"\\s*\\)`, - ).exec(parserText); - if (!fromRegistry) fail(`${PARSER_FILE}: key list ${arrayName}[] not found`); - return registryKeys(fromRegistry[1]); - } - const values = []; - const literal = /"([^"]*)"/g; - let match; - while ((match = literal.exec(block[1])) !== null) { - if (match[1] !== "") values.push(match[1]); - } - if (values.length === 0) fail(`${PARSER_FILE}: key list ${arrayName}[] is empty`); - return values; -} - -// The keys of one table in mcpp's key registry: every `{"
      ", "", …}` -// row of `kKeyRegistry`, the table-as-a-whole row (`""`) left out. -function registryKeys(table) { - const text = readSource(REGISTRY_FILE); - const block = /kKeyRegistry\s*\[\]\s*=\s*\{([\s\S]*?)\n\};/.exec(text); - if (!block) fail(`${REGISTRY_FILE}: kKeyRegistry[] not found`); - const values = []; - const row = /\{\s*"([^"]*)"\s*,\s*"([^"]*)"/g; - let match; - while ((match = row.exec(block[1])) !== null) { - if (match[1] === table && match[2] !== "") values.push(match[2]); - } - if (values.length === 0) fail(`${REGISTRY_FILE}: no keys for table ${table}`); - return values; -} - -// ─── assemble ─────────────────────────────────────────────────────────────── - -function buildKey(section, key) { - const info = KEY_INFO[section]?.[key]; - if (info === undefined) { - // The parser grew a key the hand table has not caught up with. Emit it so - // it is not reported as unknown, and say the type is a guess. - return { key, type: "string", unmodelled: true }; - } - const out = { key, type: info.type }; - if (info.values !== undefined) out.values = [...info.values]; - if (info.default !== undefined) out.default = info.default; - if (info.since !== undefined) out.since = info.since; - if (info.legacy === true) out.legacy = true; - if (info.note !== undefined) out.note = info.note; - if (info.unmodelled === true) out.unmodelled = true; - return out; -} - -function sectionKeys(section, parserText) { - const listNames = GENERATED_KEY_LISTS[section]; - let keyNames; - if (listNames !== undefined) { - keyNames = []; - for (const name of listNames) keyNames.push(...extractStringArray(parserText, name)); - } else { - keyNames = Object.keys(KEY_INFO[section] ?? {}); - } - keyNames = [...new Set(keyNames)].sort(); - if (keyNames.length === 0) return undefined; - return keyNames.map((key) => buildKey(section, key)); -} - -function main() { - const version = readPackageVersion(readSource("mcpp.toml")); - if (!version) fail(`mcpp.toml: [package] version not found`); - - const planes = parsePlaneTable(readSource(SPEC_FILE)); - const anchors = parseDocAnchors(readSource(FIELD_DOC_FILE)); - const parserText = readSource(PARSER_FILE); - const sectionNames = extractTopLevelSections(parserText); - - const sections = [...sectionNames].sort().map((name) => { - const section = { - header: `[${name}]`, - name, - plane: planes.get(name) ?? UNCLASSIFIED_PLANE, - doc: anchors.has(name) ? `${DOC_BASE}#${anchors.get(name).slug}` : DOC_BASE, - deprecatedBy: LEGACY_SECTIONS.get(name) ?? null, - }; - if (OPEN_KEYS_SECTIONS.has(name)) section.openKeys = true; - const keys = sectionKeys(name, parserText); - if (keys !== undefined) section.keys = keys; - return section; - }); - - // Refuse to write a schema that cannot drive anything: every section must be - // named by the parser, and a plane table that matched nothing is a hard - // error (parsePlaneTable already guarantees the latter). - if (sections.length === 0) fail("no sections assembled"); - - const schema = { - sourceVersion: version, - sourceCommit: readSourceCommit(), - sections, - rules: RULES, - }; - - mkdirSync(dirname(OUT_FILE), { recursive: true }); - writeFileSync(OUT_FILE, `${JSON.stringify(schema, null, 2)}\n`, "utf8"); - - const keys = sections.reduce((total, section) => total + (section.keys?.length ?? 0), 0); - process.stdout.write( - `generate-toml-schema: wrote ${OUT_FILE}\n` + - ` mcpp ${version} (${schema.sourceCommit.slice(0, 8)}), ${sections.length} sections, ${keys} keys, ` + - `${sections.filter((section) => section.plane === UNCLASSIFIED_PLANE).length} unclassified\n`, - ); -} - -main(); diff --git a/protocol/protocol.schema.json b/protocol/protocol.schema.json index 15c8399..c6581a1 100644 --- a/protocol/protocol.schema.json +++ b/protocol/protocol.schema.json @@ -2,67 +2,82 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/mcpp-community/mcpp-ide/protocol/protocol.schema.json", "title": "The mcpp-ide protocol", - "description": "JSON-RPC 2.0 over the LSP base protocol (Content-Length framing) on the standard streams of `mcpp-ide serve`. Version 1. An adapter sends `initialize` first; every other request before it is refused (-32002). Requests run concurrently; `$/cancelRequest` cancels one. The backend's tests check that the methods it registers and the notifications it sends are exactly the ones listed here.", - "version": 1, + "description": "JSON-RPC 2.0 over the LSP base protocol (Content-Length framing) on the standard streams of `mcpp-ide serve`. Version 2: the session is an LSP server (design 2026-10-30 part 2 §8) -- `initialize` in LSP's shape with mcpp-ide's parameters in `initializationOptions`, the rules layer of mcpp.toml and build.mcpp as `textDocument/*`, mcpp-ide's own methods and notifications under `mcpp-ide/`. An adapter sends `initialize` first; every other request before it is refused (-32002). Requests run concurrently; `$/cancelRequest` cancels one. The backend's tests check that the methods it registers and the notifications it sends are exactly the ones listed here.", + "version": 2, "methods": { "initialize": { - "description": "Starts the session: finds mcpp and mcppls, resolves the settings, discovers the projects of the folders and watches them.", + "description": "Starts the session (LSP's `initialize`): finds mcpp and mcppls, resolves the settings, discovers the projects of the workspace folders and watches them, and answers the server's LSP capabilities with mcpp-ide's own answer beside them.", "params": { "type": "object", "properties": { - "client": { "type": "object", "properties": { "name": { "type": "string" }, "version": { "type": "string" } } }, - "editor": { "enum": ["vscode", "jetbrains", "nvim", "cli"], "description": "The adapter: mcpp-vscode, mcpp-clion (the IntelliJ platform's id), mcpp-nvim, or an agent" }, + "processId": { "type": ["integer", "null"] }, + "clientInfo": { "type": "object", "properties": { "name": { "type": "string" }, "version": { "type": "string" } } }, "locale": { "type": "string" }, - "trusted": { "type": "boolean" }, - "settings": { "$ref": "#/$defs/settingValues" }, - "workspaceFolders": { "type": "array", "items": { "type": "string" } }, - "languageService": { "type": "object", "properties": { "candidates": { "type": "array", "items": { "type": "string" }, "description": "Payload directories (payload.json's `server`) or programs, the adapter's mcppls first" } } } + "rootUri": { "type": ["string", "null"] }, + "workspaceFolders": { "type": ["array", "null"], "items": { "type": "object", "required": ["uri"], "properties": { "uri": { "type": "string" }, "name": { "type": "string" } } } }, + "capabilities": { "type": "object" }, + "initializationOptions": { + "type": "object", + "properties": { + "editor": { "enum": ["vscode", "jetbrains", "nvim", "cli"], "description": "The adapter: mcpp-vscode, mcpp-clion (the IntelliJ platform's id), mcpp-nvim, or an agent" }, + "locale": { "type": "string", "description": "The editor's display language, `auto` resolved by the adapter" }, + "trusted": { "type": "boolean" }, + "settings": { "$ref": "#/$defs/settingValues" }, + "languageService": { "type": "object", "properties": { "candidates": { "type": "array", "items": { "type": "string" }, "description": "Payload directories (payload.json's `server`) or programs, the adapter's mcppls first" } } }, + "mcpplsServes": { "type": "boolean", "description": "C++ Modules serves build.mcpp in this editor: the rules layer leaves it the unknownSymbol rule, the module names after `import` and their hover" } + } + } } }, "result": { "type": "object", - "required": ["server", "protocol", "projects", "capabilities", "checks"], + "required": ["capabilities", "serverInfo", "protocol", "projects", "features", "checks"], "properties": { - "server": { "type": "object", "required": ["name", "version"] }, - "protocol": { "const": 1 }, + "capabilities": { "type": "object", "description": "LSP ServerCapabilities: full text synchronisation, completion, hover, definition, document symbols" }, + "serverInfo": { "type": "object", "required": ["name", "version"] }, + "protocol": { "const": 2 }, "mcpp": { "type": ["object", "null"] }, "languageService": { "$ref": "#/$defs/languageService" }, "checks": { "type": "array", "items": { "$ref": "#/$defs/check" } }, "projects": { "type": "array", "items": { "$ref": "#/$defs/project" } }, - "capabilities": { "type": "array", "items": { "type": "string" } } + "features": { "type": "array", "items": { "type": "string" } } } } }, - "settings/update": { + "textDocument/completion": { "description": "Completion in mcpp.toml (tables, keys, enum values, the forms of a table whose keys the user names, dependency versions from mcpp's catalogue when mcpp.toml.indexCompletion is on and the workspace is trusted) and in build.mcpp (`mcpp::` names, the API's snippets, the modules after `import`).", "params": { "type": "object", "required": ["textDocument", "position"], "properties": { "textDocument": { "type": "object", "required": ["uri"], "properties": { "uri": { "type": "string" } } }, "position": { "type": "object", "required": ["line", "character"], "properties": { "line": { "type": "integer" }, "character": { "type": "integer", "description": "UTF-16 code units" } } } } }, "result": { "type": ["object", "null"], "required": ["isIncomplete", "items"] } }, + "textDocument/hover": { "description": "A table's, a key's or an enum value's description in mcpp.toml; a directive's or a known module's in build.mcpp.", "params": { "type": "object", "required": ["textDocument", "position"], "properties": { "textDocument": { "type": "object", "required": ["uri"], "properties": { "uri": { "type": "string" } } }, "position": { "type": "object", "required": ["line", "character"], "properties": { "line": { "type": "integer" }, "character": { "type": "integer", "description": "UTF-16 code units" } } } } }, "result": { "type": ["object", "null"] } }, + "textDocument/definition": { "description": "In mcpp.toml: `workspace = true` to [workspace.dependencies]'s entry, `features = [\"a\"]` to [features.a], `path = \"../x\"` to that manifest's [package].", "params": { "type": "object", "required": ["textDocument", "position"], "properties": { "textDocument": { "type": "object", "required": ["uri"], "properties": { "uri": { "type": "string" } } }, "position": { "type": "object", "required": ["line", "character"], "properties": { "line": { "type": "integer" }, "character": { "type": "integer", "description": "UTF-16 code units" } } } } }, "result": { "type": ["object", "null"] } }, + "textDocument/documentSymbol": { "description": "mcpp.toml's tables and their keys.", "params": { "type": "object", "required": ["textDocument"] }, "result": { "type": ["array", "null"] } }, + "mcpp-ide/settings/update": { "description": "The adapter's settings changed: they are validated, migrated from earlier keys and resolved.", - "params": { "type": "object", "properties": { "settings": { "$ref": "#/$defs/settingValues" }, "trusted": { "type": "boolean" }, "locale": { "type": "string" } } }, + "params": { "type": "object", "properties": { "settings": { "$ref": "#/$defs/settingValues" }, "trusted": { "type": "boolean" }, "locale": { "type": "string" }, "mcpplsServes": { "type": "boolean" } } }, "result": { "$ref": "#/$defs/resolvedSettings" } }, - "settings/registry": { + "mcpp-ide/settings/registry": { "description": "Every setting with its type, default, bounds, group and texts, and the presets: what a settings panel renders.", "params": { "type": "object" }, "result": { "type": "object", "required": ["settings", "presets"] } }, - "settings/preset": { + "mcpp-ide/settings/preset": { "params": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } } }, "result": { "type": "object" } }, - "i18n/bundle": { + "mcpp-ide/i18n/bundle": { "description": "The runtime strings in a language, keyed by their English text ({} for English).", "params": { "type": "object", "properties": { "locale": { "type": "string" } } }, "result": { "type": "object", "additionalProperties": { "type": "string" } } }, - "selfCheck": { + "mcpp-ide/selfCheck": { "description": "Finds mcpp again and reports what the backend finds: the backend, mcpp and its contract, mcppls, xlings, trust.", "params": { "type": "object" }, "result": { "type": "object", "required": ["backend", "checks"], "properties": { "checks": { "type": "array", "items": { "$ref": "#/$defs/check" } } } } }, - "commands/list": { + "mcpp-ide/commands/list": { "description": "The command table (spec/commands.json) for one editor, with titles in the session's language and each command's subcommand.", "params": { "type": "object", "properties": { "editor": { "enum": ["vscode", "jetbrains", "nvim", "cli"] } } }, "result": { "type": "object", "required": ["commands"], "properties": { "commands": { "type": "array", "items": { "$ref": "#/$defs/command" } } } } }, - "menu/quick": { + "mcpp-ide/menu/quick": { "description": "The quick menu for a project: each item enabled or not, with the reason.", "params": { "type": "object", "properties": { "root": { "type": "string" }, "editor": { "type": "string" } } }, "result": { @@ -80,12 +95,12 @@ } } }, - "languageService/status": { + "mcpp-ide/languageService/status": { "description": "Which mcppls answers this editor's C++ files, whether it reads mcpp's result record (0.0.18 and later), and what it says about the workspace: `mcppls status` (0.0.20 and later) or the frozen table. Candidates are the servers the adapter knows of (the mcppls extension's or plugin's own); PATH, xlings and the plugin payload follow.", "params": { "type": "object", "properties": { "candidates": { "type": "array", "items": { "type": "string" } }, "root": { "type": "string", "description": "The workspace to ask mcppls about (default: the first folder)" } } }, "result": { "$ref": "#/$defs/languageService" } }, - "source/entries": { + "mcpp-ide/source/entries": { "description": "What a C++ file is to its project (part 2 §6.3): a test's main (tests/**, named as mcpp names it), a program's main, or neither, with the line of main read from the text (approximate) and, for a test, its framework. Nothing is built or run.", "params": { "type": "object", "required": ["file"], "properties": { "file": { "type": "string" }, "text": { "type": "string", "description": "The editor's unsaved text" }, "boundary": { "type": "string" } } }, "result": { @@ -94,7 +109,7 @@ "framework": { "type": "string" }, "main": { "type": "object", "required": ["line"], "properties": { "line": { "type": "integer" }, "approximate": { "type": "boolean" } } } } } }, - "test/cases": { + "mcpp-ide/test/cases": { "description": "A test program's cases (part 2 §7.3): as its source names them (a framework's macros), and with `list` -- trusted, and only for a source that calls its framework's macros -- as the program lists them. Without a framework, no cases: the program is the unit.", "params": { "type": "object", "properties": { "root": { "type": "string" }, "test": { "type": "string" }, "file": { "type": "string" }, "list": { "type": "boolean" }, "target": { "type": "string" } } }, "result": { @@ -104,21 +119,21 @@ "cases": { "type": "array", "items": { "type": "object", "required": ["name", "source"], "properties": { "name": { "type": "string" }, "line": { "type": "integer" }, "source": { "enum": ["static", "listed"] } } } } } } }, - "test/runCases": { + "mcpp-ide/test/runCases": { "description": "Runs some of a test program's cases (all of them when `cases` is empty): a test job whose program receives the framework's filter and report arguments after `--`. Each case's outcome is sent as test/caseResult when the job ends.", "params": { "type": "object", "required": ["root", "test", "framework"], "properties": { "root": { "type": "string" }, "test": { "type": "string" }, "framework": { "type": "string" }, "cases": { "type": "array", "items": { "type": "string" } }, "profile": { "type": "string" }, "target": { "type": "string" }, "color": { "type": "boolean" } } }, "result": { "type": "object", "required": ["jobId"] } }, - "projects/list": { + "mcpp-ide/projects/list": { "params": { "type": "object", "properties": { "folders": { "type": "array", "items": { "type": "string" } }, "depth": { "type": "integer" } } }, "result": { "type": "array", "items": { "$ref": "#/$defs/project" } } }, - "projects/forFile": { + "mcpp-ide/projects/forFile": { "description": "The project a file belongs to: the nearest mcpp.toml above it, no higher than the boundary.", "params": { "type": "object", "required": ["path"], "properties": { "path": { "type": "string" }, "boundary": { "type": "string" } } }, "result": { "anyOf": [ { "$ref": "#/$defs/project" }, { "type": "null" } ] } }, - "project/summary": { + "mcpp-ide/project/summary": { "description": "A project's executables, tests, libraries, toolchain, last build, and the platforms its last plan named.", "params": { "$ref": "#/$defs/rooted" }, "result": { @@ -141,17 +156,17 @@ } } }, - "project/new": { + "mcpp-ide/project/new": { "description": "`mcpp new ` in a parent directory, as a job; the result names the new project's root.", "params": { "type": "object", "required": ["parent", "name"], "properties": { "parent": { "type": "string" }, "name": { "type": "string" }, "template": { "type": "string" } } }, "result": { "type": "object", "required": ["jobId", "root"] } }, - "result/last": { + "mcpp-ide/result/last": { "description": "The result record target/.last-result.json names, or null.", "params": { "$ref": "#/$defs/rooted" }, "result": { "type": ["object", "null"] } }, - "job/start": { + "mcpp-ide/job/start": { "description": "Queues a job on its project's lane: one at a time per project, in order; a build or test asked for again replaces the waiting one, and `supersede` cancels the running one.", "params": { "type": "object", @@ -174,20 +189,20 @@ }, "result": { "$ref": "#/$defs/jobRef" } }, - "job/cancel": { + "mcpp-ide/job/cancel": { "params": { "type": "object", "required": ["jobId"], "properties": { "jobId": { "type": "string" } } }, "result": { "type": "object", "required": ["cancelled"], "properties": { "cancelled": { "type": "boolean" } } } }, - "job/list": { + "mcpp-ide/job/list": { "params": { "type": "object" }, "result": { "type": "array", "items": { "type": "object", "required": ["id", "kind", "root", "state"] } } }, - "run/launch": { + "mcpp-ide/run/launch": { "description": "Builds and returns how the program starts (mcpp run --print-launch): the adapter starts it in a terminal.", "params": { "$ref": "#/$defs/launchParams" }, "result": { "type": "object", "required": ["argv", "cwd", "env"], "properties": { "argv": { "type": "array", "items": { "type": "string" } }, "cwd": { "type": "string" }, "env": { "type": "object" }, "program": { "type": "string" }, "via": { "type": ["string", "null"] } } } }, - "debug/configuration": { + "mcpp-ide/debug/configuration": { "description": "Builds and returns a debug configuration for the adapter's debugger. A program that runs through another (an ok image under okrun) is debugged as that runner, its symbols read through the GDB JIT interface.", "params": { "allOf": [ @@ -197,7 +212,7 @@ }, "result": { "type": "object", "required": ["engine", "configuration", "launch"] } }, - "test/list": { + "mcpp-ide/test/list": { "description": "The tests of a project without building them (mcpp test --list).", "params": { "$ref": "#/$defs/rooted" }, "result": { @@ -206,26 +221,26 @@ "properties": { "tests": { "type": "array", "items": { "type": "object", "required": ["test"], "properties": { "test": { "type": "string" }, "member": { "type": "string" }, "main": { "type": "string" } } } } } } }, - "library/search": { + "mcpp-ide/library/search": { "params": { "type": "object", "properties": { "keyword": { "type": "string" }, "index": { "type": "string" }, "sort": { "enum": ["name", "versions"] }, "limit": { "type": "integer" }, "id": { "type": "string" }, "refresh": { "type": "boolean" }, "root": { "type": "string" } } }, "result": { "type": "object" } }, - "library/indexes": { "params": { "type": "object" }, "result": { "type": ["object", "null"] } }, - "library/add": { + "mcpp-ide/library/indexes": { "params": { "type": "object" }, "result": { "type": ["object", "null"] } }, + "mcpp-ide/library/add": { "params": { "type": "object", "required": ["root", "dependency"], "properties": { "root": { "type": "string" }, "dependency": { "type": "string" }, "version": { "type": "string" } } }, "result": { "$ref": "#/$defs/jobRef" } }, - "library/updateIndex": { "params": { "type": "object" }, "result": { "$ref": "#/$defs/jobRef" } }, - "toolchains/list": { "params": { "type": "object" }, "result": { "type": "object", "required": ["host", "rows", "defaults"] } }, - "toolchains/install": { + "mcpp-ide/library/updateIndex": { "params": { "type": "object" }, "result": { "$ref": "#/$defs/jobRef" } }, + "mcpp-ide/toolchains/list": { "params": { "type": "object" }, "result": { "type": "object", "required": ["host", "rows", "defaults"] } }, + "mcpp-ide/toolchains/install": { "params": { "type": "object", "required": ["spec"], "properties": { "spec": { "type": "string" }, "target": { "type": "string" }, "root": { "type": "string" } } }, "result": { "$ref": "#/$defs/jobRef" } }, - "toolchains/setDefault": { + "mcpp-ide/toolchains/setDefault": { "params": { "type": "object", "required": ["spec"], "properties": { "spec": { "type": "string" } } }, "result": { "type": "object", "required": ["spec"] } }, - "targets/list": { + "mcpp-ide/targets/list": { "description": "One row per target mcpp knows: its toolchains, whether one is installed, and the platform that configures it (mcpp 2026-10-28 §9); the host platform when the host graph has one; the last plan of the project.", "params": { "type": "object", "properties": { "root": { "type": "string" }, "selected": { "type": "string" } } }, "result": { @@ -262,38 +277,34 @@ } } }, - "cache/inventory": { "params": { "type": "object", "properties": { "topN": { "type": "integer" } } }, "result": { "type": "object", "required": ["totalBytes", "byKind", "topLabels"] } }, - "cache/plan": { + "mcpp-ide/cache/inventory": { "params": { "type": "object", "properties": { "topN": { "type": "integer" } } }, "result": { "type": "object", "required": ["totalBytes", "byKind", "topLabels"] } }, + "mcpp-ide/cache/plan": { "description": "What a cleanup would run, how much confirmation it needs, and its preview.", "params": { "type": "object", "required": ["action"], "properties": { "action": { "type": "string" }, "root": { "type": "string" } } }, "result": { "type": "object", "required": ["action", "argv", "level"] } }, - "cache/apply": { + "mcpp-ide/cache/apply": { "params": { "type": "object", "required": ["action", "confirmation"], "properties": { "action": { "type": "string" }, "confirmation": { "type": "object" }, "root": { "type": "string" } } }, "result": { "$ref": "#/$defs/jobRef" } }, - "graph/packages": { "params": { "$ref": "#/$defs/rooted" }, "result": { "$ref": "#/$defs/graph" } }, - "graph/modules": { "params": { "$ref": "#/$defs/rooted" }, "result": { "$ref": "#/$defs/graph" } }, - "pack/formats": { + "mcpp-ide/graph/packages": { "params": { "$ref": "#/$defs/rooted" }, "result": { "$ref": "#/$defs/graph" } }, + "mcpp-ide/graph/modules": { "params": { "$ref": "#/$defs/rooted" }, "result": { "$ref": "#/$defs/graph" } }, + "mcpp-ide/pack/formats": { "description": "The package formats mcpp offers here (mcpp pack --list-formats), the closures it packs and the linkages it takes apart from them.", "params": { "type": "object" }, "result": { "type": "object", "required": ["modes", "linkages"], "properties": { "formats": { "type": "array" }, "modes": { "type": "array", "items": { "type": "string" } }, "linkages": { "type": "array", "items": { "type": "string" } } } } }, - "pack/start": { + "mcpp-ide/pack/start": { "description": "A pack job (`mcpp pack --message-format json`): its envelope arrives in job/finished.", "params": { "type": "object", "required": ["root"], "properties": { "root": { "type": "string" }, "pkgFormat": { "type": "string" }, "mode": { "type": "string" }, "linkage": { "enum": ["static", "shared"] }, "target": { "type": "string" }, "profile": { "type": "string" } } }, "result": { "$ref": "#/$defs/jobRef" } }, - "manifest/check": { + "mcpp-ide/manifest/check": { "description": "The manifest's problems as mcpp reports them, located.", "params": { "$ref": "#/$defs/rooted" }, "result": { "type": "object", "required": ["root", "diagnostics"], "properties": { "diagnostics": { "type": "array", "items": { "$ref": "#/$defs/diagnostic" } } } } }, - "toml/complete": { "params": { "$ref": "#/$defs/textPosition" }, "result": { "type": "object", "required": ["items"] } }, - "toml/hover": { "params": { "$ref": "#/$defs/textPosition" }, "result": { "type": ["object", "null"] } }, - "toml/symbols": { "params": { "type": "object", "required": ["text"] }, "result": { "type": "array" } }, - "buildscript/complete": { "params": { "$ref": "#/$defs/textPosition" }, "result": { "type": "object", "required": ["items"] } }, - "mcpp/exec": { + "mcpp-ide/mcpp/exec": { "description": "One command of mcpp, as an editor's own parsers read it (mcpp-vscode's views): what it printed and how it ended. The backend runs it as every command it runs -- the mcpp it located, the session's client, the effects an untrusted workspace denies -- ended past timeoutMs (without one, after a day: an install runs as long as it needs) and keeping at most maxOutputBytes (default 16 MiB) of each output.", "params": { "type": "object", @@ -317,7 +328,7 @@ } } }, - "languageService/exec": { + "mcpp-ide/languageService/exec": { "description": "One command of mcppls (`query module`, for the module graph), run as the command languageService/status found -- a payload's runner and image, or a program -- ended past timeoutMs (default 120000) and keeping at most maxOutputBytes (default 16 MiB) of each output.", "params": { "type": "object", @@ -341,7 +352,7 @@ } } }, - "project/files": { + "mcpp-ide/project/files": { "description": "What an editor reads of a project to describe it: the text of mcpp.toml and mcpp.lock (null when absent) and the number of source files under the root, counted as mcpp-vscode 0.9.0 counted them.", "params": { "type": "object", "required": ["root"], "properties": { "root": { "type": "string" } } }, "result": { @@ -355,7 +366,7 @@ } } }, - "project/mcpp": { + "mcpp-ide/project/mcpp": { "description": "The project's mcpp, asked in the project through the shim (design 2026-10-30 part 2 §4.2): its version, the program that ran, what it states it can do, and whether mcpp.path decided it. `state` is ok, pinned-missing (a pinned version not installed; `install` is the command), failed, or not-probed (an untrusted workspace: the global mcpp's facts).", "params": { "type": "object", "properties": { "root": { "type": "string" } } }, "result": { @@ -374,12 +385,12 @@ } } }, - "project/refresh": { + "mcpp-ide/project/refresh": { "description": "Forgets what a project's mcpp is (every project's, without `root`), so its next use asks again: the editor regained focus.", "params": { "type": "object", "properties": { "root": { "type": "string" } } }, "result": { "type": "object" } }, - "capabilities/explain": { + "mcpp-ide/capabilities/explain": { "description": "Each feature the panels gate, whether it is available for the project, and if not which party lacks what and from which version (design 2026-10-30 part 2 §3.2).", "params": { "type": "object", "properties": { "root": { "type": "string" } } }, "result": { @@ -390,7 +401,7 @@ } } }, - "report/collect": { + "mcpp-ide/report/collect": { "description": "The IDE report: the backend, every project's mcpp, mcppls's status, the settings in effect, recent jobs and the backend's recent log. The editor writes it to a file the person chose.", "params": { "type": "object", "properties": { "root": { "type": "string" } } }, "result": { "type": "object", "required": ["kind", "version", "backend", "projects", "settings", "jobs", "log"] } @@ -399,14 +410,16 @@ "exit": { "description": "A notification: the backend ends (status 0 after shutdown, 1 otherwise).", "params": { "type": "object" }, "result": { "type": "null" } } }, "notifications": { - "job/started": { "params": { "type": "object", "required": ["jobId", "kind", "root", "argv"] } }, - "job/output": { "description": "Narration (mcpp's standard error, coloured when the job asked for colour).", "params": { "type": "object", "required": ["jobId", "text"] } }, - "job/plan": { "description": "The configuration a build plans, and the platforms mcpp names for the target and host graphs.", "params": { "type": "object", "required": ["jobId", "root", "configuration"], "properties": { "platform": { "$ref": "#/$defs/platform" }, "hostPlatform": { "$ref": "#/$defs/platform" }, "platformText": { "type": "string" }, "hostPlatformText": { "type": "string" } } } }, - "job/progress": { "params": { "type": "object", "required": ["jobId", "finished", "total"] } }, - "job/diagnostic": { "params": { "allOf": [ { "$ref": "#/$defs/diagnostic" }, { "required": ["jobId", "root"] } ] } }, - "job/artifact": { "params": { "type": "object", "required": ["jobId", "artifact"] } }, - "job/record": { "description": "A record of a job's stream with no meaning the backend names.", "params": { "type": "object", "required": ["jobId", "record"] } }, - "job/finished": { + "textDocument/publishDiagnostics": { "description": "The seven rules of mcpp.toml and the SPEC-007 checks of build.mcpp for an open document, at the severities the settings give.", "params": { "type": "object", "required": ["uri", "diagnostics"], "properties": { "uri": { "type": "string" }, "version": { "type": "integer" }, "diagnostics": { "type": "array" } } } }, + "window/showMessage": { "description": "Once a session: completing a dependency version runs `mcpp search`.", "params": { "type": "object", "required": ["type", "message"] } }, + "mcpp-ide/job/started": { "params": { "type": "object", "required": ["jobId", "kind", "root", "argv"] } }, + "mcpp-ide/job/output": { "description": "Narration (mcpp's standard error, coloured when the job asked for colour).", "params": { "type": "object", "required": ["jobId", "text"] } }, + "mcpp-ide/job/plan": { "description": "The configuration a build plans, and the platforms mcpp names for the target and host graphs.", "params": { "type": "object", "required": ["jobId", "root", "configuration"], "properties": { "platform": { "$ref": "#/$defs/platform" }, "hostPlatform": { "$ref": "#/$defs/platform" }, "platformText": { "type": "string" }, "hostPlatformText": { "type": "string" } } } }, + "mcpp-ide/job/progress": { "params": { "type": "object", "required": ["jobId", "finished", "total"] } }, + "mcpp-ide/job/diagnostic": { "params": { "allOf": [ { "$ref": "#/$defs/diagnostic" }, { "required": ["jobId", "root"] } ] } }, + "mcpp-ide/job/artifact": { "params": { "type": "object", "required": ["jobId", "artifact"] } }, + "mcpp-ide/job/record": { "description": "A record of a job's stream with no meaning the backend names.", "params": { "type": "object", "required": ["jobId", "record"] } }, + "mcpp-ide/job/finished": { "params": { "type": "object", "required": ["jobId", "status", "exitCode"], @@ -422,7 +435,7 @@ } } }, - "test/result": { + "mcpp-ide/test/result": { "params": { "type": "object", "required": ["jobId", "test", "status"], @@ -436,13 +449,13 @@ } } }, - "test/summary": { "params": { "type": "object", "required": ["jobId", "passed", "failed"] } }, - "result/changed": { "description": "A project's result record changed: a build finished, whoever ran it.", "params": { "type": "object", "required": ["root", "result"] } }, - "manifest/changed": { "params": { "type": "object", "required": ["root"] } }, - "manifest/diagnostics": { "params": { "type": "object", "required": ["root", "diagnostics"] } }, - "project/mcppChanged": { "description": "A job's envelope named another mcpp than the project's was asked as: it is asked again on its next use.", "params": { "type": "object", "required": ["root", "version"] } }, - "test/caseResult": { "description": "One case's outcome, read from the framework's report when a test/runCases job ended.", "params": { "type": "object", "required": ["jobId", "root", "test", "case", "status"], "properties": { "status": { "enum": ["passed", "failed", "skipped"] }, "durationMs": { "type": "integer" }, "message": { "type": "string" } } } }, - "languageService/changed": { "description": "mcppls answered differently from the last time it was asked: another program or version, other actions, or another state of the workspace. An editor that lists the commands asks for them again.", "params": { "type": "object", "required": ["root", "state", "workspaceState"] } } + "mcpp-ide/test/summary": { "params": { "type": "object", "required": ["jobId", "passed", "failed"] } }, + "mcpp-ide/result/changed": { "description": "A project's result record changed: a build finished, whoever ran it.", "params": { "type": "object", "required": ["root", "result"] } }, + "mcpp-ide/manifest/changed": { "params": { "type": "object", "required": ["root"] } }, + "mcpp-ide/manifest/diagnostics": { "params": { "type": "object", "required": ["root", "diagnostics"] } }, + "mcpp-ide/project/mcppChanged": { "description": "A job's envelope named another mcpp than the project's was asked as: it is asked again on its next use.", "params": { "type": "object", "required": ["root", "version"] } }, + "mcpp-ide/test/caseResult": { "description": "One case's outcome, read from the framework's report when a test/runCases job ended.", "params": { "type": "object", "required": ["jobId", "root", "test", "case", "status"], "properties": { "status": { "enum": ["passed", "failed", "skipped"] }, "durationMs": { "type": "integer" }, "message": { "type": "string" } } } }, + "mcpp-ide/languageService/changed": { "description": "mcppls answered differently from the last time it was asked: another program or version, other actions, or another state of the workspace. An editor that lists the commands asks for them again.", "params": { "type": "object", "required": ["root", "state", "workspaceState"] } } }, "$defs": { "settingValues": { "type": "object", "additionalProperties": true }, @@ -500,7 +513,6 @@ } }, "launchParams": { "type": "object", "required": ["root"], "properties": { "root": { "type": "string" }, "target": { "type": "string" }, "profile": { "type": "string" }, "bin": { "type": "string" }, "args": { "type": "array", "items": { "type": "string" } } } }, - "textPosition": { "type": "object", "required": ["text", "line", "character"], "properties": { "text": { "type": "string" }, "line": { "type": "integer" }, "character": { "type": "integer" } } }, "graph": { "type": "object", "required": ["nodes", "edges"], "properties": { "nodes": { "type": "array" }, "edges": { "type": "array" } } }, "diagnostic": { "type": "object", diff --git a/src/documents.cppm b/src/documents.cppm new file mode 100644 index 0000000..78e1ff2 --- /dev/null +++ b/src/documents.cppm @@ -0,0 +1,337 @@ +// The documents the editors keep open, and the rules layer's answers about them in LSP's shapes +// (design 2026-10-30 part 2 §8): the session is the language server of mcpp.toml and build.mcpp, and +// an editor connects to it as to any LSP server -- VS Code's language client, CLion's LSP API, +// Neovim's and Zed's clients. The editor sends the whole text on each change; positions are a +// 0-based line and a UTF-16 column. +// +// What the editor turns off stays off here: each feature reads its `mcpp.toml.*` or +// `mcpp.buildScript.*` setting, and while C++ Modules (mcppls) serves build.mcpp the answers it gives +// better are left to it (the editor says whether it does). +export module mcppide.documents; + +import std; +import nlohmann.json; +import mcppide.buildscript; +import mcppide.i18n; +import mcppide.manifest; +import mcppide.text; + +export namespace mcppide::documents { + +using Json = nlohmann::json; + +enum class Kind { manifest, buildscript, other }; + +// mcpp.toml, build.mcpp, or a file the rules layer does not read. +Kind kind_of(const std::filesystem::path& path); +// A `file:` URI's path (percent-decoded; `file:///C:/x` is `C:/x`); empty for another scheme. +std::filesystem::path path_of(std::string_view uri); +std::string uri_of(const std::filesystem::path& path); + +struct Document { + std::string uri; + std::filesystem::path path; + Kind kind = Kind::other; + int version = 0; + std::vector lines; +}; + +// The open documents, safe across the session's threads. +class Store { +public: + void open(const std::string& uri, int version, std::string_view text); + // The whole text (the server asks for full synchronisation). + void change(const std::string& uri, int version, std::string_view text); + void close(const std::string& uri); + std::optional get(const std::string& uri) const; + std::vector all() const; + +private: + mutable std::mutex mutex_; + std::map documents_; +}; + +// What the settings turn on, and in what language. +struct Options { + bool tomlCompletion = true, tomlHover = true, tomlNavigation = true, tomlDiagnostics = true; + manifest::DiagnosticSettings tomlSeverity; + bool indexCompletion = false; + bool bsIntelligence = true, bsDiagnostics = true, bsSnippets = true, bsKnownModules = true; + std::string bsSeverity = "warning"; + bool mcpplsServes = false; // C++ Modules serves build.mcpp + bool trusted = true; + std::string locale = "en"; +}; +Options options_of(const Json& settings, bool trusted, bool mcpplsServes, std::string_view locale); + +// The capabilities `initialize` answers. +Json server_capabilities(); + +// LSP Diagnostic[] for a document. +Json diagnostics(const Document& d, const Options& o); + +// A CompletionList. `catalogue` is mcpp's package catalogue (`mcpp search --format json`) when one is +// at hand; a dependency version asked without one sets `needCatalogue` and answers an incomplete +// list, which the editor asks again. +Json completion(const Document& d, int line, int character, const Options& o, const Json* catalogue, bool& needCatalogue); +Json hover(const Document& d, int line, int character, const Options& o); +// A Location, or null. `path = "../x"` reads that directory's mcpp.toml for its `[package]`. +Json definition(const Document& d, int line, int character, const Options& o); +Json symbols(const Document& d); + +} // namespace mcppide::documents + +namespace mcppide::documents { + +Kind kind_of(const std::filesystem::path& path) { + const auto name = path.filename().string(); + if (name == "mcpp.toml") return Kind::manifest; + if (name == "build.mcpp") return Kind::buildscript; + return Kind::other; +} + +std::filesystem::path path_of(std::string_view uri) { + if (!uri.starts_with("file://")) return {}; + auto rest = uri.substr(7); + // `file://host/share` is a UNC path; `file:///x` has an empty authority. + std::string authority; + if (!rest.starts_with("/")) { + const auto slash = rest.find('/'); + authority = std::string(rest.substr(0, slash)); + rest = slash == std::string_view::npos ? std::string_view() : rest.substr(slash); + } + std::string decoded; + for (std::size_t i = 0; i < rest.size(); ++i) { + if (rest[i] == '%' && i + 2 < rest.size()) { + unsigned value = 0; + if (std::from_chars(rest.data() + i + 1, rest.data() + i + 3, value, 16).ec == std::errc{}) { + decoded += static_cast(value); + i += 2; + continue; + } + } + decoded += rest[i]; + } + // `/C:/x` is the drive path `C:/x`. + if (decoded.size() >= 3 && decoded[0] == '/' && std::isalpha(static_cast(decoded[1])) && decoded[2] == ':') + decoded.erase(0, 1); + if (!authority.empty() && authority != "localhost") return std::filesystem::path("//" + authority + decoded); + return std::filesystem::path(decoded); +} + +std::string uri_of(const std::filesystem::path& path) { + auto p = path.generic_string(); + std::string out = "file://"; + if (!p.starts_with("/")) out += '/'; // a drive path + for (const unsigned char c : p) { + if (std::isalnum(c) || c == '/' || c == '-' || c == '_' || c == '.' || c == '~' || c == ':') out += static_cast(c); + else out += std::format("%{:02X}", c); + } + return out; +} + +void Store::open(const std::string& uri, int version, std::string_view text) { + const auto path = path_of(uri); + std::lock_guard lock(mutex_); + documents_[uri] = Document{uri, path, kind_of(path), version, text::lines_of(text)}; +} + +void Store::change(const std::string& uri, int version, std::string_view text) { + std::lock_guard lock(mutex_); + auto it = documents_.find(uri); + if (it == documents_.end()) { + const auto path = path_of(uri); + documents_[uri] = Document{uri, path, kind_of(path), version, text::lines_of(text)}; + return; + } + it->second.version = version; + it->second.lines = text::lines_of(text); +} + +void Store::close(const std::string& uri) { + std::lock_guard lock(mutex_); + documents_.erase(uri); +} + +std::optional Store::get(const std::string& uri) const { + std::lock_guard lock(mutex_); + auto it = documents_.find(uri); + if (it == documents_.end()) return std::nullopt; + return it->second; +} + +std::vector Store::all() const { + std::lock_guard lock(mutex_); + std::vector out; + for (const auto& [_, d] : documents_) out.push_back(d); + return out; +} + +namespace { + +bool flag(const Json& s, std::string_view key, bool fallback) { + auto it = s.find(key); + return it != s.end() && it->is_boolean() ? it->get() : fallback; +} + +std::string word(const Json& s, std::string_view key, std::string_view fallback) { + auto it = s.find(key); + if (it == s.end() || !it->is_string()) return std::string(fallback); + const auto v = it->get(); + return v == "error" || v == "warning" || v == "info" || v == "off" ? v : std::string(fallback); +} + +Json position(int line, int character) { return Json{{"line", line}, {"character", character}}; } +Json range(int line, int start, int end) { return Json{{"start", position(line, start)}, {"end", position(line, end)}}; } + +int severity_number(std::string_view s) { return s == "error" ? 1 : s == "warning" ? 2 : 3; } + +Json markdown(std::string value) { return Json{{"kind", "markdown"}, {"value", std::move(value)}}; } + +// LSP CompletionItemKind. +constexpr int kFolder = 19, kSnippet = 15, kValue = 12, kModule = 9, kFunction = 3; + +} // namespace + +Options options_of(const Json& s, bool trusted, bool mcpplsServes, std::string_view locale) { + Options o; + // mcpp-vscode 0.9's `mcpp.tomlCompletion` is the same switch under its earlier name. + o.tomlCompletion = flag(s, "mcpp.toml.completion", flag(s, "mcpp.tomlCompletion", true)); + o.tomlHover = flag(s, "mcpp.toml.hover", true); + o.tomlNavigation = flag(s, "mcpp.toml.navigation", true); + o.tomlDiagnostics = flag(s, "mcpp.toml.diagnostics.enabled", true); + o.tomlSeverity.syntax = word(s, "mcpp.toml.diagnostics.syntax", "error"); + o.tomlSeverity.unknownSection = word(s, "mcpp.toml.diagnostics.unknownSection", "warning"); + o.tomlSeverity.unknownKey = word(s, "mcpp.toml.diagnostics.unknownKey", "warning"); + o.tomlSeverity.planeSeparation = word(s, "mcpp.toml.diagnostics.planeSeparation", "warning"); + o.tomlSeverity.legacyKeys = word(s, "mcpp.toml.diagnostics.legacyKeys", "info"); + o.indexCompletion = flag(s, "mcpp.toml.indexCompletion", false); + o.bsIntelligence = flag(s, "mcpp.buildScript.intelligence", true); + o.bsDiagnostics = o.bsIntelligence && flag(s, "mcpp.buildScript.diagnostics", true); + o.bsSnippets = o.bsIntelligence && flag(s, "mcpp.buildScript.snippets", true); + o.bsKnownModules = o.bsIntelligence && flag(s, "mcpp.buildScript.imports.knownModules", true); + o.bsSeverity = word(s, "mcpp.buildScript.diagnostics.severity", "warning"); + if (o.bsSeverity == "error") o.bsSeverity = "warning"; + o.mcpplsServes = mcpplsServes; + o.trusted = trusted; + o.locale = std::string(locale); + return o; +} + +Json server_capabilities() { + return Json{ + {"textDocumentSync", Json{{"openClose", true}, {"change", 1}}}, + // `[` starts a header, `"` a version, `:` the `mcpp::` scope, `.` a module name. + {"completionProvider", Json{{"triggerCharacters", Json::array({"[", "\"", ":", "."})}, {"resolveProvider", false}}}, + {"hoverProvider", true}, + {"definitionProvider", true}, + {"documentSymbolProvider", true}, + }; +} + +Json diagnostics(const Document& d, const Options& o) { + Json out = Json::array(); + if (d.kind == Kind::manifest && o.tomlDiagnostics) { + for (const auto& x : manifest::analyse(d.lines, o.tomlSeverity)) { + // The rules count from 1, as `line:column` prints; LSP from 0. + Json j{{"range", range(x.line - 1, x.start - 1, x.end - 1)}, {"severity", severity_number(x.severity)}, + {"code", x.code}, {"source", "mcpp"}, {"message", x.message}}; + if (x.relatedKey) j["data"] = Json{{"relatedKey", *x.relatedKey}}; + out.push_back(std::move(j)); + } + } + if (d.kind == Kind::buildscript && o.bsDiagnostics && o.bsSeverity != "off") { + for (const auto& x : buildscript::analyse(d.lines, o.bsSeverity, !o.mcpplsServes)) + out.push_back(Json{{"range", range(x.line - 1, x.start - 1, x.end - 1)}, {"severity", severity_number(x.severity)}, + {"code", x.code}, {"source", "mcpp"}, {"message", x.message}}); + } + return out; +} + +Json completion(const Document& d, int line, int character, const Options& o, const Json* catalogue, bool& needCatalogue) { + needCatalogue = false; + Json items = Json::array(); + bool incomplete = false; + if (d.kind == Kind::manifest && o.tomlCompletion && line >= 0 && line < static_cast(d.lines.size())) { + // The lines up to the cursor's, as the editor asks. + const std::vector upTo(d.lines.begin(), d.lines.begin() + line + 1); + auto suggestions = manifest::completions(upTo, line, character, o.locale); + if (suggestions.empty() && o.indexCompletion && o.trusted) + if (const auto at = manifest::dependency_version_context(upTo, line, character)) { + if (catalogue) suggestions = manifest::version_suggestions(*at, manifest::catalogue_versions(*catalogue, at->name)); + else needCatalogue = incomplete = true; + } + for (const auto& s : suggestions) { + Json item{{"label", s.label}, {"kind", s.kind == "section" ? kFolder : s.kind == "version" ? kValue : kSnippet}, + {"detail", s.detail}, {"insertTextFormat", 2}, + {"textEdit", Json{{"range", range(line, s.start, s.end)}, {"newText", s.insertSnippet.value_or(s.label)}}}}; + if (s.documentation) item["documentation"] = markdown(*s.documentation); + // Versions keep their order, newest first. + if (s.kind == "version") item["sortText"] = std::format("{:05}", items.size()); + items.push_back(std::move(item)); + } + } + if (d.kind == Kind::buildscript && o.bsIntelligence && line >= 0 && line < static_cast(d.lines.size())) { + buildscript::Switches sw{o.bsSnippets, o.bsKnownModules, o.mcpplsServes}; + if (const auto found = buildscript::completion(d.lines[line], character, sw)) + for (const auto& i : *found) { + Json item{{"label", i.label}, {"kind", i.kind == "module" ? kModule : i.kind == "function" ? kFunction : kSnippet}, + {"detail", i.detail}}; + if (i.documentation) item["documentation"] = markdown(*i.documentation); + if (i.filterText) item["filterText"] = *i.filterText; + if (i.insertText) { + item["insertText"] = *i.insertText; + item["insertTextFormat"] = 2; + } + items.push_back(std::move(item)); + } + } + return Json{{"isIncomplete", incomplete}, {"items", items}}; +} + +Json hover(const Document& d, int line, int character, const Options& o) { + if (d.kind == Kind::manifest && o.tomlHover) { + const auto info = manifest::hover_at(d.lines, line, character); + if (!info) return nullptr; + auto value = std::format("**{}**\n\n{}", info->title, info->body); + if (info->documentation) + value += std::format("\n\n[{}]({})", i18n::translate("Manifest reference", o.locale), *info->documentation); + return Json{{"contents", markdown(std::move(value))}}; + } + if (d.kind == Kind::buildscript && o.bsIntelligence) { + const auto text = buildscript::hover(d.lines, line, character, o.mcpplsServes); + if (!text) return nullptr; + return Json{{"contents", markdown(*text)}}; + } + return nullptr; +} + +Json definition(const Document& d, int line, int character, const Options& o) { + if (d.kind != Kind::manifest || !o.tomlNavigation) return nullptr; + std::filesystem::path target; + // The line of `[package]` in the manifest of the directory `relative` names. + auto resolve = [&](std::string_view relative) -> std::optional { + const auto file = (d.path.parent_path() / std::filesystem::path(std::string(relative)) / "mcpp.toml").lexically_normal(); + std::ifstream in(file, std::ios::binary); + if (!in) return std::nullopt; + const auto lines = text::lines_of(std::string(std::istreambuf_iterator(in), {})); + for (std::size_t i = 0; i < lines.size(); ++i) { + const auto t = text::trim(lines[i]); + if (t.size() >= 2 && t.front() == '[' && t.back() == ']' && text::trim(t.substr(1, t.size() - 2)) == "package") { + target = file; + return static_cast(i); + } + } + return std::nullopt; + }; + const auto at = manifest::definition_at(d.lines, line, character, resolve); + if (!at) return nullptr; + return Json{{"uri", target.empty() ? d.uri : uri_of(target)}, {"range", range(at->line, at->start, at->end)}}; +} + +Json symbols(const Document& d) { + if (d.kind != Kind::manifest) return Json::array(); + return manifest::document_symbols(d.lines); +} + +} // namespace mcppide::documents diff --git a/src/rpc.cppm b/src/rpc.cppm index 03f9af5..04f9214 100644 --- a/src/rpc.cppm +++ b/src/rpc.cppm @@ -69,6 +69,9 @@ public: explicit Server(std::function write = write_stdout); void on(std::string method, Handler handler); + // A request answered on the reading thread, before the next message is read: one whose answer + // must see the documents as the messages before it left them (completion, hover), and is quick. + void on_inline(std::string method, Handler handler); void on_notification(std::string method, NotificationHandler handler); // Sends a notification to the adapter. void notify(std::string_view method, Json params); @@ -87,6 +90,7 @@ private: void reply(const Json& id, std::expected result); std::function write_; std::map> handlers_; + std::set> inline_; std::map> notifications_; std::mutex inflightMutex_; std::map>> inflight_; @@ -146,6 +150,11 @@ Server::Server(std::function write) : write_(std::move(w void Server::on(std::string method, Handler handler) { handlers_[std::move(method)] = std::move(handler); } +void Server::on_inline(std::string method, Handler handler) { + inline_.insert(method); + handlers_[std::move(method)] = std::move(handler); +} + void Server::on_notification(std::string method, NotificationHandler handler) { notifications_[std::move(method)] = std::move(handler); } @@ -217,7 +226,7 @@ void Server::dispatch(std::string_view body) { }; // `initialize` answers before anything else is read: the adapter waits // for it, and the capabilities it carries decide what the adapter sends. - if (method == "initialize") { run(); return; } + if (method == "initialize" || inline_.contains(method)) { run(); return; } auto done = std::make_shared>(false); std::lock_guard lock(workersMutex_); // Finished workers are joined here, so the list holds the requests in diff --git a/src/server.cppm b/src/server.cppm index 45de264..850ce7e 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -22,7 +22,7 @@ import mcppide.views; import mcppide.policy; import mcppide.settings; import mcppide.i18n; -import mcppide.manifest; +import mcppide.documents; import mcppide.spec; import mcppide.languageservice; import mcppide.scan; @@ -34,27 +34,39 @@ export namespace mcppide::server { using Json = nlohmann::json; inline constexpr std::string_view kVersion = "0.2.0"; -inline constexpr int kProtocol = 1; +inline constexpr int kProtocol = 2; // The methods `install` registers, for the protocol schema's test: a method -// here and not in protocol/protocol.schema.json (or the other way) fails it. +// here and not in protocol/protocol.schema.json (or the other way) fails it. The session is an LSP +// server (protocol 2, design 2026-10-30 part 2 §8): LSP's own methods, and mcpp-ide's under +// `mcpp-ide/`. inline constexpr std::array kMethods = { - "initialize", "settings/update", "settings/registry", "settings/preset", "i18n/bundle", - "selfCheck", "commands/list", "menu/quick", "languageService/status", - "projects/list", "projects/forFile", "project/summary", "project/new", "result/last", - "job/start", "job/cancel", "job/list", "run/launch", "debug/configuration", "test/list", - "library/search", "library/indexes", "library/add", "library/updateIndex", - "toolchains/list", "toolchains/install", "toolchains/setDefault", "targets/list", - "cache/inventory", "cache/plan", "cache/apply", "graph/packages", "graph/modules", - "pack/formats", "pack/start", "manifest/check", "toml/complete", "toml/hover", "toml/symbols", - "buildscript/complete", "mcpp/exec", "project/files", "languageService/exec", "project/mcpp", "project/refresh", - "capabilities/explain", "report/collect", "source/entries", "test/cases", "test/runCases", "shutdown", "exit"}; + "initialize", "shutdown", "exit", "textDocument/completion", "textDocument/hover", "textDocument/definition", + "textDocument/documentSymbol", "mcpp-ide/settings/update", "mcpp-ide/settings/registry", + "mcpp-ide/settings/preset", "mcpp-ide/i18n/bundle", "mcpp-ide/selfCheck", "mcpp-ide/commands/list", + "mcpp-ide/menu/quick", "mcpp-ide/languageService/status", "mcpp-ide/projects/list", "mcpp-ide/projects/forFile", + "mcpp-ide/project/summary", "mcpp-ide/project/new", "mcpp-ide/result/last", "mcpp-ide/job/start", + "mcpp-ide/job/cancel", "mcpp-ide/job/list", "mcpp-ide/run/launch", "mcpp-ide/debug/configuration", + "mcpp-ide/test/list", "mcpp-ide/library/search", "mcpp-ide/library/indexes", "mcpp-ide/library/add", + "mcpp-ide/library/updateIndex", "mcpp-ide/toolchains/list", "mcpp-ide/toolchains/install", + "mcpp-ide/toolchains/setDefault", "mcpp-ide/targets/list", "mcpp-ide/cache/inventory", "mcpp-ide/cache/plan", + "mcpp-ide/cache/apply", "mcpp-ide/graph/packages", "mcpp-ide/graph/modules", "mcpp-ide/pack/formats", + "mcpp-ide/pack/start", "mcpp-ide/manifest/check", "mcpp-ide/mcpp/exec", "mcpp-ide/project/files", + "mcpp-ide/languageService/exec", "mcpp-ide/project/mcpp", "mcpp-ide/project/refresh", + "mcpp-ide/capabilities/explain", "mcpp-ide/report/collect", "mcpp-ide/source/entries", "mcpp-ide/test/cases", + "mcpp-ide/test/runCases"}; + +// The notifications the backend reads. +inline constexpr std::array kReceived = { + "initialized", "textDocument/didOpen", "textDocument/didChange", "textDocument/didClose", "$/cancelRequest"}; // The notifications the backend sends. -inline constexpr std::array kNotifications = { - "job/started", "job/output", "job/plan", "job/progress", "job/diagnostic", "job/artifact", "job/record", - "job/finished", "test/result", "test/summary", "result/changed", "manifest/changed", - "manifest/diagnostics", "project/mcppChanged", "languageService/changed", "test/caseResult"}; +inline constexpr std::array kNotifications = { + "textDocument/publishDiagnostics", "window/showMessage", "mcpp-ide/job/started", "mcpp-ide/job/output", + "mcpp-ide/job/plan", "mcpp-ide/job/progress", "mcpp-ide/job/diagnostic", "mcpp-ide/job/artifact", + "mcpp-ide/job/record", "mcpp-ide/job/finished", "mcpp-ide/test/result", "mcpp-ide/test/summary", + "mcpp-ide/result/changed", "mcpp-ide/manifest/changed", "mcpp-ide/manifest/diagnostics", + "mcpp-ide/project/mcppChanged", "mcpp-ide/languageService/changed", "mcpp-ide/test/caseResult"}; class Session { public: @@ -99,6 +111,14 @@ private: Result project_refresh(const Json& p); Result capabilities_explain(const Json& p); Result report_collect(const Json& p); + // The rules layer's switches for the documents, from the settings. + documents::Options document_options() const; + // A document's diagnostics, to the editor. + void publish(const documents::Document& d); + // mcpp's catalogue for version completion, when it was read; asked in the background otherwise. + std::optional catalogue(const std::filesystem::path& cwd); + // LSP's document methods and notifications. + void install_documents(); // THE PROJECT'S mcpp (design 2026-10-30 part 2 §4.2): asked in the project, once, and asked again // when its manifest changes, when the editor regains focus, or when a job's envelope names @@ -137,6 +157,13 @@ private: std::string clientName_ = "mcpp-ide"; std::string editor_ = "vscode"; std::vector mcpplsCandidates_; + // Whether C++ Modules serves build.mcpp in the editor (it then answers what it answers better). + bool mcpplsServes_ = false; + // The documents the editor keeps open (mcpp.toml, build.mcpp), and the catalogue a dependency + // version is completed from: asked once in the background, the editor told once that it was. + documents::Store documents_; + bool catalogueAsked_ = false; + bool catalogueNoticed_ = false; // mcppls's actions as its last report named them (null until one was asked), and what the editors // were last told about it, so that a change is sent once. Json mcpplsActions_; @@ -191,7 +218,7 @@ std::vector strings_of(const Json& v) { Session::Session(rpc::Server& server) : rpc_(server) { jobs_ = std::make_unique(mcpp::Client{}, jobs::Sink{ .narration = [this](std::string_view id, std::string_view text) { - rpc_.notify("job/output", Json{{"jobId", id}, {"text", text}}); + rpc_.notify("mcpp-ide/job/output", Json{{"jobId", id}, {"text", text}}); }, .record = [this](std::string_view id, const Json& record) { on_record(id, record); }, .finished = [this](std::string_view id, const Json& summary) { @@ -214,7 +241,7 @@ Session::Session(rpc::Server& server) : rpc_(server) { jobOf_.erase(it); } } - rpc_.notify("job/finished", s); + rpc_.notify("mcpp-ide/job/finished", s); // Every envelope names the mcpp that wrote it: another version than the one probed (a // `xlings use`, a pin changed) is asked again, and the editor is told (§4.2). const auto& env = summary.contains("envelope") ? summary["envelope"] : Json(nullptr); @@ -228,7 +255,7 @@ Session::Session(rpc::Server& server) : rpc_(server) { changed = true; } } - if (changed) rpc_.notify("project/mcppChanged", Json{{"root", root.generic_string()}, {"version", seen}}); + if (changed) rpc_.notify("mcpp-ide/project/mcppChanged", Json{{"root", root.generic_string()}, {"version", seen}}); } }, .started = [this](std::string_view id, const Json& started) { @@ -238,7 +265,7 @@ Session::Session(rpc::Server& server) : rpc_(server) { } Json s = started; s["jobId"] = id; - rpc_.notify("job/started", s); + rpc_.notify("mcpp-ide/job/started", s); }, }); watcher_ = std::make_unique( @@ -332,13 +359,13 @@ void Session::on_changed(const std::filesystem::path& root, std::string_view wha if (what != "result") probes_.erase(root); // a manifest may state another mcpp floor or pin } if (what == "result") { - rpc_.notify("result/changed", Json{{"root", root.generic_string()}, {"result", detail}}); + rpc_.notify("mcpp-ide/result/changed", Json{{"root", root.generic_string()}, {"result", detail}}); return; } - rpc_.notify("manifest/changed", Json{{"root", root.generic_string()}}); + rpc_.notify("mcpp-ide/manifest/changed", Json{{"root", root.generic_string()}}); // The manifest's problems, from mcpp, located (mcpp docs/50 §2). auto checked = manifest_check(Json{{"root", root.generic_string()}}); - if (checked) rpc_.notify("manifest/diagnostics", *checked); + if (checked) rpc_.notify("mcpp-ide/manifest/diagnostics", *checked); } // A record of a job's stream becomes the notification its meaning names: the plan (with the @@ -359,20 +386,20 @@ void Session::on_record(std::string_view jobId, const Json& record) { std::lock_guard lock(mutex_); plans_[std::filesystem::path(root)] = n; } - rpc_.notify("job/plan", n); + rpc_.notify("mcpp-ide/job/plan", n); } else if (type == "progress") { - rpc_.notify("job/progress", n); + rpc_.notify("mcpp-ide/job/progress", n); } else if (type == "diagnostic") { - rpc_.notify("job/diagnostic", n); + rpc_.notify("mcpp-ide/job/diagnostic", n); } else if (type == "artifact") { - rpc_.notify("job/artifact", n); + rpc_.notify("mcpp-ide/job/artifact", n); } else if (type == "test") { - rpc_.notify("test/result", n); + rpc_.notify("mcpp-ide/test/result", n); } else if (type == "summary") { - rpc_.notify("test/summary", n); + rpc_.notify("mcpp-ide/test/summary", n); } else if (!(record.is_object() && record.contains("schemaVersion") && record.value("kind", "") != "mcpp.event")) { // The envelope that closes a stream reaches the adapter in `job/finished`. - rpc_.notify("job/record", n); + rpc_.notify("mcpp-ide/job/record", n); } } @@ -523,24 +550,34 @@ Json Session::self_check() { } std::expected Session::initialize(const Json& p) { + // LSP's `initialize`: the client and the workspace in LSP's fields, what mcpp-ide asks in + // `initializationOptions` (the editor, trust, the settings, mcppls's candidates). + const Json options = p.contains("initializationOptions") && p["initializationOptions"].is_object() ? p["initializationOptions"] + : Json::object(); { std::lock_guard lock(mutex_); - if (p.contains("client") && p["client"].is_object()) { - const auto name = p["client"].value("name", "editor"); - const auto version = p["client"].value("version", ""); + if (p.contains("clientInfo") && p["clientInfo"].is_object()) { + const auto name = p["clientInfo"].value("name", "editor"); + const auto version = p["clientInfo"].value("version", ""); clientName_ = std::format("mcpp-ide/{} ({}{}{})", kVersion, name, version.empty() ? "" : " ", version); } - editor_ = p.value("editor", "vscode"); + editor_ = options.value("editor", "vscode"); { std::lock_guard localeLock(localeMutex_); - locale_ = i18n::normalize_locale(p.value("locale", "en")); + locale_ = i18n::normalize_locale(options.value("locale", p.value("locale", "en"))); } - trusted_ = p.value("trusted", true); - settings_ = settings::resolve(p.contains("settings") ? p["settings"] : Json::object())["values"]; + trusted_ = options.value("trusted", true); + mcpplsServes_ = options.value("mcpplsServes", false); + settings_ = settings::resolve(options.contains("settings") ? options["settings"] : Json::object())["values"]; folders_.clear(); - for (const auto& f : strings_of(p.value("workspaceFolders", Json::array()))) folders_.emplace_back(f); - if (p.contains("languageService") && p["languageService"].is_object()) - mcpplsCandidates_ = strings_of(p["languageService"].value("candidates", Json::array())); + if (p.contains("workspaceFolders") && p["workspaceFolders"].is_array()) + for (const auto& f : p["workspaceFolders"]) + if (f.is_object() && f.contains("uri") && f["uri"].is_string()) + if (auto path = documents::path_of(f["uri"].get()); !path.empty()) folders_.push_back(path); + if (folders_.empty() && p.contains("rootUri") && p["rootUri"].is_string()) + if (auto path = documents::path_of(p["rootUri"].get()); !path.empty()) folders_.push_back(path); + if (options.contains("languageService") && options["languageService"].is_object()) + mcpplsCandidates_ = strings_of(options["languageService"].value("candidates", Json::array())); } locate_mcpp(); Json found = Json::array(); @@ -551,15 +588,16 @@ std::expected Session::initialize(const Json& p) { } rpc_.set_initialized(true); auto check = self_check(); - return Json{{"server", Json{{"name", "mcpp-ide"}, {"version", kVersion}}}, + return Json{{"capabilities", documents::server_capabilities()}, + {"serverInfo", Json{{"name", "mcpp-ide"}, {"version", kVersion}}}, {"protocol", kProtocol}, {"mcpp", check["mcpp"]}, {"languageService", check["languageService"]}, {"checks", check["checks"]}, {"projects", found}, - {"capabilities", Json::array({"jobs", "results", "launch", "debug", "tests", "library", "toolchains", - "targets", "platforms", "cache", "graphs", "pack", "settings", "i18n", - "commands", "manifest", "toml", "buildscript", "languageService"})}}; + {"features", Json::array({"jobs", "results", "launch", "debug", "tests", "library", "toolchains", + "targets", "platforms", "cache", "graphs", "pack", "settings", "i18n", + "commands", "manifest", "toml", "buildscript", "languageService"})}}; } std::expected Session::update_settings(const Json& p) { @@ -570,12 +608,15 @@ std::expected Session::update_settings(const Json& p) { before = settings_.value("mcpp.path", ""); settings_ = resolved["values"]; if (p.contains("trusted")) trusted_ = p["trusted"].get(); + if (p.contains("mcpplsServes") && p["mcpplsServes"].is_boolean()) mcpplsServes_ = p["mcpplsServes"].get(); if (p.contains("locale")) { std::lock_guard localeLock(localeMutex_); locale_ = i18n::normalize_locale(p["locale"].get()); } } if (before != resolved["values"].value("mcpp.path", "")) locate_mcpp(); + // A switch or a severity may have changed what each open document earns. + for (const auto& d : documents_.all()) publish(d); return resolved; } @@ -962,7 +1003,7 @@ std::expected Session::language_service(const Json& p) { mcpplsSeen_ = seen; mcpplsActions_ = report.actions; } - if (changed) rpc_.notify("languageService/changed", Json{{"root", out["root"]}, {"state", out["state"]}, + if (changed) rpc_.notify("mcpp-ide/languageService/changed", Json{{"root", out["root"]}, {"state", out["state"]}, {"workspaceState", report.workspace.is_object() ? report.workspace.value("state", "none") : "none"}}); return out; } @@ -1082,7 +1123,7 @@ std::expected Session::capabilities_explain(const Json& p) { static constexpr Gate kGates[] = { {"targets.sets", "target-sets", "2026.10.30.1"}, {"pack.formatList", "pkg-format-list", "2026.10.30.1"}, - {"tests.debug", "test-launch", "2026.11.1.1"}, + {"tests.debug", "test-launch", "2026.10.31.2"}, }; Json features = Json::array(); for (const auto& g : kGates) { @@ -1442,7 +1483,7 @@ std::expected Session::test_run_cases(const Json& p) { in.close(); if (fw) for (const auto& o : testing::read_report(*fw, text)) - rpc_.notify("test/caseResult", Json{{"jobId", jobId}, {"root", root.generic_string()}, {"test", test}, + rpc_.notify("mcpp-ide/test/caseResult", Json{{"jobId", jobId}, {"root", root.generic_string()}, {"test", test}, {"case", o.name}, {"status", o.status}, {"durationMs", o.durationMs}, {"message", o.message}}); std::error_code ignored; @@ -1469,20 +1510,122 @@ Session::PackOptions Session::pack_options(const std::filesystem::path& cwd) { return o; } +documents::Options Session::document_options() const { + std::lock_guard lock(mutex_); + std::string locale; + { + std::lock_guard localeLock(localeMutex_); + locale = locale_; + } + return documents::options_of(settings_, trusted_, mcpplsServes_, locale); +} + +void Session::publish(const documents::Document& d) { + if (d.kind == documents::Kind::other) return; + rpc_.notify("textDocument/publishDiagnostics", + Json{{"uri", d.uri}, {"version", d.version}, {"diagnostics", documents::diagnostics(d, document_options())}}); +} + +std::optional Session::catalogue(const std::filesystem::path& cwd) { + { + std::lock_guard lock(mutex_); + if (!searchCache_.is_null() && std::chrono::steady_clock::now() - searchAt_ < std::chrono::minutes(10)) return searchCache_; + if (catalogueAsked_) return std::nullopt; + catalogueAsked_ = true; + if (!catalogueNoticed_) { + catalogueNoticed_ = true; + rpc_.notify("window/showMessage", + Json{{"type", 3}, {"message", t("Completing dependency versions runs `mcpp search`, which may use the network.")}}); + } + } + std::thread([this, cwd] { + auto r = query(cwd, {"search", "--format", "json"}); + std::lock_guard lock(mutex_); + catalogueAsked_ = false; + if (r && r->envelope.is_object() && r->envelope.value("kind", "") == "mcpp.search") { + searchCache_ = r->envelope; + searchAt_ = std::chrono::steady_clock::now(); + } + }).detach(); + return std::nullopt; +} + +void Session::install_documents() { + auto& s = rpc_; + s.on_notification("initialized", [](const Json&) {}); + s.on_notification("textDocument/didOpen", [this](const Json& p) { + const auto& d = p.value("textDocument", Json::object()); + const auto uri = d.value("uri", ""); + documents_.open(uri, d.value("version", 0), d.value("text", "")); + if (auto doc = documents_.get(uri)) publish(*doc); + }); + s.on_notification("textDocument/didChange", [this](const Json& p) { + const auto& d = p.value("textDocument", Json::object()); + const auto uri = d.value("uri", ""); + const auto changes = p.value("contentChanges", Json::array()); + // Full synchronisation: the last change is the whole text. + if (changes.empty() || !changes.back().contains("text")) return; + documents_.change(uri, d.value("version", 0), changes.back()["text"].get()); + if (auto doc = documents_.get(uri)) publish(*doc); + }); + s.on_notification("textDocument/didClose", [this](const Json& p) { + const auto uri = p.value("textDocument", Json::object()).value("uri", ""); + documents_.close(uri); + rpc_.notify("textDocument/publishDiagnostics", Json{{"uri", uri}, {"diagnostics", Json::array()}}); + }); + // Answered on the reading thread: each sees the documents as the notifications before it left them. + auto at = [this](const Json& p) -> std::optional> { + auto doc = documents_.get(p.value("textDocument", Json::object()).value("uri", "")); + if (!doc) return std::nullopt; + const auto& pos = p.value("position", Json::object()); + return std::tuple{*doc, pos.value("line", 0), pos.value("character", 0)}; + }; + s.on_inline("textDocument/completion", [this, at](const Json& p, const rpc::Context&) -> Result { + auto a = at(p); + if (!a) return Json(nullptr); + const auto& [doc, line, character] = *a; + const auto options = document_options(); + bool need = false; + auto list = documents::completion(doc, line, character, options, nullptr, need); + if (need) { + if (auto c = catalogue(doc.path.parent_path())) list = documents::completion(doc, line, character, options, &*c, need); + } + return list; + }); + s.on_inline("textDocument/hover", [this, at](const Json& p, const rpc::Context&) -> Result { + auto a = at(p); + if (!a) return Json(nullptr); + const auto& [doc, line, character] = *a; + return documents::hover(doc, line, character, document_options()); + }); + s.on_inline("textDocument/definition", [this, at](const Json& p, const rpc::Context&) -> Result { + auto a = at(p); + if (!a) return Json(nullptr); + const auto& [doc, line, character] = *a; + return documents::definition(doc, line, character, document_options()); + }); + s.on_inline("textDocument/documentSymbol", [this](const Json& p, const rpc::Context&) -> Result { + auto doc = documents_.get(p.value("textDocument", Json::object()).value("uri", "")); + if (!doc) return Json(nullptr); + return documents::symbols(*doc); + }); +} + void Session::install() { auto& s = rpc_; + install_documents(); s.on("initialize", [this](const Json& p, const rpc::Context&) { return initialize(p); }); - s.on("settings/update", [this](const Json& p, const rpc::Context&) { return update_settings(p); }); - s.on("settings/registry", [this](const Json&, const rpc::Context&) -> Result { + s.on("mcpp-ide/settings/update", [this](const Json& p, const rpc::Context&) { return update_settings(p); }); + s.on("mcpp-ide/settings/registry", [this](const Json&, const rpc::Context&) -> Result { std::lock_guard lock(localeMutex_); return settings::registry(locale_); }); - s.on("settings/preset", [](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/settings/preset", [](const Json& p, const rpc::Context&) -> Result { auto preset = settings::preset(p.value("id", "")); if (!preset) return std::unexpected(invalid("no such preset")); return *preset; }); - s.on("i18n/bundle", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/i18n/bundle", [this](const Json& p, const rpc::Context&) -> Result { std::string locale; { std::lock_guard lock(localeMutex_); @@ -1490,11 +1633,11 @@ void Session::install() { } return i18n::bundle(p.value("locale", locale)); }); - s.on("selfCheck", [this](const Json&, const rpc::Context&) -> Result { locate_mcpp(); return self_check(); }); - s.on("commands/list", [this](const Json& p, const rpc::Context&) { return commands(p); }); - s.on("menu/quick", [this](const Json& p, const rpc::Context&) { return quick_menu(p); }); - s.on("languageService/status", [this](const Json& p, const rpc::Context&) { return language_service(p); }); - s.on("projects/list", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/selfCheck", [this](const Json&, const rpc::Context&) -> Result { locate_mcpp(); return self_check(); }); + s.on("mcpp-ide/commands/list", [this](const Json& p, const rpc::Context&) { return commands(p); }); + s.on("mcpp-ide/menu/quick", [this](const Json& p, const rpc::Context&) { return quick_menu(p); }); + s.on("mcpp-ide/languageService/status", [this](const Json& p, const rpc::Context&) { return language_service(p); }); + s.on("mcpp-ide/projects/list", [this](const Json& p, const rpc::Context&) -> Result { Json out = Json::array(); std::vector folders; { @@ -1509,63 +1652,63 @@ void Session::install() { } return out; }); - s.on("projects/forFile", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/projects/forFile", [this](const Json& p, const rpc::Context&) -> Result { auto proj = projects::project_for(p.value("path", ""), p.value("boundary", "")); if (!proj) return Json(nullptr); watcher_->watch(proj->root); return project_json(*proj); }); - s.on("project/summary", [this](const Json& p, const rpc::Context&) { return project_summary(p); }); - s.on("mcpp/exec", [this](const Json& p, const rpc::Context&) { return mcpp_exec(p); }); - s.on("languageService/exec", [this](const Json& p, const rpc::Context&) { return language_service_exec(p); }); - s.on("project/files", [](const Json& p, const rpc::Context&) -> Result { return projects::files(root_of(p)); }); - s.on("project/new", [this](const Json& p, const rpc::Context&) { return project_new(p); }); - s.on("result/last", [](const Json& p, const rpc::Context&) -> Result { return projects::last_result(root_of(p)); }); - s.on("job/start", [this](const Json& p, const rpc::Context&) { return job_start(p); }); - s.on("job/cancel", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/project/summary", [this](const Json& p, const rpc::Context&) { return project_summary(p); }); + s.on("mcpp-ide/mcpp/exec", [this](const Json& p, const rpc::Context&) { return mcpp_exec(p); }); + s.on("mcpp-ide/languageService/exec", [this](const Json& p, const rpc::Context&) { return language_service_exec(p); }); + s.on("mcpp-ide/project/files", [](const Json& p, const rpc::Context&) -> Result { return projects::files(root_of(p)); }); + s.on("mcpp-ide/project/new", [this](const Json& p, const rpc::Context&) { return project_new(p); }); + s.on("mcpp-ide/result/last", [](const Json& p, const rpc::Context&) -> Result { return projects::last_result(root_of(p)); }); + s.on("mcpp-ide/job/start", [this](const Json& p, const rpc::Context&) { return job_start(p); }); + s.on("mcpp-ide/job/cancel", [this](const Json& p, const rpc::Context&) -> Result { return Json{{"cancelled", jobs_->cancel(p.value("jobId", ""))}}; }); - s.on("job/list", [this](const Json&, const rpc::Context&) -> Result { return jobs_->list(); }); - s.on("run/launch", [this](const Json& p, const rpc::Context& c) { return run_launch(p, c); }); - s.on("debug/configuration", [this](const Json& p, const rpc::Context& c) { return debug_configuration(p, c); }); - s.on("test/list", [this](const Json& p, const rpc::Context&) { return test_list(p); }); - s.on("source/entries", [this](const Json& p, const rpc::Context&) { return source_entries(p); }); - s.on("test/cases", [this](const Json& p, const rpc::Context&) { return test_cases(p); }); - s.on("test/runCases", [this](const Json& p, const rpc::Context&) { return test_run_cases(p); }); - s.on("project/mcpp", [this](const Json& p, const rpc::Context&) { return project_mcpp(p); }); - s.on("project/refresh", [this](const Json& p, const rpc::Context&) { return project_refresh(p); }); - s.on("capabilities/explain", [this](const Json& p, const rpc::Context&) { return capabilities_explain(p); }); - s.on("report/collect", [this](const Json& p, const rpc::Context&) { return report_collect(p); }); - s.on("library/search", [this](const Json& p, const rpc::Context&) { return library_search(p); }); - s.on("library/indexes", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/job/list", [this](const Json&, const rpc::Context&) -> Result { return jobs_->list(); }); + s.on("mcpp-ide/run/launch", [this](const Json& p, const rpc::Context& c) { return run_launch(p, c); }); + s.on("mcpp-ide/debug/configuration", [this](const Json& p, const rpc::Context& c) { return debug_configuration(p, c); }); + s.on("mcpp-ide/test/list", [this](const Json& p, const rpc::Context&) { return test_list(p); }); + s.on("mcpp-ide/source/entries", [this](const Json& p, const rpc::Context&) { return source_entries(p); }); + s.on("mcpp-ide/test/cases", [this](const Json& p, const rpc::Context&) { return test_cases(p); }); + s.on("mcpp-ide/test/runCases", [this](const Json& p, const rpc::Context&) { return test_run_cases(p); }); + s.on("mcpp-ide/project/mcpp", [this](const Json& p, const rpc::Context&) { return project_mcpp(p); }); + s.on("mcpp-ide/project/refresh", [this](const Json& p, const rpc::Context&) { return project_refresh(p); }); + s.on("mcpp-ide/capabilities/explain", [this](const Json& p, const rpc::Context&) { return capabilities_explain(p); }); + s.on("mcpp-ide/report/collect", [this](const Json& p, const rpc::Context&) { return report_collect(p); }); + s.on("mcpp-ide/library/search", [this](const Json& p, const rpc::Context&) { return library_search(p); }); + s.on("mcpp-ide/library/indexes", [this](const Json& p, const rpc::Context&) -> Result { auto r = query(any_root(p), {"index", "list", "--format", "json"}); if (!r) return std::unexpected(r.error()); return r->envelope.is_object() ? r->envelope["data"] : Json(nullptr); }); - s.on("library/add", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/library/add", [this](const Json& p, const rpc::Context&) -> Result { auto dep = p.value("dependency", ""); if (p.contains("version") && p["version"].is_string() && !p["version"].get().empty()) dep += "@" + p["version"].get(); return job_start(Json{{"kind", "add"}, {"root", p.value("root", "")}, {"dependency", dep}}); }); - s.on("library/updateIndex", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/library/updateIndex", [this](const Json& p, const rpc::Context&) -> Result { { std::lock_guard lock(mutex_); searchCache_ = nullptr; } return job_start(Json{{"kind", "command"}, {"root", any_root(p).generic_string()}, {"args", Json::array({"index", "update"})}}); }); - s.on("toolchains/list", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/toolchains/list", [this](const Json& p, const rpc::Context&) -> Result { auto r = query(any_root(p), {"toolchain", "list", "--format", "json"}); if (!r) return std::unexpected(r.error()); return views::toolchains(r->envelope); }); - s.on("toolchains/install", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/toolchains/install", [this](const Json& p, const rpc::Context&) -> Result { Json job{{"kind", "toolchain"}, {"root", any_root(p).generic_string()}, {"spec", p.value("spec", "")}}; if (p.contains("target")) job["target"] = p["target"]; return job_start(job); }); - s.on("toolchains/setDefault", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/toolchains/setDefault", [this](const Json& p, const rpc::Context&) -> Result { auto spec = p.value("spec", ""); if (spec.empty()) return std::unexpected(invalid("toolchains/setDefault names a spec")); auto r = query(any_root(p), {"toolchain", "default", spec}); @@ -1573,8 +1716,8 @@ void Session::install() { if (r->exitCode != 0) return std::unexpected(rpc::Error{rpc::REQUEST_FAILED, r->narration}); return Json{{"spec", spec}}; }); - s.on("targets/list", [this](const Json& p, const rpc::Context&) { return targets(p); }); - s.on("cache/inventory", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/targets/list", [this](const Json& p, const rpc::Context&) { return targets(p); }); + s.on("mcpp-ide/cache/inventory", [this](const Json& p, const rpc::Context&) -> Result { auto r = query(any_root(p), {"cache", "list", "--format", "json"}); if (!r) return std::unexpected(r.error()); views::CacheOptions o; @@ -1582,11 +1725,11 @@ void Session::install() { o.topN = p.value("topN", topN.is_number() ? topN.get() : 5); return views::cache_inventory(r->envelope, o); }); - s.on("cache/plan", [this](const Json& p, const rpc::Context&) { return cache_plan(p); }); - s.on("cache/apply", [this](const Json& p, const rpc::Context&) { return cache_apply(p); }); - s.on("graph/packages", [this](const Json& p, const rpc::Context&) { return graph(p, false); }); - s.on("graph/modules", [this](const Json& p, const rpc::Context&) { return graph(p, true); }); - s.on("pack/formats", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/cache/plan", [this](const Json& p, const rpc::Context&) { return cache_plan(p); }); + s.on("mcpp-ide/cache/apply", [this](const Json& p, const rpc::Context&) { return cache_apply(p); }); + s.on("mcpp-ide/graph/packages", [this](const Json& p, const rpc::Context&) { return graph(p, false); }); + s.on("mcpp-ide/graph/modules", [this](const Json& p, const rpc::Context&) { return graph(p, true); }); + s.on("mcpp-ide/pack/formats", [this](const Json& p, const rpc::Context&) -> Result { auto r = query(any_root(p), {"pack", "--list-formats"}); if (!r) return std::unexpected(r.error()); Json out = r->envelope.is_object() && r->envelope.contains("data") ? r->envelope["data"] : Json::object(); @@ -1597,12 +1740,12 @@ void Session::install() { out["linkages"] = options.linkage ? Json::array({"static", "shared"}) : Json::array(); return out; }); - s.on("pack/start", [this](const Json& p, const rpc::Context&) -> Result { + s.on("mcpp-ide/pack/start", [this](const Json& p, const rpc::Context&) -> Result { Json job = p; job["kind"] = "pack"; return job_start(job); }); - s.on("manifest/check", [this](const Json& p, const rpc::Context&) { return manifest_check(p); }); + s.on("mcpp-ide/manifest/check", [this](const Json& p, const rpc::Context&) { return manifest_check(p); }); } } // namespace mcppide::server diff --git a/tests/rules/golden/brackets.toml.json b/tests/rules/golden/brackets.toml.json index 9d288cf..4f3a888 100644 --- a/tests/rules/golden/brackets.toml.json +++ b/tests/rules/golden/brackets.toml.json @@ -1 +1 @@ -{"diagnostics":{"default":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [","severity":"error","line":3,"startCharacter":1,"endCharacter":2}],"quiet":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [","severity":"info","line":3,"startCharacter":1,"endCharacter":2}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"2:0":[3,0,0],"2:1":[0,0,1],"2:2":[0,0,1],"2:3":[0,0,1],"3:0":[0,0,0],"3:1":[0,0,7],"3:2":[0,0,7],"3:3":[0,0,7],"3:4":[0,0,7],"3:5":[0,0,7],"3:6":[0,0,7],"3:7":[0,0,7],"3:8":[0,8,8],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"3:15":[2],"4:0":[0,0,0],"4:1":[0,0,1],"4:2":[0,0,9],"4:3":[0,0,9],"4:4":[0,0,9],"4:5":[0,0,9],"4:6":[0,0,9],"4:7":[0,0,9],"4:8":[0,0,9],"4:9":[0,0,9],"4:10":[0,0,9],"4:11":[0,0,9],"5:0":[2],"5:1":[0,0,1],"5:2":[0,0,7],"5:3":[0,0,7],"5:4":[0,0,7],"5:5":[0,0,7],"5:6":[0,0,7],"5:7":[0,0,7],"5:8":[2],"5:9":[2],"5:10":[2],"6:0":[2],"6:1":[2],"6:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"}]} +{"diagnostics":{"default":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [","severity":"error","line":3,"startCharacter":1,"endCharacter":2}],"quiet":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [","severity":"info","line":3,"startCharacter":1,"endCharacter":2}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"2:0":[3,0,0],"2:1":[0,0,1],"2:2":[0,0,1],"2:3":[0,0,1],"3:0":[0,0,0],"3:1":[0,0,7],"3:2":[0,0,7],"3:3":[0,0,7],"3:4":[0,0,7],"3:5":[0,0,7],"3:6":[0,0,7],"3:7":[0,0,7],"3:8":[0,8,8],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"3:15":[2],"4:0":[0,0,0],"4:1":[0,0,1],"4:2":[0,0,9],"4:3":[0,0,9],"4:4":[0,0,9],"4:5":[0,0,9],"4:6":[0,0,9],"4:7":[0,0,9],"4:8":[0,0,9],"4:9":[0,0,9],"4:10":[0,0,9],"4:11":[0,0,9],"5:0":[2],"5:1":[0,0,1],"5:2":[0,0,7],"5:3":[0,0,7],"5:4":[0,0,7],"5:5":[0,0,7],"5:6":[0,0,7],"5:7":[0,0,7],"5:8":[2],"5:9":[2],"5:10":[2],"6:0":[2],"6:1":[2],"6:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"}],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":10},"value":{"kind":"string","range":{"startLine":1,"startCharacter":7,"endLine":1,"endCharacter":10},"open":false,"text":"b","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":8,"endLine":1,"endCharacter":9}}},{"type":"section","segments":[],"isArray":false,"open":true,"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":1},"line":2},{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":3,"startCharacter":0,"endLine":3,"endCharacter":7}}],"range":{"startLine":3,"startCharacter":0,"endLine":3,"endCharacter":13},"value":{"kind":"string","range":{"startLine":3,"startCharacter":10,"endLine":3,"endCharacter":13},"open":false,"text":"1","quote":"\"","multiline":false,"contentRange":{"startLine":3,"startCharacter":11,"endLine":3,"endCharacter":12}}},{"type":"section","segments":[{"name":"unclosed","quoted":false,"range":{"startLine":4,"startCharacter":1,"endLine":4,"endCharacter":9}}],"isArray":false,"open":true,"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":9},"line":4},{"type":"section","segments":[{"name":"nosuch","quoted":false,"range":{"startLine":5,"startCharacter":1,"endLine":5,"endCharacter":7}}],"isArray":false,"open":false,"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":8},"line":5}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"1:0":4,"1:1":5,"1:2":5,"1:3":5,"1:4":5,"1:5":6,"1:6":7,"1:7":8,"1:8":9,"1:9":9,"1:10":10,"1:11":10,"1:12":10,"2:0":4,"2:1":1,"2:2":1,"2:3":1,"3:0":0,"3:1":11,"3:2":11,"3:3":11,"3:4":11,"3:5":11,"3:6":11,"3:7":11,"3:8":12,"3:9":13,"3:10":14,"3:11":15,"3:12":15,"3:13":16,"3:14":16,"3:15":16,"4:0":0,"4:1":1,"4:2":17,"4:3":17,"4:4":17,"4:5":17,"4:6":17,"4:7":17,"4:8":17,"4:9":17,"4:10":17,"4:11":17,"5:0":18,"5:1":1,"5:2":19,"5:3":19,"5:4":19,"5:5":19,"5:6":19,"5:7":19,"5:8":20,"5:9":20,"5:10":20,"6:0":21,"6:1":21,"6:2":21,"7:0":21,"7:1":21,"7:2":21},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["version"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":13}},{"kind":"value","section":{"kind":"top"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"top"},"keyPath":["version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":12}},{"kind":"value","section":{"kind":"top"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":9}},{"kind":"key","section":{"kind":"unknown","segments":["unclosed"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":7}},{"kind":"key","section":{"kind":"unknown","segments":["nosuch"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"unknown","segments":["nosuch"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}}]} diff --git a/tests/rules/golden/broken.toml.json b/tests/rules/golden/broken.toml.json index 50daf81..74b9882 100644 --- a/tests/rules/golden/broken.toml.json +++ b/tests/rules/golden/broken.toml.json @@ -1 +1 @@ -{"diagnostics":{"default":[{"code":"mcpp.manifest.mcppFloor","message":"[package] mcpp must be a release floor like \">=2026.9.28.3\"","severity":"error","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"mcpp"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [package]","severity":"warning","line":4,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.syntax","message":"key standard has no value","severity":"error","line":5,"startCharacter":1,"endCharacter":9,"relatedKey":"standard"},{"code":"mcpp.manifest.syntax","message":"invalid key: a b","severity":"error","line":6,"startCharacter":1,"endCharacter":4},{"code":"mcpp.manifest.syntax","message":"not a [section] header or key = value: = \"no key\"","severity":"error","line":7,"startCharacter":1,"endCharacter":11},{"code":"mcpp.manifest.syntax","message":"not a [section] header: [package] trailing","severity":"error","line":8,"startCharacter":1,"endCharacter":19},{"code":"mcpp.manifest.unknownSection","message":"unknown section [nosuch]","severity":"warning","line":9,"startCharacter":1,"endCharacter":9,"relatedKey":"nosuch"},{"code":"mcpp.manifest.unknownSection","message":"unknown section [nosuch.deeper]","severity":"warning","line":10,"startCharacter":1,"endCharacter":16,"relatedKey":"nosuch.deeper"},{"code":"mcpp.manifest.arrayTable","message":"[[package]] is an array of tables, which mcpp does not use here","severity":"error","line":12,"startCharacter":1,"endCharacter":12,"relatedKey":"package"},{"code":"mcpp.manifest.legacyKey","message":"[language] is deprecated; use [package].standard","severity":"info","line":13,"startCharacter":1,"endCharacter":11,"relatedKey":"[package].standard"},{"code":"mcpp.manifest.unknownKey","message":"unknown key edition in [language]","severity":"warning","line":14,"startCharacter":1,"endCharacter":8,"relatedKey":"edition"},{"code":"mcpp.manifest.planeSeparation","message":"xim:cmake is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]","severity":"warning","line":17,"startCharacter":1,"endCharacter":12,"relatedKey":"xim:cmake"},{"code":"mcpp.manifest.planeSeparation","message":"xpm:ninja is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]","severity":"warning","line":18,"startCharacter":1,"endCharacter":12,"relatedKey":"xpm:ninja"},{"code":"mcpp.manifest.planeSeparation","message":"compat.zlib is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key compat.zlib in [xlings]","severity":"warning","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key mcpplibs.os in [xlings]","severity":"warning","line":22,"startCharacter":1,"endCharacter":12,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.planeSeparation","message":"tool.name is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.unknownKey","message":"unknown key tool.name in [xlings]","severity":"warning","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.planeSeparation","message":"mcpplibs.os is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":26,"startCharacter":1,"endCharacter":14,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [targets]","severity":"warning","line":30,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.unknownKey","message":"unknown key unknown_knob in [profile]","severity":"warning","line":33,"startCharacter":1,"endCharacter":13,"relatedKey":"unknown_knob"}],"quiet":[{"code":"mcpp.manifest.mcppFloor","message":"[package] mcpp must be a release floor like \">=2026.9.28.3\"","severity":"warning","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"mcpp"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [package]","severity":"info","line":4,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.syntax","message":"key standard has no value","severity":"info","line":5,"startCharacter":1,"endCharacter":9,"relatedKey":"standard"},{"code":"mcpp.manifest.syntax","message":"invalid key: a b","severity":"info","line":6,"startCharacter":1,"endCharacter":4},{"code":"mcpp.manifest.syntax","message":"not a [section] header or key = value: = \"no key\"","severity":"info","line":7,"startCharacter":1,"endCharacter":11},{"code":"mcpp.manifest.syntax","message":"not a [section] header: [package] trailing","severity":"info","line":8,"startCharacter":1,"endCharacter":19},{"code":"mcpp.manifest.legacyKey","message":"[language] is deprecated; use [package].standard","severity":"warning","line":13,"startCharacter":1,"endCharacter":11,"relatedKey":"[package].standard"},{"code":"mcpp.manifest.unknownKey","message":"unknown key edition in [language]","severity":"info","line":14,"startCharacter":1,"endCharacter":8,"relatedKey":"edition"},{"code":"mcpp.manifest.unknownKey","message":"unknown key compat.zlib in [xlings]","severity":"info","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key mcpplibs.os in [xlings]","severity":"info","line":22,"startCharacter":1,"endCharacter":12,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.unknownKey","message":"unknown key tool.name in [xlings]","severity":"info","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [targets]","severity":"info","line":30,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.unknownKey","message":"unknown key unknown_knob in [profile]","severity":"info","line":33,"startCharacter":1,"endCharacter":13,"relatedKey":"unknown_knob"}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"1:13":[2],"1:14":[2],"1:15":[2],"1:16":[2],"1:17":[2],"2:0":[3,0,0],"2:1":[3,0,4],"2:2":[3,0,4],"2:3":[3,0,4],"2:4":[3,0,4],"2:5":[3,5,5],"2:6":[2],"2:7":[2],"2:8":[2],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"3:0":[4,0,0],"3:1":[4,0,6],"3:2":[4,0,6],"3:3":[4,0,6],"3:4":[4,0,6],"3:5":[4,0,6],"3:6":[4,0,6],"3:7":[4,7,7],"3:8":[2],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"3:15":[2],"3:16":[2],"3:17":[2],"4:0":[4,0,0],"4:1":[4,0,8],"4:2":[4,0,8],"4:3":[4,0,8],"4:4":[4,0,8],"4:5":[4,0,8],"4:6":[4,0,8],"4:7":[4,0,8],"4:8":[4,0,8],"4:9":[4,9,9],"4:10":[5,10,10],"4:11":[5,10,10],"4:12":[5,10,10],"5:0":[6,0,0],"5:1":[6,0,1],"5:2":[6,2,2],"5:3":[6,2,3],"5:4":[6,4,4],"5:5":[2],"5:6":[2],"5:7":[2],"5:8":[2],"5:9":[2],"6:0":[6,0,0],"6:1":[2],"6:2":[2],"6:3":[2],"6:4":[2],"6:5":[2],"6:6":[2],"6:7":[2],"6:8":[2],"6:9":[2],"6:10":[2],"6:11":[2],"6:12":[2],"7:0":[6,0,0],"7:1":[0,0,1],"7:2":[0,0,8],"7:3":[0,0,8],"7:4":[0,0,8],"7:5":[0,0,8],"7:6":[0,0,8],"7:7":[0,0,8],"7:8":[0,0,8],"7:9":[6,9,9],"7:10":[6,10,10],"7:11":[6,10,18],"7:12":[6,10,18],"7:13":[6,10,18],"7:14":[6,10,18],"7:15":[6,10,18],"7:16":[6,10,18],"7:17":[6,10,18],"7:18":[6,10,18],"7:19":[6,10,18],"7:20":[6,10,18],"8:0":[6,0,0],"8:1":[0,0,1],"8:2":[0,0,7],"8:3":[0,0,7],"8:4":[0,0,7],"8:5":[0,0,7],"8:6":[0,0,7],"8:7":[0,0,7],"8:8":[2],"8:9":[2],"8:10":[2],"9:0":[2],"9:1":[0,0,1],"9:2":[0,0,7],"9:3":[0,0,7],"9:4":[0,0,7],"9:5":[0,0,7],"9:6":[0,0,7],"9:7":[0,0,7],"9:8":[0,0,8],"9:9":[0,0,14],"9:10":[0,0,14],"9:11":[0,0,14],"9:12":[0,0,14],"9:13":[0,0,14],"9:14":[0,0,14],"9:15":[2],"9:16":[2],"9:17":[2],"10:0":[2],"10:1":[2],"10:2":[2],"10:3":[2],"10:4":[2],"10:5":[2],"10:6":[2],"10:7":[2],"10:8":[2],"10:9":[2],"10:10":[2],"10:11":[2],"10:12":[2],"10:13":[2],"10:14":[2],"10:15":[2],"10:16":[2],"10:17":[2],"11:0":[2],"11:1":[2],"11:2":[2],"11:3":[2],"11:4":[2],"11:5":[2],"11:6":[2],"11:7":[2],"11:8":[2],"11:9":[2],"11:10":[6,10,10],"11:11":[6,11,11],"11:12":[6,11,11],"11:13":[6,11,11],"12:0":[6,0,0],"12:1":[0,0,1],"12:2":[0,0,9],"12:3":[0,0,9],"12:4":[0,0,9],"12:5":[0,0,9],"12:6":[0,0,9],"12:7":[0,0,9],"12:8":[0,0,9],"12:9":[0,0,9],"12:10":[7,10,10],"12:11":[7,10,10],"12:12":[7,10,10],"13:0":[7,0,0],"13:1":[7,0,7],"13:2":[7,0,7],"13:3":[7,0,7],"13:4":[7,0,7],"13:5":[7,0,7],"13:6":[7,0,7],"13:7":[7,0,7],"13:8":[7,8,8],"13:9":[2],"13:10":[2],"13:11":[2],"13:12":[2],"13:13":[2],"13:14":[2],"13:15":[2],"13:16":[2],"13:17":[2],"13:18":[2],"14:0":[7,0,0],"14:1":[7,0,0],"14:2":[7,0,0],"15:0":[7,0,0],"15:1":[0,0,1],"15:2":[0,0,13],"15:3":[0,0,13],"15:4":[0,0,13],"15:5":[0,0,13],"15:6":[0,0,13],"15:7":[0,0,13],"15:8":[0,0,13],"15:9":[0,0,13],"15:10":[0,0,13],"15:11":[0,0,13],"15:12":[0,0,13],"15:13":[0,0,13],"15:14":[8,14,14],"15:15":[8,14,14],"15:16":[8,14,14],"16:0":[8,0,0],"16:1":[8,0,11],"16:2":[8,0,11],"16:3":[8,0,11],"16:4":[8,0,11],"16:5":[8,0,11],"16:6":[8,0,11],"16:7":[8,0,11],"16:8":[8,0,11],"16:9":[8,0,11],"16:10":[8,0,11],"16:11":[8,0,11],"16:12":[8,12,12],"16:13":[2],"16:14":[2],"16:15":[2],"16:16":[2],"16:17":[2],"16:18":[2],"16:19":[2],"16:20":[2],"16:21":[2],"16:22":[2],"16:23":[2],"16:24":[2],"17:0":[8,0,0],"17:1":[8,0,11],"17:2":[8,0,11],"17:3":[8,0,11],"17:4":[8,0,11],"17:5":[8,0,11],"17:6":[8,0,11],"17:7":[8,0,11],"17:8":[8,0,11],"17:9":[8,0,11],"17:10":[8,0,11],"17:11":[8,0,11],"17:12":[8,12,12],"17:13":[2],"17:14":[2],"17:15":[2],"17:16":[2],"17:17":[2],"17:18":[2],"17:19":[2],"17:20":[2],"17:21":[2],"17:22":[2],"17:23":[2],"17:24":[2],"17:25":[2],"17:26":[2],"17:27":[2],"17:28":[2],"17:29":[2],"17:30":[2],"17:31":[2],"17:32":[2],"17:33":[2],"17:34":[8,34,34],"17:35":[8,34,34],"17:36":[8,34,34],"18:0":[8,0,0],"18:1":[8,0,0],"18:2":[8,0,0],"19:0":[8,0,0],"19:1":[0,0,1],"19:2":[0,0,7],"19:3":[0,0,7],"19:4":[0,0,7],"19:5":[0,0,7],"19:6":[0,0,7],"19:7":[0,0,7],"19:8":[9,8,8],"19:9":[9,8,8],"19:10":[9,8,8],"20:0":[9,0,0],"20:1":[9,0,6],"20:2":[9,0,6],"20:3":[9,0,6],"20:4":[9,0,6],"20:5":[9,0,6],"20:6":[9,0,6],"20:7":[9,7,7],"20:8":[9,7,11],"20:9":[9,7,11],"20:10":[9,7,11],"20:11":[9,7,11],"20:12":[9,12,12],"20:13":[2],"20:14":[2],"20:15":[2],"20:16":[2],"20:17":[2],"20:18":[2],"20:19":[2],"20:20":[2],"20:21":[2],"20:22":[2],"20:23":[2],"21:0":[9,0,0],"21:1":[9,0,8],"21:2":[9,0,8],"21:3":[9,0,8],"21:4":[9,0,8],"21:5":[9,0,8],"21:6":[9,0,8],"21:7":[9,0,8],"21:8":[9,0,8],"21:9":[9,9,9],"21:10":[9,9,11],"21:11":[9,9,11],"21:12":[9,12,12],"21:13":[2],"21:14":[2],"21:15":[2],"21:16":[2],"21:17":[2],"21:18":[2],"21:19":[2],"21:20":[2],"21:21":[2],"21:22":[2],"21:23":[2],"21:24":[2],"21:25":[2],"21:26":[2],"21:27":[2],"21:28":[2],"21:29":[2],"21:30":[2],"21:31":[2],"21:32":[9,32,32],"21:33":[9,32,32],"21:34":[9,32,32],"22:0":[9,0,0],"22:1":[9,0,4],"22:2":[9,0,4],"22:3":[9,0,4],"22:4":[9,0,4],"22:5":[9,5,5],"22:6":[9,5,9],"22:7":[9,5,9],"22:8":[9,5,9],"22:9":[9,5,9],"22:10":[9,10,10],"22:11":[2],"22:12":[2],"22:13":[2],"22:14":[2],"22:15":[2],"22:16":[2],"22:17":[2],"22:18":[2],"22:19":[2],"22:20":[2],"22:21":[2],"22:22":[2],"22:23":[2],"22:24":[2],"22:25":[2],"22:26":[2],"22:27":[2],"22:28":[2],"22:29":[2],"22:30":[2],"22:31":[9,31,31],"22:32":[9,31,31],"22:33":[9,31,31],"23:0":[9,0,0],"23:1":[9,0,0],"23:2":[9,0,0],"24:0":[9,0,0],"24:1":[0,0,1],"24:2":[0,0,7],"24:3":[0,0,7],"24:4":[0,0,7],"24:5":[0,0,7],"24:6":[0,0,7],"24:7":[0,0,7],"24:8":[0,0,8],"24:9":[0,0,17],"24:10":[0,0,17],"24:11":[0,0,17],"24:12":[0,0,17],"24:13":[0,0,17],"24:14":[0,0,17],"24:15":[0,0,17],"24:16":[0,0,17],"24:17":[0,0,17],"24:18":[10,18,18],"24:19":[10,18,18],"24:20":[10,18,18],"25:0":[10,0,0],"25:1":[10,0,13],"25:2":[10,0,13],"25:3":[10,0,13],"25:4":[10,0,13],"25:5":[10,0,13],"25:6":[10,0,13],"25:7":[10,0,13],"25:8":[10,0,13],"25:9":[10,0,13],"25:10":[10,0,13],"25:11":[10,0,13],"25:12":[10,0,13],"25:13":[10,0,13],"25:14":[10,14,14],"25:15":[2],"25:16":[2],"25:17":[2],"25:18":[2],"25:19":[2],"25:20":[2],"25:21":[2],"25:22":[2],"25:23":[2],"25:24":[2],"25:25":[2],"26:0":[10,0,0],"26:1":[10,0,0],"26:2":[10,0,0],"27:0":[10,0,0],"27:1":[0,0,1],"27:2":[0,0,8],"27:3":[0,0,8],"27:4":[0,0,8],"27:5":[0,0,8],"27:6":[0,0,8],"27:7":[0,0,8],"27:8":[0,0,8],"27:9":[0,0,9],"27:10":[0,0,12],"27:11":[0,0,12],"27:12":[0,0,12],"27:13":[11,13,13],"27:14":[11,13,13],"27:15":[11,13,13],"28:0":[11,0,0],"28:1":[11,0,4],"28:2":[11,0,4],"28:3":[11,0,4],"28:4":[11,0,4],"28:5":[11,5,5],"28:6":[12,7,17],"28:7":[12,7,7],"28:8":[13,8,16],"28:9":[13,8,16],"28:10":[13,8,16],"28:11":[13,8,16],"28:12":[13,8,16],"28:13":[13,8,16],"28:14":[13,8,16],"28:15":[13,8,16],"28:16":[13,8,16],"28:17":[12,17,17],"28:18":[12,17,17],"28:19":[12,17,17],"29:0":[14,0,0],"29:1":[14,0,6],"29:2":[14,0,6],"29:3":[14,0,6],"29:4":[14,0,6],"29:5":[14,0,6],"29:6":[14,0,6],"29:7":[14,7,7],"29:8":[2],"29:9":[2],"29:10":[2],"29:11":[2],"29:12":[2],"30:0":[14,0,0],"30:1":[14,0,0],"30:2":[14,0,0],"31:0":[14,0,0],"31:1":[0,0,1],"31:2":[0,0,8],"31:3":[0,0,8],"31:4":[0,0,8],"31:5":[0,0,8],"31:6":[0,0,8],"31:7":[0,0,8],"31:8":[0,0,8],"31:9":[0,0,9],"31:10":[0,0,14],"31:11":[0,0,14],"31:12":[0,0,14],"31:13":[0,0,14],"31:14":[0,0,14],"31:15":[15,15,15],"31:16":[15,15,15],"31:17":[15,15,15],"32:0":[15,0,0],"32:1":[15,0,12],"32:2":[15,0,12],"32:3":[15,0,12],"32:4":[15,0,12],"32:5":[15,0,12],"32:6":[15,0,12],"32:7":[15,0,12],"32:8":[15,0,12],"32:9":[15,0,12],"32:10":[15,0,12],"32:11":[15,0,12],"32:12":[15,0,12],"32:13":[15,13,13],"32:14":[2],"32:15":[2],"32:16":[2],"32:17":[2],"32:18":[2],"32:19":[2],"32:20":[2],"32:21":[2],"33:0":[15,0,0],"33:1":[15,0,0],"33:2":[15,0,0],"34:0":[15,0,0],"34:1":[0,0,1],"34:2":[0,0,10],"34:3":[0,0,10],"34:4":[0,0,10],"34:5":[0,0,10],"34:6":[0,0,10],"34:7":[0,0,10],"34:8":[0,0,10],"34:9":[0,0,10],"34:10":[0,0,10],"34:11":[2],"34:12":[2],"34:13":[2],"35:0":[2],"35:1":[2],"35:2":[2],"35:3":[2],"35:4":[2],"35:5":[2],"35:6":[2],"35:7":[2],"35:8":[2],"35:9":[2],"35:10":[2],"35:11":[2],"35:12":[2],"35:13":[2],"35:14":[2],"35:15":[2],"35:16":[2],"35:17":[2],"35:18":[2],"35:19":[2],"36:0":[2],"36:1":[2],"36:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"2:1":2,"2:2":2,"2:3":2,"2:4":2,"2:5":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"4:7":3,"4:8":3,"4:9":3,"4:10":4,"4:11":4,"4:12":4,"7:2":0,"7:3":0,"7:4":0,"7:5":0,"7:6":0,"7:7":0,"7:8":0,"10:3":5,"10:4":5,"10:5":5,"10:6":5,"10:7":5,"10:8":5,"10:9":5,"10:10":5,"10:11":5,"10:12":5,"10:13":5,"11:3":0,"11:4":0,"11:5":0,"11:6":0,"11:7":0,"11:8":0,"11:9":0,"12:2":6,"12:3":6,"12:4":6,"12:5":6,"12:6":6,"12:7":6,"12:8":6,"12:9":6,"15:2":7,"15:3":7,"15:4":7,"15:5":7,"15:6":7,"15:7":7,"15:8":7,"15:9":7,"15:10":7,"15:11":7,"15:12":7,"15:13":7,"19:2":8,"19:3":8,"19:4":8,"19:5":8,"19:6":8,"19:7":8,"24:2":8,"24:3":8,"24:4":8,"24:5":8,"24:6":8,"24:7":8,"24:8":8,"24:9":9,"24:10":9,"24:11":9,"24:12":9,"24:13":9,"24:14":9,"24:15":9,"24:16":9,"24:17":9,"27:2":10,"27:3":10,"27:4":10,"27:5":10,"27:6":10,"27:7":10,"27:8":10,"27:9":10,"27:10":10,"27:11":10,"27:12":10,"28:1":11,"28:2":11,"28:3":11,"28:4":11,"28:5":11,"28:6":12,"28:7":13,"28:8":12,"28:9":12,"28:10":12,"28:11":12,"28:12":12,"28:13":12,"28:14":12,"28:15":12,"28:16":12,"28:17":13,"28:18":13,"28:19":13,"31:2":14,"31:3":14,"31:4":14,"31:5":14,"31:6":14,"31:7":14,"31:8":14,"31:9":14,"31:10":14,"31:11":14,"31:12":14,"31:13":14,"31:14":14,"34:2":15,"34:3":15,"34:4":15,"34:5":15,"34:6":15,"34:7":15,"34:8":15,"34:9":15,"34:10":15},"definition":{},"versionContext":{"16:13":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":14,"endCharacter":22}},"16:14":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":14,"endCharacter":14}},"16:15":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:16":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:17":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:18":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:19":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:20":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:21":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:22":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"16:23":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"16:24":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"17:25":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":26,"endCharacter":32}},"17:26":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":26,"endCharacter":26}},"17:27":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:28":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:29":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:30":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:31":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:32":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":32,"endCharacter":32}}},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"import_std","kind":"template","detail":"boolean","insertSnippet":"import_std = true","documentation":"replaced by [package].standard and the module scan Legacy key."},{"label":"modules","kind":"template","detail":"boolean","insertSnippet":"modules = true","documentation":"replaced by the module scan Legacy key."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"replaced by [package].standard Legacy key."}],[{"label":"name = \"version\"","kind":"template","detail":"SemVer version dependency","documentation":"Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported.","insertSnippet":"${1:name} = \"${2:1.0.0}\""},{"label":"name = { path = ... }","kind":"template","detail":"Path dependency (local development)","insertSnippet":"${1:name} = { path = \"${2:../mylib}\" }"},{"label":"name = { git = ..., tag = ... }","kind":"template","detail":"Git dependency (one of tag / branch / rev)","insertSnippet":"${1:name} = { git = \"${2:https://github.com/user/repo.git}\", tag = \"${3:v1.0.0}\" }"},{"label":"name = { version = ..., features = [...] }","kind":"template","detail":"Long dep spec: request a feature of that dependency","insertSnippet":"${1:name} = { version = \"${2:1.0}\", features = [\"${3:feature}\"] }"},{"label":"name = { version = ..., tools = [...] }","kind":"template","detail":"Host tools produced by the dependency (must be a bin target of that package)","insertSnippet":"${1:name} = { version = \"${2:1.0}\", tools = [\"${3:protoc}\"] }"}],[{"label":"deps","kind":"template","detail":"array","insertSnippet":"deps = []","documentation":"the project's host build tools: \"@\" or tables Since 0.0.77."},{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"tool = \"version\"","kind":"template","detail":"Pin the xlings tool version","insertSnippet":"${1:node} = \"${2:24.19.0}\""}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"opt","kind":"template","detail":"enum: s | z","insertSnippet":"opt = \"s\"","documentation":"an -O level: a number, or \"s\"/\"z\""},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"mcpp","body":"type: `string`\n\nsince: 2026.9.28.3\n\nrelease floor, only the \">=\" form is accepted (docs/04 §2.1)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"[language]","body":"plane: `unclassified`\n\nlegacy: replaced by `[package].standard`\n\nkeys: 3","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#41-legacy-language-compatibility-layer"},{"title":"[dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[xlings]","body":"plane: `tool`\n\nkeys: 4","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[xlings.workspace]","body":"plane: `tool`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"nonsense","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[profile]","body":"plane: `compile`\n\nkeys: 10","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"[toolchain]","body":"plane: `compile`\n\nkeys: 3 (open: the user picks the names)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#27-toolchain--toolchain-configuration"}]} +{"diagnostics":{"default":[{"code":"mcpp.manifest.mcppFloor","message":"[package] mcpp must be a release floor like \">=2026.9.28.3\"","severity":"error","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"mcpp"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [package]","severity":"warning","line":4,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.syntax","message":"key standard has no value","severity":"error","line":5,"startCharacter":1,"endCharacter":9,"relatedKey":"standard"},{"code":"mcpp.manifest.syntax","message":"invalid key: a b","severity":"error","line":6,"startCharacter":1,"endCharacter":4},{"code":"mcpp.manifest.syntax","message":"not a [section] header or key = value: = \"no key\"","severity":"error","line":7,"startCharacter":1,"endCharacter":11},{"code":"mcpp.manifest.syntax","message":"not a [section] header: [package] trailing","severity":"error","line":8,"startCharacter":1,"endCharacter":19},{"code":"mcpp.manifest.unknownSection","message":"unknown section [nosuch]","severity":"warning","line":9,"startCharacter":1,"endCharacter":9,"relatedKey":"nosuch"},{"code":"mcpp.manifest.unknownSection","message":"unknown section [nosuch.deeper]","severity":"warning","line":10,"startCharacter":1,"endCharacter":16,"relatedKey":"nosuch.deeper"},{"code":"mcpp.manifest.arrayTable","message":"[[package]] is an array of tables, which mcpp does not use here","severity":"error","line":12,"startCharacter":1,"endCharacter":12,"relatedKey":"package"},{"code":"mcpp.manifest.legacyKey","message":"[language] is deprecated; use [package].standard","severity":"info","line":13,"startCharacter":1,"endCharacter":11,"relatedKey":"[package].standard"},{"code":"mcpp.manifest.unknownKey","message":"unknown key edition in [language]","severity":"warning","line":14,"startCharacter":1,"endCharacter":8,"relatedKey":"edition"},{"code":"mcpp.manifest.planeSeparation","message":"xim:cmake is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]","severity":"warning","line":17,"startCharacter":1,"endCharacter":12,"relatedKey":"xim:cmake"},{"code":"mcpp.manifest.planeSeparation","message":"xpm:ninja is a tool payload, not a library dependency; declare it in [xlings]/[xlings.workspace]","severity":"warning","line":18,"startCharacter":1,"endCharacter":12,"relatedKey":"xpm:ninja"},{"code":"mcpp.manifest.planeSeparation","message":"compat.zlib is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key compat.zlib in [xlings]","severity":"warning","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key mcpplibs.os in [xlings]","severity":"warning","line":22,"startCharacter":1,"endCharacter":12,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.planeSeparation","message":"tool.name is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.unknownKey","message":"unknown key tool.name in [xlings]","severity":"warning","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.planeSeparation","message":"mcpplibs.os is an mcpp package, not a tool payload; declare it in [dependencies]","severity":"warning","line":26,"startCharacter":1,"endCharacter":14,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [targets]","severity":"warning","line":30,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.unknownKey","message":"unknown key unknown_knob in [profile]","severity":"warning","line":33,"startCharacter":1,"endCharacter":13,"relatedKey":"unknown_knob"}],"quiet":[{"code":"mcpp.manifest.mcppFloor","message":"[package] mcpp must be a release floor like \">=2026.9.28.3\"","severity":"warning","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"mcpp"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [package]","severity":"info","line":4,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.syntax","message":"key standard has no value","severity":"info","line":5,"startCharacter":1,"endCharacter":9,"relatedKey":"standard"},{"code":"mcpp.manifest.syntax","message":"invalid key: a b","severity":"info","line":6,"startCharacter":1,"endCharacter":4},{"code":"mcpp.manifest.syntax","message":"not a [section] header or key = value: = \"no key\"","severity":"info","line":7,"startCharacter":1,"endCharacter":11},{"code":"mcpp.manifest.syntax","message":"not a [section] header: [package] trailing","severity":"info","line":8,"startCharacter":1,"endCharacter":19},{"code":"mcpp.manifest.legacyKey","message":"[language] is deprecated; use [package].standard","severity":"warning","line":13,"startCharacter":1,"endCharacter":11,"relatedKey":"[package].standard"},{"code":"mcpp.manifest.unknownKey","message":"unknown key edition in [language]","severity":"info","line":14,"startCharacter":1,"endCharacter":8,"relatedKey":"edition"},{"code":"mcpp.manifest.unknownKey","message":"unknown key compat.zlib in [xlings]","severity":"info","line":21,"startCharacter":1,"endCharacter":12,"relatedKey":"compat.zlib"},{"code":"mcpp.manifest.unknownKey","message":"unknown key mcpplibs.os in [xlings]","severity":"info","line":22,"startCharacter":1,"endCharacter":12,"relatedKey":"mcpplibs.os"},{"code":"mcpp.manifest.unknownKey","message":"unknown key tool.name in [xlings]","severity":"info","line":23,"startCharacter":1,"endCharacter":10,"relatedKey":"tool.name"},{"code":"mcpp.manifest.unknownKey","message":"unknown key colour in [targets]","severity":"info","line":30,"startCharacter":1,"endCharacter":7,"relatedKey":"colour"},{"code":"mcpp.manifest.unknownKey","message":"unknown key unknown_knob in [profile]","severity":"info","line":33,"startCharacter":1,"endCharacter":13,"relatedKey":"unknown_knob"}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"1:13":[2],"1:14":[2],"1:15":[2],"1:16":[2],"1:17":[2],"2:0":[3,0,0],"2:1":[3,0,4],"2:2":[3,0,4],"2:3":[3,0,4],"2:4":[3,0,4],"2:5":[3,5,5],"2:6":[2],"2:7":[2],"2:8":[2],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"3:0":[4,0,0],"3:1":[4,0,6],"3:2":[4,0,6],"3:3":[4,0,6],"3:4":[4,0,6],"3:5":[4,0,6],"3:6":[4,0,6],"3:7":[4,7,7],"3:8":[2],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"3:15":[2],"3:16":[2],"3:17":[2],"4:0":[4,0,0],"4:1":[4,0,8],"4:2":[4,0,8],"4:3":[4,0,8],"4:4":[4,0,8],"4:5":[4,0,8],"4:6":[4,0,8],"4:7":[4,0,8],"4:8":[4,0,8],"4:9":[4,9,9],"4:10":[5,10,10],"4:11":[5,10,10],"4:12":[5,10,10],"5:0":[6,0,0],"5:1":[6,0,1],"5:2":[6,2,2],"5:3":[6,2,3],"5:4":[6,4,4],"5:5":[2],"5:6":[2],"5:7":[2],"5:8":[2],"5:9":[2],"6:0":[6,0,0],"6:1":[2],"6:2":[2],"6:3":[2],"6:4":[2],"6:5":[2],"6:6":[2],"6:7":[2],"6:8":[2],"6:9":[2],"6:10":[2],"6:11":[2],"6:12":[2],"7:0":[6,0,0],"7:1":[0,0,1],"7:2":[0,0,8],"7:3":[0,0,8],"7:4":[0,0,8],"7:5":[0,0,8],"7:6":[0,0,8],"7:7":[0,0,8],"7:8":[0,0,8],"7:9":[6,9,9],"7:10":[6,10,10],"7:11":[6,10,18],"7:12":[6,10,18],"7:13":[6,10,18],"7:14":[6,10,18],"7:15":[6,10,18],"7:16":[6,10,18],"7:17":[6,10,18],"7:18":[6,10,18],"7:19":[6,10,18],"7:20":[6,10,18],"8:0":[6,0,0],"8:1":[0,0,1],"8:2":[0,0,7],"8:3":[0,0,7],"8:4":[0,0,7],"8:5":[0,0,7],"8:6":[0,0,7],"8:7":[0,0,7],"8:8":[2],"8:9":[2],"8:10":[2],"9:0":[2],"9:1":[0,0,1],"9:2":[0,0,7],"9:3":[0,0,7],"9:4":[0,0,7],"9:5":[0,0,7],"9:6":[0,0,7],"9:7":[0,0,7],"9:8":[0,0,8],"9:9":[0,0,14],"9:10":[0,0,14],"9:11":[0,0,14],"9:12":[0,0,14],"9:13":[0,0,14],"9:14":[0,0,14],"9:15":[2],"9:16":[2],"9:17":[2],"10:0":[2],"10:1":[2],"10:2":[2],"10:3":[2],"10:4":[2],"10:5":[2],"10:6":[2],"10:7":[2],"10:8":[2],"10:9":[2],"10:10":[2],"10:11":[2],"10:12":[2],"10:13":[2],"10:14":[2],"10:15":[2],"10:16":[2],"10:17":[2],"11:0":[2],"11:1":[2],"11:2":[2],"11:3":[2],"11:4":[2],"11:5":[2],"11:6":[2],"11:7":[2],"11:8":[2],"11:9":[2],"11:10":[6,10,10],"11:11":[6,11,11],"11:12":[6,11,11],"11:13":[6,11,11],"12:0":[6,0,0],"12:1":[0,0,1],"12:2":[0,0,9],"12:3":[0,0,9],"12:4":[0,0,9],"12:5":[0,0,9],"12:6":[0,0,9],"12:7":[0,0,9],"12:8":[0,0,9],"12:9":[0,0,9],"12:10":[7,10,10],"12:11":[7,10,10],"12:12":[7,10,10],"13:0":[7,0,0],"13:1":[7,0,7],"13:2":[7,0,7],"13:3":[7,0,7],"13:4":[7,0,7],"13:5":[7,0,7],"13:6":[7,0,7],"13:7":[7,0,7],"13:8":[7,8,8],"13:9":[2],"13:10":[2],"13:11":[2],"13:12":[2],"13:13":[2],"13:14":[2],"13:15":[2],"13:16":[2],"13:17":[2],"13:18":[2],"14:0":[7,0,0],"14:1":[7,0,0],"14:2":[7,0,0],"15:0":[7,0,0],"15:1":[0,0,1],"15:2":[0,0,13],"15:3":[0,0,13],"15:4":[0,0,13],"15:5":[0,0,13],"15:6":[0,0,13],"15:7":[0,0,13],"15:8":[0,0,13],"15:9":[0,0,13],"15:10":[0,0,13],"15:11":[0,0,13],"15:12":[0,0,13],"15:13":[0,0,13],"15:14":[8,14,14],"15:15":[8,14,14],"15:16":[8,14,14],"16:0":[8,0,0],"16:1":[8,0,11],"16:2":[8,0,11],"16:3":[8,0,11],"16:4":[8,0,11],"16:5":[8,0,11],"16:6":[8,0,11],"16:7":[8,0,11],"16:8":[8,0,11],"16:9":[8,0,11],"16:10":[8,0,11],"16:11":[8,0,11],"16:12":[8,12,12],"16:13":[2],"16:14":[2],"16:15":[2],"16:16":[2],"16:17":[2],"16:18":[2],"16:19":[2],"16:20":[2],"16:21":[2],"16:22":[2],"16:23":[2],"16:24":[2],"17:0":[8,0,0],"17:1":[8,0,11],"17:2":[8,0,11],"17:3":[8,0,11],"17:4":[8,0,11],"17:5":[8,0,11],"17:6":[8,0,11],"17:7":[8,0,11],"17:8":[8,0,11],"17:9":[8,0,11],"17:10":[8,0,11],"17:11":[8,0,11],"17:12":[8,12,12],"17:13":[2],"17:14":[2],"17:15":[2],"17:16":[2],"17:17":[2],"17:18":[2],"17:19":[2],"17:20":[2],"17:21":[2],"17:22":[2],"17:23":[2],"17:24":[2],"17:25":[2],"17:26":[2],"17:27":[2],"17:28":[2],"17:29":[2],"17:30":[2],"17:31":[2],"17:32":[2],"17:33":[2],"17:34":[8,34,34],"17:35":[8,34,34],"17:36":[8,34,34],"18:0":[8,0,0],"18:1":[8,0,0],"18:2":[8,0,0],"19:0":[8,0,0],"19:1":[0,0,1],"19:2":[0,0,7],"19:3":[0,0,7],"19:4":[0,0,7],"19:5":[0,0,7],"19:6":[0,0,7],"19:7":[0,0,7],"19:8":[9,8,8],"19:9":[9,8,8],"19:10":[9,8,8],"20:0":[9,0,0],"20:1":[9,0,6],"20:2":[9,0,6],"20:3":[9,0,6],"20:4":[9,0,6],"20:5":[9,0,6],"20:6":[9,0,6],"20:7":[9,7,7],"20:8":[9,7,11],"20:9":[9,7,11],"20:10":[9,7,11],"20:11":[9,7,11],"20:12":[9,12,12],"20:13":[2],"20:14":[2],"20:15":[2],"20:16":[2],"20:17":[2],"20:18":[2],"20:19":[2],"20:20":[2],"20:21":[2],"20:22":[2],"20:23":[2],"21:0":[9,0,0],"21:1":[9,0,8],"21:2":[9,0,8],"21:3":[9,0,8],"21:4":[9,0,8],"21:5":[9,0,8],"21:6":[9,0,8],"21:7":[9,0,8],"21:8":[9,0,8],"21:9":[9,9,9],"21:10":[9,9,11],"21:11":[9,9,11],"21:12":[9,12,12],"21:13":[2],"21:14":[2],"21:15":[2],"21:16":[2],"21:17":[2],"21:18":[2],"21:19":[2],"21:20":[2],"21:21":[2],"21:22":[2],"21:23":[2],"21:24":[2],"21:25":[2],"21:26":[2],"21:27":[2],"21:28":[2],"21:29":[2],"21:30":[2],"21:31":[2],"21:32":[9,32,32],"21:33":[9,32,32],"21:34":[9,32,32],"22:0":[9,0,0],"22:1":[9,0,4],"22:2":[9,0,4],"22:3":[9,0,4],"22:4":[9,0,4],"22:5":[9,5,5],"22:6":[9,5,9],"22:7":[9,5,9],"22:8":[9,5,9],"22:9":[9,5,9],"22:10":[9,10,10],"22:11":[2],"22:12":[2],"22:13":[2],"22:14":[2],"22:15":[2],"22:16":[2],"22:17":[2],"22:18":[2],"22:19":[2],"22:20":[2],"22:21":[2],"22:22":[2],"22:23":[2],"22:24":[2],"22:25":[2],"22:26":[2],"22:27":[2],"22:28":[2],"22:29":[2],"22:30":[2],"22:31":[9,31,31],"22:32":[9,31,31],"22:33":[9,31,31],"23:0":[9,0,0],"23:1":[9,0,0],"23:2":[9,0,0],"24:0":[9,0,0],"24:1":[0,0,1],"24:2":[0,0,7],"24:3":[0,0,7],"24:4":[0,0,7],"24:5":[0,0,7],"24:6":[0,0,7],"24:7":[0,0,7],"24:8":[0,0,8],"24:9":[0,0,17],"24:10":[0,0,17],"24:11":[0,0,17],"24:12":[0,0,17],"24:13":[0,0,17],"24:14":[0,0,17],"24:15":[0,0,17],"24:16":[0,0,17],"24:17":[0,0,17],"24:18":[10,18,18],"24:19":[10,18,18],"24:20":[10,18,18],"25:0":[10,0,0],"25:1":[10,0,13],"25:2":[10,0,13],"25:3":[10,0,13],"25:4":[10,0,13],"25:5":[10,0,13],"25:6":[10,0,13],"25:7":[10,0,13],"25:8":[10,0,13],"25:9":[10,0,13],"25:10":[10,0,13],"25:11":[10,0,13],"25:12":[10,0,13],"25:13":[10,0,13],"25:14":[10,14,14],"25:15":[2],"25:16":[2],"25:17":[2],"25:18":[2],"25:19":[2],"25:20":[2],"25:21":[2],"25:22":[2],"25:23":[2],"25:24":[2],"25:25":[2],"26:0":[10,0,0],"26:1":[10,0,0],"26:2":[10,0,0],"27:0":[10,0,0],"27:1":[0,0,1],"27:2":[0,0,8],"27:3":[0,0,8],"27:4":[0,0,8],"27:5":[0,0,8],"27:6":[0,0,8],"27:7":[0,0,8],"27:8":[0,0,8],"27:9":[0,0,9],"27:10":[0,0,12],"27:11":[0,0,12],"27:12":[0,0,12],"27:13":[11,13,13],"27:14":[11,13,13],"27:15":[11,13,13],"28:0":[11,0,0],"28:1":[11,0,4],"28:2":[11,0,4],"28:3":[11,0,4],"28:4":[11,0,4],"28:5":[11,5,5],"28:6":[12,7,17],"28:7":[12,7,7],"28:8":[13,8,16],"28:9":[13,8,16],"28:10":[13,8,16],"28:11":[13,8,16],"28:12":[13,8,16],"28:13":[13,8,16],"28:14":[13,8,16],"28:15":[13,8,16],"28:16":[13,8,16],"28:17":[12,17,17],"28:18":[12,17,17],"28:19":[12,17,17],"29:0":[14,0,0],"29:1":[14,0,6],"29:2":[14,0,6],"29:3":[14,0,6],"29:4":[14,0,6],"29:5":[14,0,6],"29:6":[14,0,6],"29:7":[14,7,7],"29:8":[2],"29:9":[2],"29:10":[2],"29:11":[2],"29:12":[2],"30:0":[14,0,0],"30:1":[14,0,0],"30:2":[14,0,0],"31:0":[14,0,0],"31:1":[0,0,1],"31:2":[0,0,8],"31:3":[0,0,8],"31:4":[0,0,8],"31:5":[0,0,8],"31:6":[0,0,8],"31:7":[0,0,8],"31:8":[0,0,8],"31:9":[0,0,9],"31:10":[0,0,14],"31:11":[0,0,14],"31:12":[0,0,14],"31:13":[0,0,14],"31:14":[0,0,14],"31:15":[15,15,15],"31:16":[15,15,15],"31:17":[15,15,15],"32:0":[15,0,0],"32:1":[15,0,12],"32:2":[15,0,12],"32:3":[15,0,12],"32:4":[15,0,12],"32:5":[15,0,12],"32:6":[15,0,12],"32:7":[15,0,12],"32:8":[15,0,12],"32:9":[15,0,12],"32:10":[15,0,12],"32:11":[15,0,12],"32:12":[15,0,12],"32:13":[15,13,13],"32:14":[2],"32:15":[2],"32:16":[2],"32:17":[2],"32:18":[2],"32:19":[2],"32:20":[2],"32:21":[2],"33:0":[15,0,0],"33:1":[15,0,0],"33:2":[15,0,0],"34:0":[15,0,0],"34:1":[0,0,1],"34:2":[0,0,10],"34:3":[0,0,10],"34:4":[0,0,10],"34:5":[0,0,10],"34:6":[0,0,10],"34:7":[0,0,10],"34:8":[0,0,10],"34:9":[0,0,10],"34:10":[0,0,10],"34:11":[2],"34:12":[2],"34:13":[2],"35:0":[2],"35:1":[2],"35:2":[2],"35:3":[2],"35:4":[2],"35:5":[2],"35:6":[2],"35:7":[2],"35:8":[2],"35:9":[2],"35:10":[2],"35:11":[2],"35:12":[2],"35:13":[2],"35:14":[2],"35:15":[2],"35:16":[2],"35:17":[2],"35:18":[2],"35:19":[2],"36:0":[2],"36:1":[2],"36:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"2:1":2,"2:2":2,"2:3":2,"2:4":2,"2:5":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"4:7":3,"4:8":3,"4:9":3,"4:10":4,"4:11":4,"4:12":4,"7:2":0,"7:3":0,"7:4":0,"7:5":0,"7:6":0,"7:7":0,"7:8":0,"10:3":5,"10:4":5,"10:5":5,"10:6":5,"10:7":5,"10:8":5,"10:9":5,"10:10":5,"10:11":5,"10:12":5,"10:13":5,"11:3":0,"11:4":0,"11:5":0,"11:6":0,"11:7":0,"11:8":0,"11:9":0,"12:2":6,"12:3":6,"12:4":6,"12:5":6,"12:6":6,"12:7":6,"12:8":6,"12:9":6,"15:2":7,"15:3":7,"15:4":7,"15:5":7,"15:6":7,"15:7":7,"15:8":7,"15:9":7,"15:10":7,"15:11":7,"15:12":7,"15:13":7,"19:2":8,"19:3":8,"19:4":8,"19:5":8,"19:6":8,"19:7":8,"24:2":8,"24:3":8,"24:4":8,"24:5":8,"24:6":8,"24:7":8,"24:8":8,"24:9":9,"24:10":9,"24:11":9,"24:12":9,"24:13":9,"24:14":9,"24:15":9,"24:16":9,"24:17":9,"27:2":10,"27:3":10,"27:4":10,"27:5":10,"27:6":10,"27:7":10,"27:8":10,"27:9":10,"27:10":10,"27:11":10,"27:12":10,"28:1":11,"28:2":11,"28:3":11,"28:4":11,"28:5":11,"28:6":12,"28:7":13,"28:8":12,"28:9":12,"28:10":12,"28:11":12,"28:12":12,"28:13":12,"28:14":12,"28:15":12,"28:16":12,"28:17":13,"28:18":13,"28:19":13,"31:2":14,"31:3":14,"31:4":14,"31:5":14,"31:6":14,"31:7":14,"31:8":14,"31:9":14,"31:10":14,"31:11":14,"31:12":14,"31:13":14,"31:14":14,"34:2":15,"34:3":15,"34:4":15,"34:5":15,"34:6":15,"34:7":15,"34:8":15,"34:9":15,"34:10":15},"definition":{},"versionContext":{"16:13":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":14,"endCharacter":22}},"16:14":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":14,"endCharacter":14}},"16:15":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:16":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:17":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:18":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:19":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:20":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:21":{"name":"xim:cmake","insideString":true,"range":{"startCharacter":15,"endCharacter":21}},"16:22":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"16:23":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"16:24":{"name":"xim:cmake","insideString":false,"range":{"startCharacter":22,"endCharacter":22}},"17:25":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":26,"endCharacter":32}},"17:26":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":26,"endCharacter":26}},"17:27":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:28":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:29":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:30":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:31":{"name":"xpm:ninja","insideString":true,"range":{"startCharacter":27,"endCharacter":31}},"17:32":{"name":"xpm:ninja","insideString":false,"range":{"startCharacter":32,"endCharacter":32}}},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"import_std","kind":"template","detail":"boolean","insertSnippet":"import_std = true","documentation":"replaced by [package].standard and the module scan Legacy key."},{"label":"modules","kind":"template","detail":"boolean","insertSnippet":"modules = true","documentation":"replaced by the module scan Legacy key."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"replaced by [package].standard Legacy key."}],[{"label":"name = \"version\"","kind":"template","detail":"SemVer version dependency","documentation":"Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported.","insertSnippet":"${1:name} = \"${2:1.0.0}\""},{"label":"name = { path = ... }","kind":"template","detail":"Path dependency (local development)","insertSnippet":"${1:name} = { path = \"${2:../mylib}\" }"},{"label":"name = { git = ..., tag = ... }","kind":"template","detail":"Git dependency (one of tag / branch / rev)","insertSnippet":"${1:name} = { git = \"${2:https://github.com/user/repo.git}\", tag = \"${3:v1.0.0}\" }"},{"label":"name = { version = ..., features = [...] }","kind":"template","detail":"Long dep spec: request a feature of that dependency","insertSnippet":"${1:name} = { version = \"${2:1.0}\", features = [\"${3:feature}\"] }"},{"label":"name = { version = ..., tools = [...] }","kind":"template","detail":"Host tools produced by the dependency (must be a bin target of that package)","insertSnippet":"${1:name} = { version = \"${2:1.0}\", tools = [\"${3:protoc}\"] }"}],[{"label":"deps","kind":"template","detail":"array","insertSnippet":"deps = []","documentation":"the project's host build tools: \"@\" or tables Since 0.0.77."},{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"tool = \"version\"","kind":"template","detail":"Pin the xlings tool version","insertSnippet":"${1:node} = \"${2:24.19.0}\""}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"opt","kind":"template","detail":"enum: s | z","insertSnippet":"opt = \"s\"","documentation":"an -O level: a number, or \"s\"/\"z\""},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"mcpp","body":"type: `string`\n\nsince: 2026.9.28.3\n\nrelease floor, only the \">=\" form is accepted (docs/04 §2.1)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"[language]","body":"plane: `unclassified`\n\nlegacy: replaced by `[package].standard`\n\nkeys: 3","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#41-legacy-language-compatibility-layer"},{"title":"[dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[xlings]","body":"plane: `tool`\n\nkeys: 4","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[xlings.workspace]","body":"plane: `tool`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"nonsense","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[profile]","body":"plane: `compile`\n\nkeys: 10","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"[toolchain]","body":"plane: `compile`\n\nkeys: 3 (open: the user picks the names)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#27-toolchain--toolchain-configuration"}],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":15},"value":{"kind":"string","range":{"startLine":1,"startCharacter":7,"endLine":1,"endCharacter":15},"open":false,"text":"broken","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":8,"endLine":1,"endCharacter":14}}},{"type":"keyValue","keyPath":[{"name":"mcpp","quoted":false,"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":4}}],"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":15},"value":{"kind":"string","range":{"startLine":2,"startCharacter":7,"endLine":2,"endCharacter":15},"open":false,"text":"2026.9","quote":"\"","multiline":false,"contentRange":{"startLine":2,"startCharacter":8,"endLine":2,"endCharacter":14}}},{"type":"keyValue","keyPath":[{"name":"colour","quoted":false,"range":{"startLine":3,"startCharacter":0,"endLine":3,"endCharacter":6}}],"range":{"startLine":3,"startCharacter":0,"endLine":3,"endCharacter":15},"value":{"kind":"string","range":{"startLine":3,"startCharacter":9,"endLine":3,"endCharacter":15},"open":false,"text":"blue","quote":"\"","multiline":false,"contentRange":{"startLine":3,"startCharacter":10,"endLine":3,"endCharacter":14}}},{"type":"keyValue","keyPath":[{"name":"standard","quoted":false,"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":8}}],"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":10}},{"type":"keyValue","keyPath":[{"name":"a","quoted":false,"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":1}}],"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":2}},{"type":"keyValue","keyPath":[{"name":"b","quoted":false,"range":{"startLine":5,"startCharacter":2,"endLine":5,"endCharacter":3}}],"range":{"startLine":5,"startCharacter":2,"endLine":5,"endCharacter":7},"value":{"kind":"integer","range":{"startLine":5,"startCharacter":6,"endLine":5,"endCharacter":7},"open":false,"text":"1"}},{"type":"keyValue","keyPath":[],"range":{"startLine":6,"startCharacter":0,"endLine":6,"endCharacter":10},"value":{"kind":"string","range":{"startLine":6,"startCharacter":2,"endLine":6,"endCharacter":10},"open":false,"text":"no key","quote":"\"","multiline":false,"contentRange":{"startLine":6,"startCharacter":3,"endLine":6,"endCharacter":9}}},{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":7,"startCharacter":1,"endLine":7,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":7,"startCharacter":0,"endLine":7,"endCharacter":9},"line":7},{"type":"keyValue","keyPath":[{"name":"trailing","quoted":false,"range":{"startLine":7,"startCharacter":10,"endLine":7,"endCharacter":18}}],"range":{"startLine":7,"startCharacter":10,"endLine":7,"endCharacter":18}},{"type":"section","segments":[{"name":"nosuch","quoted":false,"range":{"startLine":8,"startCharacter":1,"endLine":8,"endCharacter":7}}],"isArray":false,"open":false,"range":{"startLine":8,"startCharacter":0,"endLine":8,"endCharacter":8},"line":8},{"type":"section","segments":[{"name":"nosuch","quoted":false,"range":{"startLine":9,"startCharacter":1,"endLine":9,"endCharacter":7}},{"name":"deeper","quoted":false,"range":{"startLine":9,"startCharacter":8,"endLine":9,"endCharacter":14}}],"isArray":false,"open":false,"range":{"startLine":9,"startCharacter":0,"endLine":9,"endCharacter":15},"line":9},{"type":"section","segments":[{"name":"build","quoted":false,"range":{"startLine":10,"startCharacter":2,"endLine":10,"endCharacter":7}},{"name":"flags","quoted":false,"range":{"startLine":10,"startCharacter":8,"endLine":10,"endCharacter":13}}],"isArray":true,"open":false,"range":{"startLine":10,"startCharacter":0,"endLine":10,"endCharacter":15},"line":10},{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":11,"startCharacter":2,"endLine":11,"endCharacter":9}}],"isArray":true,"open":false,"range":{"startLine":11,"startCharacter":0,"endLine":11,"endCharacter":11},"line":11},{"type":"section","segments":[{"name":"language","quoted":false,"range":{"startLine":12,"startCharacter":1,"endLine":12,"endCharacter":9}}],"isArray":false,"open":false,"range":{"startLine":12,"startCharacter":0,"endLine":12,"endCharacter":10},"line":12},{"type":"keyValue","keyPath":[{"name":"edition","quoted":false,"range":{"startLine":13,"startCharacter":0,"endLine":13,"endCharacter":7}}],"range":{"startLine":13,"startCharacter":0,"endLine":13,"endCharacter":16},"value":{"kind":"string","range":{"startLine":13,"startCharacter":10,"endLine":13,"endCharacter":16},"open":false,"text":"2023","quote":"\"","multiline":false,"contentRange":{"startLine":13,"startCharacter":11,"endLine":13,"endCharacter":15}}},{"type":"section","segments":[{"name":"dependencies","quoted":false,"range":{"startLine":15,"startCharacter":1,"endLine":15,"endCharacter":13}}],"isArray":false,"open":false,"range":{"startLine":15,"startCharacter":0,"endLine":15,"endCharacter":14},"line":15},{"type":"keyValue","keyPath":[{"name":"xim:cmake","quoted":true,"range":{"startLine":16,"startCharacter":0,"endLine":16,"endCharacter":11}}],"range":{"startLine":16,"startCharacter":0,"endLine":16,"endCharacter":22},"value":{"kind":"string","range":{"startLine":16,"startCharacter":14,"endLine":16,"endCharacter":22},"open":false,"text":"3.31.0","quote":"\"","multiline":false,"contentRange":{"startLine":16,"startCharacter":15,"endLine":16,"endCharacter":21}}},{"type":"keyValue","keyPath":[{"name":"xpm:ninja","quoted":true,"range":{"startLine":17,"startCharacter":0,"endLine":17,"endCharacter":11}}],"range":{"startLine":17,"startCharacter":0,"endLine":17,"endCharacter":34},"value":{"kind":"inlineTable","range":{"startLine":17,"startCharacter":14,"endLine":17,"endCharacter":34},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":17,"startCharacter":16,"endLine":17,"endCharacter":23}}],"range":{"startLine":17,"startCharacter":16,"endLine":17,"endCharacter":32},"value":{"kind":"string","range":{"startLine":17,"startCharacter":26,"endLine":17,"endCharacter":32},"open":false,"text":"1.12","quote":"\"","multiline":false,"contentRange":{"startLine":17,"startCharacter":27,"endLine":17,"endCharacter":31}}}]}},{"type":"section","segments":[{"name":"xlings","quoted":false,"range":{"startLine":19,"startCharacter":1,"endLine":19,"endCharacter":7}}],"isArray":false,"open":false,"range":{"startLine":19,"startCharacter":0,"endLine":19,"endCharacter":8},"line":19},{"type":"keyValue","keyPath":[{"name":"compat","quoted":false,"range":{"startLine":20,"startCharacter":0,"endLine":20,"endCharacter":6}},{"name":"zlib","quoted":false,"range":{"startLine":20,"startCharacter":7,"endLine":20,"endCharacter":11}}],"range":{"startLine":20,"startCharacter":0,"endLine":20,"endCharacter":21},"value":{"kind":"string","range":{"startLine":20,"startCharacter":14,"endLine":20,"endCharacter":21},"open":false,"text":"1.3.1","quote":"\"","multiline":false,"contentRange":{"startLine":20,"startCharacter":15,"endLine":20,"endCharacter":20}}},{"type":"keyValue","keyPath":[{"name":"mcpplibs","quoted":false,"range":{"startLine":21,"startCharacter":0,"endLine":21,"endCharacter":8}},{"name":"os","quoted":false,"range":{"startLine":21,"startCharacter":9,"endLine":21,"endCharacter":11}}],"range":{"startLine":21,"startCharacter":0,"endLine":21,"endCharacter":32},"value":{"kind":"inlineTable","range":{"startLine":21,"startCharacter":14,"endLine":21,"endCharacter":32},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"path","quoted":false,"range":{"startLine":21,"startCharacter":16,"endLine":21,"endCharacter":20}}],"range":{"startLine":21,"startCharacter":16,"endLine":21,"endCharacter":30},"value":{"kind":"string","range":{"startLine":21,"startCharacter":23,"endLine":21,"endCharacter":30},"open":false,"text":"../os","quote":"\"","multiline":false,"contentRange":{"startLine":21,"startCharacter":24,"endLine":21,"endCharacter":29}}}]}},{"type":"keyValue","keyPath":[{"name":"tool","quoted":false,"range":{"startLine":22,"startCharacter":0,"endLine":22,"endCharacter":4}},{"name":"name","quoted":false,"range":{"startLine":22,"startCharacter":5,"endLine":22,"endCharacter":9}}],"range":{"startLine":22,"startCharacter":0,"endLine":22,"endCharacter":31},"value":{"kind":"inlineTable","range":{"startLine":22,"startCharacter":12,"endLine":22,"endCharacter":31},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":22,"startCharacter":14,"endLine":22,"endCharacter":21}}],"range":{"startLine":22,"startCharacter":14,"endLine":22,"endCharacter":29},"value":{"kind":"string","range":{"startLine":22,"startCharacter":24,"endLine":22,"endCharacter":29},"open":false,"text":"1.0","quote":"\"","multiline":false,"contentRange":{"startLine":22,"startCharacter":25,"endLine":22,"endCharacter":28}}}]}},{"type":"section","segments":[{"name":"xlings","quoted":false,"range":{"startLine":24,"startCharacter":1,"endLine":24,"endCharacter":7}},{"name":"workspace","quoted":false,"range":{"startLine":24,"startCharacter":8,"endLine":24,"endCharacter":17}}],"isArray":false,"open":false,"range":{"startLine":24,"startCharacter":0,"endLine":24,"endCharacter":18},"line":24},{"type":"keyValue","keyPath":[{"name":"mcpplibs.os","quoted":true,"range":{"startLine":25,"startCharacter":0,"endLine":25,"endCharacter":13}}],"range":{"startLine":25,"startCharacter":0,"endLine":25,"endCharacter":23},"value":{"kind":"string","range":{"startLine":25,"startCharacter":16,"endLine":25,"endCharacter":23},"open":false,"text":"0.2.3","quote":"\"","multiline":false,"contentRange":{"startLine":25,"startCharacter":17,"endLine":25,"endCharacter":22}}},{"type":"section","segments":[{"name":"targets","quoted":false,"range":{"startLine":27,"startCharacter":1,"endLine":27,"endCharacter":8}},{"name":"app","quoted":false,"range":{"startLine":27,"startCharacter":9,"endLine":27,"endCharacter":12}}],"isArray":false,"open":false,"range":{"startLine":27,"startCharacter":0,"endLine":27,"endCharacter":13},"line":27},{"type":"keyValue","keyPath":[{"name":"kind","quoted":false,"range":{"startLine":28,"startCharacter":0,"endLine":28,"endCharacter":4}}],"range":{"startLine":28,"startCharacter":0,"endLine":28,"endCharacter":17},"value":{"kind":"string","range":{"startLine":28,"startCharacter":7,"endLine":28,"endCharacter":17},"open":false,"text":"nonsense","quote":"\"","multiline":false,"contentRange":{"startLine":28,"startCharacter":8,"endLine":28,"endCharacter":16}}},{"type":"keyValue","keyPath":[{"name":"colour","quoted":false,"range":{"startLine":29,"startCharacter":0,"endLine":29,"endCharacter":6}}],"range":{"startLine":29,"startCharacter":0,"endLine":29,"endCharacter":10},"value":{"kind":"integer","range":{"startLine":29,"startCharacter":9,"endLine":29,"endCharacter":10},"open":false,"text":"1"}},{"type":"section","segments":[{"name":"profile","quoted":false,"range":{"startLine":31,"startCharacter":1,"endLine":31,"endCharacter":8}},{"name":"debug","quoted":false,"range":{"startLine":31,"startCharacter":9,"endLine":31,"endCharacter":14}}],"isArray":false,"open":false,"range":{"startLine":31,"startCharacter":0,"endLine":31,"endCharacter":15},"line":31},{"type":"keyValue","keyPath":[{"name":"unknown_knob","quoted":false,"range":{"startLine":32,"startCharacter":0,"endLine":32,"endCharacter":12}}],"range":{"startLine":32,"startCharacter":0,"endLine":32,"endCharacter":19},"value":{"kind":"boolean","range":{"startLine":32,"startCharacter":15,"endLine":32,"endCharacter":19},"open":false,"text":"true"}},{"type":"section","segments":[{"name":"toolchain","quoted":false,"range":{"startLine":34,"startCharacter":1,"endLine":34,"endCharacter":10}}],"isArray":false,"open":false,"range":{"startLine":34,"startCharacter":0,"endLine":34,"endCharacter":11},"line":34},{"type":"keyValue","keyPath":[{"name":"anything","quoted":false,"range":{"startLine":35,"startCharacter":0,"endLine":35,"endCharacter":8}}],"range":{"startLine":35,"startCharacter":0,"endLine":35,"endCharacter":17},"value":{"kind":"string","range":{"startLine":35,"startCharacter":11,"endLine":35,"endCharacter":17},"open":false,"text":"goes","quote":"\"","multiline":false,"contentRange":{"startLine":35,"startCharacter":12,"endLine":35,"endCharacter":16}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"1:0":4,"1:1":5,"1:2":5,"1:3":5,"1:4":5,"1:5":6,"1:6":7,"1:7":8,"1:8":9,"1:9":9,"1:10":9,"1:11":9,"1:12":9,"1:13":9,"1:14":9,"1:15":10,"1:16":10,"1:17":10,"2:0":4,"2:1":5,"2:2":5,"2:3":5,"2:4":5,"2:5":11,"2:6":12,"2:7":13,"2:8":14,"2:9":14,"2:10":14,"2:11":14,"2:12":14,"2:13":14,"2:14":14,"2:15":15,"2:16":15,"2:17":15,"3:0":4,"3:1":16,"3:2":16,"3:3":16,"3:4":16,"3:5":16,"3:6":16,"3:7":17,"3:8":18,"3:9":19,"3:10":20,"3:11":20,"3:12":20,"3:13":20,"3:14":20,"3:15":21,"3:16":21,"3:17":21,"4:0":4,"4:1":22,"4:2":22,"4:3":22,"4:4":22,"4:5":22,"4:6":22,"4:7":22,"4:8":22,"4:9":23,"4:10":24,"4:11":24,"4:12":24,"5:0":4,"5:1":25,"5:2":26,"5:3":27,"5:4":28,"5:5":29,"5:6":29,"5:7":29,"5:8":29,"5:9":29,"6:0":4,"6:1":30,"6:2":31,"6:3":32,"6:4":32,"6:5":32,"6:6":32,"6:7":32,"6:8":32,"6:9":32,"6:10":33,"6:11":33,"6:12":33,"7:0":4,"7:1":1,"7:2":2,"7:3":2,"7:4":2,"7:5":2,"7:6":2,"7:7":2,"7:8":2,"7:9":3,"7:10":34,"7:11":35,"7:12":35,"7:13":35,"7:14":35,"7:15":35,"7:16":35,"7:17":35,"7:18":35,"7:19":35,"7:20":35,"8:0":4,"8:1":1,"8:2":36,"8:3":36,"8:4":36,"8:5":36,"8:6":36,"8:7":36,"8:8":37,"8:9":37,"8:10":37,"9:0":38,"9:1":1,"9:2":36,"9:3":36,"9:4":36,"9:5":36,"9:6":36,"9:7":36,"9:8":39,"9:9":40,"9:10":40,"9:11":40,"9:12":40,"9:13":40,"9:14":40,"9:15":41,"9:16":41,"9:17":41,"10:0":42,"10:1":43,"10:2":44,"10:3":45,"10:4":45,"10:5":45,"10:6":45,"10:7":45,"10:8":46,"10:9":47,"10:10":47,"10:11":47,"10:12":47,"10:13":47,"10:14":48,"10:15":49,"10:16":49,"10:17":49,"11:0":50,"11:1":43,"11:2":44,"11:3":51,"11:4":51,"11:5":51,"11:6":51,"11:7":51,"11:8":51,"11:9":51,"11:10":34,"11:11":52,"11:12":52,"11:13":52,"12:0":4,"12:1":1,"12:2":53,"12:3":53,"12:4":53,"12:5":53,"12:6":53,"12:7":53,"12:8":53,"12:9":53,"12:10":54,"12:11":54,"12:12":54,"13:0":55,"13:1":56,"13:2":56,"13:3":56,"13:4":56,"13:5":56,"13:6":56,"13:7":56,"13:8":57,"13:9":58,"13:10":59,"13:11":60,"13:12":60,"13:13":60,"13:14":60,"13:15":60,"13:16":61,"13:17":61,"13:18":61,"14:0":55,"14:1":55,"14:2":55,"15:0":55,"15:1":1,"15:2":62,"15:3":62,"15:4":62,"15:5":62,"15:6":62,"15:7":62,"15:8":62,"15:9":62,"15:10":62,"15:11":62,"15:12":62,"15:13":62,"15:14":63,"15:15":63,"15:16":63,"16:0":64,"16:1":65,"16:2":65,"16:3":65,"16:4":65,"16:5":65,"16:6":65,"16:7":65,"16:8":65,"16:9":65,"16:10":65,"16:11":65,"16:12":66,"16:13":67,"16:14":68,"16:15":69,"16:16":69,"16:17":69,"16:18":69,"16:19":69,"16:20":69,"16:21":69,"16:22":70,"16:23":70,"16:24":70,"17:0":64,"17:1":65,"17:2":65,"17:3":65,"17:4":65,"17:5":65,"17:6":65,"17:7":65,"17:8":65,"17:9":65,"17:10":65,"17:11":65,"17:12":71,"17:13":72,"17:14":73,"17:15":74,"17:16":75,"17:17":76,"17:18":76,"17:19":76,"17:20":76,"17:21":76,"17:22":76,"17:23":76,"17:24":77,"17:25":78,"17:26":79,"17:27":80,"17:28":80,"17:29":80,"17:30":80,"17:31":80,"17:32":81,"17:33":82,"17:34":83,"17:35":83,"17:36":83,"18:0":64,"18:1":64,"18:2":64,"19:0":64,"19:1":1,"19:2":36,"19:3":36,"19:4":36,"19:5":36,"19:6":36,"19:7":36,"19:8":84,"19:9":84,"19:10":84,"20:0":85,"20:1":86,"20:2":86,"20:3":86,"20:4":86,"20:5":86,"20:6":86,"20:7":87,"20:8":88,"20:9":88,"20:10":88,"20:11":88,"20:12":89,"20:13":90,"20:14":91,"20:15":92,"20:16":92,"20:17":92,"20:18":92,"20:19":92,"20:20":92,"20:21":93,"20:22":93,"20:23":93,"21:0":85,"21:1":94,"21:2":94,"21:3":94,"21:4":94,"21:5":94,"21:6":94,"21:7":94,"21:8":94,"21:9":95,"21:10":96,"21:11":96,"21:12":97,"21:13":98,"21:14":99,"21:15":100,"21:16":101,"21:17":102,"21:18":102,"21:19":102,"21:20":102,"21:21":103,"21:22":104,"21:23":105,"21:24":106,"21:25":106,"21:26":106,"21:27":106,"21:28":106,"21:29":106,"21:30":107,"21:31":108,"21:32":109,"21:33":109,"21:34":109,"22:0":85,"22:1":110,"22:2":110,"22:3":110,"22:4":110,"22:5":111,"22:6":112,"22:7":112,"22:8":112,"22:9":112,"22:10":113,"22:11":114,"22:12":115,"22:13":116,"22:14":117,"22:15":118,"22:16":118,"22:17":118,"22:18":118,"22:19":118,"22:20":118,"22:21":118,"22:22":119,"22:23":120,"22:24":121,"22:25":122,"22:26":122,"22:27":122,"22:28":122,"22:29":123,"22:30":124,"22:31":125,"22:32":125,"22:33":125,"23:0":85,"23:1":85,"23:2":85,"24:0":85,"24:1":1,"24:2":36,"24:3":36,"24:4":36,"24:5":36,"24:6":36,"24:7":36,"24:8":126,"24:9":127,"24:10":127,"24:11":127,"24:12":127,"24:13":127,"24:14":127,"24:15":127,"24:16":127,"24:17":127,"24:18":128,"24:19":128,"24:20":128,"25:0":129,"25:1":130,"25:2":130,"25:3":130,"25:4":130,"25:5":130,"25:6":130,"25:7":130,"25:8":130,"25:9":130,"25:10":130,"25:11":130,"25:12":130,"25:13":130,"25:14":131,"25:15":132,"25:16":133,"25:17":134,"25:18":134,"25:19":134,"25:20":134,"25:21":134,"25:22":134,"25:23":135,"25:24":135,"25:25":135,"26:0":129,"26:1":129,"26:2":129,"27:0":129,"27:1":1,"27:2":2,"27:3":2,"27:4":2,"27:5":2,"27:6":2,"27:7":2,"27:8":2,"27:9":136,"27:10":137,"27:11":137,"27:12":137,"27:13":138,"27:14":138,"27:15":138,"28:0":139,"28:1":140,"28:2":140,"28:3":140,"28:4":140,"28:5":141,"28:6":142,"28:7":143,"28:8":144,"28:9":144,"28:10":144,"28:11":144,"28:12":144,"28:13":144,"28:14":144,"28:15":144,"28:16":144,"28:17":145,"28:18":145,"28:19":145,"29:0":139,"29:1":146,"29:2":146,"29:3":146,"29:4":146,"29:5":146,"29:6":146,"29:7":147,"29:8":148,"29:9":148,"29:10":148,"29:11":148,"29:12":148,"30:0":139,"30:1":139,"30:2":139,"31:0":139,"31:1":1,"31:2":2,"31:3":2,"31:4":2,"31:5":2,"31:6":2,"31:7":2,"31:8":2,"31:9":149,"31:10":150,"31:11":150,"31:12":150,"31:13":150,"31:14":150,"31:15":151,"31:16":151,"31:17":151,"32:0":152,"32:1":153,"32:2":153,"32:3":153,"32:4":153,"32:5":153,"32:6":153,"32:7":153,"32:8":153,"32:9":153,"32:10":153,"32:11":153,"32:12":153,"32:13":154,"32:14":155,"32:15":155,"32:16":155,"32:17":155,"32:18":155,"32:19":155,"32:20":155,"32:21":155,"33:0":152,"33:1":152,"33:2":152,"34:0":152,"34:1":1,"34:2":156,"34:3":156,"34:4":156,"34:5":156,"34:6":156,"34:7":156,"34:8":156,"34:9":156,"34:10":156,"34:11":157,"34:12":157,"34:13":157,"35:0":158,"35:1":159,"35:2":159,"35:3":159,"35:4":159,"35:5":159,"35:6":159,"35:7":159,"35:8":159,"35:9":160,"35:10":161,"35:11":162,"35:12":163,"35:13":163,"35:14":163,"35:15":163,"35:16":163,"35:17":164,"35:18":164,"35:19":164,"36:0":158,"36:1":158,"36:2":158,"37:0":158,"37:1":158,"37:2":158},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["mcpp"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["mcpp"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["mcpp"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["mcpp"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["colour"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["colour"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["colour"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["colour"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["colour"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["standard"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["standard"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":1}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["a"],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":2,"endCharacter":3}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["b"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["b"],"valueKind":"integer","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":[],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":2,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":[],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":[],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":3,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":[],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":18}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":7}},{"kind":"key","section":{"kind":"unknown","segments":["nosuch"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"unknown","segments":["nosuch"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":["nosuch"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"section-header","segments":["nosuch"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":14}},{"kind":"key","section":{"kind":"unknown","segments":["nosuch","deeper"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"unknown","segments":["nosuch","deeper"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":2,"endCharacter":7}},{"kind":"section-header","segments":["build"],"isArray":true,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"section-header","segments":["build"],"isArray":true,"replaceRange":{"startCharacter":8,"endCharacter":13}},{"kind":"key","section":{"kind":"unknown","segments":["build","flags"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"unknown","segments":["build","flags"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"unknown","segments":["build","flags"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":2,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"language"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"language"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"language"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"language"},"containerPath":[],"keyPrefix":["edition"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"language"},"keyPath":["edition"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"language"},"keyPath":["edition"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"language"},"keyPath":["edition"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"language"},"keyPath":["edition"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["xim:cmake"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["xim:cmake"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":22}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["xim:cmake"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["xim:cmake"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["xim:cmake"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["xpm:ninja"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["xpm:ninja"],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["xpm:ninja"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["xpm:ninja"],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["xpm:ninja"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["xpm:ninja"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":23}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["xpm:ninja"],"keyPrefix":["version"],"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["xpm:ninja","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":26,"endCharacter":32}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["xpm:ninja","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":26,"endCharacter":26}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["xpm:ninja","version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":27,"endCharacter":31}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["xpm:ninja","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":32,"endCharacter":32}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["xpm:ninja"],"keyPrefix":[],"replaceRange":{"startCharacter":33,"endCharacter":33}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["compat"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["compat"],"replaceRange":{"startCharacter":7,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["compat","zlib"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["compat","zlib"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["compat","zlib"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["compat","zlib"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["compat","zlib"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["mcpplibs"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["mcpplibs"],"replaceRange":{"startCharacter":9,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["mcpplibs","os"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["mcpplibs","os"],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["mcpplibs","os"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["mcpplibs","os"],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["mcpplibs","os"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["mcpplibs","os"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":20}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["mcpplibs","os"],"keyPrefix":["path"],"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["mcpplibs","os","path"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":30}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["mcpplibs","os","path"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["mcpplibs","os","path"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":29}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["mcpplibs","os","path"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":30,"endCharacter":30}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["mcpplibs","os"],"keyPrefix":[],"replaceRange":{"startCharacter":31,"endCharacter":31}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":32,"endCharacter":32}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["tool"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["tool"],"replaceRange":{"startCharacter":5,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["tool","name"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["tool","name"],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["tool","name"],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["tool","name"],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["tool","name"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["tool","name"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":21}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["tool","name"],"keyPrefix":["version"],"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["tool","name","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":29}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["tool","name","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["tool","name","version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":25,"endCharacter":28}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["tool","name","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":29,"endCharacter":29}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":["tool","name"],"keyPrefix":[],"replaceRange":{"startCharacter":30,"endCharacter":30}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":31,"endCharacter":31}},{"kind":"section-header","segments":["xlings"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"section-header","segments":["xlings"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"xlings.workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"key","section":{"kind":"known","group":"xlings.workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"xlings.workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"xlings.workspace"},"containerPath":[],"keyPrefix":["mcpplibs.os"],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"xlings.workspace"},"keyPath":["mcpplibs.os"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"xlings.workspace"},"keyPath":["mcpplibs.os"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"xlings.workspace"},"keyPath":["mcpplibs.os"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":22}},{"kind":"value","section":{"kind":"known","group":"xlings.workspace"},"keyPath":["mcpplibs.os"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":["kind"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":["colour"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["colour"],"valueKind":"integer","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":10}},{"kind":"section-header","segments":["profile"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"section-header","segments":["profile"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"profile"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"profile"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"profile"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"profile"},"containerPath":[],"keyPrefix":["unknown_knob"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"profile"},"keyPath":["unknown_knob"],"valueKind":"boolean","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":19}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"toolchain"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"toolchain"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"toolchain"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"toolchain"},"containerPath":[],"keyPrefix":["anything"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"toolchain"},"keyPath":["anything"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"toolchain"},"keyPath":["anything"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"toolchain"},"keyPath":["anything"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"toolchain"},"keyPath":["anything"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}}]} diff --git a/tests/rules/golden/comments.toml.json b/tests/rules/golden/comments.toml.json index e7dc28f..6788685 100644 --- a/tests/rules/golden/comments.toml.json +++ b/tests/rules/golden/comments.toml.json @@ -1 +1 @@ -{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,1,1],"0:2":[0,2,2],"0:3":[0,3,3],"0:4":[0,4,4],"0:5":[0,5,5],"0:6":[0,6,6],"0:7":[0,7,7],"0:8":[0,8,8],"0:9":[0,9,9],"0:10":[0,10,10],"0:11":[0,11,11],"0:12":[0,12,12],"0:13":[0,13,13],"0:14":[0,14,14],"0:15":[0,15,15],"0:16":[0,16,16],"0:17":[0,16,16],"0:18":[0,16,16],"1:0":[0,0,0],"1:1":[0,0,0],"1:2":[0,0,0],"2:0":[0,0,0],"2:1":[0,1,1],"2:2":[0,2,2],"2:3":[0,3,3],"2:4":[0,3,3],"2:5":[0,3,3],"3:0":[0,0,0],"3:1":[0,0,0],"3:2":[0,0,0]},"hover":{},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}]],"hovers":[]} +{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,1,1],"0:2":[0,2,2],"0:3":[0,3,3],"0:4":[0,4,4],"0:5":[0,5,5],"0:6":[0,6,6],"0:7":[0,7,7],"0:8":[0,8,8],"0:9":[0,9,9],"0:10":[0,10,10],"0:11":[0,11,11],"0:12":[0,12,12],"0:13":[0,13,13],"0:14":[0,14,14],"0:15":[0,15,15],"0:16":[0,16,16],"0:17":[0,16,16],"0:18":[0,16,16],"1:0":[0,0,0],"1:1":[0,0,0],"1:2":[0,0,0],"2:0":[0,0,0],"2:1":[0,1,1],"2:2":[0,2,2],"2:3":[0,3,3],"2:4":[0,3,3],"2:5":[0,3,3],"3:0":[0,0,0],"3:1":[0,0,0],"3:2":[0,0,0]},"hover":{},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}]],"hovers":[],"document":{"nodes":[]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":4,"0:5":5,"0:6":6,"0:7":7,"0:8":8,"0:9":9,"0:10":10,"0:11":11,"0:12":12,"0:13":13,"0:14":14,"0:15":15,"0:16":16,"0:17":16,"0:18":16,"1:0":0,"1:1":0,"1:2":0,"2:0":0,"2:1":1,"2:2":2,"2:3":3,"2:4":3,"2:5":3,"3:0":0,"3:1":0,"3:2":0,"4:0":0,"4:1":0,"4:2":0},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":3,"endCharacter":3}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}}]} diff --git a/tests/rules/golden/complete.toml.json b/tests/rules/golden/complete.toml.json index 34d5209..55a7701 100644 --- a/tests/rules/golden/complete.toml.json +++ b/tests/rules/golden/complete.toml.json @@ -1 +1 @@ -{"diagnostics":{"default":[{"code":"mcpp.manifest.unknownKey","message":"unknown key simd in [features]","severity":"warning","line":34,"startCharacter":1,"endCharacter":5,"relatedKey":"simd"},{"code":"mcpp.manifest.unknownKey","message":"unknown key extra in [features]","severity":"warning","line":35,"startCharacter":1,"endCharacter":6,"relatedKey":"extra"},{"code":"mcpp.manifest.unknownKey","message":"unknown key description2 in [runtime]","severity":"warning","line":77,"startCharacter":1,"endCharacter":13,"relatedKey":"description2"}],"quiet":[{"code":"mcpp.manifest.unknownKey","message":"unknown key simd in [features]","severity":"info","line":34,"startCharacter":1,"endCharacter":5,"relatedKey":"simd"},{"code":"mcpp.manifest.unknownKey","message":"unknown key extra in [features]","severity":"info","line":35,"startCharacter":1,"endCharacter":6,"relatedKey":"extra"},{"code":"mcpp.manifest.unknownKey","message":"unknown key description2 in [runtime]","severity":"info","line":77,"startCharacter":1,"endCharacter":13,"relatedKey":"description2"}]},"completion":{"0:0":[0,0,0],"0:1":[0,1,1],"0:2":[0,2,2],"0:3":[0,3,3],"0:4":[0,4,4],"0:5":[0,5,5],"0:6":[0,6,6],"0:7":[0,7,7],"0:8":[0,8,8],"0:9":[0,9,9],"0:10":[0,10,10],"0:11":[0,11,11],"0:12":[0,12,12],"0:13":[0,13,13],"0:14":[0,14,14],"0:15":[0,15,15],"0:16":[0,16,16],"0:17":[0,17,17],"0:18":[0,18,18],"0:19":[0,19,19],"0:20":[0,20,20],"0:21":[0,21,21],"0:22":[0,22,22],"0:23":[0,23,23],"0:24":[0,24,24],"0:25":[0,25,25],"0:26":[0,26,26],"0:27":[0,27,27],"0:28":[0,28,28],"0:29":[0,29,29],"0:30":[0,30,30],"0:31":[0,31,31],"0:32":[0,32,32],"0:33":[0,33,33],"0:34":[0,34,34],"0:35":[0,35,35],"0:36":[0,36,36],"0:37":[0,37,37],"0:38":[0,38,38],"0:39":[0,39,39],"0:40":[0,40,40],"0:41":[0,41,41],"0:42":[0,42,42],"0:43":[0,43,43],"0:44":[0,44,44],"0:45":[0,45,45],"0:46":[0,46,46],"0:47":[0,47,47],"0:48":[0,47,47],"0:49":[0,47,47],"1:0":[0,0,0],"1:1":[0,0,1],"1:2":[0,0,8],"1:3":[0,0,8],"1:4":[0,0,8],"1:5":[0,0,8],"1:6":[0,0,8],"1:7":[0,0,8],"1:8":[0,0,8],"1:9":[1,9,9],"1:10":[1,9,9],"1:11":[1,9,9],"2:0":[1,0,0],"2:1":[1,0,4],"2:2":[1,0,4],"2:3":[1,0,4],"2:4":[1,0,4],"2:5":[1,5,5],"2:6":[1,6,6],"2:7":[1,7,7],"2:8":[1,8,8],"2:9":[1,9,9],"2:10":[1,10,10],"2:11":[1,11,11],"2:12":[1,12,12],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"2:18":[2],"2:19":[2],"2:20":[2],"2:21":[2],"2:22":[2],"2:23":[2],"2:24":[2],"2:25":[2],"2:26":[2],"3:0":[3,0,0],"3:1":[3,0,7],"3:2":[3,0,7],"3:3":[3,0,7],"3:4":[3,0,7],"3:5":[3,0,7],"3:6":[3,0,7],"3:7":[3,0,7],"3:8":[3,8,8],"3:9":[3,9,9],"3:10":[3,10,10],"3:11":[3,11,11],"3:12":[3,12,12],"3:13":[2],"3:14":[2],"3:15":[2],"3:16":[2],"3:17":[2],"3:18":[2],"3:19":[2],"3:20":[2],"3:21":[2],"3:22":[2],"3:23":[2],"4:0":[4,0,0],"4:1":[4,0,4],"4:2":[4,0,4],"4:3":[4,0,4],"4:4":[4,0,4],"4:5":[4,5,5],"4:6":[4,6,6],"4:7":[4,7,7],"4:8":[4,8,8],"4:9":[4,9,9],"4:10":[4,10,10],"4:11":[4,11,11],"4:12":[4,12,12],"4:13":[2],"4:14":[2],"4:15":[2],"4:16":[2],"4:17":[2],"4:18":[2],"4:19":[2],"4:20":[2],"4:21":[2],"4:22":[2],"4:23":[2],"4:24":[2],"4:25":[2],"4:26":[2],"4:27":[2],"4:28":[2],"4:29":[2],"4:30":[2],"4:31":[2],"4:32":[2],"5:0":[5,0,0],"5:1":[5,0,8],"5:2":[5,0,8],"5:3":[5,0,8],"5:4":[5,0,8],"5:5":[5,0,8],"5:6":[5,0,8],"5:7":[5,0,8],"5:8":[5,0,8],"5:9":[5,9,9],"5:10":[5,10,10],"5:11":[5,11,11],"5:12":[5,12,12],"5:13":[6,14,21],"5:14":[6,14,14],"5:15":[7,15,20],"5:16":[7,15,20],"5:17":[7,15,20],"5:18":[7,15,20],"5:19":[7,15,20],"5:20":[7,15,20],"5:21":[6,21,21],"5:22":[6,21,21],"5:23":[6,21,21],"6:0":[8,0,0],"6:1":[8,0,11],"6:2":[8,0,11],"6:3":[8,0,11],"6:4":[8,0,11],"6:5":[8,0,11],"6:6":[8,0,11],"6:7":[8,0,11],"6:8":[8,0,11],"6:9":[8,0,11],"6:10":[8,0,11],"6:11":[8,0,11],"6:12":[8,12,12],"6:13":[2],"6:14":[2],"6:15":[2],"6:16":[2],"6:17":[2],"6:18":[2],"6:19":[2],"6:20":[2],"6:21":[2],"6:22":[2],"6:23":[2],"6:24":[2],"6:25":[2],"6:26":[2],"6:27":[2],"6:28":[2],"6:29":[2],"6:30":[2],"6:31":[2],"6:32":[2],"6:33":[2],"6:34":[2],"6:35":[2],"6:36":[2],"6:37":[2],"6:38":[2],"6:39":[2],"6:40":[2],"6:41":[2],"6:42":[2],"6:43":[2],"6:44":[2],"6:45":[2],"6:46":[2],"7:0":[9,0,0],"7:1":[9,0,7],"7:2":[9,0,7],"7:3":[9,0,7],"7:4":[9,0,7],"7:5":[9,0,7],"7:6":[9,0,7],"7:7":[9,0,7],"7:8":[9,8,8],"7:9":[9,9,9],"7:10":[9,10,10],"7:11":[9,11,11],"7:12":[9,12,12],"7:13":[2],"7:14":[2],"7:15":[2],"7:16":[2],"7:17":[2],"7:18":[2],"7:19":[2],"7:20":[2],"7:21":[2],"7:22":[2],"7:23":[2],"7:24":[2],"7:25":[2],"7:26":[2],"7:27":[2],"7:28":[2],"7:29":[2],"7:30":[2],"7:31":[2],"7:32":[2],"7:33":[2],"7:34":[2],"7:35":[9,35,35],"7:36":[9,35,35],"7:37":[9,35,35],"8:0":[10,0,0],"8:1":[10,0,0],"8:2":[10,0,0],"9:0":[10,0,0],"9:1":[0,0,1],"9:2":[0,0,6],"9:3":[0,0,6],"9:4":[0,0,6],"9:5":[0,0,6],"9:6":[0,0,6],"9:7":[11,7,7],"9:8":[11,7,7],"9:9":[11,7,7],"10:0":[11,0,0],"10:1":[11,0,8],"10:2":[11,0,8],"10:3":[11,0,8],"10:4":[11,0,8],"10:5":[11,0,8],"10:6":[11,0,8],"10:7":[11,0,8],"10:8":[11,0,8],"10:9":[11,9,9],"10:10":[2],"10:11":[2],"10:12":[2],"10:13":[2],"10:14":[2],"10:15":[2],"10:16":[2],"10:17":[2],"10:18":[2],"10:19":[2],"10:20":[2],"10:21":[2],"10:22":[2],"10:23":[2],"10:24":[2],"10:25":[2],"10:26":[2],"10:27":[11,27,27],"10:28":[11,27,27],"10:29":[11,27,27],"11:0":[12,0,0],"11:1":[12,0,12],"11:2":[12,0,12],"11:3":[12,0,12],"11:4":[12,0,12],"11:5":[12,0,12],"11:6":[12,0,12],"11:7":[12,0,12],"11:8":[12,0,12],"11:9":[12,0,12],"11:10":[12,0,12],"11:11":[12,0,12],"11:12":[12,0,12],"11:13":[12,13,13],"11:14":[13,15,21],"11:15":[13,15,15],"11:16":[14,16,20],"11:17":[14,16,20],"11:18":[14,16,20],"11:19":[14,16,20],"11:20":[14,16,20],"11:21":[13,21,21],"11:22":[13,21,21],"11:23":[13,21,21],"12:0":[15,0,0],"12:1":[15,0,7],"12:2":[15,0,7],"12:3":[15,0,7],"12:4":[15,0,7],"12:5":[15,0,7],"12:6":[15,0,7],"12:7":[15,0,7],"12:8":[15,8,8],"12:9":[15,9,9],"12:10":[2],"12:11":[2],"12:12":[2],"12:13":[2],"12:14":[2],"13:0":[2],"13:1":[2],"13:2":[2],"13:3":[2],"13:4":[2],"13:5":[2],"13:6":[2],"13:7":[2],"13:8":[2],"13:9":[2],"13:10":[2],"13:11":[2],"13:12":[2],"13:13":[2],"13:14":[2],"13:15":[2],"13:16":[2],"13:17":[2],"13:18":[2],"13:19":[2],"13:20":[2],"13:21":[2],"13:22":[2],"14:0":[2],"14:1":[2],"14:2":[2],"14:3":[2],"14:4":[2],"14:5":[2],"14:6":[2],"14:7":[2],"14:8":[2],"14:9":[2],"14:10":[2],"14:11":[2],"14:12":[2],"14:13":[2],"14:14":[2],"14:15":[2],"14:16":[2],"14:17":[2],"14:18":[2],"14:19":[2],"14:20":[2],"14:21":[2],"14:22":[2],"14:23":[2],"14:24":[2],"14:25":[2],"14:26":[2],"14:27":[2],"14:28":[2],"14:29":[2],"14:30":[2],"14:31":[2],"14:32":[2],"14:33":[2],"14:34":[2],"14:35":[2],"14:36":[2],"14:37":[2],"14:38":[2],"14:39":[2],"14:40":[2],"14:41":[2],"14:42":[2],"14:43":[2],"14:44":[2],"14:45":[2],"14:46":[2],"14:47":[2],"14:48":[2],"14:49":[2],"14:50":[2],"14:51":[2],"15:0":[2],"15:1":[16,1,1],"15:2":[16,1,1],"15:3":[16,1,1],"16:0":[16,0,0],"16:1":[16,0,0],"16:2":[16,0,0],"17:0":[16,0,0],"17:1":[0,0,1],"17:2":[0,0,13],"17:3":[0,0,13],"17:4":[0,0,13],"17:5":[0,0,13],"17:6":[0,0,13],"17:7":[0,0,13],"17:8":[0,0,13],"17:9":[0,0,13],"17:10":[0,0,13],"17:11":[0,0,13],"17:12":[0,0,13],"17:13":[0,0,13],"17:14":[17,14,14],"17:15":[17,14,14],"17:16":[17,14,14],"18:0":[17,0,0],"18:1":[17,0,3],"18:2":[17,0,3],"18:3":[17,0,3],"18:4":[17,4,4],"18:5":[17,5,5],"18:6":[17,6,6],"18:7":[17,7,7],"18:8":[17,8,8],"18:9":[17,9,9],"18:10":[17,10,10],"18:11":[17,11,11],"18:12":[17,12,12],"18:13":[17,13,13],"18:14":[2],"18:15":[2],"18:16":[2],"18:17":[2],"18:18":[2],"18:19":[2],"18:20":[2],"18:21":[2],"18:22":[2],"18:23":[2],"18:24":[2],"18:25":[2],"19:0":[17,0,0],"19:1":[17,0,6],"19:2":[17,0,6],"19:3":[17,0,6],"19:4":[17,0,6],"19:5":[17,0,6],"19:6":[17,0,6],"19:7":[17,7,7],"19:8":[17,7,11],"19:9":[17,7,11],"19:10":[17,7,11],"19:11":[17,7,11],"19:12":[17,12,12],"19:13":[17,13,13],"19:14":[2],"19:15":[2],"19:16":[2],"19:17":[2],"19:18":[2],"19:19":[2],"19:20":[2],"19:21":[2],"19:22":[2],"19:23":[2],"19:24":[2],"19:25":[2],"19:26":[2],"19:27":[2],"19:28":[2],"19:29":[2],"19:30":[2],"19:31":[2],"19:32":[2],"19:33":[2],"19:34":[2],"19:35":[2],"19:36":[17,36,36],"19:37":[17,36,36],"19:38":[17,36,36],"20:0":[17,0,0],"20:1":[17,0,5],"20:2":[17,0,5],"20:3":[17,0,5],"20:4":[17,0,5],"20:5":[17,0,5],"20:6":[17,6,6],"20:7":[17,7,7],"20:8":[17,8,8],"20:9":[17,9,9],"20:10":[17,10,10],"20:11":[17,11,11],"20:12":[17,12,12],"20:13":[17,13,13],"20:14":[2],"20:15":[2],"20:16":[2],"20:17":[2],"20:18":[2],"20:19":[2],"20:20":[2],"20:21":[2],"20:22":[2],"20:23":[2],"20:24":[2],"20:25":[2],"20:26":[2],"20:27":[2],"20:28":[2],"20:29":[2],"20:30":[2],"20:31":[2],"20:32":[2],"20:33":[2],"20:34":[17,34,34],"20:35":[17,34,34],"20:36":[17,34,34],"21:0":[17,0,0],"21:1":[17,0,6],"21:2":[17,0,6],"21:3":[17,0,6],"21:4":[17,0,6],"21:5":[17,0,6],"21:6":[17,0,6],"21:7":[17,7,7],"21:8":[17,8,8],"21:9":[17,9,9],"21:10":[17,10,10],"21:11":[17,11,11],"21:12":[17,12,12],"21:13":[17,13,13],"21:14":[2],"21:15":[2],"21:16":[2],"21:17":[2],"21:18":[2],"21:19":[2],"21:20":[2],"21:21":[2],"21:22":[2],"21:23":[2],"21:24":[2],"21:25":[2],"21:26":[2],"21:27":[2],"21:28":[2],"21:29":[2],"21:30":[2],"21:31":[2],"21:32":[2],"21:33":[2],"21:34":[2],"21:35":[2],"21:36":[2],"21:37":[2],"21:38":[2],"21:39":[2],"21:40":[2],"21:41":[2],"21:42":[2],"21:43":[2],"21:44":[2],"21:45":[2],"21:46":[2],"21:47":[2],"21:48":[2],"21:49":[2],"21:50":[2],"21:51":[2],"21:52":[2],"21:53":[2],"21:54":[2],"21:55":[2],"21:56":[2],"21:57":[2],"21:58":[2],"21:59":[2],"21:60":[2],"21:61":[2],"21:62":[2],"21:63":[2],"21:64":[2],"21:65":[2],"21:66":[2],"21:67":[2],"21:68":[2],"21:69":[2],"21:70":[2],"21:71":[2],"21:72":[2],"21:73":[2],"21:74":[2],"21:75":[17,75,75],"21:76":[17,75,75],"21:77":[17,75,75],"22:0":[17,0,0],"22:1":[17,0,6],"22:2":[17,0,6],"22:3":[17,0,6],"22:4":[17,0,6],"22:5":[17,0,6],"22:6":[17,0,6],"22:7":[17,7,7],"22:8":[17,8,8],"22:9":[17,9,9],"22:10":[17,10,10],"22:11":[17,11,11],"22:12":[17,12,12],"22:13":[17,13,13],"22:14":[2],"22:15":[2],"22:16":[2],"22:17":[2],"22:18":[2],"22:19":[2],"22:20":[2],"22:21":[2],"22:22":[2],"22:23":[2],"22:24":[2],"22:25":[2],"22:26":[2],"22:27":[2],"22:28":[2],"22:29":[2],"22:30":[2],"22:31":[2],"22:32":[2],"22:33":[2],"22:34":[2],"22:35":[17,35,35],"22:36":[17,35,35],"22:37":[17,35,35],"23:0":[17,0,0],"23:1":[17,0,7],"23:2":[17,0,7],"23:3":[17,0,7],"23:4":[17,0,7],"23:5":[17,0,7],"23:6":[17,0,7],"23:7":[17,0,7],"23:8":[17,8,8],"23:9":[17,9,9],"23:10":[17,10,10],"23:11":[17,11,11],"23:12":[17,12,12],"23:13":[17,13,13],"23:14":[2],"23:15":[2],"23:16":[2],"23:17":[2],"23:18":[2],"23:19":[2],"23:20":[2],"23:21":[2],"23:22":[2],"23:23":[2],"23:24":[2],"23:25":[2],"23:26":[2],"23:27":[2],"23:28":[2],"23:29":[2],"23:30":[2],"23:31":[2],"23:32":[2],"23:33":[2],"23:34":[2],"23:35":[2],"23:36":[2],"23:37":[2],"23:38":[2],"23:39":[2],"23:40":[2],"23:41":[2],"23:42":[2],"23:43":[2],"23:44":[2],"23:45":[2],"23:46":[2],"23:47":[2],"23:48":[2],"23:49":[2],"23:50":[2],"23:51":[2],"23:52":[2],"23:53":[2],"23:54":[2],"23:55":[2],"23:56":[2],"23:57":[2],"23:58":[2],"23:59":[2],"23:60":[2],"23:61":[2],"23:62":[2],"23:63":[17,63,63],"23:64":[17,63,63],"23:65":[17,63,63],"24:0":[17,0,0],"24:1":[17,0,0],"24:2":[17,0,0],"25:0":[17,0,0],"25:1":[0,0,1],"25:2":[0,0,13],"25:3":[0,0,13],"25:4":[0,0,13],"25:5":[0,0,13],"25:6":[0,0,13],"25:7":[0,0,13],"25:8":[0,0,13],"25:9":[0,0,13],"25:10":[0,0,13],"25:11":[0,0,13],"25:12":[0,0,13],"25:13":[0,0,13],"25:14":[0,0,14],"25:15":[0,0,22],"25:16":[0,0,22],"25:17":[0,0,22],"25:18":[0,0,22],"25:19":[0,0,22],"25:20":[0,0,22],"25:21":[0,0,22],"25:22":[0,0,22],"25:23":[17,23,23],"25:24":[17,23,23],"25:25":[17,23,23],"26:0":[17,0,0],"26:1":[17,0,2],"26:2":[17,0,2],"26:3":[17,3,3],"26:4":[2],"26:5":[2],"26:6":[2],"26:7":[2],"26:8":[2],"26:9":[2],"26:10":[2],"26:11":[2],"26:12":[2],"26:13":[2],"26:14":[2],"27:0":[17,0,0],"27:1":[17,0,0],"27:2":[17,0,0],"28:0":[17,0,0],"28:1":[0,0,1],"28:2":[0,0,17],"28:3":[0,0,17],"28:4":[0,0,17],"28:5":[0,0,17],"28:6":[0,0,17],"28:7":[0,0,17],"28:8":[0,0,17],"28:9":[0,0,17],"28:10":[0,0,17],"28:11":[0,0,17],"28:12":[0,0,17],"28:13":[0,0,17],"28:14":[0,0,17],"28:15":[0,0,17],"28:16":[0,0,17],"28:17":[0,0,17],"28:18":[17,18,18],"28:19":[17,18,18],"28:20":[17,18,18],"29:0":[17,0,0],"29:1":[17,0,5],"29:2":[17,0,5],"29:3":[17,0,5],"29:4":[17,0,5],"29:5":[17,0,5],"29:6":[17,6,6],"29:7":[2],"29:8":[2],"29:9":[2],"29:10":[2],"29:11":[2],"29:12":[2],"29:13":[2],"29:14":[2],"29:15":[2],"29:16":[2],"29:17":[2],"29:18":[2],"30:0":[17,0,0],"30:1":[17,0,0],"30:2":[17,0,0],"31:0":[17,0,0],"31:1":[0,0,1],"31:2":[0,0,9],"31:3":[0,0,9],"31:4":[0,0,9],"31:5":[0,0,9],"31:6":[0,0,9],"31:7":[0,0,9],"31:8":[0,0,9],"31:9":[0,0,9],"31:10":[18,10,10],"31:11":[18,10,10],"31:12":[18,10,10],"32:0":[18,0,0],"32:1":[18,0,7],"32:2":[18,0,7],"32:3":[18,0,7],"32:4":[18,0,7],"32:5":[18,0,7],"32:6":[18,0,7],"32:7":[18,0,7],"32:8":[18,8,8],"32:9":[2],"32:10":[2],"32:11":[2],"32:12":[2],"32:13":[2],"32:14":[2],"32:15":[2],"32:16":[2],"32:17":[2],"32:18":[18,18,18],"32:19":[18,18,18],"32:20":[18,18,18],"33:0":[19,0,0],"33:1":[19,0,4],"33:2":[19,0,4],"33:3":[19,0,4],"33:4":[19,0,4],"33:5":[19,5,5],"33:6":[19,6,6],"33:7":[19,7,7],"33:8":[19,8,8],"33:9":[2],"33:10":[2],"33:11":[2],"33:12":[2],"33:13":[2],"33:14":[2],"33:15":[2],"33:16":[2],"33:17":[2],"33:18":[2],"33:19":[2],"33:20":[2],"33:21":[2],"33:22":[2],"33:23":[2],"33:24":[2],"33:25":[2],"33:26":[2],"33:27":[2],"33:28":[2],"33:29":[2],"33:30":[2],"33:31":[2],"33:32":[2],"33:33":[2],"33:34":[2],"33:35":[2],"33:36":[19,36,36],"33:37":[19,36,36],"33:38":[19,36,36],"34:0":[19,0,0],"34:1":[19,0,5],"34:2":[19,0,5],"34:3":[19,0,5],"34:4":[19,0,5],"34:5":[19,0,5],"34:6":[19,6,6],"34:7":[19,7,7],"34:8":[19,8,8],"34:9":[2],"34:10":[2],"34:11":[2],"34:12":[19,12,12],"34:13":[19,12,12],"34:14":[19,12,12],"35:0":[19,0,0],"35:1":[19,0,0],"35:2":[19,0,0],"36:0":[19,0,0],"36:1":[0,0,1],"36:2":[0,0,9],"36:3":[0,0,9],"36:4":[0,0,9],"36:5":[0,0,9],"36:6":[0,0,9],"36:7":[0,0,9],"36:8":[0,0,9],"36:9":[0,0,9],"36:10":[0,0,10],"36:11":[0,0,14],"36:12":[0,0,14],"36:13":[0,0,14],"36:14":[0,0,14],"36:15":[19,15,15],"36:16":[19,15,15],"36:17":[19,15,15],"37:0":[19,0,0],"37:1":[19,0,7],"37:2":[19,0,7],"37:3":[19,0,7],"37:4":[19,0,7],"37:5":[19,0,7],"37:6":[19,0,7],"37:7":[19,0,7],"37:8":[19,8,8],"37:9":[2],"37:10":[2],"37:11":[2],"37:12":[2],"37:13":[2],"37:14":[2],"37:15":[2],"37:16":[2],"37:17":[2],"37:18":[2],"37:19":[2],"37:20":[2],"37:21":[2],"37:22":[2],"37:23":[2],"37:24":[2],"37:25":[19,25,25],"37:26":[19,25,25],"37:27":[19,25,25],"38:0":[20,0,0],"38:1":[20,0,0],"38:2":[20,0,0],"39:0":[20,0,0],"39:1":[0,0,1],"39:2":[0,0,8],"39:3":[0,0,8],"39:4":[0,0,8],"39:5":[0,0,8],"39:6":[0,0,8],"39:7":[0,0,8],"39:8":[0,0,8],"39:9":[0,0,9],"39:10":[0,0,12],"39:11":[0,0,12],"39:12":[0,0,12],"39:13":[21,13,13],"39:14":[21,13,13],"39:15":[21,13,13],"40:0":[21,0,0],"40:1":[21,0,4],"40:2":[21,0,4],"40:3":[21,0,4],"40:4":[21,0,4],"40:5":[21,5,5],"40:6":[22,7,12],"40:7":[22,7,7],"40:8":[23,8,11],"40:9":[23,8,11],"40:10":[23,8,11],"40:11":[23,8,11],"40:12":[22,12,12],"40:13":[22,12,12],"40:14":[22,12,12],"41:0":[24,0,0],"41:1":[24,0,4],"41:2":[24,0,4],"41:3":[24,0,4],"41:4":[24,0,4],"41:5":[24,5,5],"41:6":[2],"41:7":[2],"41:8":[2],"41:9":[2],"41:10":[2],"41:11":[2],"41:12":[2],"41:13":[2],"41:14":[2],"41:15":[2],"41:16":[2],"41:17":[2],"41:18":[2],"41:19":[2],"41:20":[2],"41:21":[2],"41:22":[2],"41:23":[2],"42:0":[25,0,0],"42:1":[25,0,0],"42:2":[25,0,0],"43:0":[25,0,0],"43:1":[0,0,1],"43:2":[0,0,8],"43:3":[0,0,8],"43:4":[0,0,8],"43:5":[0,0,8],"43:6":[0,0,8],"43:7":[0,0,8],"43:8":[0,0,8],"43:9":[0,0,9],"43:10":[0,0,13],"43:11":[0,0,13],"43:12":[0,0,13],"43:13":[0,0,13],"43:14":[25,14,14],"43:15":[25,14,14],"43:16":[25,14,14],"44:0":[25,0,0],"44:1":[25,0,4],"44:2":[25,0,4],"44:3":[25,0,4],"44:4":[25,0,4],"44:5":[25,5,5],"44:6":[22,7,14],"44:7":[22,7,7],"44:8":[23,8,13],"44:9":[23,8,13],"44:10":[23,8,13],"44:11":[23,8,13],"44:12":[23,8,13],"44:13":[23,8,13],"44:14":[22,14,14],"44:15":[22,14,14],"44:16":[22,14,14],"45:0":[25,0,0],"45:1":[25,0,0],"45:2":[25,0,0],"46:0":[25,0,0],"46:1":[0,0,1],"46:2":[0,0,8],"46:3":[0,0,8],"46:4":[0,0,8],"46:5":[0,0,8],"46:6":[0,0,8],"46:7":[0,0,8],"46:8":[0,0,8],"46:9":[0,0,9],"46:10":[0,0,16],"46:11":[0,0,16],"46:12":[0,0,16],"46:13":[0,0,16],"46:14":[0,0,16],"46:15":[0,0,16],"46:16":[0,0,16],"46:17":[26,17,17],"46:18":[26,17,17],"46:19":[26,17,17],"47:0":[26,0,0],"47:1":[26,0,3],"47:2":[26,0,3],"47:3":[26,0,3],"47:4":[26,4,4],"47:5":[27,6,7],"47:6":[27,6,7],"47:7":[27,6,7],"47:8":[27,6,7],"47:9":[27,6,7],"48:0":[28,0,0],"48:1":[28,0,0],"48:2":[28,0,0],"49:0":[28,0,0],"49:1":[0,0,1],"49:2":[0,0,7],"49:3":[0,0,7],"49:4":[0,0,7],"49:5":[0,0,7],"49:6":[0,0,7],"49:7":[0,0,7],"49:8":[0,0,8],"49:9":[0,0,27],"49:10":[0,0,27],"49:11":[0,0,27],"49:12":[0,0,27],"49:13":[0,0,27],"49:14":[0,0,27],"49:15":[0,0,27],"49:16":[0,0,27],"49:17":[0,0,27],"49:18":[0,0,27],"49:19":[0,0,27],"49:20":[0,0,27],"49:21":[0,0,27],"49:22":[0,0,27],"49:23":[0,0,27],"49:24":[0,0,27],"49:25":[0,0,27],"49:26":[0,0,27],"49:27":[0,0,27],"49:28":[0,0,28],"49:29":[0,0,33],"49:30":[0,0,33],"49:31":[0,0,33],"49:32":[0,0,33],"49:33":[0,0,33],"49:34":[16,34,34],"49:35":[16,34,34],"49:36":[16,34,34],"50:0":[16,0,0],"50:1":[16,0,8],"50:2":[16,0,8],"50:3":[16,0,8],"50:4":[16,0,8],"50:5":[16,0,8],"50:6":[16,0,8],"50:7":[16,0,8],"50:8":[16,0,8],"50:9":[16,9,9],"50:10":[2],"50:11":[2],"50:12":[2],"50:13":[2],"50:14":[2],"50:15":[2],"50:16":[2],"50:17":[2],"50:18":[2],"50:19":[2],"50:20":[2],"50:21":[2],"50:22":[2],"50:23":[16,23,23],"50:24":[16,23,23],"50:25":[16,23,23],"51:0":[16,0,0],"51:1":[16,0,0],"51:2":[16,0,0],"52:0":[16,0,0],"52:1":[0,0,1],"52:2":[0,0,7],"52:3":[0,0,7],"52:4":[0,0,7],"52:5":[0,0,7],"52:6":[0,0,7],"52:7":[0,0,7],"52:8":[0,0,8],"52:9":[0,0,22],"52:10":[0,0,22],"52:11":[0,0,22],"52:12":[0,0,22],"52:13":[0,0,22],"52:14":[0,0,22],"52:15":[0,0,22],"52:16":[0,0,22],"52:17":[0,0,22],"52:18":[0,0,22],"52:19":[0,0,22],"52:20":[0,0,22],"52:21":[0,0,22],"52:22":[0,0,22],"52:23":[29,23,23],"52:24":[29,23,23],"52:25":[29,23,23],"53:0":[29,0,0],"53:1":[29,0,8],"53:2":[29,0,8],"53:3":[29,0,8],"53:4":[29,0,8],"53:5":[29,0,8],"53:6":[29,0,8],"53:7":[29,0,8],"53:8":[29,0,8],"53:9":[29,9,9],"53:10":[2],"53:11":[2],"53:12":[2],"53:13":[2],"53:14":[2],"53:15":[2],"53:16":[2],"53:17":[2],"53:18":[2],"53:19":[2],"53:20":[2],"53:21":[2],"53:22":[2],"53:23":[2],"53:24":[2],"53:25":[2],"53:26":[2],"53:27":[2],"53:28":[2],"53:29":[2],"53:30":[2],"53:31":[2],"53:32":[2],"53:33":[2],"53:34":[2],"53:35":[2],"53:36":[2],"53:37":[2],"53:38":[2],"53:39":[2],"53:40":[2],"53:41":[2],"53:42":[2],"53:43":[2],"53:44":[2],"53:45":[2],"53:46":[2],"53:47":[2],"54:0":[30,0,0],"54:1":[30,0,0],"54:2":[30,0,0],"55:0":[30,0,0],"55:1":[0,0,1],"55:2":[0,0,10],"55:3":[0,0,10],"55:4":[0,0,10],"55:5":[0,0,10],"55:6":[0,0,10],"55:7":[0,0,10],"55:8":[0,0,10],"55:9":[0,0,10],"55:10":[0,0,10],"55:11":[31,11,11],"55:12":[31,11,11],"55:13":[31,11,11],"56:0":[31,0,0],"56:1":[31,0,7],"56:2":[31,0,7],"56:3":[31,0,7],"56:4":[31,0,7],"56:5":[31,0,7],"56:6":[31,0,7],"56:7":[31,0,7],"56:8":[31,8,8],"56:9":[2],"56:10":[2],"56:11":[2],"56:12":[2],"56:13":[2],"56:14":[2],"56:15":[2],"56:16":[2],"56:17":[2],"56:18":[2],"56:19":[2],"56:20":[31,20,20],"56:21":[31,20,20],"56:22":[31,20,20],"57:0":[32,0,0],"57:1":[32,0,0],"57:2":[32,0,0],"58:0":[32,0,0],"58:1":[0,0,1],"58:2":[0,0,10],"58:3":[0,0,10],"58:4":[0,0,10],"58:5":[0,0,10],"58:6":[0,0,10],"58:7":[0,0,10],"58:8":[0,0,10],"58:9":[0,0,10],"58:10":[0,0,10],"58:11":[0,0,11],"58:12":[0,0,23],"58:13":[0,0,23],"58:14":[0,0,23],"58:15":[0,0,23],"58:16":[0,0,23],"58:17":[0,0,23],"58:18":[0,0,23],"58:19":[0,0,23],"58:20":[0,0,23],"58:21":[0,0,23],"58:22":[0,0,23],"58:23":[0,0,23],"58:24":[17,24,24],"58:25":[17,24,24],"58:26":[17,24,24],"59:0":[17,0,0],"59:1":[17,0,6],"59:2":[17,0,6],"59:3":[17,0,6],"59:4":[17,0,6],"59:5":[17,0,6],"59:6":[17,0,6],"59:7":[17,7,7],"59:8":[2],"59:9":[2],"59:10":[2],"59:11":[2],"59:12":[2],"59:13":[2],"59:14":[2],"59:15":[2],"59:16":[2],"59:17":[2],"59:18":[2],"60:0":[17,0,0],"60:1":[17,0,0],"60:2":[17,0,0],"61:0":[17,0,0],"61:1":[0,0,1],"61:2":[0,0,7],"61:3":[0,0,7],"61:4":[0,0,7],"61:5":[0,0,7],"61:6":[0,0,7],"61:7":[0,0,7],"61:8":[33,8,8],"61:9":[33,8,8],"61:10":[33,8,8],"62:0":[33,0,0],"62:1":[33,0,4],"62:2":[33,0,4],"62:3":[33,0,4],"62:4":[33,0,4],"62:5":[33,5,5],"62:6":[2],"62:7":[2],"62:8":[2],"62:9":[2],"62:10":[2],"62:11":[2],"62:12":[2],"62:13":[2],"62:14":[2],"62:15":[2],"62:16":[33,16,16],"62:17":[33,16,16],"62:18":[33,16,16],"63:0":[34,0,0],"63:1":[34,0,0],"63:2":[34,0,0],"64:0":[34,0,0],"64:1":[0,0,1],"64:2":[0,0,7],"64:3":[0,0,7],"64:4":[0,0,7],"64:5":[0,0,7],"64:6":[0,0,7],"64:7":[0,0,7],"64:8":[0,0,8],"64:9":[0,0,17],"64:10":[0,0,17],"64:11":[0,0,17],"64:12":[0,0,17],"64:13":[0,0,17],"64:14":[0,0,17],"64:15":[0,0,17],"64:16":[0,0,17],"64:17":[0,0,17],"64:18":[35,18,18],"64:19":[35,18,18],"64:20":[35,18,18],"65:0":[35,0,0],"65:1":[35,0,4],"65:2":[35,0,4],"65:3":[35,0,4],"65:4":[35,0,4],"65:5":[35,5,5],"65:6":[2],"65:7":[2],"65:8":[2],"65:9":[2],"65:10":[2],"65:11":[2],"65:12":[2],"65:13":[2],"65:14":[2],"65:15":[2],"65:16":[2],"65:17":[2],"65:18":[2],"66:0":[35,0,0],"66:1":[35,0,0],"66:2":[35,0,0],"67:0":[35,0,0],"67:1":[0,0,1],"67:2":[0,0,10],"67:3":[0,0,10],"67:4":[0,0,10],"67:5":[0,0,10],"67:6":[0,0,10],"67:7":[0,0,10],"67:8":[0,0,10],"67:9":[0,0,10],"67:10":[0,0,10],"67:11":[2],"67:12":[2],"67:13":[2],"68:0":[2],"68:1":[2],"68:2":[2],"68:3":[2],"68:4":[2],"68:5":[2],"68:6":[2],"68:7":[2],"68:8":[2],"68:9":[2],"68:10":[2],"68:11":[2],"68:12":[2],"68:13":[2],"68:14":[2],"68:15":[2],"68:16":[2],"68:17":[2],"68:18":[2],"68:19":[2],"68:20":[2],"68:21":[2],"68:22":[2],"68:23":[2],"69:0":[2],"69:1":[2],"69:2":[2],"70:0":[2],"70:1":[0,0,1],"70:2":[0,0,5],"70:3":[0,0,5],"70:4":[0,0,5],"70:5":[0,0,5],"70:6":[36,6,6],"70:7":[36,6,6],"70:8":[36,6,6],"71:0":[36,0,0],"71:1":[36,0,6],"71:2":[36,0,6],"71:3":[36,0,6],"71:4":[36,0,6],"71:5":[36,0,6],"71:6":[36,0,6],"71:7":[36,7,7],"71:8":[2],"71:9":[2],"71:10":[2],"71:11":[2],"71:12":[2],"71:13":[2],"71:14":[2],"71:15":[2],"71:16":[2],"72:0":[37,0,0],"72:1":[37,0,0],"72:2":[37,0,0],"73:0":[37,0,0],"73:1":[0,0,1],"73:2":[0,0,8],"73:3":[0,0,8],"73:4":[0,0,8],"73:5":[0,0,8],"73:6":[0,0,8],"73:7":[0,0,8],"73:8":[0,0,8],"73:9":[38,9,9],"73:10":[38,9,9],"73:11":[38,9,9],"74:0":[38,0,0],"74:1":[38,0,9],"74:2":[38,0,9],"74:3":[38,0,9],"74:4":[38,0,9],"74:5":[38,0,9],"74:6":[38,0,9],"74:7":[38,0,9],"74:8":[38,0,9],"74:9":[38,0,9],"74:10":[38,10,10],"74:11":[2],"74:12":[2],"74:13":[2],"74:14":[2],"74:15":[2],"74:16":[2],"74:17":[38,17,17],"74:18":[38,17,17],"74:19":[38,17,17],"75:0":[39,0,0],"75:1":[39,0,0],"75:2":[39,0,0],"76:0":[39,0,0],"76:1":[39,0,12],"76:2":[39,0,12],"76:3":[39,0,12],"76:4":[39,0,12],"76:5":[39,0,12],"76:6":[39,0,12],"76:7":[39,0,12],"76:8":[39,0,12],"76:9":[39,0,12],"76:10":[39,0,12],"76:11":[39,0,12],"76:12":[39,0,12],"76:13":[39,13,13],"76:14":[2],"76:15":[2],"76:16":[2],"76:17":[2],"76:18":[2],"76:19":[2],"76:20":[2],"77:0":[2],"77:1":[2],"77:2":[2],"77:3":[2],"77:4":[2],"77:5":[2],"77:6":[2],"77:7":[2],"77:8":[2],"77:9":[2],"77:10":[2],"77:11":[2],"77:12":[2],"77:13":[2],"77:14":[2],"78:0":[2],"78:1":[2],"78:2":[2],"78:3":[2],"78:4":[2],"78:5":[2],"78:6":[2],"78:7":[2],"78:8":[2],"78:9":[2],"78:10":[2],"78:11":[2],"78:12":[2],"78:13":[2],"78:14":[2],"78:15":[2],"78:16":[2],"78:17":[2],"78:18":[2],"78:19":[2],"78:20":[2],"78:21":[2],"78:22":[2],"78:23":[2],"78:24":[2],"78:25":[2],"78:26":[2],"78:27":[2],"78:28":[2],"78:29":[2],"78:30":[2],"79:0":[2],"79:1":[2],"79:2":[2],"79:3":[2],"79:4":[2],"79:5":[2],"80:0":[39,0,0],"80:1":[39,0,0],"80:2":[39,0,0]},"hover":{"1:2":0,"1:3":0,"1:4":0,"1:5":0,"1:6":0,"1:7":0,"1:8":0,"2:1":1,"2:2":1,"2:3":1,"2:4":1,"2:5":1,"2:6":1,"2:7":1,"2:8":1,"2:9":1,"2:10":1,"2:11":1,"2:12":1,"3:1":2,"3:2":2,"3:3":2,"3:4":2,"3:5":2,"3:6":2,"3:7":2,"3:8":2,"3:9":2,"3:10":2,"3:11":2,"3:12":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"4:7":3,"4:8":3,"4:9":3,"4:10":3,"4:11":3,"4:12":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":4,"5:11":4,"5:12":4,"5:13":5,"5:14":6,"5:15":5,"5:16":5,"5:17":5,"5:18":5,"5:19":5,"5:20":5,"5:21":6,"5:22":6,"5:23":6,"6:1":7,"6:2":7,"6:3":7,"6:4":7,"6:5":7,"6:6":7,"6:7":7,"6:8":7,"6:9":7,"6:10":7,"6:11":7,"6:12":7,"7:1":8,"7:2":8,"7:3":8,"7:4":8,"7:5":8,"7:6":8,"7:7":8,"7:8":8,"7:9":8,"7:10":8,"7:11":8,"7:12":8,"9:2":9,"9:3":9,"9:4":9,"9:5":9,"9:6":9,"10:1":10,"10:2":10,"10:3":10,"10:4":10,"10:5":10,"10:6":10,"10:7":10,"10:8":10,"10:9":10,"11:1":11,"11:2":11,"11:3":11,"11:4":11,"11:5":11,"11:6":11,"11:7":11,"11:8":11,"11:9":11,"11:10":11,"11:11":11,"11:12":11,"11:13":11,"11:14":12,"11:15":13,"11:16":12,"11:17":12,"11:18":12,"11:19":12,"11:20":12,"11:21":13,"11:22":13,"11:23":13,"12:1":14,"12:2":14,"12:3":14,"12:4":14,"12:5":14,"12:6":14,"12:7":14,"12:8":14,"12:9":14,"17:2":15,"17:3":15,"17:4":15,"17:5":15,"17:6":15,"17:7":15,"17:8":15,"17:9":15,"17:10":15,"17:11":15,"17:12":15,"17:13":15,"25:2":15,"25:3":15,"25:4":15,"25:5":15,"25:6":15,"25:7":15,"25:8":15,"25:9":15,"25:10":15,"25:11":15,"25:12":15,"25:13":15,"25:14":15,"25:15":15,"25:16":15,"25:17":15,"25:18":15,"25:19":15,"25:20":15,"25:21":15,"25:22":15,"28:2":16,"28:3":16,"28:4":16,"28:5":16,"28:6":16,"28:7":16,"28:8":16,"28:9":16,"28:10":16,"28:11":16,"28:12":16,"28:13":16,"28:14":16,"28:15":16,"28:16":16,"28:17":16,"31:2":17,"31:3":17,"31:4":17,"31:5":17,"31:6":17,"31:7":17,"31:8":17,"31:9":17,"32:1":18,"32:2":18,"32:3":18,"32:4":18,"32:5":18,"32:6":18,"32:7":18,"32:8":18,"36:2":17,"36:3":17,"36:4":17,"36:5":17,"36:6":17,"36:7":17,"36:8":17,"36:9":17,"36:10":17,"36:11":17,"36:12":17,"36:13":17,"36:14":17,"37:1":19,"37:2":19,"37:3":19,"37:4":19,"37:5":19,"37:6":19,"37:7":19,"37:8":19,"39:2":20,"39:3":20,"39:4":20,"39:5":20,"39:6":20,"39:7":20,"39:8":20,"39:9":20,"39:10":20,"39:11":20,"39:12":20,"40:1":21,"40:2":21,"40:3":21,"40:4":21,"40:5":21,"40:6":22,"40:7":23,"40:8":22,"40:9":22,"40:10":22,"40:11":22,"40:12":23,"40:13":23,"40:14":23,"41:1":24,"41:2":24,"41:3":24,"41:4":24,"41:5":24,"43:2":20,"43:3":20,"43:4":20,"43:5":20,"43:6":20,"43:7":20,"43:8":20,"43:9":20,"43:10":20,"43:11":20,"43:12":20,"43:13":20,"44:1":21,"44:2":21,"44:3":21,"44:4":21,"44:5":21,"44:6":25,"44:7":23,"44:8":25,"44:9":25,"44:10":25,"44:11":25,"44:12":25,"44:13":25,"44:14":23,"44:15":23,"44:16":23,"46:2":26,"46:3":26,"46:4":26,"46:5":26,"46:6":26,"46:7":26,"46:8":26,"46:9":26,"46:10":26,"46:11":26,"46:12":26,"46:13":26,"46:14":26,"46:15":26,"46:16":26,"47:1":27,"47:2":27,"47:3":27,"47:4":27,"47:5":28,"47:6":28,"47:7":28,"47:8":28,"47:9":28,"49:2":29,"49:3":29,"49:4":29,"49:5":29,"49:6":29,"49:7":29,"49:8":29,"49:9":29,"49:10":29,"49:11":29,"49:12":29,"49:13":29,"49:14":29,"49:15":29,"49:16":29,"49:17":29,"49:18":29,"49:19":29,"49:20":29,"49:21":29,"49:22":29,"49:23":29,"49:24":29,"49:25":29,"49:26":29,"49:27":29,"49:28":29,"49:29":9,"49:30":9,"49:31":9,"49:32":9,"49:33":9,"50:1":10,"50:2":10,"50:3":10,"50:4":10,"50:5":10,"50:6":10,"50:7":10,"50:8":10,"50:9":10,"52:2":29,"52:3":29,"52:4":29,"52:5":29,"52:6":29,"52:7":29,"52:8":29,"52:9":29,"52:10":29,"52:11":29,"52:12":29,"52:13":29,"52:14":29,"52:15":29,"52:16":29,"52:17":29,"52:18":29,"52:19":29,"52:20":29,"52:21":29,"52:22":29,"53:1":30,"53:2":30,"53:3":30,"53:4":30,"53:5":30,"53:6":30,"53:7":30,"53:8":30,"53:9":30,"55:2":31,"55:3":31,"55:4":31,"55:5":31,"55:6":31,"55:7":31,"55:8":31,"55:9":31,"55:10":31,"56:1":32,"56:2":32,"56:3":32,"56:4":32,"56:5":32,"56:6":32,"56:7":32,"56:8":32,"58:2":31,"58:3":31,"58:4":31,"58:5":31,"58:6":31,"58:7":31,"58:8":31,"58:9":31,"58:10":31,"58:11":31,"61:2":33,"61:3":33,"61:4":33,"61:5":33,"61:6":33,"61:7":33,"62:1":34,"62:2":34,"62:3":34,"62:4":34,"62:5":34,"64:2":33,"64:3":33,"64:4":33,"64:5":33,"64:6":33,"64:7":33,"64:8":33,"64:9":35,"64:10":35,"64:11":35,"64:12":35,"64:13":35,"64:14":35,"64:15":35,"64:16":35,"64:17":35,"67:2":36,"67:3":36,"67:4":36,"67:5":36,"67:6":36,"67:7":36,"67:8":36,"67:9":36,"67:10":36,"70:2":37,"70:3":37,"70:4":37,"70:5":37,"71:1":38,"71:2":38,"71:3":38,"71:4":38,"71:5":38,"71:6":38,"71:7":38,"73:2":39,"73:3":39,"73:4":39,"73:5":39,"73:6":39,"73:7":39,"73:8":39,"74:1":40,"74:2":40,"74:3":40,"74:4":40,"74:5":40,"74:6":40,"74:7":40,"74:8":40,"74:9":40,"74:10":40},"definition":{"20:17":{"line":2,"startCharacter":0,"endCharacter":9},"20:18":{"line":2,"startCharacter":0,"endCharacter":9},"20:19":{"line":2,"startCharacter":0,"endCharacter":9},"20:20":{"line":2,"startCharacter":0,"endCharacter":9},"20:21":{"line":2,"startCharacter":0,"endCharacter":9},"20:24":{"line":2,"startCharacter":0,"endCharacter":9},"20:25":{"line":2,"startCharacter":0,"endCharacter":9},"20:26":{"line":2,"startCharacter":0,"endCharacter":9},"20:27":{"line":2,"startCharacter":0,"endCharacter":9},"20:28":{"line":2,"startCharacter":0,"endCharacter":9},"20:29":{"line":2,"startCharacter":0,"endCharacter":9},"20:30":{"line":2,"startCharacter":0,"endCharacter":9},"20:31":{"line":2,"startCharacter":0,"endCharacter":9},"20:32":{"line":2,"startCharacter":0,"endCharacter":9},"22:17":{"line":59,"startCharacter":0,"endCharacter":6},"22:18":{"line":59,"startCharacter":0,"endCharacter":6},"22:19":{"line":59,"startCharacter":0,"endCharacter":6},"22:20":{"line":59,"startCharacter":0,"endCharacter":6},"22:21":{"line":59,"startCharacter":0,"endCharacter":6},"22:22":{"line":59,"startCharacter":0,"endCharacter":6},"22:23":{"line":59,"startCharacter":0,"endCharacter":6},"22:24":{"line":59,"startCharacter":0,"endCharacter":6},"22:25":{"line":59,"startCharacter":0,"endCharacter":6},"22:26":{"line":59,"startCharacter":0,"endCharacter":6},"22:29":{"line":59,"startCharacter":0,"endCharacter":6},"22:30":{"line":59,"startCharacter":0,"endCharacter":6},"22:31":{"line":59,"startCharacter":0,"endCharacter":6},"22:32":{"line":59,"startCharacter":0,"endCharacter":6},"22:33":{"line":59,"startCharacter":0,"endCharacter":6},"23:54":{"line":36,"startCharacter":0,"endCharacter":15},"23:55":{"line":36,"startCharacter":0,"endCharacter":15},"23:56":{"line":36,"startCharacter":0,"endCharacter":15},"23:57":{"line":36,"startCharacter":0,"endCharacter":15},"23:58":{"line":36,"startCharacter":0,"endCharacter":15},"23:59":{"line":36,"startCharacter":0,"endCharacter":15},"23:60":{"line":36,"startCharacter":0,"endCharacter":15}},"versionContext":{"18:14":{"name":"fmt","insideString":false,"range":{"startCharacter":15,"endCharacter":23}},"18:15":{"name":"fmt","insideString":false,"range":{"startCharacter":15,"endCharacter":15}},"18:16":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:17":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:18":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:19":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:20":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:21":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:22":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:23":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"18:24":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"18:25":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"19:26":{"name":"compat","insideString":false,"range":{"startCharacter":27,"endCharacter":34}},"19:27":{"name":"compat","insideString":false,"range":{"startCharacter":27,"endCharacter":27}},"19:28":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:29":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:30":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:31":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:32":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:33":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:34":{"name":"compat","insideString":false,"range":{"startCharacter":34,"endCharacter":34}},"23:26":{"name":"simdlib","insideString":false,"range":{"startCharacter":27,"endCharacter":32}},"23:27":{"name":"simdlib","insideString":false,"range":{"startCharacter":27,"endCharacter":27}},"23:28":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:29":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:30":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:31":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:32":{"name":"simdlib","insideString":false,"range":{"startCharacter":32,"endCharacter":32}},"26:4":{"name":"os","insideString":false,"range":{"startCharacter":5,"endCharacter":12}},"26:5":{"name":"os","insideString":false,"range":{"startCharacter":5,"endCharacter":5}},"26:6":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:7":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:8":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:9":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:10":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:11":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:12":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"26:13":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"26:14":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"29:7":{"name":"gtest","insideString":false,"range":{"startCharacter":8,"endCharacter":16}},"29:8":{"name":"gtest","insideString":false,"range":{"startCharacter":8,"endCharacter":8}},"29:9":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:10":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:11":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:12":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:13":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:14":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:15":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:16":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"29:17":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"29:18":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:8":{"name":"shared","insideString":false,"range":{"startCharacter":9,"endCharacter":16}},"59:9":{"name":"shared","insideString":false,"range":{"startCharacter":9,"endCharacter":9}},"59:10":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:11":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:12":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:13":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:14":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:15":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:16":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:17":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:18":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}}},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"c++20"},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"c++23"},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"c++26"},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"c++2a"},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"c++2c"},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"gnu++20"},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"gnu++23"},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"gnu++26"},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"c++latest"},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"c++fly"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"auto"},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"on"},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"off"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"name = \"version\"","kind":"template","detail":"SemVer version dependency","documentation":"Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported.","insertSnippet":"${1:name} = \"${2:1.0.0}\""},{"label":"name = { path = ... }","kind":"template","detail":"Path dependency (local development)","insertSnippet":"${1:name} = { path = \"${2:../mylib}\" }"},{"label":"name = { git = ..., tag = ... }","kind":"template","detail":"Git dependency (one of tag / branch / rev)","insertSnippet":"${1:name} = { git = \"${2:https://github.com/user/repo.git}\", tag = \"${3:v1.0.0}\" }"},{"label":"name = { version = ..., features = [...] }","kind":"template","detail":"Long dep spec: request a feature of that dependency","insertSnippet":"${1:name} = { version = \"${2:1.0}\", features = [\"${3:feature}\"] }"},{"label":"name = { version = ..., tools = [...] }","kind":"template","detail":"Host tools produced by the dependency (must be a bin target of that package)","insertSnippet":"${1:name} = { version = \"${2:1.0}\", tools = [\"${3:protoc}\"] }"}],[{"label":"default","kind":"template","detail":"array","insertSnippet":"default = []","documentation":"the features a consumer gets unless it opts out Since 0.0.47."},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []","documentation":"[features.]: sources compiled only when the feature is active Since 0.0.95."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []","documentation":"[features.]: sources compiled only when the feature is active Since 0.0.95."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"opt","kind":"template","detail":"enum: s | z","insertSnippet":"opt = \"s\"","documentation":"an -O level: a number, or \"s\"/\"z\""},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}],[{"label":"s","kind":"template","detail":"value of opt","insertSnippet":"\"s\""},{"label":"z","kind":"template","detail":"value of opt","insertSnippet":"\"z\""}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}],[{"label":"carrier","kind":"template","detail":"table","insertSnippet":"carrier = { }","documentation":"{ package, section, main }: the program mcpp pack builds of a loaded artifact for a system (docs/35) Since 2026.10.27.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled","insertSnippet":"cxx_runtime = \"self-contained\""},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\""},{"label":"min_api_level","kind":"template","detail":"number","insertSnippet":"min_api_level = 0"},{"label":"platform","kind":"template","detail":"string","insertSnippet":"platform = \"\"","documentation":"the platform of the selected targets: \"@\", { path = \"...\" }, { git = ... }, or \"none\"; not accepted under a cfg(platform = ...) selector Since 2026.10.27.1."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"the target-axis form of [package] requires, unioned in when the selector matches Since 2026.10.26.1."},{"label":"run_under","kind":"template","detail":"array","insertSnippet":"run_under = []","documentation":"an argv placed before the runner: what reaches the machine the artifact runs on Since 2026.10.24.1."},{"label":"runner","kind":"template","detail":"array","insertSnippet":"runner = []","documentation":"argv template for mcpp run/test on this target, or { cmd, host = \"same-arch\" | \"any\", exclusive } (2026.10.27.1+)"},{"label":"runners","kind":"template","detail":"table","insertSnippet":"runners = { }","documentation":"named runners, = [argv]; also written as [target..runners] Since 2026.9.4.2."},{"label":"sysroot","kind":"template","detail":"string","insertSnippet":"sysroot = \"\""},{"label":"toolchain","kind":"template","detail":"string","insertSnippet":"toolchain = \"\""}],[{"label":"carrier","kind":"template","detail":"table","insertSnippet":"carrier = { }","documentation":"{ package, section, main }: the program mcpp pack builds of a loaded artifact for a system (docs/35) Since 2026.10.27.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled","insertSnippet":"cxx_runtime = \"self-contained\""},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\""},{"label":"min_api_level","kind":"template","detail":"number","insertSnippet":"min_api_level = 0"},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"the target-axis form of [package] requires, unioned in when the selector matches Since 2026.10.26.1."},{"label":"run_under","kind":"template","detail":"array","insertSnippet":"run_under = []","documentation":"an argv placed before the runner: what reaches the machine the artifact runs on Since 2026.10.24.1."},{"label":"runner","kind":"template","detail":"array","insertSnippet":"runner = []","documentation":"argv template for mcpp run/test on this target, or { cmd, host = \"same-arch\" | \"any\", exclusive } (2026.10.27.1+)"},{"label":"runners","kind":"template","detail":"table","insertSnippet":"runners = { }","documentation":"named runners, = [argv]; also written as [target..runners] Since 2026.9.4.2."},{"label":"sysroot","kind":"template","detail":"string","insertSnippet":"sysroot = \"\""},{"label":"toolchain","kind":"template","detail":"string","insertSnippet":"toolchain = \"\""}],[{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []","documentation":"member globs left out Since 0.0.11."},{"label":"members","kind":"template","detail":"array","insertSnippet":"members = []","documentation":"member directories or globs Since 0.0.11."}],[{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []","documentation":"member globs left out Since 0.0.11."}],[{"label":"deps","kind":"template","detail":"array","insertSnippet":"deps = []","documentation":"the project's host build tools: \"@\" or tables Since 0.0.77."},{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"tool = \"version\"","kind":"template","detail":"Pin the xlings tool version","insertSnippet":"${1:node} = \"${2:24.19.0}\""}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"format","kind":"template","detail":"string","insertSnippet":"format = \"\"","documentation":"the default of --pkg-format: tar, zip, dir, single-file, or a format a package provides Since 2026.10.27.1."},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"artifacts","kind":"template","detail":"array","insertSnippet":"artifacts = []","documentation":"[[runtime.artifacts]]: prebuilt artifacts the package provides Since 2026.8.10.1."},{"label":"capabilities","kind":"template","detail":"array","insertSnippet":"capabilities = []","documentation":"Since 0.0.46."},{"label":"deploy","kind":"template","detail":"array","insertSnippet":"deploy = []","documentation":"[[runtime.deploy]]: { from, to } files placed beside the artifact Since 2026.9.12.2."},{"label":"deploy_files","kind":"template","detail":"array","insertSnippet":"deploy_files = []","documentation":"files placed beside the artifact Since 2026.8.10.1."},{"label":"dlopen_libs","kind":"template","detail":"array","insertSnippet":"dlopen_libs = []","documentation":"libraries the program opens at run time Since 0.0.46."},{"label":"frameworks","kind":"template","detail":"array","insertSnippet":"frameworks = []","documentation":"Since 2026.8.10.1."},{"label":"libraries","kind":"template","detail":"array","insertSnippet":"libraries = []","documentation":"Since 2026.8.10.1."},{"label":"library_dirs","kind":"template","detail":"array","insertSnippet":"library_dirs = []","documentation":"Since 0.0.46."},{"label":"link_library_dirs","kind":"template","detail":"array","insertSnippet":"link_library_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"Since 0.0.48."},{"label":"requirements","kind":"template","detail":"array","insertSnippet":"requirements = []","documentation":"[[runtime.requirements]]: { kind, value, phase, required, discovery } Since 2026.8.10.1."},{"label":"runtime_search_dirs","kind":"template","detail":"array","insertSnippet":"runtime_search_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"transitive_needed_dirs","kind":"template","detail":"array","insertSnippet":"transitive_needed_dirs = []","documentation":"Since 2026.8.10.1."}],[{"label":"artifacts","kind":"template","detail":"array","insertSnippet":"artifacts = []","documentation":"[[runtime.artifacts]]: prebuilt artifacts the package provides Since 2026.8.10.1."},{"label":"capabilities","kind":"template","detail":"array","insertSnippet":"capabilities = []","documentation":"Since 0.0.46."},{"label":"deploy","kind":"template","detail":"array","insertSnippet":"deploy = []","documentation":"[[runtime.deploy]]: { from, to } files placed beside the artifact Since 2026.9.12.2."},{"label":"deploy_files","kind":"template","detail":"array","insertSnippet":"deploy_files = []","documentation":"files placed beside the artifact Since 2026.8.10.1."},{"label":"dlopen_libs","kind":"template","detail":"array","insertSnippet":"dlopen_libs = []","documentation":"libraries the program opens at run time Since 0.0.46."},{"label":"frameworks","kind":"template","detail":"array","insertSnippet":"frameworks = []","documentation":"Since 2026.8.10.1."},{"label":"library_dirs","kind":"template","detail":"array","insertSnippet":"library_dirs = []","documentation":"Since 0.0.46."},{"label":"link_library_dirs","kind":"template","detail":"array","insertSnippet":"link_library_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"Since 0.0.48."},{"label":"requirements","kind":"template","detail":"array","insertSnippet":"requirements = []","documentation":"[[runtime.requirements]]: { kind, value, phase, required, discovery } Since 2026.8.10.1."},{"label":"runtime_search_dirs","kind":"template","detail":"array","insertSnippet":"runtime_search_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"transitive_needed_dirs","kind":"template","detail":"array","insertSnippet":"transitive_needed_dirs = []","documentation":"Since 2026.8.10.1."}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"version","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"mcpp","body":"type: `string`\n\nsince: 2026.9.28.3\n\nrelease floor, only the \">=\" form is accepted (docs/04 §2.1)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"c++23","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"description","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"authors","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"cxxflags","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"auto","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"sources","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"[dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[dev-dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[features]","body":"plane: `gate`\n\nkeys: 12","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"default","body":"type: `array`\n\nsince: 0.0.47\n\nthe features a consumer gets unless it opts out","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"sources","body":"type: `array`\n\nsince: 0.0.95\n\n[features.]: sources compiled only when the feature is active","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"bin","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"main","body":"type: `string`\n\na file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"entry","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[profile]","body":"plane: `compile`\n\nkeys: 10","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"opt","body":"type: `enum`\n\nvalues: `s` | `z`\n\nan -O level: a number, or \"s\"/\"z\"\n\nunmodelled: the type is inferred from the docs, not read from mcpp","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"3","body":"enum value of `opt`\n\none of: `s` | `z`\n\nan -O level: a number, or \"s\"/\"z\"","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"[target]","body":"plane: `condition`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#271-target--platform-conditional-dependencies--flags"},{"title":"platform","body":"type: `string`\n\nsince: 2026.10.27.1\n\nthe platform of the selected targets: \"@\", { path = \"...\" }, { git = ... }, or \"none\"; not accepted under a cfg(platform = ...) selector","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#271-target--platform-conditional-dependencies--flags"},{"title":"[workspace]","body":"plane: `unclassified`\n\nkeys: 2","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#218-workspace--tables-a-workspace-shares-202610101"},{"title":"members","body":"type: `array`\n\nsince: 0.0.11\n\nmember directories or globs","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#218-workspace--tables-a-workspace-shares-202610101"},{"title":"[xlings]","body":"plane: `tool`\n\nkeys: 4","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"deps","body":"type: `array`\n\nsince: 0.0.77\n\nthe project's host build tools: \"@\" or tables\n\nunmodelled: the type is inferred from the docs, not read from mcpp","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[xlings.workspace]","body":"plane: `tool`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[toolchain]","body":"plane: `compile`\n\nkeys: 3 (open: the user picks the names)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#27-toolchain--toolchain-configuration"},{"title":"[pack]","body":"plane: `unclassified`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"format","body":"type: `string`\n\nsince: 2026.10.27.1\n\nthe default of --pkg-format: tar, zip, dir, single-file, or a format a package provides","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"[runtime]","body":"plane: `metadata`\n\nkeys: 13","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#211-runtime--provider-neutral-runtime-contract"},{"title":"libraries","body":"type: `array`\n\nsince: 2026.8.10.1","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#211-runtime--provider-neutral-runtime-contract"}]} +{"diagnostics":{"default":[{"code":"mcpp.manifest.unknownKey","message":"unknown key simd in [features]","severity":"warning","line":34,"startCharacter":1,"endCharacter":5,"relatedKey":"simd"},{"code":"mcpp.manifest.unknownKey","message":"unknown key extra in [features]","severity":"warning","line":35,"startCharacter":1,"endCharacter":6,"relatedKey":"extra"},{"code":"mcpp.manifest.unknownKey","message":"unknown key description2 in [runtime]","severity":"warning","line":77,"startCharacter":1,"endCharacter":13,"relatedKey":"description2"}],"quiet":[{"code":"mcpp.manifest.unknownKey","message":"unknown key simd in [features]","severity":"info","line":34,"startCharacter":1,"endCharacter":5,"relatedKey":"simd"},{"code":"mcpp.manifest.unknownKey","message":"unknown key extra in [features]","severity":"info","line":35,"startCharacter":1,"endCharacter":6,"relatedKey":"extra"},{"code":"mcpp.manifest.unknownKey","message":"unknown key description2 in [runtime]","severity":"info","line":77,"startCharacter":1,"endCharacter":13,"relatedKey":"description2"}]},"completion":{"0:0":[0,0,0],"0:1":[0,1,1],"0:2":[0,2,2],"0:3":[0,3,3],"0:4":[0,4,4],"0:5":[0,5,5],"0:6":[0,6,6],"0:7":[0,7,7],"0:8":[0,8,8],"0:9":[0,9,9],"0:10":[0,10,10],"0:11":[0,11,11],"0:12":[0,12,12],"0:13":[0,13,13],"0:14":[0,14,14],"0:15":[0,15,15],"0:16":[0,16,16],"0:17":[0,17,17],"0:18":[0,18,18],"0:19":[0,19,19],"0:20":[0,20,20],"0:21":[0,21,21],"0:22":[0,22,22],"0:23":[0,23,23],"0:24":[0,24,24],"0:25":[0,25,25],"0:26":[0,26,26],"0:27":[0,27,27],"0:28":[0,28,28],"0:29":[0,29,29],"0:30":[0,30,30],"0:31":[0,31,31],"0:32":[0,32,32],"0:33":[0,33,33],"0:34":[0,34,34],"0:35":[0,35,35],"0:36":[0,36,36],"0:37":[0,37,37],"0:38":[0,38,38],"0:39":[0,39,39],"0:40":[0,40,40],"0:41":[0,41,41],"0:42":[0,42,42],"0:43":[0,43,43],"0:44":[0,44,44],"0:45":[0,45,45],"0:46":[0,46,46],"0:47":[0,47,47],"0:48":[0,47,47],"0:49":[0,47,47],"1:0":[0,0,0],"1:1":[0,0,1],"1:2":[0,0,8],"1:3":[0,0,8],"1:4":[0,0,8],"1:5":[0,0,8],"1:6":[0,0,8],"1:7":[0,0,8],"1:8":[0,0,8],"1:9":[1,9,9],"1:10":[1,9,9],"1:11":[1,9,9],"2:0":[1,0,0],"2:1":[1,0,4],"2:2":[1,0,4],"2:3":[1,0,4],"2:4":[1,0,4],"2:5":[1,5,5],"2:6":[1,6,6],"2:7":[1,7,7],"2:8":[1,8,8],"2:9":[1,9,9],"2:10":[1,10,10],"2:11":[1,11,11],"2:12":[1,12,12],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"2:18":[2],"2:19":[2],"2:20":[2],"2:21":[2],"2:22":[2],"2:23":[2],"2:24":[2],"2:25":[2],"2:26":[2],"3:0":[3,0,0],"3:1":[3,0,7],"3:2":[3,0,7],"3:3":[3,0,7],"3:4":[3,0,7],"3:5":[3,0,7],"3:6":[3,0,7],"3:7":[3,0,7],"3:8":[3,8,8],"3:9":[3,9,9],"3:10":[3,10,10],"3:11":[3,11,11],"3:12":[3,12,12],"3:13":[2],"3:14":[2],"3:15":[2],"3:16":[2],"3:17":[2],"3:18":[2],"3:19":[2],"3:20":[2],"3:21":[2],"3:22":[2],"3:23":[2],"4:0":[4,0,0],"4:1":[4,0,4],"4:2":[4,0,4],"4:3":[4,0,4],"4:4":[4,0,4],"4:5":[4,5,5],"4:6":[4,6,6],"4:7":[4,7,7],"4:8":[4,8,8],"4:9":[4,9,9],"4:10":[4,10,10],"4:11":[4,11,11],"4:12":[4,12,12],"4:13":[2],"4:14":[2],"4:15":[2],"4:16":[2],"4:17":[2],"4:18":[2],"4:19":[2],"4:20":[2],"4:21":[2],"4:22":[2],"4:23":[2],"4:24":[2],"4:25":[2],"4:26":[2],"4:27":[2],"4:28":[2],"4:29":[2],"4:30":[2],"4:31":[2],"4:32":[2],"5:0":[5,0,0],"5:1":[5,0,8],"5:2":[5,0,8],"5:3":[5,0,8],"5:4":[5,0,8],"5:5":[5,0,8],"5:6":[5,0,8],"5:7":[5,0,8],"5:8":[5,0,8],"5:9":[5,9,9],"5:10":[5,10,10],"5:11":[5,11,11],"5:12":[5,12,12],"5:13":[6,14,21],"5:14":[6,14,14],"5:15":[7,15,20],"5:16":[7,15,20],"5:17":[7,15,20],"5:18":[7,15,20],"5:19":[7,15,20],"5:20":[7,15,20],"5:21":[6,21,21],"5:22":[6,21,21],"5:23":[6,21,21],"6:0":[8,0,0],"6:1":[8,0,11],"6:2":[8,0,11],"6:3":[8,0,11],"6:4":[8,0,11],"6:5":[8,0,11],"6:6":[8,0,11],"6:7":[8,0,11],"6:8":[8,0,11],"6:9":[8,0,11],"6:10":[8,0,11],"6:11":[8,0,11],"6:12":[8,12,12],"6:13":[2],"6:14":[2],"6:15":[2],"6:16":[2],"6:17":[2],"6:18":[2],"6:19":[2],"6:20":[2],"6:21":[2],"6:22":[2],"6:23":[2],"6:24":[2],"6:25":[2],"6:26":[2],"6:27":[2],"6:28":[2],"6:29":[2],"6:30":[2],"6:31":[2],"6:32":[2],"6:33":[2],"6:34":[2],"6:35":[2],"6:36":[2],"6:37":[2],"6:38":[2],"6:39":[2],"6:40":[2],"6:41":[2],"6:42":[2],"6:43":[2],"6:44":[2],"6:45":[2],"6:46":[2],"7:0":[9,0,0],"7:1":[9,0,7],"7:2":[9,0,7],"7:3":[9,0,7],"7:4":[9,0,7],"7:5":[9,0,7],"7:6":[9,0,7],"7:7":[9,0,7],"7:8":[9,8,8],"7:9":[9,9,9],"7:10":[9,10,10],"7:11":[9,11,11],"7:12":[9,12,12],"7:13":[2],"7:14":[2],"7:15":[2],"7:16":[2],"7:17":[2],"7:18":[2],"7:19":[2],"7:20":[2],"7:21":[2],"7:22":[2],"7:23":[2],"7:24":[2],"7:25":[2],"7:26":[2],"7:27":[2],"7:28":[2],"7:29":[2],"7:30":[2],"7:31":[2],"7:32":[2],"7:33":[2],"7:34":[2],"7:35":[9,35,35],"7:36":[9,35,35],"7:37":[9,35,35],"8:0":[10,0,0],"8:1":[10,0,0],"8:2":[10,0,0],"9:0":[10,0,0],"9:1":[0,0,1],"9:2":[0,0,6],"9:3":[0,0,6],"9:4":[0,0,6],"9:5":[0,0,6],"9:6":[0,0,6],"9:7":[11,7,7],"9:8":[11,7,7],"9:9":[11,7,7],"10:0":[11,0,0],"10:1":[11,0,8],"10:2":[11,0,8],"10:3":[11,0,8],"10:4":[11,0,8],"10:5":[11,0,8],"10:6":[11,0,8],"10:7":[11,0,8],"10:8":[11,0,8],"10:9":[11,9,9],"10:10":[2],"10:11":[2],"10:12":[2],"10:13":[2],"10:14":[2],"10:15":[2],"10:16":[2],"10:17":[2],"10:18":[2],"10:19":[2],"10:20":[2],"10:21":[2],"10:22":[2],"10:23":[2],"10:24":[2],"10:25":[2],"10:26":[2],"10:27":[11,27,27],"10:28":[11,27,27],"10:29":[11,27,27],"11:0":[12,0,0],"11:1":[12,0,12],"11:2":[12,0,12],"11:3":[12,0,12],"11:4":[12,0,12],"11:5":[12,0,12],"11:6":[12,0,12],"11:7":[12,0,12],"11:8":[12,0,12],"11:9":[12,0,12],"11:10":[12,0,12],"11:11":[12,0,12],"11:12":[12,0,12],"11:13":[12,13,13],"11:14":[13,15,21],"11:15":[13,15,15],"11:16":[14,16,20],"11:17":[14,16,20],"11:18":[14,16,20],"11:19":[14,16,20],"11:20":[14,16,20],"11:21":[13,21,21],"11:22":[13,21,21],"11:23":[13,21,21],"12:0":[15,0,0],"12:1":[15,0,7],"12:2":[15,0,7],"12:3":[15,0,7],"12:4":[15,0,7],"12:5":[15,0,7],"12:6":[15,0,7],"12:7":[15,0,7],"12:8":[15,8,8],"12:9":[15,9,9],"12:10":[2],"12:11":[2],"12:12":[2],"12:13":[2],"12:14":[2],"13:0":[2],"13:1":[2],"13:2":[2],"13:3":[2],"13:4":[2],"13:5":[2],"13:6":[2],"13:7":[2],"13:8":[2],"13:9":[2],"13:10":[2],"13:11":[2],"13:12":[2],"13:13":[2],"13:14":[2],"13:15":[2],"13:16":[2],"13:17":[2],"13:18":[2],"13:19":[2],"13:20":[2],"13:21":[2],"13:22":[2],"14:0":[2],"14:1":[2],"14:2":[2],"14:3":[2],"14:4":[2],"14:5":[2],"14:6":[2],"14:7":[2],"14:8":[2],"14:9":[2],"14:10":[2],"14:11":[2],"14:12":[2],"14:13":[2],"14:14":[2],"14:15":[2],"14:16":[2],"14:17":[2],"14:18":[2],"14:19":[2],"14:20":[2],"14:21":[2],"14:22":[2],"14:23":[2],"14:24":[2],"14:25":[2],"14:26":[2],"14:27":[2],"14:28":[2],"14:29":[2],"14:30":[2],"14:31":[2],"14:32":[2],"14:33":[2],"14:34":[2],"14:35":[2],"14:36":[2],"14:37":[2],"14:38":[2],"14:39":[2],"14:40":[2],"14:41":[2],"14:42":[2],"14:43":[2],"14:44":[2],"14:45":[2],"14:46":[2],"14:47":[2],"14:48":[2],"14:49":[2],"14:50":[2],"14:51":[2],"15:0":[2],"15:1":[16,1,1],"15:2":[16,1,1],"15:3":[16,1,1],"16:0":[16,0,0],"16:1":[16,0,0],"16:2":[16,0,0],"17:0":[16,0,0],"17:1":[0,0,1],"17:2":[0,0,13],"17:3":[0,0,13],"17:4":[0,0,13],"17:5":[0,0,13],"17:6":[0,0,13],"17:7":[0,0,13],"17:8":[0,0,13],"17:9":[0,0,13],"17:10":[0,0,13],"17:11":[0,0,13],"17:12":[0,0,13],"17:13":[0,0,13],"17:14":[17,14,14],"17:15":[17,14,14],"17:16":[17,14,14],"18:0":[17,0,0],"18:1":[17,0,3],"18:2":[17,0,3],"18:3":[17,0,3],"18:4":[17,4,4],"18:5":[17,5,5],"18:6":[17,6,6],"18:7":[17,7,7],"18:8":[17,8,8],"18:9":[17,9,9],"18:10":[17,10,10],"18:11":[17,11,11],"18:12":[17,12,12],"18:13":[17,13,13],"18:14":[2],"18:15":[2],"18:16":[2],"18:17":[2],"18:18":[2],"18:19":[2],"18:20":[2],"18:21":[2],"18:22":[2],"18:23":[2],"18:24":[2],"18:25":[2],"19:0":[17,0,0],"19:1":[17,0,6],"19:2":[17,0,6],"19:3":[17,0,6],"19:4":[17,0,6],"19:5":[17,0,6],"19:6":[17,0,6],"19:7":[17,7,7],"19:8":[17,7,11],"19:9":[17,7,11],"19:10":[17,7,11],"19:11":[17,7,11],"19:12":[17,12,12],"19:13":[17,13,13],"19:14":[2],"19:15":[2],"19:16":[2],"19:17":[2],"19:18":[2],"19:19":[2],"19:20":[2],"19:21":[2],"19:22":[2],"19:23":[2],"19:24":[2],"19:25":[2],"19:26":[2],"19:27":[2],"19:28":[2],"19:29":[2],"19:30":[2],"19:31":[2],"19:32":[2],"19:33":[2],"19:34":[2],"19:35":[2],"19:36":[17,36,36],"19:37":[17,36,36],"19:38":[17,36,36],"20:0":[17,0,0],"20:1":[17,0,5],"20:2":[17,0,5],"20:3":[17,0,5],"20:4":[17,0,5],"20:5":[17,0,5],"20:6":[17,6,6],"20:7":[17,7,7],"20:8":[17,8,8],"20:9":[17,9,9],"20:10":[17,10,10],"20:11":[17,11,11],"20:12":[17,12,12],"20:13":[17,13,13],"20:14":[2],"20:15":[2],"20:16":[2],"20:17":[2],"20:18":[2],"20:19":[2],"20:20":[2],"20:21":[2],"20:22":[2],"20:23":[2],"20:24":[2],"20:25":[2],"20:26":[2],"20:27":[2],"20:28":[2],"20:29":[2],"20:30":[2],"20:31":[2],"20:32":[2],"20:33":[2],"20:34":[17,34,34],"20:35":[17,34,34],"20:36":[17,34,34],"21:0":[17,0,0],"21:1":[17,0,6],"21:2":[17,0,6],"21:3":[17,0,6],"21:4":[17,0,6],"21:5":[17,0,6],"21:6":[17,0,6],"21:7":[17,7,7],"21:8":[17,8,8],"21:9":[17,9,9],"21:10":[17,10,10],"21:11":[17,11,11],"21:12":[17,12,12],"21:13":[17,13,13],"21:14":[2],"21:15":[2],"21:16":[2],"21:17":[2],"21:18":[2],"21:19":[2],"21:20":[2],"21:21":[2],"21:22":[2],"21:23":[2],"21:24":[2],"21:25":[2],"21:26":[2],"21:27":[2],"21:28":[2],"21:29":[2],"21:30":[2],"21:31":[2],"21:32":[2],"21:33":[2],"21:34":[2],"21:35":[2],"21:36":[2],"21:37":[2],"21:38":[2],"21:39":[2],"21:40":[2],"21:41":[2],"21:42":[2],"21:43":[2],"21:44":[2],"21:45":[2],"21:46":[2],"21:47":[2],"21:48":[2],"21:49":[2],"21:50":[2],"21:51":[2],"21:52":[2],"21:53":[2],"21:54":[2],"21:55":[2],"21:56":[2],"21:57":[2],"21:58":[2],"21:59":[2],"21:60":[2],"21:61":[2],"21:62":[2],"21:63":[2],"21:64":[2],"21:65":[2],"21:66":[2],"21:67":[2],"21:68":[2],"21:69":[2],"21:70":[2],"21:71":[2],"21:72":[2],"21:73":[2],"21:74":[2],"21:75":[17,75,75],"21:76":[17,75,75],"21:77":[17,75,75],"22:0":[17,0,0],"22:1":[17,0,6],"22:2":[17,0,6],"22:3":[17,0,6],"22:4":[17,0,6],"22:5":[17,0,6],"22:6":[17,0,6],"22:7":[17,7,7],"22:8":[17,8,8],"22:9":[17,9,9],"22:10":[17,10,10],"22:11":[17,11,11],"22:12":[17,12,12],"22:13":[17,13,13],"22:14":[2],"22:15":[2],"22:16":[2],"22:17":[2],"22:18":[2],"22:19":[2],"22:20":[2],"22:21":[2],"22:22":[2],"22:23":[2],"22:24":[2],"22:25":[2],"22:26":[2],"22:27":[2],"22:28":[2],"22:29":[2],"22:30":[2],"22:31":[2],"22:32":[2],"22:33":[2],"22:34":[2],"22:35":[17,35,35],"22:36":[17,35,35],"22:37":[17,35,35],"23:0":[17,0,0],"23:1":[17,0,7],"23:2":[17,0,7],"23:3":[17,0,7],"23:4":[17,0,7],"23:5":[17,0,7],"23:6":[17,0,7],"23:7":[17,0,7],"23:8":[17,8,8],"23:9":[17,9,9],"23:10":[17,10,10],"23:11":[17,11,11],"23:12":[17,12,12],"23:13":[17,13,13],"23:14":[2],"23:15":[2],"23:16":[2],"23:17":[2],"23:18":[2],"23:19":[2],"23:20":[2],"23:21":[2],"23:22":[2],"23:23":[2],"23:24":[2],"23:25":[2],"23:26":[2],"23:27":[2],"23:28":[2],"23:29":[2],"23:30":[2],"23:31":[2],"23:32":[2],"23:33":[2],"23:34":[2],"23:35":[2],"23:36":[2],"23:37":[2],"23:38":[2],"23:39":[2],"23:40":[2],"23:41":[2],"23:42":[2],"23:43":[2],"23:44":[2],"23:45":[2],"23:46":[2],"23:47":[2],"23:48":[2],"23:49":[2],"23:50":[2],"23:51":[2],"23:52":[2],"23:53":[2],"23:54":[2],"23:55":[2],"23:56":[2],"23:57":[2],"23:58":[2],"23:59":[2],"23:60":[2],"23:61":[2],"23:62":[2],"23:63":[17,63,63],"23:64":[17,63,63],"23:65":[17,63,63],"24:0":[17,0,0],"24:1":[17,0,0],"24:2":[17,0,0],"25:0":[17,0,0],"25:1":[0,0,1],"25:2":[0,0,13],"25:3":[0,0,13],"25:4":[0,0,13],"25:5":[0,0,13],"25:6":[0,0,13],"25:7":[0,0,13],"25:8":[0,0,13],"25:9":[0,0,13],"25:10":[0,0,13],"25:11":[0,0,13],"25:12":[0,0,13],"25:13":[0,0,13],"25:14":[0,0,14],"25:15":[0,0,22],"25:16":[0,0,22],"25:17":[0,0,22],"25:18":[0,0,22],"25:19":[0,0,22],"25:20":[0,0,22],"25:21":[0,0,22],"25:22":[0,0,22],"25:23":[17,23,23],"25:24":[17,23,23],"25:25":[17,23,23],"26:0":[17,0,0],"26:1":[17,0,2],"26:2":[17,0,2],"26:3":[17,3,3],"26:4":[2],"26:5":[2],"26:6":[2],"26:7":[2],"26:8":[2],"26:9":[2],"26:10":[2],"26:11":[2],"26:12":[2],"26:13":[2],"26:14":[2],"27:0":[17,0,0],"27:1":[17,0,0],"27:2":[17,0,0],"28:0":[17,0,0],"28:1":[0,0,1],"28:2":[0,0,17],"28:3":[0,0,17],"28:4":[0,0,17],"28:5":[0,0,17],"28:6":[0,0,17],"28:7":[0,0,17],"28:8":[0,0,17],"28:9":[0,0,17],"28:10":[0,0,17],"28:11":[0,0,17],"28:12":[0,0,17],"28:13":[0,0,17],"28:14":[0,0,17],"28:15":[0,0,17],"28:16":[0,0,17],"28:17":[0,0,17],"28:18":[17,18,18],"28:19":[17,18,18],"28:20":[17,18,18],"29:0":[17,0,0],"29:1":[17,0,5],"29:2":[17,0,5],"29:3":[17,0,5],"29:4":[17,0,5],"29:5":[17,0,5],"29:6":[17,6,6],"29:7":[2],"29:8":[2],"29:9":[2],"29:10":[2],"29:11":[2],"29:12":[2],"29:13":[2],"29:14":[2],"29:15":[2],"29:16":[2],"29:17":[2],"29:18":[2],"30:0":[17,0,0],"30:1":[17,0,0],"30:2":[17,0,0],"31:0":[17,0,0],"31:1":[0,0,1],"31:2":[0,0,9],"31:3":[0,0,9],"31:4":[0,0,9],"31:5":[0,0,9],"31:6":[0,0,9],"31:7":[0,0,9],"31:8":[0,0,9],"31:9":[0,0,9],"31:10":[18,10,10],"31:11":[18,10,10],"31:12":[18,10,10],"32:0":[18,0,0],"32:1":[18,0,7],"32:2":[18,0,7],"32:3":[18,0,7],"32:4":[18,0,7],"32:5":[18,0,7],"32:6":[18,0,7],"32:7":[18,0,7],"32:8":[18,8,8],"32:9":[2],"32:10":[2],"32:11":[2],"32:12":[2],"32:13":[2],"32:14":[2],"32:15":[2],"32:16":[2],"32:17":[2],"32:18":[18,18,18],"32:19":[18,18,18],"32:20":[18,18,18],"33:0":[19,0,0],"33:1":[19,0,4],"33:2":[19,0,4],"33:3":[19,0,4],"33:4":[19,0,4],"33:5":[19,5,5],"33:6":[19,6,6],"33:7":[19,7,7],"33:8":[19,8,8],"33:9":[2],"33:10":[2],"33:11":[2],"33:12":[2],"33:13":[2],"33:14":[2],"33:15":[2],"33:16":[2],"33:17":[2],"33:18":[2],"33:19":[2],"33:20":[2],"33:21":[2],"33:22":[2],"33:23":[2],"33:24":[2],"33:25":[2],"33:26":[2],"33:27":[2],"33:28":[2],"33:29":[2],"33:30":[2],"33:31":[2],"33:32":[2],"33:33":[2],"33:34":[2],"33:35":[2],"33:36":[19,36,36],"33:37":[19,36,36],"33:38":[19,36,36],"34:0":[19,0,0],"34:1":[19,0,5],"34:2":[19,0,5],"34:3":[19,0,5],"34:4":[19,0,5],"34:5":[19,0,5],"34:6":[19,6,6],"34:7":[19,7,7],"34:8":[19,8,8],"34:9":[2],"34:10":[2],"34:11":[2],"34:12":[19,12,12],"34:13":[19,12,12],"34:14":[19,12,12],"35:0":[19,0,0],"35:1":[19,0,0],"35:2":[19,0,0],"36:0":[19,0,0],"36:1":[0,0,1],"36:2":[0,0,9],"36:3":[0,0,9],"36:4":[0,0,9],"36:5":[0,0,9],"36:6":[0,0,9],"36:7":[0,0,9],"36:8":[0,0,9],"36:9":[0,0,9],"36:10":[0,0,10],"36:11":[0,0,14],"36:12":[0,0,14],"36:13":[0,0,14],"36:14":[0,0,14],"36:15":[19,15,15],"36:16":[19,15,15],"36:17":[19,15,15],"37:0":[19,0,0],"37:1":[19,0,7],"37:2":[19,0,7],"37:3":[19,0,7],"37:4":[19,0,7],"37:5":[19,0,7],"37:6":[19,0,7],"37:7":[19,0,7],"37:8":[19,8,8],"37:9":[2],"37:10":[2],"37:11":[2],"37:12":[2],"37:13":[2],"37:14":[2],"37:15":[2],"37:16":[2],"37:17":[2],"37:18":[2],"37:19":[2],"37:20":[2],"37:21":[2],"37:22":[2],"37:23":[2],"37:24":[2],"37:25":[19,25,25],"37:26":[19,25,25],"37:27":[19,25,25],"38:0":[20,0,0],"38:1":[20,0,0],"38:2":[20,0,0],"39:0":[20,0,0],"39:1":[0,0,1],"39:2":[0,0,8],"39:3":[0,0,8],"39:4":[0,0,8],"39:5":[0,0,8],"39:6":[0,0,8],"39:7":[0,0,8],"39:8":[0,0,8],"39:9":[0,0,9],"39:10":[0,0,12],"39:11":[0,0,12],"39:12":[0,0,12],"39:13":[21,13,13],"39:14":[21,13,13],"39:15":[21,13,13],"40:0":[21,0,0],"40:1":[21,0,4],"40:2":[21,0,4],"40:3":[21,0,4],"40:4":[21,0,4],"40:5":[21,5,5],"40:6":[22,7,12],"40:7":[22,7,7],"40:8":[23,8,11],"40:9":[23,8,11],"40:10":[23,8,11],"40:11":[23,8,11],"40:12":[22,12,12],"40:13":[22,12,12],"40:14":[22,12,12],"41:0":[24,0,0],"41:1":[24,0,4],"41:2":[24,0,4],"41:3":[24,0,4],"41:4":[24,0,4],"41:5":[24,5,5],"41:6":[2],"41:7":[2],"41:8":[2],"41:9":[2],"41:10":[2],"41:11":[2],"41:12":[2],"41:13":[2],"41:14":[2],"41:15":[2],"41:16":[2],"41:17":[2],"41:18":[2],"41:19":[2],"41:20":[2],"41:21":[2],"41:22":[2],"41:23":[2],"42:0":[25,0,0],"42:1":[25,0,0],"42:2":[25,0,0],"43:0":[25,0,0],"43:1":[0,0,1],"43:2":[0,0,8],"43:3":[0,0,8],"43:4":[0,0,8],"43:5":[0,0,8],"43:6":[0,0,8],"43:7":[0,0,8],"43:8":[0,0,8],"43:9":[0,0,9],"43:10":[0,0,13],"43:11":[0,0,13],"43:12":[0,0,13],"43:13":[0,0,13],"43:14":[25,14,14],"43:15":[25,14,14],"43:16":[25,14,14],"44:0":[25,0,0],"44:1":[25,0,4],"44:2":[25,0,4],"44:3":[25,0,4],"44:4":[25,0,4],"44:5":[25,5,5],"44:6":[22,7,14],"44:7":[22,7,7],"44:8":[23,8,13],"44:9":[23,8,13],"44:10":[23,8,13],"44:11":[23,8,13],"44:12":[23,8,13],"44:13":[23,8,13],"44:14":[22,14,14],"44:15":[22,14,14],"44:16":[22,14,14],"45:0":[25,0,0],"45:1":[25,0,0],"45:2":[25,0,0],"46:0":[25,0,0],"46:1":[0,0,1],"46:2":[0,0,8],"46:3":[0,0,8],"46:4":[0,0,8],"46:5":[0,0,8],"46:6":[0,0,8],"46:7":[0,0,8],"46:8":[0,0,8],"46:9":[0,0,9],"46:10":[0,0,16],"46:11":[0,0,16],"46:12":[0,0,16],"46:13":[0,0,16],"46:14":[0,0,16],"46:15":[0,0,16],"46:16":[0,0,16],"46:17":[26,17,17],"46:18":[26,17,17],"46:19":[26,17,17],"47:0":[26,0,0],"47:1":[26,0,3],"47:2":[26,0,3],"47:3":[26,0,3],"47:4":[26,4,4],"47:5":[27,6,7],"47:6":[27,6,7],"47:7":[27,6,7],"47:8":[27,6,7],"47:9":[27,6,7],"48:0":[28,0,0],"48:1":[28,0,0],"48:2":[28,0,0],"49:0":[28,0,0],"49:1":[0,0,1],"49:2":[0,0,7],"49:3":[0,0,7],"49:4":[0,0,7],"49:5":[0,0,7],"49:6":[0,0,7],"49:7":[0,0,7],"49:8":[0,0,8],"49:9":[0,0,27],"49:10":[0,0,27],"49:11":[0,0,27],"49:12":[0,0,27],"49:13":[0,0,27],"49:14":[0,0,27],"49:15":[0,0,27],"49:16":[0,0,27],"49:17":[0,0,27],"49:18":[0,0,27],"49:19":[0,0,27],"49:20":[0,0,27],"49:21":[0,0,27],"49:22":[0,0,27],"49:23":[0,0,27],"49:24":[0,0,27],"49:25":[0,0,27],"49:26":[0,0,27],"49:27":[0,0,27],"49:28":[0,0,28],"49:29":[0,0,33],"49:30":[0,0,33],"49:31":[0,0,33],"49:32":[0,0,33],"49:33":[0,0,33],"49:34":[16,34,34],"49:35":[16,34,34],"49:36":[16,34,34],"50:0":[16,0,0],"50:1":[16,0,8],"50:2":[16,0,8],"50:3":[16,0,8],"50:4":[16,0,8],"50:5":[16,0,8],"50:6":[16,0,8],"50:7":[16,0,8],"50:8":[16,0,8],"50:9":[16,9,9],"50:10":[2],"50:11":[2],"50:12":[2],"50:13":[2],"50:14":[2],"50:15":[2],"50:16":[2],"50:17":[2],"50:18":[2],"50:19":[2],"50:20":[2],"50:21":[2],"50:22":[2],"50:23":[16,23,23],"50:24":[16,23,23],"50:25":[16,23,23],"51:0":[16,0,0],"51:1":[16,0,0],"51:2":[16,0,0],"52:0":[16,0,0],"52:1":[0,0,1],"52:2":[0,0,7],"52:3":[0,0,7],"52:4":[0,0,7],"52:5":[0,0,7],"52:6":[0,0,7],"52:7":[0,0,7],"52:8":[0,0,8],"52:9":[0,0,22],"52:10":[0,0,22],"52:11":[0,0,22],"52:12":[0,0,22],"52:13":[0,0,22],"52:14":[0,0,22],"52:15":[0,0,22],"52:16":[0,0,22],"52:17":[0,0,22],"52:18":[0,0,22],"52:19":[0,0,22],"52:20":[0,0,22],"52:21":[0,0,22],"52:22":[0,0,22],"52:23":[29,23,23],"52:24":[29,23,23],"52:25":[29,23,23],"53:0":[29,0,0],"53:1":[29,0,8],"53:2":[29,0,8],"53:3":[29,0,8],"53:4":[29,0,8],"53:5":[29,0,8],"53:6":[29,0,8],"53:7":[29,0,8],"53:8":[29,0,8],"53:9":[29,9,9],"53:10":[2],"53:11":[2],"53:12":[2],"53:13":[2],"53:14":[2],"53:15":[2],"53:16":[2],"53:17":[2],"53:18":[2],"53:19":[2],"53:20":[2],"53:21":[2],"53:22":[2],"53:23":[2],"53:24":[2],"53:25":[2],"53:26":[2],"53:27":[2],"53:28":[2],"53:29":[2],"53:30":[2],"53:31":[2],"53:32":[2],"53:33":[2],"53:34":[2],"53:35":[2],"53:36":[2],"53:37":[2],"53:38":[2],"53:39":[2],"53:40":[2],"53:41":[2],"53:42":[2],"53:43":[2],"53:44":[2],"53:45":[2],"53:46":[2],"53:47":[2],"54:0":[30,0,0],"54:1":[30,0,0],"54:2":[30,0,0],"55:0":[30,0,0],"55:1":[0,0,1],"55:2":[0,0,10],"55:3":[0,0,10],"55:4":[0,0,10],"55:5":[0,0,10],"55:6":[0,0,10],"55:7":[0,0,10],"55:8":[0,0,10],"55:9":[0,0,10],"55:10":[0,0,10],"55:11":[31,11,11],"55:12":[31,11,11],"55:13":[31,11,11],"56:0":[31,0,0],"56:1":[31,0,7],"56:2":[31,0,7],"56:3":[31,0,7],"56:4":[31,0,7],"56:5":[31,0,7],"56:6":[31,0,7],"56:7":[31,0,7],"56:8":[31,8,8],"56:9":[2],"56:10":[2],"56:11":[2],"56:12":[2],"56:13":[2],"56:14":[2],"56:15":[2],"56:16":[2],"56:17":[2],"56:18":[2],"56:19":[2],"56:20":[31,20,20],"56:21":[31,20,20],"56:22":[31,20,20],"57:0":[32,0,0],"57:1":[32,0,0],"57:2":[32,0,0],"58:0":[32,0,0],"58:1":[0,0,1],"58:2":[0,0,10],"58:3":[0,0,10],"58:4":[0,0,10],"58:5":[0,0,10],"58:6":[0,0,10],"58:7":[0,0,10],"58:8":[0,0,10],"58:9":[0,0,10],"58:10":[0,0,10],"58:11":[0,0,11],"58:12":[0,0,23],"58:13":[0,0,23],"58:14":[0,0,23],"58:15":[0,0,23],"58:16":[0,0,23],"58:17":[0,0,23],"58:18":[0,0,23],"58:19":[0,0,23],"58:20":[0,0,23],"58:21":[0,0,23],"58:22":[0,0,23],"58:23":[0,0,23],"58:24":[17,24,24],"58:25":[17,24,24],"58:26":[17,24,24],"59:0":[17,0,0],"59:1":[17,0,6],"59:2":[17,0,6],"59:3":[17,0,6],"59:4":[17,0,6],"59:5":[17,0,6],"59:6":[17,0,6],"59:7":[17,7,7],"59:8":[2],"59:9":[2],"59:10":[2],"59:11":[2],"59:12":[2],"59:13":[2],"59:14":[2],"59:15":[2],"59:16":[2],"59:17":[2],"59:18":[2],"60:0":[17,0,0],"60:1":[17,0,0],"60:2":[17,0,0],"61:0":[17,0,0],"61:1":[0,0,1],"61:2":[0,0,7],"61:3":[0,0,7],"61:4":[0,0,7],"61:5":[0,0,7],"61:6":[0,0,7],"61:7":[0,0,7],"61:8":[33,8,8],"61:9":[33,8,8],"61:10":[33,8,8],"62:0":[33,0,0],"62:1":[33,0,4],"62:2":[33,0,4],"62:3":[33,0,4],"62:4":[33,0,4],"62:5":[33,5,5],"62:6":[2],"62:7":[2],"62:8":[2],"62:9":[2],"62:10":[2],"62:11":[2],"62:12":[2],"62:13":[2],"62:14":[2],"62:15":[2],"62:16":[33,16,16],"62:17":[33,16,16],"62:18":[33,16,16],"63:0":[34,0,0],"63:1":[34,0,0],"63:2":[34,0,0],"64:0":[34,0,0],"64:1":[0,0,1],"64:2":[0,0,7],"64:3":[0,0,7],"64:4":[0,0,7],"64:5":[0,0,7],"64:6":[0,0,7],"64:7":[0,0,7],"64:8":[0,0,8],"64:9":[0,0,17],"64:10":[0,0,17],"64:11":[0,0,17],"64:12":[0,0,17],"64:13":[0,0,17],"64:14":[0,0,17],"64:15":[0,0,17],"64:16":[0,0,17],"64:17":[0,0,17],"64:18":[35,18,18],"64:19":[35,18,18],"64:20":[35,18,18],"65:0":[35,0,0],"65:1":[35,0,4],"65:2":[35,0,4],"65:3":[35,0,4],"65:4":[35,0,4],"65:5":[35,5,5],"65:6":[2],"65:7":[2],"65:8":[2],"65:9":[2],"65:10":[2],"65:11":[2],"65:12":[2],"65:13":[2],"65:14":[2],"65:15":[2],"65:16":[2],"65:17":[2],"65:18":[2],"66:0":[35,0,0],"66:1":[35,0,0],"66:2":[35,0,0],"67:0":[35,0,0],"67:1":[0,0,1],"67:2":[0,0,10],"67:3":[0,0,10],"67:4":[0,0,10],"67:5":[0,0,10],"67:6":[0,0,10],"67:7":[0,0,10],"67:8":[0,0,10],"67:9":[0,0,10],"67:10":[0,0,10],"67:11":[2],"67:12":[2],"67:13":[2],"68:0":[2],"68:1":[2],"68:2":[2],"68:3":[2],"68:4":[2],"68:5":[2],"68:6":[2],"68:7":[2],"68:8":[2],"68:9":[2],"68:10":[2],"68:11":[2],"68:12":[2],"68:13":[2],"68:14":[2],"68:15":[2],"68:16":[2],"68:17":[2],"68:18":[2],"68:19":[2],"68:20":[2],"68:21":[2],"68:22":[2],"68:23":[2],"69:0":[2],"69:1":[2],"69:2":[2],"70:0":[2],"70:1":[0,0,1],"70:2":[0,0,5],"70:3":[0,0,5],"70:4":[0,0,5],"70:5":[0,0,5],"70:6":[36,6,6],"70:7":[36,6,6],"70:8":[36,6,6],"71:0":[36,0,0],"71:1":[36,0,6],"71:2":[36,0,6],"71:3":[36,0,6],"71:4":[36,0,6],"71:5":[36,0,6],"71:6":[36,0,6],"71:7":[36,7,7],"71:8":[2],"71:9":[2],"71:10":[2],"71:11":[2],"71:12":[2],"71:13":[2],"71:14":[2],"71:15":[2],"71:16":[2],"72:0":[37,0,0],"72:1":[37,0,0],"72:2":[37,0,0],"73:0":[37,0,0],"73:1":[0,0,1],"73:2":[0,0,8],"73:3":[0,0,8],"73:4":[0,0,8],"73:5":[0,0,8],"73:6":[0,0,8],"73:7":[0,0,8],"73:8":[0,0,8],"73:9":[38,9,9],"73:10":[38,9,9],"73:11":[38,9,9],"74:0":[38,0,0],"74:1":[38,0,9],"74:2":[38,0,9],"74:3":[38,0,9],"74:4":[38,0,9],"74:5":[38,0,9],"74:6":[38,0,9],"74:7":[38,0,9],"74:8":[38,0,9],"74:9":[38,0,9],"74:10":[38,10,10],"74:11":[2],"74:12":[2],"74:13":[2],"74:14":[2],"74:15":[2],"74:16":[2],"74:17":[38,17,17],"74:18":[38,17,17],"74:19":[38,17,17],"75:0":[39,0,0],"75:1":[39,0,0],"75:2":[39,0,0],"76:0":[39,0,0],"76:1":[39,0,12],"76:2":[39,0,12],"76:3":[39,0,12],"76:4":[39,0,12],"76:5":[39,0,12],"76:6":[39,0,12],"76:7":[39,0,12],"76:8":[39,0,12],"76:9":[39,0,12],"76:10":[39,0,12],"76:11":[39,0,12],"76:12":[39,0,12],"76:13":[39,13,13],"76:14":[2],"76:15":[2],"76:16":[2],"76:17":[2],"76:18":[2],"76:19":[2],"76:20":[2],"77:0":[2],"77:1":[2],"77:2":[2],"77:3":[2],"77:4":[2],"77:5":[2],"77:6":[2],"77:7":[2],"77:8":[2],"77:9":[2],"77:10":[2],"77:11":[2],"77:12":[2],"77:13":[2],"77:14":[2],"78:0":[2],"78:1":[2],"78:2":[2],"78:3":[2],"78:4":[2],"78:5":[2],"78:6":[2],"78:7":[2],"78:8":[2],"78:9":[2],"78:10":[2],"78:11":[2],"78:12":[2],"78:13":[2],"78:14":[2],"78:15":[2],"78:16":[2],"78:17":[2],"78:18":[2],"78:19":[2],"78:20":[2],"78:21":[2],"78:22":[2],"78:23":[2],"78:24":[2],"78:25":[2],"78:26":[2],"78:27":[2],"78:28":[2],"78:29":[2],"78:30":[2],"79:0":[2],"79:1":[2],"79:2":[2],"79:3":[2],"79:4":[2],"79:5":[2],"80:0":[39,0,0],"80:1":[39,0,0],"80:2":[39,0,0]},"hover":{"1:2":0,"1:3":0,"1:4":0,"1:5":0,"1:6":0,"1:7":0,"1:8":0,"2:1":1,"2:2":1,"2:3":1,"2:4":1,"2:5":1,"2:6":1,"2:7":1,"2:8":1,"2:9":1,"2:10":1,"2:11":1,"2:12":1,"3:1":2,"3:2":2,"3:3":2,"3:4":2,"3:5":2,"3:6":2,"3:7":2,"3:8":2,"3:9":2,"3:10":2,"3:11":2,"3:12":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"4:7":3,"4:8":3,"4:9":3,"4:10":3,"4:11":3,"4:12":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":4,"5:11":4,"5:12":4,"5:13":5,"5:14":6,"5:15":5,"5:16":5,"5:17":5,"5:18":5,"5:19":5,"5:20":5,"5:21":6,"5:22":6,"5:23":6,"6:1":7,"6:2":7,"6:3":7,"6:4":7,"6:5":7,"6:6":7,"6:7":7,"6:8":7,"6:9":7,"6:10":7,"6:11":7,"6:12":7,"7:1":8,"7:2":8,"7:3":8,"7:4":8,"7:5":8,"7:6":8,"7:7":8,"7:8":8,"7:9":8,"7:10":8,"7:11":8,"7:12":8,"9:2":9,"9:3":9,"9:4":9,"9:5":9,"9:6":9,"10:1":10,"10:2":10,"10:3":10,"10:4":10,"10:5":10,"10:6":10,"10:7":10,"10:8":10,"10:9":10,"11:1":11,"11:2":11,"11:3":11,"11:4":11,"11:5":11,"11:6":11,"11:7":11,"11:8":11,"11:9":11,"11:10":11,"11:11":11,"11:12":11,"11:13":11,"11:14":12,"11:15":13,"11:16":12,"11:17":12,"11:18":12,"11:19":12,"11:20":12,"11:21":13,"11:22":13,"11:23":13,"12:1":14,"12:2":14,"12:3":14,"12:4":14,"12:5":14,"12:6":14,"12:7":14,"12:8":14,"12:9":14,"17:2":15,"17:3":15,"17:4":15,"17:5":15,"17:6":15,"17:7":15,"17:8":15,"17:9":15,"17:10":15,"17:11":15,"17:12":15,"17:13":15,"25:2":15,"25:3":15,"25:4":15,"25:5":15,"25:6":15,"25:7":15,"25:8":15,"25:9":15,"25:10":15,"25:11":15,"25:12":15,"25:13":15,"25:14":15,"25:15":15,"25:16":15,"25:17":15,"25:18":15,"25:19":15,"25:20":15,"25:21":15,"25:22":15,"28:2":16,"28:3":16,"28:4":16,"28:5":16,"28:6":16,"28:7":16,"28:8":16,"28:9":16,"28:10":16,"28:11":16,"28:12":16,"28:13":16,"28:14":16,"28:15":16,"28:16":16,"28:17":16,"31:2":17,"31:3":17,"31:4":17,"31:5":17,"31:6":17,"31:7":17,"31:8":17,"31:9":17,"32:1":18,"32:2":18,"32:3":18,"32:4":18,"32:5":18,"32:6":18,"32:7":18,"32:8":18,"36:2":17,"36:3":17,"36:4":17,"36:5":17,"36:6":17,"36:7":17,"36:8":17,"36:9":17,"36:10":17,"36:11":17,"36:12":17,"36:13":17,"36:14":17,"37:1":19,"37:2":19,"37:3":19,"37:4":19,"37:5":19,"37:6":19,"37:7":19,"37:8":19,"39:2":20,"39:3":20,"39:4":20,"39:5":20,"39:6":20,"39:7":20,"39:8":20,"39:9":20,"39:10":20,"39:11":20,"39:12":20,"40:1":21,"40:2":21,"40:3":21,"40:4":21,"40:5":21,"40:6":22,"40:7":23,"40:8":22,"40:9":22,"40:10":22,"40:11":22,"40:12":23,"40:13":23,"40:14":23,"41:1":24,"41:2":24,"41:3":24,"41:4":24,"41:5":24,"43:2":20,"43:3":20,"43:4":20,"43:5":20,"43:6":20,"43:7":20,"43:8":20,"43:9":20,"43:10":20,"43:11":20,"43:12":20,"43:13":20,"44:1":21,"44:2":21,"44:3":21,"44:4":21,"44:5":21,"44:6":25,"44:7":23,"44:8":25,"44:9":25,"44:10":25,"44:11":25,"44:12":25,"44:13":25,"44:14":23,"44:15":23,"44:16":23,"46:2":26,"46:3":26,"46:4":26,"46:5":26,"46:6":26,"46:7":26,"46:8":26,"46:9":26,"46:10":26,"46:11":26,"46:12":26,"46:13":26,"46:14":26,"46:15":26,"46:16":26,"47:1":27,"47:2":27,"47:3":27,"47:4":27,"47:5":28,"47:6":28,"47:7":28,"47:8":28,"47:9":28,"49:2":29,"49:3":29,"49:4":29,"49:5":29,"49:6":29,"49:7":29,"49:8":29,"49:9":29,"49:10":29,"49:11":29,"49:12":29,"49:13":29,"49:14":29,"49:15":29,"49:16":29,"49:17":29,"49:18":29,"49:19":29,"49:20":29,"49:21":29,"49:22":29,"49:23":29,"49:24":29,"49:25":29,"49:26":29,"49:27":29,"49:28":29,"49:29":9,"49:30":9,"49:31":9,"49:32":9,"49:33":9,"50:1":10,"50:2":10,"50:3":10,"50:4":10,"50:5":10,"50:6":10,"50:7":10,"50:8":10,"50:9":10,"52:2":29,"52:3":29,"52:4":29,"52:5":29,"52:6":29,"52:7":29,"52:8":29,"52:9":29,"52:10":29,"52:11":29,"52:12":29,"52:13":29,"52:14":29,"52:15":29,"52:16":29,"52:17":29,"52:18":29,"52:19":29,"52:20":29,"52:21":29,"52:22":29,"53:1":30,"53:2":30,"53:3":30,"53:4":30,"53:5":30,"53:6":30,"53:7":30,"53:8":30,"53:9":30,"55:2":31,"55:3":31,"55:4":31,"55:5":31,"55:6":31,"55:7":31,"55:8":31,"55:9":31,"55:10":31,"56:1":32,"56:2":32,"56:3":32,"56:4":32,"56:5":32,"56:6":32,"56:7":32,"56:8":32,"58:2":31,"58:3":31,"58:4":31,"58:5":31,"58:6":31,"58:7":31,"58:8":31,"58:9":31,"58:10":31,"58:11":31,"61:2":33,"61:3":33,"61:4":33,"61:5":33,"61:6":33,"61:7":33,"62:1":34,"62:2":34,"62:3":34,"62:4":34,"62:5":34,"64:2":33,"64:3":33,"64:4":33,"64:5":33,"64:6":33,"64:7":33,"64:8":33,"64:9":35,"64:10":35,"64:11":35,"64:12":35,"64:13":35,"64:14":35,"64:15":35,"64:16":35,"64:17":35,"67:2":36,"67:3":36,"67:4":36,"67:5":36,"67:6":36,"67:7":36,"67:8":36,"67:9":36,"67:10":36,"70:2":37,"70:3":37,"70:4":37,"70:5":37,"71:1":38,"71:2":38,"71:3":38,"71:4":38,"71:5":38,"71:6":38,"71:7":38,"73:2":39,"73:3":39,"73:4":39,"73:5":39,"73:6":39,"73:7":39,"73:8":39,"74:1":40,"74:2":40,"74:3":40,"74:4":40,"74:5":40,"74:6":40,"74:7":40,"74:8":40,"74:9":40,"74:10":40},"definition":{"20:17":{"line":2,"startCharacter":0,"endCharacter":9},"20:18":{"line":2,"startCharacter":0,"endCharacter":9},"20:19":{"line":2,"startCharacter":0,"endCharacter":9},"20:20":{"line":2,"startCharacter":0,"endCharacter":9},"20:21":{"line":2,"startCharacter":0,"endCharacter":9},"20:24":{"line":2,"startCharacter":0,"endCharacter":9},"20:25":{"line":2,"startCharacter":0,"endCharacter":9},"20:26":{"line":2,"startCharacter":0,"endCharacter":9},"20:27":{"line":2,"startCharacter":0,"endCharacter":9},"20:28":{"line":2,"startCharacter":0,"endCharacter":9},"20:29":{"line":2,"startCharacter":0,"endCharacter":9},"20:30":{"line":2,"startCharacter":0,"endCharacter":9},"20:31":{"line":2,"startCharacter":0,"endCharacter":9},"20:32":{"line":2,"startCharacter":0,"endCharacter":9},"22:17":{"line":59,"startCharacter":0,"endCharacter":6},"22:18":{"line":59,"startCharacter":0,"endCharacter":6},"22:19":{"line":59,"startCharacter":0,"endCharacter":6},"22:20":{"line":59,"startCharacter":0,"endCharacter":6},"22:21":{"line":59,"startCharacter":0,"endCharacter":6},"22:22":{"line":59,"startCharacter":0,"endCharacter":6},"22:23":{"line":59,"startCharacter":0,"endCharacter":6},"22:24":{"line":59,"startCharacter":0,"endCharacter":6},"22:25":{"line":59,"startCharacter":0,"endCharacter":6},"22:26":{"line":59,"startCharacter":0,"endCharacter":6},"22:29":{"line":59,"startCharacter":0,"endCharacter":6},"22:30":{"line":59,"startCharacter":0,"endCharacter":6},"22:31":{"line":59,"startCharacter":0,"endCharacter":6},"22:32":{"line":59,"startCharacter":0,"endCharacter":6},"22:33":{"line":59,"startCharacter":0,"endCharacter":6},"23:54":{"line":36,"startCharacter":0,"endCharacter":15},"23:55":{"line":36,"startCharacter":0,"endCharacter":15},"23:56":{"line":36,"startCharacter":0,"endCharacter":15},"23:57":{"line":36,"startCharacter":0,"endCharacter":15},"23:58":{"line":36,"startCharacter":0,"endCharacter":15},"23:59":{"line":36,"startCharacter":0,"endCharacter":15},"23:60":{"line":36,"startCharacter":0,"endCharacter":15}},"versionContext":{"18:14":{"name":"fmt","insideString":false,"range":{"startCharacter":15,"endCharacter":23}},"18:15":{"name":"fmt","insideString":false,"range":{"startCharacter":15,"endCharacter":15}},"18:16":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:17":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:18":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:19":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:20":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:21":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:22":{"name":"fmt","insideString":true,"range":{"startCharacter":16,"endCharacter":22}},"18:23":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"18:24":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"18:25":{"name":"fmt","insideString":false,"range":{"startCharacter":23,"endCharacter":23}},"19:26":{"name":"compat","insideString":false,"range":{"startCharacter":27,"endCharacter":34}},"19:27":{"name":"compat","insideString":false,"range":{"startCharacter":27,"endCharacter":27}},"19:28":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:29":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:30":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:31":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:32":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:33":{"name":"compat","insideString":true,"range":{"startCharacter":28,"endCharacter":33}},"19:34":{"name":"compat","insideString":false,"range":{"startCharacter":34,"endCharacter":34}},"23:26":{"name":"simdlib","insideString":false,"range":{"startCharacter":27,"endCharacter":32}},"23:27":{"name":"simdlib","insideString":false,"range":{"startCharacter":27,"endCharacter":27}},"23:28":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:29":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:30":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:31":{"name":"simdlib","insideString":true,"range":{"startCharacter":28,"endCharacter":31}},"23:32":{"name":"simdlib","insideString":false,"range":{"startCharacter":32,"endCharacter":32}},"26:4":{"name":"os","insideString":false,"range":{"startCharacter":5,"endCharacter":12}},"26:5":{"name":"os","insideString":false,"range":{"startCharacter":5,"endCharacter":5}},"26:6":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:7":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:8":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:9":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:10":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:11":{"name":"os","insideString":true,"range":{"startCharacter":6,"endCharacter":11}},"26:12":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"26:13":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"26:14":{"name":"os","insideString":false,"range":{"startCharacter":12,"endCharacter":12}},"29:7":{"name":"gtest","insideString":false,"range":{"startCharacter":8,"endCharacter":16}},"29:8":{"name":"gtest","insideString":false,"range":{"startCharacter":8,"endCharacter":8}},"29:9":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:10":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:11":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:12":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:13":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:14":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:15":{"name":"gtest","insideString":true,"range":{"startCharacter":9,"endCharacter":15}},"29:16":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"29:17":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"29:18":{"name":"gtest","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:8":{"name":"shared","insideString":false,"range":{"startCharacter":9,"endCharacter":16}},"59:9":{"name":"shared","insideString":false,"range":{"startCharacter":9,"endCharacter":9}},"59:10":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:11":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:12":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:13":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:14":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:15":{"name":"shared","insideString":true,"range":{"startCharacter":10,"endCharacter":15}},"59:16":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:17":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}},"59:18":{"name":"shared","insideString":false,"range":{"startCharacter":16,"endCharacter":16}}},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"c++20"},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"c++23"},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"c++26"},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"c++2a"},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"c++2c"},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"gnu++20"},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"gnu++23"},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"gnu++26"},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"c++latest"},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"c++fly"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"auto"},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"on"},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"off"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"name = \"version\"","kind":"template","detail":"SemVer version dependency","documentation":"Caret constraint (^) by default; ~, = and range combinations such as \">=1.0, <2.0\" are also supported.","insertSnippet":"${1:name} = \"${2:1.0.0}\""},{"label":"name = { path = ... }","kind":"template","detail":"Path dependency (local development)","insertSnippet":"${1:name} = { path = \"${2:../mylib}\" }"},{"label":"name = { git = ..., tag = ... }","kind":"template","detail":"Git dependency (one of tag / branch / rev)","insertSnippet":"${1:name} = { git = \"${2:https://github.com/user/repo.git}\", tag = \"${3:v1.0.0}\" }"},{"label":"name = { version = ..., features = [...] }","kind":"template","detail":"Long dep spec: request a feature of that dependency","insertSnippet":"${1:name} = { version = \"${2:1.0}\", features = [\"${3:feature}\"] }"},{"label":"name = { version = ..., tools = [...] }","kind":"template","detail":"Host tools produced by the dependency (must be a bin target of that package)","insertSnippet":"${1:name} = { version = \"${2:1.0}\", tools = [\"${3:protoc}\"] }"}],[{"label":"default","kind":"template","detail":"array","insertSnippet":"default = []","documentation":"the features a consumer gets unless it opts out Since 0.0.47."},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []","documentation":"[features.]: sources compiled only when the feature is active Since 0.0.95."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []","documentation":"[features.]: sources compiled only when the feature is active Since 0.0.95."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []","documentation":"[features.]: defines the active feature adds to the package's units Since 0.0.69."},{"label":"device_extensions","kind":"template","detail":"array","insertSnippet":"device_extensions = []","documentation":"[features.]: the source extensions of a device language this rule package compiles Since 2026.9.7.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"[[features..flags]]: { glob, cflags, cxxflags, defines } tables for the feature's sources Since 0.0.101."},{"label":"forward","kind":"template","detail":"array","insertSnippet":"forward = []","documentation":"[features.]: \"/\" entries activated with this feature Since 0.0.99."},{"label":"implies","kind":"template","detail":"array","insertSnippet":"implies = []","documentation":"[features.]: the features this one activates Since 0.0.69."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"[features.]: the capabilities the feature provides Since 0.0.69."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"[features.]: the capabilities the feature requires Since 0.0.69."},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"[features.]: { threads, exceptions } the artefact's ABI switch must have when the feature is active Since 2026.9.12.2."},{"label":"rule_module","kind":"template","detail":"string","insertSnippet":"rule_module = \"\"","documentation":"[features.]: the module of the build rule this feature contributes Since 2026.9.7.1."},{"label":"tools","kind":"template","detail":"array","insertSnippet":"tools = []","documentation":"[features.]: the host tools the feature makes available Since 2026.9.27.1."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"opt","kind":"template","detail":"enum: s | z","insertSnippet":"opt = \"s\"","documentation":"an -O level: a number, or \"s\"/\"z\""},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}],[{"label":"s","kind":"template","detail":"value of opt","insertSnippet":"\"s\""},{"label":"z","kind":"template","detail":"value of opt","insertSnippet":"\"z\""}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"debug","kind":"template","detail":"boolean","insertSnippet":"debug = true","documentation":"-g"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"dependency_linkage = \"static\""},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"lto","kind":"template","detail":"boolean","insertSnippet":"lto = true","documentation":"-flto"},{"label":"ndebug","kind":"template","detail":"boolean","insertSnippet":"ndebug = true","documentation":"whether NDEBUG is defined for the profile's units Since 2026.10.24.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true","documentation":"-s at link time"},{"label":"trim_paths","kind":"template","detail":"boolean","insertSnippet":"trim_paths = true","documentation":"whether the build's own paths are rewritten out of the artifacts Since 2026.10.24.1."}],[{"label":"carrier","kind":"template","detail":"table","insertSnippet":"carrier = { }","documentation":"{ package, section, main }: the program mcpp pack builds of a loaded artifact for a system (docs/35) Since 2026.10.27.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled","insertSnippet":"cxx_runtime = \"self-contained\""},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\""},{"label":"min_api_level","kind":"template","detail":"number","insertSnippet":"min_api_level = 0"},{"label":"platform","kind":"template","detail":"string","insertSnippet":"platform = \"\"","documentation":"the platform of the selected targets: \"@\", { path = \"...\" }, { git = ... }, or \"none\"; not accepted under a cfg(platform = ...) selector Since 2026.10.27.1."},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"the target-axis form of [package] requires, unioned in when the selector matches Since 2026.10.26.1."},{"label":"run_under","kind":"template","detail":"array","insertSnippet":"run_under = []","documentation":"an argv placed before the runner: what reaches the machine the artifact runs on Since 2026.10.24.1."},{"label":"runner","kind":"template","detail":"array","insertSnippet":"runner = []","documentation":"argv template for mcpp run/test on this target, or { cmd, host = \"same-arch\" | \"any\", exclusive } (2026.10.27.1+)"},{"label":"runners","kind":"template","detail":"table","insertSnippet":"runners = { }","documentation":"named runners, = [argv]; also written as [target..runners] Since 2026.9.4.2."},{"label":"sysroot","kind":"template","detail":"string","insertSnippet":"sysroot = \"\""},{"label":"toolchain","kind":"template","detail":"string","insertSnippet":"toolchain = \"\""}],[{"label":"carrier","kind":"template","detail":"table","insertSnippet":"carrier = { }","documentation":"{ package, section, main }: the program mcpp pack builds of a loaded artifact for a system (docs/35) Since 2026.10.27.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled","insertSnippet":"cxx_runtime = \"self-contained\""},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\""},{"label":"min_api_level","kind":"template","detail":"number","insertSnippet":"min_api_level = 0"},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []","documentation":"the target-axis form of [package] requires, unioned in when the selector matches Since 2026.10.26.1."},{"label":"run_under","kind":"template","detail":"array","insertSnippet":"run_under = []","documentation":"an argv placed before the runner: what reaches the machine the artifact runs on Since 2026.10.24.1."},{"label":"runner","kind":"template","detail":"array","insertSnippet":"runner = []","documentation":"argv template for mcpp run/test on this target, or { cmd, host = \"same-arch\" | \"any\", exclusive } (2026.10.27.1+)"},{"label":"runners","kind":"template","detail":"table","insertSnippet":"runners = { }","documentation":"named runners, = [argv]; also written as [target..runners] Since 2026.9.4.2."},{"label":"sysroot","kind":"template","detail":"string","insertSnippet":"sysroot = \"\""},{"label":"toolchain","kind":"template","detail":"string","insertSnippet":"toolchain = \"\""}],[{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []","documentation":"member globs left out Since 0.0.11."},{"label":"members","kind":"template","detail":"array","insertSnippet":"members = []","documentation":"member directories or globs Since 0.0.11."}],[{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []","documentation":"member globs left out Since 0.0.11."}],[{"label":"deps","kind":"template","detail":"array","insertSnippet":"deps = []","documentation":"the project's host build tools: \"@\" or tables Since 0.0.77."},{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"overrides","kind":"template","detail":"table","insertSnippet":"overrides = { }","documentation":" = : what a dependency's xlings entries resolve to; usually [xlings.overrides] Since 2026.10.1.3."},{"label":"subos","kind":"template","detail":"string","insertSnippet":"subos = \"\"","documentation":"the SubOS the project's environment is materialised in Since 0.0.77."},{"label":"workspace","kind":"template","detail":"table","insertSnippet":"workspace = { }","documentation":" = \"\": the project's tool pins; usually [xlings.workspace] Since 0.0.77."}],[{"label":"tool = \"version\"","kind":"template","detail":"Pin the xlings tool version","insertSnippet":"${1:node} = \"${2:24.19.0}\""}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"format","kind":"template","detail":"string","insertSnippet":"format = \"\"","documentation":"the default of --pkg-format: tar, zip, dir, single-file, or a format a package provides Since 2026.10.27.1."},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"artifacts","kind":"template","detail":"array","insertSnippet":"artifacts = []","documentation":"[[runtime.artifacts]]: prebuilt artifacts the package provides Since 2026.8.10.1."},{"label":"capabilities","kind":"template","detail":"array","insertSnippet":"capabilities = []","documentation":"Since 0.0.46."},{"label":"deploy","kind":"template","detail":"array","insertSnippet":"deploy = []","documentation":"[[runtime.deploy]]: { from, to } files placed beside the artifact Since 2026.9.12.2."},{"label":"deploy_files","kind":"template","detail":"array","insertSnippet":"deploy_files = []","documentation":"files placed beside the artifact Since 2026.8.10.1."},{"label":"dlopen_libs","kind":"template","detail":"array","insertSnippet":"dlopen_libs = []","documentation":"libraries the program opens at run time Since 0.0.46."},{"label":"frameworks","kind":"template","detail":"array","insertSnippet":"frameworks = []","documentation":"Since 2026.8.10.1."},{"label":"libraries","kind":"template","detail":"array","insertSnippet":"libraries = []","documentation":"Since 2026.8.10.1."},{"label":"library_dirs","kind":"template","detail":"array","insertSnippet":"library_dirs = []","documentation":"Since 0.0.46."},{"label":"link_library_dirs","kind":"template","detail":"array","insertSnippet":"link_library_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"Since 0.0.48."},{"label":"requirements","kind":"template","detail":"array","insertSnippet":"requirements = []","documentation":"[[runtime.requirements]]: { kind, value, phase, required, discovery } Since 2026.8.10.1."},{"label":"runtime_search_dirs","kind":"template","detail":"array","insertSnippet":"runtime_search_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"transitive_needed_dirs","kind":"template","detail":"array","insertSnippet":"transitive_needed_dirs = []","documentation":"Since 2026.8.10.1."}],[{"label":"artifacts","kind":"template","detail":"array","insertSnippet":"artifacts = []","documentation":"[[runtime.artifacts]]: prebuilt artifacts the package provides Since 2026.8.10.1."},{"label":"capabilities","kind":"template","detail":"array","insertSnippet":"capabilities = []","documentation":"Since 0.0.46."},{"label":"deploy","kind":"template","detail":"array","insertSnippet":"deploy = []","documentation":"[[runtime.deploy]]: { from, to } files placed beside the artifact Since 2026.9.12.2."},{"label":"deploy_files","kind":"template","detail":"array","insertSnippet":"deploy_files = []","documentation":"files placed beside the artifact Since 2026.8.10.1."},{"label":"dlopen_libs","kind":"template","detail":"array","insertSnippet":"dlopen_libs = []","documentation":"libraries the program opens at run time Since 0.0.46."},{"label":"frameworks","kind":"template","detail":"array","insertSnippet":"frameworks = []","documentation":"Since 2026.8.10.1."},{"label":"library_dirs","kind":"template","detail":"array","insertSnippet":"library_dirs = []","documentation":"Since 0.0.46."},{"label":"link_library_dirs","kind":"template","detail":"array","insertSnippet":"link_library_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []","documentation":"Since 0.0.48."},{"label":"requirements","kind":"template","detail":"array","insertSnippet":"requirements = []","documentation":"[[runtime.requirements]]: { kind, value, phase, required, discovery } Since 2026.8.10.1."},{"label":"runtime_search_dirs","kind":"template","detail":"array","insertSnippet":"runtime_search_dirs = []","documentation":"Since 2026.8.10.1."},{"label":"transitive_needed_dirs","kind":"template","detail":"array","insertSnippet":"transitive_needed_dirs = []","documentation":"Since 2026.8.10.1."}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"version","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"mcpp","body":"type: `string`\n\nsince: 2026.9.28.3\n\nrelease floor, only the \">=\" form is accepted (docs/04 §2.1)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"c++23","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"description","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"authors","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"cxxflags","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"auto","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"sources","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"[dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[dev-dependencies]","body":"plane: `dependency`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#25-dependencies-dev-dependencies-build-dependencies"},{"title":"[features]","body":"plane: `gate`\n\nkeys: 12","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"default","body":"type: `array`\n\nsince: 0.0.47\n\nthe features a consumer gets unless it opts out","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"sources","body":"type: `array`\n\nsince: 0.0.95\n\n[features.]: sources compiled only when the feature is active","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/06-features-and-capabilities.md#features--features-cargo-style-additive"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"bin","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"main","body":"type: `string`\n\na file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"entry","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[profile]","body":"plane: `compile`\n\nkeys: 10","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"opt","body":"type: `enum`\n\nvalues: `s` | `z`\n\nan -O level: a number, or \"s\"/\"z\"\n\nunmodelled: the type is inferred from the docs, not read from mcpp","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"3","body":"enum value of `opt`\n\none of: `s` | `z`\n\nan -O level: a number, or \"s\"/\"z\"","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#29-profilename--build-profiles"},{"title":"[target]","body":"plane: `condition`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#271-target--platform-conditional-dependencies--flags"},{"title":"platform","body":"type: `string`\n\nsince: 2026.10.27.1\n\nthe platform of the selected targets: \"@\", { path = \"...\" }, { git = ... }, or \"none\"; not accepted under a cfg(platform = ...) selector","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#271-target--platform-conditional-dependencies--flags"},{"title":"[workspace]","body":"plane: `unclassified`\n\nkeys: 2","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#218-workspace--tables-a-workspace-shares-202610101"},{"title":"members","body":"type: `array`\n\nsince: 0.0.11\n\nmember directories or globs","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#218-workspace--tables-a-workspace-shares-202610101"},{"title":"[xlings]","body":"plane: `tool`\n\nkeys: 4","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"deps","body":"type: `array`\n\nsince: 0.0.77\n\nthe project's host build tools: \"@\" or tables\n\nunmodelled: the type is inferred from the docs, not read from mcpp","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[xlings.workspace]","body":"plane: `tool`\n\nkeys: none declared in the snapshot","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#213-xlings--the-projects-environment"},{"title":"[toolchain]","body":"plane: `compile`\n\nkeys: 3 (open: the user picks the names)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#27-toolchain--toolchain-configuration"},{"title":"[pack]","body":"plane: `unclassified`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"format","body":"type: `string`\n\nsince: 2026.10.27.1\n\nthe default of --pkg-format: tar, zip, dir, single-file, or a format a package provides","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"[runtime]","body":"plane: `metadata`\n\nkeys: 13","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#211-runtime--provider-neutral-runtime-contract"},{"title":"libraries","body":"type: `array`\n\nsince: 2026.8.10.1","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#211-runtime--provider-neutral-runtime-contract"}],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":1,"startCharacter":1,"endLine":1,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":9},"line":1},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":4}}],"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":24},"value":{"kind":"string","range":{"startLine":2,"startCharacter":14,"endLine":2,"endCharacter":24},"open":false,"text":"demo.app","quote":"\"","multiline":false,"contentRange":{"startLine":2,"startCharacter":15,"endLine":2,"endCharacter":23}}},{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":3,"startCharacter":0,"endLine":3,"endCharacter":7}}],"range":{"startLine":3,"startCharacter":0,"endLine":3,"endCharacter":21},"value":{"kind":"string","range":{"startLine":3,"startCharacter":14,"endLine":3,"endCharacter":21},"open":false,"text":"0.3.0","quote":"\"","multiline":false,"contentRange":{"startLine":3,"startCharacter":15,"endLine":3,"endCharacter":20}}},{"type":"keyValue","keyPath":[{"name":"mcpp","quoted":false,"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":4}}],"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":30},"value":{"kind":"string","range":{"startLine":4,"startCharacter":14,"endLine":4,"endCharacter":30},"open":false,"text":">=2026.10.30.1","quote":"\"","multiline":false,"contentRange":{"startLine":4,"startCharacter":15,"endLine":4,"endCharacter":29}}},{"type":"keyValue","keyPath":[{"name":"standard","quoted":false,"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":8}}],"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":21},"value":{"kind":"string","range":{"startLine":5,"startCharacter":14,"endLine":5,"endCharacter":21},"open":false,"text":"c++23","quote":"\"","multiline":false,"contentRange":{"startLine":5,"startCharacter":15,"endLine":5,"endCharacter":20}}},{"type":"keyValue","keyPath":[{"name":"description","quoted":false,"range":{"startLine":6,"startCharacter":0,"endLine":6,"endCharacter":11}}],"range":{"startLine":6,"startCharacter":0,"endLine":6,"endCharacter":44},"value":{"kind":"string","range":{"startLine":6,"startCharacter":14,"endLine":6,"endCharacter":44},"open":false,"text":"演示 a manifest — with 中文 text","quote":"\"","multiline":false,"contentRange":{"startLine":6,"startCharacter":15,"endLine":6,"endCharacter":43}}},{"type":"keyValue","keyPath":[{"name":"authors","quoted":false,"range":{"startLine":7,"startCharacter":0,"endLine":7,"endCharacter":7}}],"range":{"startLine":7,"startCharacter":0,"endLine":7,"endCharacter":35},"value":{"kind":"array","range":{"startLine":7,"startCharacter":14,"endLine":7,"endCharacter":35},"open":false,"elements":[{"kind":"string","range":{"startLine":7,"startCharacter":15,"endLine":7,"endCharacter":34},"open":false,"text":"A ","quote":"\"","multiline":false,"contentRange":{"startLine":7,"startCharacter":16,"endLine":7,"endCharacter":33}}]}},{"type":"section","segments":[{"name":"build","quoted":false,"range":{"startLine":9,"startCharacter":1,"endLine":9,"endCharacter":6}}],"isArray":false,"open":false,"range":{"startLine":9,"startCharacter":0,"endLine":9,"endCharacter":7},"line":9},{"type":"keyValue","keyPath":[{"name":"cxxflags","quoted":false,"range":{"startLine":10,"startCharacter":0,"endLine":10,"endCharacter":8}}],"range":{"startLine":10,"startCharacter":0,"endLine":10,"endCharacter":27},"value":{"kind":"array","range":{"startLine":10,"startCharacter":11,"endLine":10,"endCharacter":27},"open":false,"elements":[{"kind":"string","range":{"startLine":10,"startCharacter":12,"endLine":10,"endCharacter":17},"open":false,"text":"-O2","quote":"\"","multiline":false,"contentRange":{"startLine":10,"startCharacter":13,"endLine":10,"endCharacter":16}},{"kind":"string","range":{"startLine":10,"startCharacter":19,"endLine":10,"endCharacter":26},"open":false,"text":"-Wall","quote":"\"","multiline":false,"contentRange":{"startLine":10,"startCharacter":20,"endLine":10,"endCharacter":25}}]}},{"type":"keyValue","keyPath":[{"name":"bmi_schedule","quoted":false,"range":{"startLine":11,"startCharacter":0,"endLine":11,"endCharacter":12}}],"range":{"startLine":11,"startCharacter":0,"endLine":11,"endCharacter":21},"value":{"kind":"string","range":{"startLine":11,"startCharacter":15,"endLine":11,"endCharacter":21},"open":false,"text":"auto","quote":"\"","multiline":false,"contentRange":{"startLine":11,"startCharacter":16,"endLine":11,"endCharacter":20}}},{"type":"keyValue","keyPath":[{"name":"sources","quoted":false,"range":{"startLine":12,"startCharacter":0,"endLine":12,"endCharacter":7}}],"range":{"startLine":12,"startCharacter":0,"endLine":15,"endCharacter":1},"value":{"kind":"array","range":{"startLine":12,"startCharacter":11,"endLine":15,"endCharacter":1},"open":false,"elements":[{"kind":"string","range":{"startLine":13,"startCharacter":4,"endLine":13,"endCharacter":19},"open":false,"text":"src/**/*.cppm","quote":"\"","multiline":false,"contentRange":{"startLine":13,"startCharacter":5,"endLine":13,"endCharacter":18}},{"kind":"string","range":{"startLine":14,"startCharacter":4,"endLine":14,"endCharacter":18},"open":false,"text":"src/**/*.cpp","quote":"\"","multiline":false,"contentRange":{"startLine":14,"startCharacter":5,"endLine":14,"endCharacter":17}}]}},{"type":"section","segments":[{"name":"dependencies","quoted":false,"range":{"startLine":17,"startCharacter":1,"endLine":17,"endCharacter":13}}],"isArray":false,"open":false,"range":{"startLine":17,"startCharacter":0,"endLine":17,"endCharacter":14},"line":17},{"type":"keyValue","keyPath":[{"name":"fmt","quoted":false,"range":{"startLine":18,"startCharacter":0,"endLine":18,"endCharacter":3}}],"range":{"startLine":18,"startCharacter":0,"endLine":18,"endCharacter":23},"value":{"kind":"string","range":{"startLine":18,"startCharacter":15,"endLine":18,"endCharacter":23},"open":false,"text":"11.0.2","quote":"\"","multiline":false,"contentRange":{"startLine":18,"startCharacter":16,"endLine":18,"endCharacter":22}}},{"type":"keyValue","keyPath":[{"name":"compat","quoted":false,"range":{"startLine":19,"startCharacter":0,"endLine":19,"endCharacter":6}},{"name":"zlib","quoted":false,"range":{"startLine":19,"startCharacter":7,"endLine":19,"endCharacter":11}}],"range":{"startLine":19,"startCharacter":0,"endLine":19,"endCharacter":36},"value":{"kind":"inlineTable","range":{"startLine":19,"startCharacter":15,"endLine":19,"endCharacter":36},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":19,"startCharacter":17,"endLine":19,"endCharacter":24}}],"range":{"startLine":19,"startCharacter":17,"endLine":19,"endCharacter":34},"value":{"kind":"string","range":{"startLine":19,"startCharacter":27,"endLine":19,"endCharacter":34},"open":false,"text":"1.3.1","quote":"\"","multiline":false,"contentRange":{"startLine":19,"startCharacter":28,"endLine":19,"endCharacter":33}}}]}},{"type":"keyValue","keyPath":[{"name":"local","quoted":false,"range":{"startLine":20,"startCharacter":0,"endLine":20,"endCharacter":5}}],"range":{"startLine":20,"startCharacter":0,"endLine":20,"endCharacter":34},"value":{"kind":"inlineTable","range":{"startLine":20,"startCharacter":15,"endLine":20,"endCharacter":34},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"path","quoted":false,"range":{"startLine":20,"startCharacter":17,"endLine":20,"endCharacter":21}}],"range":{"startLine":20,"startCharacter":17,"endLine":20,"endCharacter":32},"value":{"kind":"string","range":{"startLine":20,"startCharacter":24,"endLine":20,"endCharacter":32},"open":false,"text":"../lib","quote":"\"","multiline":false,"contentRange":{"startLine":20,"startCharacter":25,"endLine":20,"endCharacter":31}}}]}},{"type":"keyValue","keyPath":[{"name":"remote","quoted":false,"range":{"startLine":21,"startCharacter":0,"endLine":21,"endCharacter":6}}],"range":{"startLine":21,"startCharacter":0,"endLine":21,"endCharacter":75},"value":{"kind":"inlineTable","range":{"startLine":21,"startCharacter":15,"endLine":21,"endCharacter":75},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"git","quoted":false,"range":{"startLine":21,"startCharacter":17,"endLine":21,"endCharacter":20}}],"range":{"startLine":21,"startCharacter":17,"endLine":21,"endCharacter":57},"value":{"kind":"string","range":{"startLine":21,"startCharacter":23,"endLine":21,"endCharacter":57},"open":false,"text":"https://github.com/user/repo.git","quote":"\"","multiline":false,"contentRange":{"startLine":21,"startCharacter":24,"endLine":21,"endCharacter":56}}},{"type":"keyValue","keyPath":[{"name":"tag","quoted":false,"range":{"startLine":21,"startCharacter":59,"endLine":21,"endCharacter":62}}],"range":{"startLine":21,"startCharacter":59,"endLine":21,"endCharacter":73},"value":{"kind":"string","range":{"startLine":21,"startCharacter":65,"endLine":21,"endCharacter":73},"open":false,"text":"v1.0.0","quote":"\"","multiline":false,"contentRange":{"startLine":21,"startCharacter":66,"endLine":21,"endCharacter":72}}}]}},{"type":"keyValue","keyPath":[{"name":"shared","quoted":false,"range":{"startLine":22,"startCharacter":0,"endLine":22,"endCharacter":6}}],"range":{"startLine":22,"startCharacter":0,"endLine":22,"endCharacter":35},"value":{"kind":"inlineTable","range":{"startLine":22,"startCharacter":15,"endLine":22,"endCharacter":35},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"workspace","quoted":false,"range":{"startLine":22,"startCharacter":17,"endLine":22,"endCharacter":26}}],"range":{"startLine":22,"startCharacter":17,"endLine":22,"endCharacter":34},"value":{"kind":"boolean","range":{"startLine":22,"startCharacter":29,"endLine":22,"endCharacter":33},"open":false,"text":"true"}}]}},{"type":"keyValue","keyPath":[{"name":"simdlib","quoted":false,"range":{"startLine":23,"startCharacter":0,"endLine":23,"endCharacter":7}}],"range":{"startLine":23,"startCharacter":0,"endLine":23,"endCharacter":63},"value":{"kind":"inlineTable","range":{"startLine":23,"startCharacter":15,"endLine":23,"endCharacter":63},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":23,"startCharacter":17,"endLine":23,"endCharacter":24}}],"range":{"startLine":23,"startCharacter":17,"endLine":23,"endCharacter":32},"value":{"kind":"string","range":{"startLine":23,"startCharacter":27,"endLine":23,"endCharacter":32},"open":false,"text":"2.0","quote":"\"","multiline":false,"contentRange":{"startLine":23,"startCharacter":28,"endLine":23,"endCharacter":31}}},{"type":"keyValue","keyPath":[{"name":"features","quoted":false,"range":{"startLine":23,"startCharacter":34,"endLine":23,"endCharacter":42}}],"range":{"startLine":23,"startCharacter":34,"endLine":23,"endCharacter":61},"value":{"kind":"array","range":{"startLine":23,"startCharacter":45,"endLine":23,"endCharacter":61},"open":false,"elements":[{"kind":"string","range":{"startLine":23,"startCharacter":46,"endLine":23,"endCharacter":52},"open":false,"text":"avx2","quote":"\"","multiline":false,"contentRange":{"startLine":23,"startCharacter":47,"endLine":23,"endCharacter":51}},{"kind":"string","range":{"startLine":23,"startCharacter":54,"endLine":23,"endCharacter":60},"open":false,"text":"simd","quote":"\"","multiline":false,"contentRange":{"startLine":23,"startCharacter":55,"endLine":23,"endCharacter":59}}]}}]}},{"type":"section","segments":[{"name":"dependencies","quoted":false,"range":{"startLine":25,"startCharacter":1,"endLine":25,"endCharacter":13}},{"name":"mcpplibs","quoted":false,"range":{"startLine":25,"startCharacter":14,"endLine":25,"endCharacter":22}}],"isArray":false,"open":false,"range":{"startLine":25,"startCharacter":0,"endLine":25,"endCharacter":23},"line":25},{"type":"keyValue","keyPath":[{"name":"os","quoted":false,"range":{"startLine":26,"startCharacter":0,"endLine":26,"endCharacter":2}}],"range":{"startLine":26,"startCharacter":0,"endLine":26,"endCharacter":12},"value":{"kind":"string","range":{"startLine":26,"startCharacter":5,"endLine":26,"endCharacter":12},"open":false,"text":"0.2.3","quote":"\"","multiline":false,"contentRange":{"startLine":26,"startCharacter":6,"endLine":26,"endCharacter":11}}},{"type":"section","segments":[{"name":"dev-dependencies","quoted":false,"range":{"startLine":28,"startCharacter":1,"endLine":28,"endCharacter":17}}],"isArray":false,"open":false,"range":{"startLine":28,"startCharacter":0,"endLine":28,"endCharacter":18},"line":28},{"type":"keyValue","keyPath":[{"name":"gtest","quoted":false,"range":{"startLine":29,"startCharacter":0,"endLine":29,"endCharacter":5}}],"range":{"startLine":29,"startCharacter":0,"endLine":29,"endCharacter":16},"value":{"kind":"string","range":{"startLine":29,"startCharacter":8,"endLine":29,"endCharacter":16},"open":false,"text":"1.15.0","quote":"\"","multiline":false,"contentRange":{"startLine":29,"startCharacter":9,"endLine":29,"endCharacter":15}}},{"type":"section","segments":[{"name":"features","quoted":false,"range":{"startLine":31,"startCharacter":1,"endLine":31,"endCharacter":9}}],"isArray":false,"open":false,"range":{"startLine":31,"startCharacter":0,"endLine":31,"endCharacter":10},"line":31},{"type":"keyValue","keyPath":[{"name":"default","quoted":false,"range":{"startLine":32,"startCharacter":0,"endLine":32,"endCharacter":7}}],"range":{"startLine":32,"startCharacter":0,"endLine":32,"endCharacter":18},"value":{"kind":"array","range":{"startLine":32,"startCharacter":10,"endLine":32,"endCharacter":18},"open":false,"elements":[{"kind":"string","range":{"startLine":32,"startCharacter":11,"endLine":32,"endCharacter":17},"open":false,"text":"simd","quote":"\"","multiline":false,"contentRange":{"startLine":32,"startCharacter":12,"endLine":32,"endCharacter":16}}]}},{"type":"keyValue","keyPath":[{"name":"simd","quoted":false,"range":{"startLine":33,"startCharacter":0,"endLine":33,"endCharacter":4}}],"range":{"startLine":33,"startCharacter":0,"endLine":33,"endCharacter":36},"value":{"kind":"inlineTable","range":{"startLine":33,"startCharacter":10,"endLine":33,"endCharacter":36},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"defines","quoted":false,"range":{"startLine":33,"startCharacter":12,"endLine":33,"endCharacter":19}}],"range":{"startLine":33,"startCharacter":12,"endLine":33,"endCharacter":34},"value":{"kind":"array","range":{"startLine":33,"startCharacter":22,"endLine":33,"endCharacter":34},"open":false,"elements":[{"kind":"string","range":{"startLine":33,"startCharacter":23,"endLine":33,"endCharacter":33},"open":false,"text":"USE_SIMD","quote":"\"","multiline":false,"contentRange":{"startLine":33,"startCharacter":24,"endLine":33,"endCharacter":32}}]}}]}},{"type":"keyValue","keyPath":[{"name":"extra","quoted":false,"range":{"startLine":34,"startCharacter":0,"endLine":34,"endCharacter":5}}],"range":{"startLine":34,"startCharacter":0,"endLine":34,"endCharacter":12},"value":{"kind":"array","range":{"startLine":34,"startCharacter":10,"endLine":34,"endCharacter":12},"open":false,"elements":[]}},{"type":"section","segments":[{"name":"features","quoted":false,"range":{"startLine":36,"startCharacter":1,"endLine":36,"endCharacter":9}},{"name":"simd","quoted":false,"range":{"startLine":36,"startCharacter":10,"endLine":36,"endCharacter":14}}],"isArray":false,"open":false,"range":{"startLine":36,"startCharacter":0,"endLine":36,"endCharacter":15},"line":36},{"type":"keyValue","keyPath":[{"name":"sources","quoted":false,"range":{"startLine":37,"startCharacter":0,"endLine":37,"endCharacter":7}}],"range":{"startLine":37,"startCharacter":0,"endLine":37,"endCharacter":25},"value":{"kind":"array","range":{"startLine":37,"startCharacter":10,"endLine":37,"endCharacter":25},"open":false,"elements":[{"kind":"string","range":{"startLine":37,"startCharacter":11,"endLine":37,"endCharacter":24},"open":false,"text":"src/simd/**","quote":"\"","multiline":false,"contentRange":{"startLine":37,"startCharacter":12,"endLine":37,"endCharacter":23}}]}},{"type":"section","segments":[{"name":"targets","quoted":false,"range":{"startLine":39,"startCharacter":1,"endLine":39,"endCharacter":8}},{"name":"app","quoted":false,"range":{"startLine":39,"startCharacter":9,"endLine":39,"endCharacter":12}}],"isArray":false,"open":false,"range":{"startLine":39,"startCharacter":0,"endLine":39,"endCharacter":13},"line":39},{"type":"keyValue","keyPath":[{"name":"kind","quoted":false,"range":{"startLine":40,"startCharacter":0,"endLine":40,"endCharacter":4}}],"range":{"startLine":40,"startCharacter":0,"endLine":40,"endCharacter":12},"value":{"kind":"string","range":{"startLine":40,"startCharacter":7,"endLine":40,"endCharacter":12},"open":false,"text":"bin","quote":"\"","multiline":false,"contentRange":{"startLine":40,"startCharacter":8,"endLine":40,"endCharacter":11}}},{"type":"keyValue","keyPath":[{"name":"main","quoted":false,"range":{"startLine":41,"startCharacter":0,"endLine":41,"endCharacter":4}}],"range":{"startLine":41,"startCharacter":0,"endLine":41,"endCharacter":21},"value":{"kind":"string","range":{"startLine":41,"startCharacter":7,"endLine":41,"endCharacter":21},"open":false,"text":"src/main.cpp","quote":"\"","multiline":false,"contentRange":{"startLine":41,"startCharacter":8,"endLine":41,"endCharacter":20}}},{"type":"section","segments":[{"name":"targets","quoted":false,"range":{"startLine":43,"startCharacter":1,"endLine":43,"endCharacter":8}},{"name":"tool","quoted":false,"range":{"startLine":43,"startCharacter":9,"endLine":43,"endCharacter":13}}],"isArray":false,"open":false,"range":{"startLine":43,"startCharacter":0,"endLine":43,"endCharacter":14},"line":43},{"type":"keyValue","keyPath":[{"name":"kind","quoted":false,"range":{"startLine":44,"startCharacter":0,"endLine":44,"endCharacter":4}}],"range":{"startLine":44,"startCharacter":0,"endLine":44,"endCharacter":14},"value":{"kind":"string","range":{"startLine":44,"startCharacter":7,"endLine":44,"endCharacter":14},"open":false,"text":"entry","quote":"\"","multiline":false,"contentRange":{"startLine":44,"startCharacter":8,"endLine":44,"endCharacter":13}}},{"type":"section","segments":[{"name":"profile","quoted":false,"range":{"startLine":46,"startCharacter":1,"endLine":46,"endCharacter":8}},{"name":"release","quoted":false,"range":{"startLine":46,"startCharacter":9,"endLine":46,"endCharacter":16}}],"isArray":false,"open":false,"range":{"startLine":46,"startCharacter":0,"endLine":46,"endCharacter":17},"line":46},{"type":"keyValue","keyPath":[{"name":"opt","quoted":false,"range":{"startLine":47,"startCharacter":0,"endLine":47,"endCharacter":3}}],"range":{"startLine":47,"startCharacter":0,"endLine":47,"endCharacter":7},"value":{"kind":"integer","range":{"startLine":47,"startCharacter":6,"endLine":47,"endCharacter":7},"open":false,"text":"3"}},{"type":"section","segments":[{"name":"target","quoted":false,"range":{"startLine":49,"startCharacter":1,"endLine":49,"endCharacter":7}},{"name":"cfg(os = \"linux\")","quoted":true,"range":{"startLine":49,"startCharacter":8,"endLine":49,"endCharacter":27}},{"name":"build","quoted":false,"range":{"startLine":49,"startCharacter":28,"endLine":49,"endCharacter":33}}],"isArray":false,"open":false,"range":{"startLine":49,"startCharacter":0,"endLine":49,"endCharacter":34},"line":49},{"type":"keyValue","keyPath":[{"name":"cxxflags","quoted":false,"range":{"startLine":50,"startCharacter":0,"endLine":50,"endCharacter":8}}],"range":{"startLine":50,"startCharacter":0,"endLine":50,"endCharacter":23},"value":{"kind":"array","range":{"startLine":50,"startCharacter":11,"endLine":50,"endCharacter":23},"open":false,"elements":[{"kind":"string","range":{"startLine":50,"startCharacter":12,"endLine":50,"endCharacter":22},"open":false,"text":"-pthread","quote":"\"","multiline":false,"contentRange":{"startLine":50,"startCharacter":13,"endLine":50,"endCharacter":21}}]}},{"type":"section","segments":[{"name":"target","quoted":false,"range":{"startLine":52,"startCharacter":1,"endLine":52,"endCharacter":7}},{"name":"x86_64-openkal","quoted":false,"range":{"startLine":52,"startCharacter":8,"endLine":52,"endCharacter":22}}],"isArray":false,"open":false,"range":{"startLine":52,"startCharacter":0,"endLine":52,"endCharacter":23},"line":52},{"type":"keyValue","keyPath":[{"name":"platform","quoted":false,"range":{"startLine":53,"startCharacter":0,"endLine":53,"endCharacter":8}}],"range":{"startLine":53,"startCharacter":0,"endLine":53,"endCharacter":45},"value":{"kind":"string","range":{"startLine":53,"startCharacter":11,"endLine":53,"endCharacter":45},"open":false,"text":"mcpplibs.openkal-platform@0.18.0","quote":"\"","multiline":false,"contentRange":{"startLine":53,"startCharacter":12,"endLine":53,"endCharacter":44}}},{"type":"section","segments":[{"name":"workspace","quoted":false,"range":{"startLine":55,"startCharacter":1,"endLine":55,"endCharacter":10}}],"isArray":false,"open":false,"range":{"startLine":55,"startCharacter":0,"endLine":55,"endCharacter":11},"line":55},{"type":"keyValue","keyPath":[{"name":"members","quoted":false,"range":{"startLine":56,"startCharacter":0,"endLine":56,"endCharacter":7}}],"range":{"startLine":56,"startCharacter":0,"endLine":56,"endCharacter":20},"value":{"kind":"array","range":{"startLine":56,"startCharacter":10,"endLine":56,"endCharacter":20},"open":false,"elements":[{"kind":"string","range":{"startLine":56,"startCharacter":11,"endLine":56,"endCharacter":19},"open":false,"text":"libs/*","quote":"\"","multiline":false,"contentRange":{"startLine":56,"startCharacter":12,"endLine":56,"endCharacter":18}}]}},{"type":"section","segments":[{"name":"workspace","quoted":false,"range":{"startLine":58,"startCharacter":1,"endLine":58,"endCharacter":10}},{"name":"dependencies","quoted":false,"range":{"startLine":58,"startCharacter":11,"endLine":58,"endCharacter":23}}],"isArray":false,"open":false,"range":{"startLine":58,"startCharacter":0,"endLine":58,"endCharacter":24},"line":58},{"type":"keyValue","keyPath":[{"name":"shared","quoted":false,"range":{"startLine":59,"startCharacter":0,"endLine":59,"endCharacter":6}}],"range":{"startLine":59,"startCharacter":0,"endLine":59,"endCharacter":16},"value":{"kind":"string","range":{"startLine":59,"startCharacter":9,"endLine":59,"endCharacter":16},"open":false,"text":"1.2.0","quote":"\"","multiline":false,"contentRange":{"startLine":59,"startCharacter":10,"endLine":59,"endCharacter":15}}},{"type":"section","segments":[{"name":"xlings","quoted":false,"range":{"startLine":61,"startCharacter":1,"endLine":61,"endCharacter":7}}],"isArray":false,"open":false,"range":{"startLine":61,"startCharacter":0,"endLine":61,"endCharacter":8},"line":61},{"type":"keyValue","keyPath":[{"name":"deps","quoted":false,"range":{"startLine":62,"startCharacter":0,"endLine":62,"endCharacter":4}}],"range":{"startLine":62,"startCharacter":0,"endLine":62,"endCharacter":16},"value":{"kind":"array","range":{"startLine":62,"startCharacter":7,"endLine":62,"endCharacter":16},"open":false,"elements":[{"kind":"string","range":{"startLine":62,"startCharacter":8,"endLine":62,"endCharacter":15},"open":false,"text":"cmake","quote":"\"","multiline":false,"contentRange":{"startLine":62,"startCharacter":9,"endLine":62,"endCharacter":14}}]}},{"type":"section","segments":[{"name":"xlings","quoted":false,"range":{"startLine":64,"startCharacter":1,"endLine":64,"endCharacter":7}},{"name":"workspace","quoted":false,"range":{"startLine":64,"startCharacter":8,"endLine":64,"endCharacter":17}}],"isArray":false,"open":false,"range":{"startLine":64,"startCharacter":0,"endLine":64,"endCharacter":18},"line":64},{"type":"keyValue","keyPath":[{"name":"node","quoted":false,"range":{"startLine":65,"startCharacter":0,"endLine":65,"endCharacter":4}}],"range":{"startLine":65,"startCharacter":0,"endLine":65,"endCharacter":16},"value":{"kind":"string","range":{"startLine":65,"startCharacter":7,"endLine":65,"endCharacter":16},"open":false,"text":"24.19.0","quote":"\"","multiline":false,"contentRange":{"startLine":65,"startCharacter":8,"endLine":65,"endCharacter":15}}},{"type":"section","segments":[{"name":"toolchain","quoted":false,"range":{"startLine":67,"startCharacter":1,"endLine":67,"endCharacter":10}}],"isArray":false,"open":false,"range":{"startLine":67,"startCharacter":0,"endLine":67,"endCharacter":11},"line":67},{"type":"keyValue","keyPath":[{"name":"linux","quoted":false,"range":{"startLine":68,"startCharacter":0,"endLine":68,"endCharacter":5}}],"range":{"startLine":68,"startCharacter":0,"endLine":68,"endCharacter":21},"value":{"kind":"string","range":{"startLine":68,"startCharacter":8,"endLine":68,"endCharacter":21},"open":false,"text":"llvm@23.1.3","quote":"\"","multiline":false,"contentRange":{"startLine":68,"startCharacter":9,"endLine":68,"endCharacter":20}}},{"type":"section","segments":[{"name":"pack","quoted":false,"range":{"startLine":70,"startCharacter":1,"endLine":70,"endCharacter":5}}],"isArray":false,"open":false,"range":{"startLine":70,"startCharacter":0,"endLine":70,"endCharacter":6},"line":70},{"type":"keyValue","keyPath":[{"name":"format","quoted":false,"range":{"startLine":71,"startCharacter":0,"endLine":71,"endCharacter":6}}],"range":{"startLine":71,"startCharacter":0,"endLine":71,"endCharacter":14},"value":{"kind":"string","range":{"startLine":71,"startCharacter":9,"endLine":71,"endCharacter":14},"open":false,"text":"tar","quote":"\"","multiline":false,"contentRange":{"startLine":71,"startCharacter":10,"endLine":71,"endCharacter":13}}},{"type":"section","segments":[{"name":"runtime","quoted":false,"range":{"startLine":73,"startCharacter":1,"endLine":73,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":73,"startCharacter":0,"endLine":73,"endCharacter":9},"line":73},{"type":"keyValue","keyPath":[{"name":"libraries","quoted":false,"range":{"startLine":74,"startCharacter":0,"endLine":74,"endCharacter":9}}],"range":{"startLine":74,"startCharacter":0,"endLine":74,"endCharacter":17},"value":{"kind":"array","range":{"startLine":74,"startCharacter":12,"endLine":74,"endCharacter":17},"open":false,"elements":[{"kind":"string","range":{"startLine":74,"startCharacter":13,"endLine":74,"endCharacter":16},"open":false,"text":"m","quote":"\"","multiline":false,"contentRange":{"startLine":74,"startCharacter":14,"endLine":74,"endCharacter":15}}]}},{"type":"keyValue","keyPath":[{"name":"description2","quoted":false,"range":{"startLine":76,"startCharacter":0,"endLine":76,"endCharacter":12}}],"range":{"startLine":76,"startCharacter":0,"endLine":79,"endCharacter":3},"value":{"kind":"string","range":{"startLine":76,"startCharacter":15,"endLine":79,"endCharacter":3},"open":false,"text":"\na multi-line\nstring = with an equals sign\n","quote":"\"","multiline":true,"contentRange":{"startLine":76,"startCharacter":18,"endLine":79,"endCharacter":0}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":4,"0:5":5,"0:6":6,"0:7":7,"0:8":8,"0:9":9,"0:10":10,"0:11":11,"0:12":12,"0:13":13,"0:14":14,"0:15":15,"0:16":16,"0:17":17,"0:18":18,"0:19":19,"0:20":20,"0:21":21,"0:22":22,"0:23":23,"0:24":24,"0:25":25,"0:26":26,"0:27":27,"0:28":28,"0:29":29,"0:30":30,"0:31":31,"0:32":32,"0:33":33,"0:34":34,"0:35":35,"0:36":36,"0:37":37,"0:38":38,"0:39":39,"0:40":40,"0:41":41,"0:42":42,"0:43":43,"0:44":44,"0:45":45,"0:46":46,"0:47":47,"0:48":47,"0:49":47,"1:0":0,"1:1":48,"1:2":49,"1:3":49,"1:4":49,"1:5":49,"1:6":49,"1:7":49,"1:8":49,"1:9":50,"1:10":50,"1:11":50,"2:0":51,"2:1":52,"2:2":52,"2:3":52,"2:4":52,"2:5":53,"2:6":54,"2:7":55,"2:8":56,"2:9":57,"2:10":58,"2:11":59,"2:12":60,"2:13":61,"2:14":62,"2:15":63,"2:16":63,"2:17":63,"2:18":63,"2:19":63,"2:20":63,"2:21":63,"2:22":63,"2:23":63,"2:24":64,"2:25":64,"2:26":64,"3:0":51,"3:1":65,"3:2":65,"3:3":65,"3:4":65,"3:5":65,"3:6":65,"3:7":65,"3:8":66,"3:9":67,"3:10":68,"3:11":69,"3:12":70,"3:13":71,"3:14":72,"3:15":73,"3:16":73,"3:17":73,"3:18":73,"3:19":73,"3:20":73,"3:21":74,"3:22":74,"3:23":74,"4:0":51,"4:1":52,"4:2":52,"4:3":52,"4:4":52,"4:5":75,"4:6":76,"4:7":77,"4:8":78,"4:9":79,"4:10":80,"4:11":81,"4:12":82,"4:13":83,"4:14":84,"4:15":85,"4:16":85,"4:17":85,"4:18":85,"4:19":85,"4:20":85,"4:21":85,"4:22":85,"4:23":85,"4:24":85,"4:25":85,"4:26":85,"4:27":85,"4:28":85,"4:29":85,"4:30":86,"4:31":86,"4:32":86,"5:0":51,"5:1":87,"5:2":87,"5:3":87,"5:4":87,"5:5":87,"5:6":87,"5:7":87,"5:8":87,"5:9":88,"5:10":89,"5:11":90,"5:12":91,"5:13":92,"5:14":93,"5:15":94,"5:16":94,"5:17":94,"5:18":94,"5:19":94,"5:20":94,"5:21":95,"5:22":95,"5:23":95,"6:0":51,"6:1":96,"6:2":96,"6:3":96,"6:4":96,"6:5":96,"6:6":96,"6:7":96,"6:8":96,"6:9":96,"6:10":96,"6:11":96,"6:12":97,"6:13":98,"6:14":99,"6:15":100,"6:16":100,"6:17":100,"6:18":100,"6:19":100,"6:20":100,"6:21":100,"6:22":100,"6:23":100,"6:24":100,"6:25":100,"6:26":100,"6:27":100,"6:28":100,"6:29":100,"6:30":100,"6:31":100,"6:32":100,"6:33":100,"6:34":100,"6:35":100,"6:36":100,"6:37":100,"6:38":100,"6:39":100,"6:40":100,"6:41":100,"6:42":100,"6:43":100,"6:44":101,"6:45":101,"6:46":101,"7:0":51,"7:1":65,"7:2":65,"7:3":65,"7:4":65,"7:5":65,"7:6":65,"7:7":65,"7:8":102,"7:9":103,"7:10":104,"7:11":105,"7:12":106,"7:13":107,"7:14":108,"7:15":109,"7:16":110,"7:17":110,"7:18":110,"7:19":110,"7:20":110,"7:21":110,"7:22":110,"7:23":110,"7:24":110,"7:25":110,"7:26":110,"7:27":110,"7:28":110,"7:29":110,"7:30":110,"7:31":110,"7:32":110,"7:33":110,"7:34":111,"7:35":112,"7:36":112,"7:37":112,"8:0":51,"8:1":51,"8:2":51,"9:0":51,"9:1":48,"9:2":113,"9:3":113,"9:4":113,"9:5":113,"9:6":113,"9:7":114,"9:8":114,"9:9":114,"10:0":115,"10:1":116,"10:2":116,"10:3":116,"10:4":116,"10:5":116,"10:6":116,"10:7":116,"10:8":116,"10:9":117,"10:10":118,"10:11":119,"10:12":120,"10:13":121,"10:14":121,"10:15":121,"10:16":121,"10:17":122,"10:18":123,"10:19":124,"10:20":125,"10:21":125,"10:22":125,"10:23":125,"10:24":125,"10:25":125,"10:26":126,"10:27":127,"10:28":127,"10:29":127,"11:0":115,"11:1":128,"11:2":128,"11:3":128,"11:4":128,"11:5":128,"11:6":128,"11:7":128,"11:8":128,"11:9":128,"11:10":128,"11:11":128,"11:12":128,"11:13":129,"11:14":130,"11:15":131,"11:16":132,"11:17":132,"11:18":132,"11:19":132,"11:20":132,"11:21":133,"11:22":133,"11:23":133,"12:0":115,"12:1":134,"12:2":134,"12:3":134,"12:4":134,"12:5":134,"12:6":134,"12:7":134,"12:8":135,"12:9":136,"12:10":137,"12:11":138,"12:12":139,"12:13":139,"12:14":139,"13:0":140,"13:1":141,"13:2":142,"13:3":143,"13:4":144,"13:5":145,"13:6":145,"13:7":145,"13:8":145,"13:9":145,"13:10":145,"13:11":145,"13:12":145,"13:13":145,"13:14":145,"13:15":145,"13:16":145,"13:17":145,"13:18":145,"13:19":146,"13:20":147,"13:21":147,"13:22":147,"14:0":140,"14:1":141,"14:2":142,"14:3":143,"14:4":144,"14:5":148,"14:6":148,"14:7":148,"14:8":148,"14:9":148,"14:10":148,"14:11":148,"14:12":148,"14:13":148,"14:14":148,"14:15":148,"14:16":148,"14:17":148,"14:18":149,"14:19":150,"14:20":147,"14:21":151,"14:22":152,"14:23":153,"14:24":154,"14:25":155,"14:26":156,"14:27":157,"14:28":158,"14:29":159,"14:30":160,"14:31":161,"14:32":162,"14:33":163,"14:34":164,"14:35":165,"14:36":166,"14:37":167,"14:38":168,"14:39":169,"14:40":170,"14:41":171,"14:42":172,"14:43":173,"14:44":174,"14:45":175,"14:46":176,"14:47":177,"14:48":178,"14:49":179,"14:50":179,"14:51":179,"15:0":140,"15:1":180,"15:2":180,"15:3":180,"16:0":115,"16:1":115,"16:2":115,"17:0":115,"17:1":48,"17:2":181,"17:3":181,"17:4":181,"17:5":181,"17:6":181,"17:7":181,"17:8":181,"17:9":181,"17:10":181,"17:11":181,"17:12":181,"17:13":181,"17:14":182,"17:15":182,"17:16":182,"18:0":183,"18:1":184,"18:2":184,"18:3":184,"18:4":185,"18:5":186,"18:6":187,"18:7":188,"18:8":189,"18:9":190,"18:10":191,"18:11":192,"18:12":193,"18:13":194,"18:14":195,"18:15":196,"18:16":197,"18:17":197,"18:18":197,"18:19":197,"18:20":197,"18:21":197,"18:22":197,"18:23":198,"18:24":198,"18:25":198,"19:0":183,"19:1":199,"19:2":199,"19:3":199,"19:4":199,"19:5":199,"19:6":199,"19:7":200,"19:8":201,"19:9":201,"19:10":201,"19:11":201,"19:12":202,"19:13":203,"19:14":204,"19:15":205,"19:16":206,"19:17":207,"19:18":208,"19:19":208,"19:20":208,"19:21":208,"19:22":208,"19:23":208,"19:24":208,"19:25":209,"19:26":210,"19:27":211,"19:28":212,"19:29":212,"19:30":212,"19:31":212,"19:32":212,"19:33":212,"19:34":213,"19:35":214,"19:36":215,"19:37":215,"19:38":215,"20:0":183,"20:1":216,"20:2":216,"20:3":216,"20:4":216,"20:5":216,"20:6":217,"20:7":218,"20:8":219,"20:9":220,"20:10":221,"20:11":222,"20:12":223,"20:13":224,"20:14":225,"20:15":226,"20:16":227,"20:17":228,"20:18":229,"20:19":229,"20:20":229,"20:21":229,"20:22":230,"20:23":231,"20:24":232,"20:25":233,"20:26":233,"20:27":233,"20:28":233,"20:29":233,"20:30":233,"20:31":233,"20:32":234,"20:33":235,"20:34":236,"20:35":236,"20:36":236,"21:0":183,"21:1":199,"21:2":199,"21:3":199,"21:4":199,"21:5":199,"21:6":199,"21:7":237,"21:8":238,"21:9":239,"21:10":240,"21:11":241,"21:12":242,"21:13":243,"21:14":244,"21:15":245,"21:16":246,"21:17":247,"21:18":248,"21:19":248,"21:20":248,"21:21":249,"21:22":250,"21:23":251,"21:24":252,"21:25":252,"21:26":252,"21:27":252,"21:28":252,"21:29":252,"21:30":252,"21:31":252,"21:32":252,"21:33":252,"21:34":252,"21:35":252,"21:36":252,"21:37":252,"21:38":252,"21:39":252,"21:40":252,"21:41":252,"21:42":252,"21:43":252,"21:44":252,"21:45":252,"21:46":252,"21:47":252,"21:48":252,"21:49":252,"21:50":252,"21:51":252,"21:52":252,"21:53":252,"21:54":252,"21:55":252,"21:56":252,"21:57":253,"21:58":254,"21:59":255,"21:60":256,"21:61":256,"21:62":256,"21:63":257,"21:64":258,"21:65":259,"21:66":260,"21:67":260,"21:68":260,"21:69":260,"21:70":260,"21:71":260,"21:72":260,"21:73":261,"21:74":262,"21:75":263,"21:76":263,"21:77":263,"22:0":183,"22:1":199,"22:2":199,"22:3":199,"22:4":199,"22:5":199,"22:6":199,"22:7":264,"22:8":265,"22:9":266,"22:10":267,"22:11":268,"22:12":269,"22:13":270,"22:14":271,"22:15":272,"22:16":273,"22:17":274,"22:18":275,"22:19":275,"22:20":275,"22:21":275,"22:22":275,"22:23":275,"22:24":275,"22:25":275,"22:26":275,"22:27":276,"22:28":277,"22:29":277,"22:30":277,"22:31":277,"22:32":277,"22:33":277,"22:34":278,"22:35":279,"22:36":279,"22:37":279,"23:0":183,"23:1":280,"23:2":280,"23:3":280,"23:4":280,"23:5":280,"23:6":280,"23:7":280,"23:8":281,"23:9":282,"23:10":283,"23:11":284,"23:12":285,"23:13":286,"23:14":287,"23:15":288,"23:16":289,"23:17":290,"23:18":291,"23:19":291,"23:20":291,"23:21":291,"23:22":291,"23:23":291,"23:24":291,"23:25":292,"23:26":293,"23:27":294,"23:28":295,"23:29":295,"23:30":295,"23:31":295,"23:32":296,"23:33":297,"23:34":298,"23:35":299,"23:36":299,"23:37":299,"23:38":299,"23:39":299,"23:40":299,"23:41":299,"23:42":299,"23:43":300,"23:44":301,"23:45":302,"23:46":303,"23:47":304,"23:48":304,"23:49":304,"23:50":304,"23:51":304,"23:52":305,"23:53":306,"23:54":307,"23:55":308,"23:56":308,"23:57":308,"23:58":308,"23:59":308,"23:60":309,"23:61":310,"23:62":311,"23:63":312,"23:64":312,"23:65":312,"24:0":183,"24:1":183,"24:2":183,"25:0":183,"25:1":48,"25:2":181,"25:3":181,"25:4":181,"25:5":181,"25:6":181,"25:7":181,"25:8":181,"25:9":181,"25:10":181,"25:11":181,"25:12":181,"25:13":181,"25:14":313,"25:15":314,"25:16":314,"25:17":314,"25:18":314,"25:19":314,"25:20":314,"25:21":314,"25:22":314,"25:23":315,"25:24":315,"25:25":315,"26:0":183,"26:1":316,"26:2":316,"26:3":317,"26:4":318,"26:5":319,"26:6":320,"26:7":320,"26:8":320,"26:9":320,"26:10":320,"26:11":320,"26:12":321,"26:13":321,"26:14":321,"27:0":183,"27:1":183,"27:2":183,"28:0":183,"28:1":48,"28:2":322,"28:3":322,"28:4":322,"28:5":322,"28:6":322,"28:7":322,"28:8":322,"28:9":322,"28:10":322,"28:11":322,"28:12":322,"28:13":322,"28:14":322,"28:15":322,"28:16":322,"28:17":322,"28:18":323,"28:19":323,"28:20":323,"29:0":324,"29:1":325,"29:2":325,"29:3":325,"29:4":325,"29:5":325,"29:6":326,"29:7":327,"29:8":328,"29:9":329,"29:10":329,"29:11":329,"29:12":329,"29:13":329,"29:14":329,"29:15":329,"29:16":330,"29:17":330,"29:18":330,"30:0":324,"30:1":324,"30:2":324,"31:0":324,"31:1":48,"31:2":331,"31:3":331,"31:4":331,"31:5":331,"31:6":331,"31:7":331,"31:8":331,"31:9":331,"31:10":332,"31:11":332,"31:12":332,"32:0":333,"32:1":334,"32:2":334,"32:3":334,"32:4":334,"32:5":334,"32:6":334,"32:7":334,"32:8":335,"32:9":336,"32:10":337,"32:11":338,"32:12":339,"32:13":339,"32:14":339,"32:15":339,"32:16":339,"32:17":340,"32:18":341,"32:19":341,"32:20":341,"33:0":333,"33:1":342,"33:2":342,"33:3":342,"33:4":342,"33:5":343,"33:6":344,"33:7":345,"33:8":346,"33:9":347,"33:10":348,"33:11":349,"33:12":350,"33:13":351,"33:14":351,"33:15":351,"33:16":351,"33:17":351,"33:18":351,"33:19":351,"33:20":352,"33:21":353,"33:22":354,"33:23":355,"33:24":356,"33:25":356,"33:26":356,"33:27":356,"33:28":356,"33:29":356,"33:30":356,"33:31":356,"33:32":356,"33:33":357,"33:34":358,"33:35":359,"33:36":360,"33:37":360,"33:38":360,"34:0":333,"34:1":361,"34:2":361,"34:3":361,"34:4":361,"34:5":361,"34:6":362,"34:7":363,"34:8":364,"34:9":365,"34:10":366,"34:11":367,"34:12":368,"34:13":368,"34:14":368,"35:0":333,"35:1":333,"35:2":333,"36:0":333,"36:1":48,"36:2":331,"36:3":331,"36:4":331,"36:5":331,"36:6":331,"36:7":331,"36:8":331,"36:9":331,"36:10":369,"36:11":370,"36:12":370,"36:13":370,"36:14":370,"36:15":371,"36:16":371,"36:17":371,"37:0":333,"37:1":334,"37:2":334,"37:3":334,"37:4":334,"37:5":334,"37:6":334,"37:7":334,"37:8":372,"37:9":373,"37:10":374,"37:11":375,"37:12":376,"37:13":376,"37:14":376,"37:15":376,"37:16":376,"37:17":376,"37:18":376,"37:19":376,"37:20":376,"37:21":376,"37:22":376,"37:23":376,"37:24":377,"37:25":378,"37:26":378,"37:27":378,"38:0":333,"38:1":333,"38:2":333,"39:0":333,"39:1":48,"39:2":49,"39:3":49,"39:4":49,"39:5":49,"39:6":49,"39:7":49,"39:8":49,"39:9":379,"39:10":380,"39:11":380,"39:12":380,"39:13":381,"39:14":381,"39:15":381,"40:0":382,"40:1":383,"40:2":383,"40:3":383,"40:4":383,"40:5":384,"40:6":385,"40:7":386,"40:8":387,"40:9":387,"40:10":387,"40:11":387,"40:12":388,"40:13":388,"40:14":388,"41:0":382,"41:1":383,"41:2":383,"41:3":383,"41:4":383,"41:5":389,"41:6":390,"41:7":391,"41:8":392,"41:9":392,"41:10":392,"41:11":392,"41:12":392,"41:13":392,"41:14":392,"41:15":392,"41:16":392,"41:17":392,"41:18":392,"41:19":392,"41:20":392,"41:21":393,"41:22":393,"41:23":393,"42:0":382,"42:1":382,"42:2":382,"43:0":382,"43:1":48,"43:2":49,"43:3":49,"43:4":49,"43:5":49,"43:6":49,"43:7":49,"43:8":49,"43:9":379,"43:10":394,"43:11":394,"43:12":394,"43:13":394,"43:14":395,"43:15":395,"43:16":395,"44:0":382,"44:1":383,"44:2":383,"44:3":383,"44:4":383,"44:5":384,"44:6":396,"44:7":386,"44:8":397,"44:9":397,"44:10":397,"44:11":397,"44:12":397,"44:13":397,"44:14":398,"44:15":398,"44:16":398,"45:0":382,"45:1":382,"45:2":382,"46:0":382,"46:1":48,"46:2":49,"46:3":49,"46:4":49,"46:5":49,"46:6":49,"46:7":49,"46:8":49,"46:9":399,"46:10":400,"46:11":400,"46:12":400,"46:13":400,"46:14":400,"46:15":400,"46:16":400,"46:17":401,"46:18":401,"46:19":401,"47:0":402,"47:1":403,"47:2":403,"47:3":403,"47:4":404,"47:5":405,"47:6":405,"47:7":405,"47:8":405,"47:9":405,"48:0":402,"48:1":402,"48:2":402,"49:0":402,"49:1":48,"49:2":406,"49:3":406,"49:4":406,"49:5":406,"49:6":406,"49:7":406,"49:8":407,"49:9":408,"49:10":408,"49:11":408,"49:12":408,"49:13":408,"49:14":408,"49:15":408,"49:16":408,"49:17":408,"49:18":408,"49:19":408,"49:20":408,"49:21":408,"49:22":408,"49:23":408,"49:24":408,"49:25":408,"49:26":408,"49:27":408,"49:28":409,"49:29":410,"49:30":410,"49:31":410,"49:32":410,"49:33":410,"49:34":411,"49:35":411,"49:36":411,"50:0":115,"50:1":116,"50:2":116,"50:3":116,"50:4":116,"50:5":116,"50:6":116,"50:7":116,"50:8":116,"50:9":117,"50:10":118,"50:11":119,"50:12":120,"50:13":412,"50:14":412,"50:15":412,"50:16":412,"50:17":412,"50:18":412,"50:19":412,"50:20":412,"50:21":412,"50:22":413,"50:23":414,"50:24":414,"50:25":414,"51:0":115,"51:1":115,"51:2":115,"52:0":115,"52:1":48,"52:2":406,"52:3":406,"52:4":406,"52:5":406,"52:6":406,"52:7":406,"52:8":407,"52:9":415,"52:10":415,"52:11":415,"52:12":415,"52:13":415,"52:14":415,"52:15":415,"52:16":415,"52:17":415,"52:18":415,"52:19":415,"52:20":415,"52:21":415,"52:22":415,"52:23":416,"52:24":416,"52:25":416,"53:0":417,"53:1":418,"53:2":418,"53:3":418,"53:4":418,"53:5":418,"53:6":418,"53:7":418,"53:8":418,"53:9":419,"53:10":420,"53:11":421,"53:12":422,"53:13":422,"53:14":422,"53:15":422,"53:16":422,"53:17":422,"53:18":422,"53:19":422,"53:20":422,"53:21":422,"53:22":422,"53:23":422,"53:24":422,"53:25":422,"53:26":422,"53:27":422,"53:28":422,"53:29":422,"53:30":422,"53:31":422,"53:32":422,"53:33":422,"53:34":422,"53:35":422,"53:36":422,"53:37":422,"53:38":422,"53:39":422,"53:40":422,"53:41":422,"53:42":422,"53:43":422,"53:44":422,"53:45":423,"53:46":423,"53:47":423,"54:0":417,"54:1":417,"54:2":417,"55:0":417,"55:1":48,"55:2":424,"55:3":424,"55:4":424,"55:5":424,"55:6":424,"55:7":424,"55:8":424,"55:9":424,"55:10":424,"55:11":425,"55:12":425,"55:13":425,"56:0":426,"56:1":427,"56:2":427,"56:3":427,"56:4":427,"56:5":427,"56:6":427,"56:7":427,"56:8":428,"56:9":429,"56:10":430,"56:11":431,"56:12":432,"56:13":432,"56:14":432,"56:15":432,"56:16":432,"56:17":432,"56:18":432,"56:19":433,"56:20":434,"56:21":434,"56:22":434,"57:0":426,"57:1":426,"57:2":426,"58:0":426,"58:1":48,"58:2":424,"58:3":424,"58:4":424,"58:5":424,"58:6":424,"58:7":424,"58:8":424,"58:9":424,"58:10":424,"58:11":435,"58:12":436,"58:13":436,"58:14":436,"58:15":436,"58:16":436,"58:17":436,"58:18":436,"58:19":436,"58:20":436,"58:21":436,"58:22":436,"58:23":436,"58:24":437,"58:25":437,"58:26":437,"59:0":438,"59:1":439,"59:2":439,"59:3":439,"59:4":439,"59:5":439,"59:6":439,"59:7":440,"59:8":441,"59:9":442,"59:10":443,"59:11":443,"59:12":443,"59:13":443,"59:14":443,"59:15":443,"59:16":444,"59:17":444,"59:18":444,"60:0":438,"60:1":438,"60:2":438,"61:0":438,"61:1":48,"61:2":406,"61:3":406,"61:4":406,"61:5":406,"61:6":406,"61:7":406,"61:8":445,"61:9":445,"61:10":445,"62:0":446,"62:1":447,"62:2":447,"62:3":447,"62:4":447,"62:5":448,"62:6":449,"62:7":450,"62:8":451,"62:9":452,"62:10":452,"62:11":452,"62:12":452,"62:13":452,"62:14":452,"62:15":453,"62:16":454,"62:17":454,"62:18":454,"63:0":446,"63:1":446,"63:2":446,"64:0":446,"64:1":48,"64:2":406,"64:3":406,"64:4":406,"64:5":406,"64:6":406,"64:7":406,"64:8":455,"64:9":456,"64:10":456,"64:11":456,"64:12":456,"64:13":456,"64:14":456,"64:15":456,"64:16":456,"64:17":456,"64:18":457,"64:19":457,"64:20":457,"65:0":458,"65:1":459,"65:2":459,"65:3":459,"65:4":459,"65:5":460,"65:6":461,"65:7":462,"65:8":463,"65:9":463,"65:10":463,"65:11":463,"65:12":463,"65:13":463,"65:14":463,"65:15":463,"65:16":464,"65:17":464,"65:18":464,"66:0":458,"66:1":458,"66:2":458,"67:0":458,"67:1":48,"67:2":424,"67:3":424,"67:4":424,"67:5":424,"67:6":424,"67:7":424,"67:8":424,"67:9":424,"67:10":424,"67:11":465,"67:12":465,"67:13":465,"68:0":466,"68:1":467,"68:2":467,"68:3":467,"68:4":467,"68:5":467,"68:6":468,"68:7":469,"68:8":470,"68:9":471,"68:10":471,"68:11":471,"68:12":471,"68:13":471,"68:14":471,"68:15":471,"68:16":471,"68:17":471,"68:18":471,"68:19":471,"68:20":471,"68:21":472,"68:22":472,"68:23":472,"69:0":466,"69:1":466,"69:2":466,"70:0":466,"70:1":48,"70:2":473,"70:3":473,"70:4":473,"70:5":473,"70:6":474,"70:7":474,"70:8":474,"71:0":475,"71:1":476,"71:2":476,"71:3":476,"71:4":476,"71:5":476,"71:6":476,"71:7":477,"71:8":478,"71:9":479,"71:10":480,"71:11":480,"71:12":480,"71:13":480,"71:14":481,"71:15":481,"71:16":481,"72:0":475,"72:1":475,"72:2":475,"73:0":475,"73:1":48,"73:2":49,"73:3":49,"73:4":49,"73:5":49,"73:6":49,"73:7":49,"73:8":49,"73:9":482,"73:10":482,"73:11":482,"74:0":483,"74:1":484,"74:2":484,"74:3":484,"74:4":484,"74:5":484,"74:6":484,"74:7":484,"74:8":484,"74:9":484,"74:10":485,"74:11":486,"74:12":487,"74:13":488,"74:14":489,"74:15":489,"74:16":490,"74:17":491,"74:18":491,"74:19":491,"75:0":483,"75:1":483,"75:2":483,"76:0":483,"76:1":492,"76:2":492,"76:3":492,"76:4":492,"76:5":492,"76:6":492,"76:7":492,"76:8":492,"76:9":492,"76:10":492,"76:11":492,"76:12":492,"76:13":493,"76:14":494,"76:15":495,"76:16":496,"76:17":497,"76:18":498,"76:19":498,"76:20":498,"77:0":499,"77:1":499,"77:2":499,"77:3":499,"77:4":499,"77:5":499,"77:6":499,"77:7":499,"77:8":499,"77:9":499,"77:10":499,"77:11":499,"77:12":499,"77:13":499,"77:14":499,"78:0":500,"78:1":500,"78:2":500,"78:3":500,"78:4":500,"78:5":500,"78:6":500,"78:7":500,"78:8":500,"78:9":500,"78:10":500,"78:11":500,"78:12":500,"78:13":500,"78:14":500,"78:15":500,"78:16":500,"78:17":500,"78:18":500,"78:19":500,"78:20":500,"78:21":500,"78:22":500,"78:23":500,"78:24":500,"78:25":500,"78:26":500,"78:27":500,"78:28":500,"78:29":500,"78:30":500,"79:0":501,"79:1":502,"79:2":503,"79:3":504,"79:4":504,"79:5":504,"80:0":483,"80:1":483,"80:2":483,"81:0":483,"81:1":483,"81:2":483},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":3,"endCharacter":3}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":20,"endCharacter":20}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":25,"endCharacter":25}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":26,"endCharacter":26}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":27,"endCharacter":27}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":28,"endCharacter":28}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":29,"endCharacter":29}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":30,"endCharacter":30}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":31,"endCharacter":31}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":32,"endCharacter":32}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":33,"endCharacter":33}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":35,"endCharacter":35}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":36,"endCharacter":36}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":37,"endCharacter":37}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":38,"endCharacter":38}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":39,"endCharacter":39}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":40,"endCharacter":40}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":41,"endCharacter":41}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":42,"endCharacter":42}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":43,"endCharacter":43}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":44,"endCharacter":44}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":45,"endCharacter":45}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":46,"endCharacter":46}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":47,"endCharacter":47}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":24}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["version"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["version"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["version"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["version"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["version"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["mcpp"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["mcpp"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":30}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["mcpp"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["mcpp"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":29}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["mcpp"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":30,"endCharacter":30}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["standard"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["standard"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["standard"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["standard"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["standard"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["standard"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["standard"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["standard"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["description"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["description"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":44}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["description"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["description"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":43}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["description"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":44,"endCharacter":44}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["authors"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["authors"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["authors"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["authors"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["authors"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["authors"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["authors"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["authors"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["authors"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":33}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["authors"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":35,"endCharacter":35}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":["cxxflags"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":20,"endCharacter":25}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":26,"endCharacter":26}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":27,"endCharacter":27}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":["bmi_schedule"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":["sources"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":["sources"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":3,"endCharacter":3}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":5,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":20,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":5,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":25,"endCharacter":25}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":26,"endCharacter":26}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":27,"endCharacter":27}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":28,"endCharacter":28}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":29,"endCharacter":29}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":30,"endCharacter":30}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":31,"endCharacter":31}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":32,"endCharacter":32}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":33,"endCharacter":33}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":35,"endCharacter":35}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":36,"endCharacter":36}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":37,"endCharacter":37}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":38,"endCharacter":38}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":39,"endCharacter":39}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":40,"endCharacter":40}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":41,"endCharacter":41}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":42,"endCharacter":42}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":43,"endCharacter":43}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":44,"endCharacter":44}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":45,"endCharacter":45}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":46,"endCharacter":46}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":47,"endCharacter":47}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":48,"endCharacter":48}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":49,"endCharacter":49}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":3}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["fmt"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["fmt"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["fmt"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["fmt"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":22}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["fmt"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["compat"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["compat"],"replaceRange":{"startCharacter":7,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["compat","zlib"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["compat","zlib"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["compat","zlib"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["compat","zlib"],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["compat","zlib"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["compat","zlib"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["compat","zlib"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":24}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["compat","zlib"],"keyPrefix":["version"],"replaceRange":{"startCharacter":25,"endCharacter":25}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["compat","zlib","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":27,"endCharacter":34}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["compat","zlib","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":27,"endCharacter":27}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["compat","zlib","version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":28,"endCharacter":33}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["compat","zlib","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["compat","zlib"],"keyPrefix":[],"replaceRange":{"startCharacter":35,"endCharacter":35}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":36,"endCharacter":36}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["local"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["local"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["local"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["local"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["local"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["local"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["local"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["local"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["local"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["local"],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["local"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["local"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["local"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":21}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["local"],"keyPrefix":["path"],"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["local","path"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":32}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["local","path"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["local","path"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":25,"endCharacter":31}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["local","path"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":32,"endCharacter":32}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["local"],"keyPrefix":[],"replaceRange":{"startCharacter":33,"endCharacter":33}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["remote"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["remote"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["remote"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["remote"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["remote"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["remote"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["remote"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":20}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":["git"],"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["remote","git"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":57}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["remote","git"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["remote","git"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":56}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["remote","git"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":57,"endCharacter":57}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":58,"endCharacter":58}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":59,"endCharacter":59}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":59,"endCharacter":62}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":["tag"],"replaceRange":{"startCharacter":63,"endCharacter":63}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["remote","tag"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":65,"endCharacter":73}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["remote","tag"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":65,"endCharacter":65}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["remote","tag"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":66,"endCharacter":72}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["remote","tag"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":73,"endCharacter":73}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["remote"],"keyPrefix":[],"replaceRange":{"startCharacter":74,"endCharacter":74}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":75,"endCharacter":75}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["shared"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["shared"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["shared"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["shared"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["shared"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["shared"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["shared"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["shared"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["shared"],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["shared"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["shared"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["shared"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":26}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["shared"],"keyPrefix":["workspace"],"replaceRange":{"startCharacter":27,"endCharacter":27}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["shared","workspace"],"valueKind":"boolean","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":29,"endCharacter":33}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["shared"],"keyPrefix":[],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":35,"endCharacter":35}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["simdlib"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["simdlib"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["simdlib"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["simdlib"],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["simdlib"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["simdlib"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":24}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":["version"],"replaceRange":{"startCharacter":25,"endCharacter":25}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":27,"endCharacter":32}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":27,"endCharacter":27}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":28,"endCharacter":31}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":32,"endCharacter":32}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":33,"endCharacter":33}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":34,"endCharacter":42}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":["features"],"replaceRange":{"startCharacter":43,"endCharacter":43}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":44,"endCharacter":44}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":45,"endCharacter":45}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":46,"endCharacter":46}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":47,"endCharacter":51}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":52,"endCharacter":52}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":53,"endCharacter":53}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":54,"endCharacter":54}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":55,"endCharacter":59}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["simdlib","features"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":60,"endCharacter":60}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":61,"endCharacter":61}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":["simdlib"],"keyPrefix":[],"replaceRange":{"startCharacter":62,"endCharacter":62}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":63,"endCharacter":63}},{"kind":"section-header","segments":["dependencies"],"isArray":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"section-header","segments":["dependencies"],"isArray":false,"replaceRange":{"startCharacter":14,"endCharacter":22}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":2}},{"kind":"key","section":{"kind":"known","group":"dependencies"},"containerPath":[],"keyPrefix":["os"],"replaceRange":{"startCharacter":3,"endCharacter":3}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["os"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":5,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["os"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["os"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"dependencies"},"keyPath":["os"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"dev-dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"key","section":{"kind":"known","group":"dev-dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"dev-dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"dev-dependencies"},"containerPath":[],"keyPrefix":["gtest"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"known","group":"dev-dependencies"},"keyPath":["gtest"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"dev-dependencies"},"keyPath":["gtest"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"dev-dependencies"},"keyPath":["gtest"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"dev-dependencies"},"keyPath":["gtest"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["default"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["default"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["default"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["default"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["default"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["default"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["simd"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["simd"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["simd"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["simd"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":19}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":["defines"],"replaceRange":{"startCharacter":20,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","defines"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","defines"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","defines"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","defines"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":32}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","defines"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":33,"endCharacter":33}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":35,"endCharacter":35}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":36,"endCharacter":36}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["extra"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["extra"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["extra"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["extra"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["extra"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["extra"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"section-header","segments":["features"],"isArray":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"section-header","segments":["features"],"isArray":false,"replaceRange":{"startCharacter":10,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["sources"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["sources"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["sources"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":25,"endCharacter":25}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":["kind"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":["main"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["main"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["main"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["main"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["main"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"section-header","segments":["profile"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"section-header","segments":["profile"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"profile"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"profile"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"profile"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":3}},{"kind":"key","section":{"kind":"known","group":"profile"},"containerPath":[],"keyPrefix":["opt"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"value","section":{"kind":"known","group":"profile"},"keyPath":["opt"],"valueKind":"integer","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":7}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":7}},{"kind":"section-header","segments":["target"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"section-header","segments":["target"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":27}},{"kind":"section-header","segments":["target","cfg(os = \"linux\")"],"isArray":false,"replaceRange":{"startCharacter":28,"endCharacter":28}},{"kind":"section-header","segments":["target","cfg(os = \"linux\")"],"isArray":false,"replaceRange":{"startCharacter":28,"endCharacter":33}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"section-header","segments":["target"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":22}},{"kind":"key","section":{"kind":"known","group":"target"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"key","section":{"kind":"known","group":"target"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"target"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"target"},"containerPath":[],"keyPrefix":["platform"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"target"},"keyPath":["platform"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":45}},{"kind":"value","section":{"kind":"known","group":"target"},"keyPath":["platform"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"target"},"keyPath":["platform"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":44}},{"kind":"value","section":{"kind":"known","group":"target"},"keyPath":["platform"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":45,"endCharacter":45}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"workspace"},"containerPath":[],"keyPrefix":["members"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"workspace"},"keyPath":["members"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"workspace"},"keyPath":["members"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"workspace"},"keyPath":["members"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"workspace"},"keyPath":["members"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"workspace"},"keyPath":["members"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"key","section":{"kind":"known","group":"workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":20,"endCharacter":20}},{"kind":"section-header","segments":["workspace"],"isArray":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"section-header","segments":["workspace"],"isArray":false,"replaceRange":{"startCharacter":11,"endCharacter":23}},{"kind":"key","section":{"kind":"known","group":"workspace.dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"key","section":{"kind":"known","group":"workspace.dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"workspace.dependencies"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"workspace.dependencies"},"containerPath":[],"keyPrefix":["shared"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"workspace.dependencies"},"keyPath":["shared"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"workspace.dependencies"},"keyPath":["shared"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"workspace.dependencies"},"keyPath":["shared"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"workspace.dependencies"},"keyPath":["shared"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":["deps"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["deps"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["deps"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["deps"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["deps"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"xlings"},"keyPath":["deps"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"xlings"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"section-header","segments":["xlings"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"section-header","segments":["xlings"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"xlings.workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"key","section":{"kind":"known","group":"xlings.workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"xlings.workspace"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"xlings.workspace"},"containerPath":[],"keyPrefix":["node"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"xlings.workspace"},"keyPath":["node"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"xlings.workspace"},"keyPath":["node"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"xlings.workspace"},"keyPath":["node"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"xlings.workspace"},"keyPath":["node"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"toolchain"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"toolchain"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"toolchain"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"toolchain"},"containerPath":[],"keyPrefix":["linux"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"known","group":"toolchain"},"keyPath":["linux"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"toolchain"},"keyPath":["linux"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"toolchain"},"keyPath":["linux"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"toolchain"},"keyPath":["linux"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"pack"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"pack"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"pack"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"pack"},"containerPath":[],"keyPrefix":["format"],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"pack"},"keyPath":["format"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"pack"},"keyPath":["format"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"pack"},"keyPath":["format"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"pack"},"keyPath":["format"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"runtime"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"runtime"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"runtime"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"runtime"},"containerPath":[],"keyPrefix":["libraries"],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["libraries"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["libraries"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["libraries"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["libraries"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["libraries"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"runtime"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"runtime"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"runtime"},"containerPath":[],"keyPrefix":["description2"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":28}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"known","group":"runtime"},"keyPath":["description2"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":3,"endCharacter":3}}]} diff --git a/tests/rules/golden/crlf.toml.json b/tests/rules/golden/crlf.toml.json index 10c0465..7c06371 100644 --- a/tests/rules/golden/crlf.toml.json +++ b/tests/rules/golden/crlf.toml.json @@ -1 +1 @@ -{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"1:13":[2],"1:14":[2],"1:15":[2],"2:0":[3,0,0],"2:1":[3,0,7],"2:2":[3,0,7],"2:3":[3,0,7],"2:4":[3,0,7],"2:5":[3,0,7],"2:6":[3,0,7],"2:7":[3,0,7],"2:8":[3,8,8],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"2:18":[2],"2:19":[2],"3:0":[4,0,0],"3:1":[4,0,0],"3:2":[4,0,0],"4:0":[4,0,0],"4:1":[0,0,1],"4:2":[0,0,6],"4:3":[0,0,6],"4:4":[0,0,6],"4:5":[0,0,6],"4:6":[0,0,6],"4:7":[5,7,7],"4:8":[5,7,7],"4:9":[5,7,7],"5:0":[5,0,0],"5:1":[5,0,12],"5:2":[5,0,12],"5:3":[5,0,12],"5:4":[5,0,12],"5:5":[5,0,12],"5:6":[5,0,12],"5:7":[5,0,12],"5:8":[5,0,12],"5:9":[5,0,12],"5:10":[5,0,12],"5:11":[5,0,12],"5:12":[5,0,12],"5:13":[5,13,13],"5:14":[6,15,19],"5:15":[6,15,15],"5:16":[7,16,18],"5:17":[7,16,18],"5:18":[7,16,18],"5:19":[6,19,19],"5:20":[6,19,19],"5:21":[6,19,19],"6:0":[8,0,0],"6:1":[8,0,0],"6:2":[8,0,0]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"2:1":2,"2:2":2,"2:3":2,"2:4":2,"2:5":2,"2:6":2,"2:7":2,"2:8":2,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":4,"5:11":4,"5:12":4,"5:13":4,"5:14":5,"5:15":6,"5:16":5,"5:17":5,"5:18":5,"5:19":6,"5:20":6,"5:21":6},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"auto"},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"on"},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"off"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"version","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"on","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"}]} +{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"1:13":[2],"1:14":[2],"1:15":[2],"2:0":[3,0,0],"2:1":[3,0,7],"2:2":[3,0,7],"2:3":[3,0,7],"2:4":[3,0,7],"2:5":[3,0,7],"2:6":[3,0,7],"2:7":[3,0,7],"2:8":[3,8,8],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"2:13":[2],"2:14":[2],"2:15":[2],"2:16":[2],"2:17":[2],"2:18":[2],"2:19":[2],"3:0":[4,0,0],"3:1":[4,0,0],"3:2":[4,0,0],"4:0":[4,0,0],"4:1":[0,0,1],"4:2":[0,0,6],"4:3":[0,0,6],"4:4":[0,0,6],"4:5":[0,0,6],"4:6":[0,0,6],"4:7":[5,7,7],"4:8":[5,7,7],"4:9":[5,7,7],"5:0":[5,0,0],"5:1":[5,0,12],"5:2":[5,0,12],"5:3":[5,0,12],"5:4":[5,0,12],"5:5":[5,0,12],"5:6":[5,0,12],"5:7":[5,0,12],"5:8":[5,0,12],"5:9":[5,0,12],"5:10":[5,0,12],"5:11":[5,0,12],"5:12":[5,0,12],"5:13":[5,13,13],"5:14":[6,15,19],"5:15":[6,15,15],"5:16":[7,16,18],"5:17":[7,16,18],"5:18":[7,16,18],"5:19":[6,19,19],"5:20":[6,19,19],"5:21":[6,19,19],"6:0":[8,0,0],"6:1":[8,0,0],"6:2":[8,0,0]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"2:1":2,"2:2":2,"2:3":2,"2:4":2,"2:5":2,"2:6":2,"2:7":2,"2:8":2,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"4:6":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":4,"5:11":4,"5:12":4,"5:13":4,"5:14":5,"5:15":6,"5:16":5,"5:17":5,"5:18":5,"5:19":6,"5:20":6,"5:21":6},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"auto"},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"on"},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"off"}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"version","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"on","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"}],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":13},"value":{"kind":"string","range":{"startLine":1,"startCharacter":7,"endLine":1,"endCharacter":13},"open":false,"text":"crlf","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":8,"endLine":1,"endCharacter":12}}},{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":7}}],"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":17},"value":{"kind":"string","range":{"startLine":2,"startCharacter":10,"endLine":2,"endCharacter":17},"open":false,"text":"1.0.0","quote":"\"","multiline":false,"contentRange":{"startLine":2,"startCharacter":11,"endLine":2,"endCharacter":16}}},{"type":"section","segments":[{"name":"build","quoted":false,"range":{"startLine":4,"startCharacter":1,"endLine":4,"endCharacter":6}}],"isArray":false,"open":false,"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":7},"line":4},{"type":"keyValue","keyPath":[{"name":"bmi_schedule","quoted":false,"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":12}}],"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":19},"value":{"kind":"string","range":{"startLine":5,"startCharacter":15,"endLine":5,"endCharacter":19},"open":false,"text":"on","quote":"\"","multiline":false,"contentRange":{"startLine":5,"startCharacter":16,"endLine":5,"endCharacter":18}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"1:0":4,"1:1":5,"1:2":5,"1:3":5,"1:4":5,"1:5":6,"1:6":7,"1:7":8,"1:8":9,"1:9":9,"1:10":9,"1:11":9,"1:12":9,"1:13":10,"1:14":10,"1:15":10,"2:0":4,"2:1":11,"2:2":11,"2:3":11,"2:4":11,"2:5":11,"2:6":11,"2:7":11,"2:8":12,"2:9":13,"2:10":14,"2:11":15,"2:12":15,"2:13":15,"2:14":15,"2:15":15,"2:16":15,"2:17":16,"2:18":16,"2:19":16,"3:0":4,"3:1":4,"3:2":4,"4:0":4,"4:1":1,"4:2":17,"4:3":17,"4:4":17,"4:5":17,"4:6":17,"4:7":18,"4:8":18,"4:9":18,"5:0":19,"5:1":20,"5:2":20,"5:3":20,"5:4":20,"5:5":20,"5:6":20,"5:7":20,"5:8":20,"5:9":20,"5:10":20,"5:11":20,"5:12":20,"5:13":21,"5:14":22,"5:15":23,"5:16":24,"5:17":24,"5:18":24,"5:19":25,"5:20":25,"5:21":25,"6:0":19,"6:1":19,"6:2":19,"7:0":19,"7:1":19,"7:2":19},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["version"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":["bmi_schedule"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":19}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}}]} diff --git a/tests/rules/golden/editing.toml.json b/tests/rules/golden/editing.toml.json index 04e1ec0..583045c 100644 --- a/tests/rules/golden/editing.toml.json +++ b/tests/rules/golden/editing.toml.json @@ -1 +1 @@ -{"diagnostics":{"default":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [pack","severity":"error","line":1,"startCharacter":1,"endCharacter":6}],"quiet":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [pack","severity":"info","line":1,"startCharacter":1,"endCharacter":6}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,5],"0:3":[0,0,5],"0:4":[0,0,5],"0:5":[0,0,5],"0:6":[0,0,5],"0:7":[0,0,5],"1:0":[1,0,0],"1:1":[0,0,1],"1:2":[0,0,8],"1:3":[0,0,8],"1:4":[0,0,8],"1:5":[0,0,8],"1:6":[0,0,8],"1:7":[0,0,8],"1:8":[0,0,8],"1:9":[0,0,9],"1:10":[0,0,9],"1:11":[0,0,9],"2:0":[2,0,0],"2:1":[3],"2:2":[3],"2:3":[3],"2:4":[3],"3:0":[0,0,0],"3:1":[0,0,1],"3:2":[0,0,8],"3:3":[0,0,8],"3:4":[0,0,8],"3:5":[0,0,8],"3:6":[0,0,8],"3:7":[0,0,8],"3:8":[0,0,8],"3:9":[4,9,9],"3:10":[4,9,9],"3:11":[4,9,9],"4:0":[4,0,0],"4:1":[4,0,4],"4:2":[4,0,4],"4:3":[4,0,4],"4:4":[4,0,4],"4:5":[4,5,5],"4:6":[3],"4:7":[3],"4:8":[3],"4:9":[3],"4:10":[3],"4:11":[3],"4:12":[3],"4:13":[3],"4:14":[3],"4:15":[3],"5:0":[5,0,0],"5:1":[5,0,8],"5:2":[5,0,8],"5:3":[5,0,8],"5:4":[5,0,8],"5:5":[5,0,8],"5:6":[5,0,8],"5:7":[5,0,8],"5:8":[5,0,8],"5:9":[5,9,9],"5:10":[6,11,12],"5:11":[6,11,11],"5:12":[7,12,12],"5:13":[7,12,12],"5:14":[7,12,12],"6:0":[8,0,0],"6:1":[8,0,4],"6:2":[8,0,4],"6:3":[8,0,4],"6:4":[8,0,4],"6:5":[8,5,5],"6:6":[3],"6:7":[3],"6:8":[3],"6:9":[3],"7:0":[8,0,0],"7:1":[0,0,1],"7:2":[0,0,8],"7:3":[0,0,8],"7:4":[0,0,8],"7:5":[0,0,8],"7:6":[0,0,8],"7:7":[0,0,8],"7:8":[0,0,8],"7:9":[0,0,9],"7:10":[0,0,12],"7:11":[0,0,12],"7:12":[0,0,12],"7:13":[2,13,13],"7:14":[2,13,13],"7:15":[2,13,13],"8:0":[2,0,0],"8:1":[2,0,4],"8:2":[2,0,4],"8:3":[2,0,4],"8:4":[2,0,4],"8:5":[2,5,5],"8:6":[9,7,9],"8:7":[9,7,7],"8:8":[10,8,9],"8:9":[10,8,9],"8:10":[10,8,9],"8:11":[10,8,9],"9:0":[11,0,0],"9:1":[11,0,4],"9:2":[11,0,4],"9:3":[11,0,4],"9:4":[11,0,4],"9:5":[11,5,5],"9:6":[3],"9:7":[3],"9:8":[3],"9:9":[3],"10:0":[12,0,0],"10:1":[0,0,1],"10:2":[0,0,6],"10:3":[0,0,6],"10:4":[0,0,6],"10:5":[0,0,6],"10:6":[0,0,6],"10:7":[13,7,7],"10:8":[13,7,7],"10:9":[13,7,7],"11:0":[13,0,0],"11:1":[13,0,12],"11:2":[13,0,12],"11:3":[13,0,12],"11:4":[13,0,12],"11:5":[13,0,12],"11:6":[13,0,12],"11:7":[13,0,12],"11:8":[13,0,12],"11:9":[13,0,12],"11:10":[13,0,12],"11:11":[13,0,12],"11:12":[13,0,12],"11:13":[13,13,13],"11:14":[14,15,17],"11:15":[14,15,17],"11:16":[14,15,17],"11:17":[14,15,17],"11:18":[14,15,17],"11:19":[14,15,17],"12:0":[15,0,0],"12:1":[15,0,8],"12:2":[15,0,8],"12:3":[15,0,8],"12:4":[15,0,8],"12:5":[15,0,8],"12:6":[15,0,8],"12:7":[15,0,8],"12:8":[15,0,8],"12:9":[15,9,9],"12:10":[3],"12:11":[3],"12:12":[3],"12:13":[3],"12:14":[3],"12:15":[3],"12:16":[3],"12:17":[3],"12:18":[3],"12:19":[3],"12:20":[3],"12:21":[3],"13:0":[3],"13:1":[3],"13:2":[3],"13:3":[3],"13:4":[3],"13:5":[3],"13:6":[3],"13:7":[3],"13:8":[3],"13:9":[3],"13:10":[3],"13:11":[3],"13:12":[3],"13:13":[3],"13:14":[3],"13:15":[3],"13:16":[3],"14:0":[3],"14:1":[3],"14:2":[3],"14:3":[3],"14:4":[3],"14:5":[3],"14:6":[3],"14:7":[3],"14:8":[3],"14:9":[3],"14:10":[3],"14:11":[3],"14:12":[3],"15:0":[3],"15:1":[3],"15:2":[3],"15:3":[3],"15:4":[3],"15:5":[3],"15:6":[3],"15:7":[3],"15:8":[3],"15:9":[3],"15:10":[3],"15:11":[3],"15:12":[3],"15:13":[3],"15:14":[3],"15:15":[3],"15:16":[3],"15:17":[3],"15:18":[3],"15:19":[3],"15:20":[3],"15:21":[3],"15:22":[3],"16:0":[3],"16:1":[3],"16:2":[3],"16:3":[3],"16:4":[3],"16:5":[3],"16:6":[3],"16:7":[3],"16:8":[3],"16:9":[3],"16:10":[3],"16:11":[3],"16:12":[3],"16:13":[3],"16:14":[3],"16:15":[3],"16:16":[3],"16:17":[3],"16:18":[3],"16:19":[3],"16:20":[3],"16:21":[3],"16:22":[3],"16:23":[3],"16:24":[3],"16:25":[3],"16:26":[3],"16:27":[3],"16:28":[3],"17:0":[3],"17:1":[3],"17:2":[3],"17:3":[3],"17:4":[3],"17:5":[3],"17:6":[3],"17:7":[3],"17:8":[3],"17:9":[3],"17:10":[3],"17:11":[3],"17:12":[3],"17:13":[3],"17:14":[3],"17:15":[3],"17:16":[3],"17:17":[3],"17:18":[3],"17:19":[3],"17:20":[3],"17:21":[3],"17:22":[3],"17:23":[3],"18:0":[3],"18:1":[3],"18:2":[3],"18:3":[3],"18:4":[3],"18:5":[3],"18:6":[3],"18:7":[3],"18:8":[3],"18:9":[3],"18:10":[3],"18:11":[3],"18:12":[3],"18:13":[3],"18:14":[3],"18:15":[3],"19:0":[3],"19:1":[3],"19:2":[3],"19:3":[3],"19:4":[3],"19:5":[3],"19:6":[3],"19:7":[3],"19:8":[3],"19:9":[3],"19:10":[3],"19:11":[3],"19:12":[3],"19:13":[3],"19:14":[3],"19:15":[3],"19:16":[3],"19:17":[3],"19:18":[3],"19:19":[3],"19:20":[3],"20:0":[3],"20:1":[3],"20:2":[3],"20:3":[3],"20:4":[3],"20:5":[3],"20:6":[3],"20:7":[3],"20:8":[3],"20:9":[3],"20:10":[3],"20:11":[3],"20:12":[3],"20:13":[3],"20:14":[3],"20:15":[3],"21:0":[3],"21:1":[3],"21:2":[3],"21:3":[3],"21:4":[3],"21:5":[3],"21:6":[3],"21:7":[3],"21:8":[3],"21:9":[3],"21:10":[3],"21:11":[3],"21:12":[3],"22:0":[3],"22:1":[3],"22:2":[3],"22:3":[3],"22:4":[3],"22:5":[3],"22:6":[3],"22:7":[3],"22:8":[3],"22:9":[3],"22:10":[3],"22:11":[3],"22:12":[3],"22:13":[3],"22:14":[3],"22:15":[3],"22:16":[3],"22:17":[3],"22:18":[3],"22:19":[3],"22:20":[3],"22:21":[3],"22:22":[3],"22:23":[3],"22:24":[3],"22:25":[3],"22:26":[3],"22:27":[3],"22:28":[3],"22:29":[3],"22:30":[3],"22:31":[3],"22:32":[3],"22:33":[3],"22:34":[3],"22:35":[3],"22:36":[3],"22:37":[3],"22:38":[3],"22:39":[3],"22:40":[3],"22:41":[3],"22:42":[3],"23:0":[3],"23:1":[3],"23:2":[3],"23:3":[3],"23:4":[3],"23:5":[3],"23:6":[3],"23:7":[3],"23:8":[3],"23:9":[3],"23:10":[3],"23:11":[3],"23:12":[3],"23:13":[3],"23:14":[3],"23:15":[3],"23:16":[3],"23:17":[3],"23:18":[3],"24:0":[3],"24:1":[3],"24:2":[3]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"1:6":1,"1:7":1,"1:8":1,"1:9":1,"1:10":1,"1:11":1,"3:2":2,"3:3":2,"3:4":2,"3:5":2,"3:6":2,"3:7":2,"3:8":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":5,"5:11":6,"5:12":6,"5:13":6,"5:14":6,"7:2":1,"7:3":1,"7:4":1,"7:5":1,"7:6":1,"7:7":1,"7:8":1,"7:9":1,"7:10":1,"7:11":1,"7:12":1,"8:1":7,"8:2":7,"8:3":7,"8:4":7,"8:5":7,"8:6":8,"8:7":9,"8:8":10,"8:9":10,"8:10":10,"8:11":10,"9:1":11,"9:2":11,"9:3":11,"9:4":11,"9:5":11,"10:2":12,"10:3":12,"10:4":12,"10:5":12,"10:6":12,"11:1":13,"11:2":13,"11:3":13,"11:4":13,"11:5":13,"11:6":13,"11:7":13,"11:8":13,"11:9":13,"11:10":13,"11:11":13,"11:12":13,"11:13":13,"11:14":14,"11:15":14,"11:16":14,"11:17":14,"11:18":14,"11:19":14,"12:1":15,"12:2":15,"12:3":15,"12:4":15,"12:5":15,"12:6":15,"12:7":15,"12:8":15,"12:9":15},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"format","kind":"template","detail":"string","insertSnippet":"format = \"\"","documentation":"the default of --pkg-format: tar, zip, dir, single-file, or a format a package provides Since 2026.10.27.1."},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"c++20"},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"c++23"},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"c++26"},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"c++2a"},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"c++2c"},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"gnu++20"},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"gnu++23"},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"gnu++26"},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"c++latest"},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"c++fly"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}]],"hovers":[{"title":"[pack]","body":"plane: `unclassified`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"\"","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"\"b","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"b","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"main","body":"type: `string`\n\na file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"au","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"cxxflags","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"}]} +{"diagnostics":{"default":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [pack","severity":"error","line":1,"startCharacter":1,"endCharacter":6}],"quiet":[{"code":"mcpp.manifest.syntax","message":"not a [section] header: [pack","severity":"info","line":1,"startCharacter":1,"endCharacter":6}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,5],"0:3":[0,0,5],"0:4":[0,0,5],"0:5":[0,0,5],"0:6":[0,0,5],"0:7":[0,0,5],"1:0":[1,0,0],"1:1":[0,0,1],"1:2":[0,0,8],"1:3":[0,0,8],"1:4":[0,0,8],"1:5":[0,0,8],"1:6":[0,0,8],"1:7":[0,0,8],"1:8":[0,0,8],"1:9":[0,0,9],"1:10":[0,0,9],"1:11":[0,0,9],"2:0":[2,0,0],"2:1":[3],"2:2":[3],"2:3":[3],"2:4":[3],"3:0":[0,0,0],"3:1":[0,0,1],"3:2":[0,0,8],"3:3":[0,0,8],"3:4":[0,0,8],"3:5":[0,0,8],"3:6":[0,0,8],"3:7":[0,0,8],"3:8":[0,0,8],"3:9":[4,9,9],"3:10":[4,9,9],"3:11":[4,9,9],"4:0":[4,0,0],"4:1":[4,0,4],"4:2":[4,0,4],"4:3":[4,0,4],"4:4":[4,0,4],"4:5":[4,5,5],"4:6":[3],"4:7":[3],"4:8":[3],"4:9":[3],"4:10":[3],"4:11":[3],"4:12":[3],"4:13":[3],"4:14":[3],"4:15":[3],"5:0":[5,0,0],"5:1":[5,0,8],"5:2":[5,0,8],"5:3":[5,0,8],"5:4":[5,0,8],"5:5":[5,0,8],"5:6":[5,0,8],"5:7":[5,0,8],"5:8":[5,0,8],"5:9":[5,9,9],"5:10":[6,11,12],"5:11":[6,11,11],"5:12":[7,12,12],"5:13":[7,12,12],"5:14":[7,12,12],"6:0":[8,0,0],"6:1":[8,0,4],"6:2":[8,0,4],"6:3":[8,0,4],"6:4":[8,0,4],"6:5":[8,5,5],"6:6":[3],"6:7":[3],"6:8":[3],"6:9":[3],"7:0":[8,0,0],"7:1":[0,0,1],"7:2":[0,0,8],"7:3":[0,0,8],"7:4":[0,0,8],"7:5":[0,0,8],"7:6":[0,0,8],"7:7":[0,0,8],"7:8":[0,0,8],"7:9":[0,0,9],"7:10":[0,0,12],"7:11":[0,0,12],"7:12":[0,0,12],"7:13":[2,13,13],"7:14":[2,13,13],"7:15":[2,13,13],"8:0":[2,0,0],"8:1":[2,0,4],"8:2":[2,0,4],"8:3":[2,0,4],"8:4":[2,0,4],"8:5":[2,5,5],"8:6":[9,7,9],"8:7":[9,7,7],"8:8":[10,8,9],"8:9":[10,8,9],"8:10":[10,8,9],"8:11":[10,8,9],"9:0":[11,0,0],"9:1":[11,0,4],"9:2":[11,0,4],"9:3":[11,0,4],"9:4":[11,0,4],"9:5":[11,5,5],"9:6":[3],"9:7":[3],"9:8":[3],"9:9":[3],"10:0":[12,0,0],"10:1":[0,0,1],"10:2":[0,0,6],"10:3":[0,0,6],"10:4":[0,0,6],"10:5":[0,0,6],"10:6":[0,0,6],"10:7":[13,7,7],"10:8":[13,7,7],"10:9":[13,7,7],"11:0":[13,0,0],"11:1":[13,0,12],"11:2":[13,0,12],"11:3":[13,0,12],"11:4":[13,0,12],"11:5":[13,0,12],"11:6":[13,0,12],"11:7":[13,0,12],"11:8":[13,0,12],"11:9":[13,0,12],"11:10":[13,0,12],"11:11":[13,0,12],"11:12":[13,0,12],"11:13":[13,13,13],"11:14":[14,15,17],"11:15":[14,15,17],"11:16":[14,15,17],"11:17":[14,15,17],"11:18":[14,15,17],"11:19":[14,15,17],"12:0":[15,0,0],"12:1":[15,0,8],"12:2":[15,0,8],"12:3":[15,0,8],"12:4":[15,0,8],"12:5":[15,0,8],"12:6":[15,0,8],"12:7":[15,0,8],"12:8":[15,0,8],"12:9":[15,9,9],"12:10":[3],"12:11":[3],"12:12":[3],"12:13":[3],"12:14":[3],"12:15":[3],"12:16":[3],"12:17":[3],"12:18":[3],"12:19":[3],"12:20":[3],"12:21":[3],"13:0":[3],"13:1":[3],"13:2":[3],"13:3":[3],"13:4":[3],"13:5":[3],"13:6":[3],"13:7":[3],"13:8":[3],"13:9":[3],"13:10":[3],"13:11":[3],"13:12":[3],"13:13":[3],"13:14":[3],"13:15":[3],"13:16":[3],"14:0":[3],"14:1":[3],"14:2":[3],"14:3":[3],"14:4":[3],"14:5":[3],"14:6":[3],"14:7":[3],"14:8":[3],"14:9":[3],"14:10":[3],"14:11":[3],"14:12":[3],"15:0":[3],"15:1":[3],"15:2":[3],"15:3":[3],"15:4":[3],"15:5":[3],"15:6":[3],"15:7":[3],"15:8":[3],"15:9":[3],"15:10":[3],"15:11":[3],"15:12":[3],"15:13":[3],"15:14":[3],"15:15":[3],"15:16":[3],"15:17":[3],"15:18":[3],"15:19":[3],"15:20":[3],"15:21":[3],"15:22":[3],"16:0":[3],"16:1":[3],"16:2":[3],"16:3":[3],"16:4":[3],"16:5":[3],"16:6":[3],"16:7":[3],"16:8":[3],"16:9":[3],"16:10":[3],"16:11":[3],"16:12":[3],"16:13":[3],"16:14":[3],"16:15":[3],"16:16":[3],"16:17":[3],"16:18":[3],"16:19":[3],"16:20":[3],"16:21":[3],"16:22":[3],"16:23":[3],"16:24":[3],"16:25":[3],"16:26":[3],"16:27":[3],"16:28":[3],"17:0":[3],"17:1":[3],"17:2":[3],"17:3":[3],"17:4":[3],"17:5":[3],"17:6":[3],"17:7":[3],"17:8":[3],"17:9":[3],"17:10":[3],"17:11":[3],"17:12":[3],"17:13":[3],"17:14":[3],"17:15":[3],"17:16":[3],"17:17":[3],"17:18":[3],"17:19":[3],"17:20":[3],"17:21":[3],"17:22":[3],"17:23":[3],"18:0":[3],"18:1":[3],"18:2":[3],"18:3":[3],"18:4":[3],"18:5":[3],"18:6":[3],"18:7":[3],"18:8":[3],"18:9":[3],"18:10":[3],"18:11":[3],"18:12":[3],"18:13":[3],"18:14":[3],"18:15":[3],"19:0":[3],"19:1":[3],"19:2":[3],"19:3":[3],"19:4":[3],"19:5":[3],"19:6":[3],"19:7":[3],"19:8":[3],"19:9":[3],"19:10":[3],"19:11":[3],"19:12":[3],"19:13":[3],"19:14":[3],"19:15":[3],"19:16":[3],"19:17":[3],"19:18":[3],"19:19":[3],"19:20":[3],"20:0":[3],"20:1":[3],"20:2":[3],"20:3":[3],"20:4":[3],"20:5":[3],"20:6":[3],"20:7":[3],"20:8":[3],"20:9":[3],"20:10":[3],"20:11":[3],"20:12":[3],"20:13":[3],"20:14":[3],"20:15":[3],"21:0":[3],"21:1":[3],"21:2":[3],"21:3":[3],"21:4":[3],"21:5":[3],"21:6":[3],"21:7":[3],"21:8":[3],"21:9":[3],"21:10":[3],"21:11":[3],"21:12":[3],"22:0":[3],"22:1":[3],"22:2":[3],"22:3":[3],"22:4":[3],"22:5":[3],"22:6":[3],"22:7":[3],"22:8":[3],"22:9":[3],"22:10":[3],"22:11":[3],"22:12":[3],"22:13":[3],"22:14":[3],"22:15":[3],"22:16":[3],"22:17":[3],"22:18":[3],"22:19":[3],"22:20":[3],"22:21":[3],"22:22":[3],"22:23":[3],"22:24":[3],"22:25":[3],"22:26":[3],"22:27":[3],"22:28":[3],"22:29":[3],"22:30":[3],"22:31":[3],"22:32":[3],"22:33":[3],"22:34":[3],"22:35":[3],"22:36":[3],"22:37":[3],"22:38":[3],"22:39":[3],"22:40":[3],"22:41":[3],"22:42":[3],"23:0":[3],"23:1":[3],"23:2":[3],"23:3":[3],"23:4":[3],"23:5":[3],"23:6":[3],"23:7":[3],"23:8":[3],"23:9":[3],"23:10":[3],"23:11":[3],"23:12":[3],"23:13":[3],"23:14":[3],"23:15":[3],"23:16":[3],"23:17":[3],"23:18":[3],"24:0":[3],"24:1":[3],"24:2":[3]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"1:2":1,"1:3":1,"1:4":1,"1:5":1,"1:6":1,"1:7":1,"1:8":1,"1:9":1,"1:10":1,"1:11":1,"3:2":2,"3:3":2,"3:4":2,"3:5":2,"3:6":2,"3:7":2,"3:8":2,"4:1":3,"4:2":3,"4:3":3,"4:4":3,"4:5":3,"5:1":4,"5:2":4,"5:3":4,"5:4":4,"5:5":4,"5:6":4,"5:7":4,"5:8":4,"5:9":4,"5:10":5,"5:11":6,"5:12":6,"5:13":6,"5:14":6,"7:2":1,"7:3":1,"7:4":1,"7:5":1,"7:6":1,"7:7":1,"7:8":1,"7:9":1,"7:10":1,"7:11":1,"7:12":1,"8:1":7,"8:2":7,"8:3":7,"8:4":7,"8:5":7,"8:6":8,"8:7":9,"8:8":10,"8:9":10,"8:10":10,"8:11":10,"9:1":11,"9:2":11,"9:3":11,"9:4":11,"9:5":11,"10:2":12,"10:3":12,"10:4":12,"10:5":12,"10:6":12,"11:1":13,"11:2":13,"11:3":13,"11:4":13,"11:5":13,"11:6":13,"11:7":13,"11:8":13,"11:9":13,"11:10":13,"11:11":13,"11:12":13,"11:13":13,"11:14":14,"11:15":14,"11:16":14,"11:17":14,"11:18":14,"11:19":14,"12:1":15,"12:2":15,"12:3":15,"12:4":15,"12:5":15,"12:6":15,"12:7":15,"12:8":15,"12:9":15},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"bundle-project","kind":"template","detail":"string","insertSnippet":"bundle-project = \"\"","documentation":"table of fine-grained overrides"},{"label":"compress","kind":"template","detail":"string","insertSnippet":"compress = \"\"","documentation":"the default of --compress: zstd, gzip, xz or none, with an optional : Since 2026.10.27.1."},{"label":"debug_symbols","kind":"template","detail":"string","insertSnippet":"debug_symbols = \"\""},{"label":"default_mode","kind":"template","detail":"enum: static | bundle-project | bundle-all","insertSnippet":"default_mode = \"static\""},{"label":"exclude","kind":"template","detail":"array","insertSnippet":"exclude = []"},{"label":"format","kind":"template","detail":"string","insertSnippet":"format = \"\"","documentation":"the default of --pkg-format: tar, zip, dir, single-file, or a format a package provides Since 2026.10.27.1."},{"label":"include","kind":"template","detail":"array","insertSnippet":"include = []"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"the default of --linkage Since 2026.10.27.1."},{"label":"sign","kind":"template","detail":"array","insertSnippet":"sign = []","documentation":"an argv run on each program, image, library and loader before the container is written; {} is the file Since 2026.10.27.1."},{"label":"strip","kind":"template","detail":"boolean","insertSnippet":"strip = true"},{"label":"universal","kind":"template","detail":"boolean","insertSnippet":"universal = true","documentation":"false is --no-universal: a pack of a universal platform's target covers that target alone Since 2026.10.27.1."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"kind","kind":"template","detail":"enum: bin | lib | shared | app | entry","insertSnippet":"kind = \"bin\"","documentation":"\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases Since 2026.9.12.3."},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"\"c++20\""},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"\"c++23\""},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"\"c++26\""},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"\"c++2a\""},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"\"c++2c\""},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++20\""},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++23\""},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"\"gnu++26\""},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"\"c++latest\""},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"\"c++fly\""}],[{"label":"c++20","kind":"template","detail":"value of standard","insertSnippet":"c++20"},{"label":"c++23","kind":"template","detail":"value of standard","insertSnippet":"c++23"},{"label":"c++26","kind":"template","detail":"value of standard","insertSnippet":"c++26"},{"label":"c++2a","kind":"template","detail":"value of standard","insertSnippet":"c++2a"},{"label":"c++2c","kind":"template","detail":"value of standard","insertSnippet":"c++2c"},{"label":"gnu++20","kind":"template","detail":"value of standard","insertSnippet":"gnu++20"},{"label":"gnu++23","kind":"template","detail":"value of standard","insertSnippet":"gnu++23"},{"label":"gnu++26","kind":"template","detail":"value of standard","insertSnippet":"gnu++26"},{"label":"c++latest","kind":"template","detail":"value of standard","insertSnippet":"c++latest"},{"label":"c++fly","kind":"template","detail":"value of standard","insertSnippet":"c++fly"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"\"bin\""},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"\"lib\""},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"\"shared\""},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"\"app\""},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"\"entry\""}],[{"label":"bin","kind":"template","detail":"value of kind","insertSnippet":"bin"},{"label":"lib","kind":"template","detail":"value of kind","insertSnippet":"lib"},{"label":"shared","kind":"template","detail":"value of kind","insertSnippet":"shared"},{"label":"app","kind":"template","detail":"value of kind","insertSnippet":"app"},{"label":"entry","kind":"template","detail":"value of kind","insertSnippet":"entry"}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"main","kind":"template","detail":"string","insertSnippet":"main = \"\"","documentation":"a file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)"},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"exports","kind":"template","detail":"string","insertSnippet":"exports = \"\"","documentation":"a path to a symbol-pattern file, or an inline array of patterns (docs/04 §2.2)"},{"label":"linkage","kind":"template","detail":"enum: static | shared","insertSnippet":"linkage = \"static\"","documentation":"Since 2026.9.15.2."},{"label":"required_features","kind":"template","detail":"array","insertSnippet":"required_features = []"},{"label":"soname","kind":"template","detail":"string","insertSnippet":"soname = \"\""},{"label":"start","kind":"template","detail":"string","insertSnippet":"start = \"\"","documentation":"the symbol a freestanding image is entered at Since 2026.10.26.2."},{"label":"windows_auto_export","kind":"template","detail":"string","insertSnippet":"windows_auto_export = \"\""},{"label":"windows_code_page","kind":"template","detail":"enum: utf-8 | legacy","insertSnippet":"windows_code_page = \"utf-8\"","documentation":"Since 2026.9.26.1."},{"label":"windows_entry","kind":"template","detail":"enum: main | wmain | WinMain | wWinMain","insertSnippet":"windows_entry = \"main\"","documentation":"Since 2026.9.12.2."},{"label":"windows_long_paths","kind":"template","detail":"string","insertSnippet":"windows_long_paths = \"\""},{"label":"windows_subsystem","kind":"template","detail":"enum: console | windows","insertSnippet":"windows_subsystem = \"console\"","documentation":"Since 2026.9.12.2."}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"bmi_schedule","kind":"template","detail":"enum: auto | on | off (default: \"auto\")","insertSnippet":"bmi_schedule = \"auto\"","documentation":"module-edge scheduling; \"auto\" currently means off"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}],[{"label":"auto","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"auto\""},{"label":"on","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"on\""},{"label":"off","kind":"template","detail":"value of bmi_schedule","insertSnippet":"\"off\""}],[{"label":"accel","kind":"template","detail":"string","insertSnippet":"accel = \"\"","documentation":"which device backends/architectures this build targets"},{"label":"allow_host_libs","kind":"template","detail":"boolean","insertSnippet":"allow_host_libs = true"},{"label":"build_program_timeout","kind":"template","detail":"number (default: 600)","insertSnippet":"build_program_timeout = 0","documentation":"seconds a build.mcpp may run; 0 = no limit"},{"label":"c_standard","kind":"template","detail":"string (default: \"c11\")","insertSnippet":"c_standard = \"\""},{"label":"cache","kind":"template","detail":"enum: global | local | off (default: \"global\")","insertSnippet":"cache = \"global\""},{"label":"cflags","kind":"template","detail":"array","insertSnippet":"cflags = []"},{"label":"compile_commands","kind":"template","detail":"string","insertSnippet":"compile_commands = \"\"","documentation":"where the project's compile database is published, a path relative to the project root Since 2026.10.26.1."},{"label":"cxx_runtime","kind":"template","detail":"enum: self-contained | toolchain-coupled | host-coupled (default: \"self-contained\")","insertSnippet":"cxx_runtime = \"self-contained\"","documentation":"docs/20; static_stdlib is the old spelling"},{"label":"cxxflags","kind":"template","detail":"array","insertSnippet":"cxxflags = []"},{"label":"default-profile","kind":"template","detail":"string","insertSnippet":"default-profile = \"\"","documentation":"profile name, e.g. \"release\""},{"label":"defines","kind":"template","detail":"array","insertSnippet":"defines = []"},{"label":"dependency_linkage","kind":"template","detail":"enum: static | shared (default: \"static\")","insertSnippet":"dependency_linkage = \"static\""},{"label":"dialect_cxxflags","kind":"template","detail":"array","insertSnippet":"dialect_cxxflags = []","documentation":"Since 2026.9.28.1."},{"label":"flags","kind":"template","detail":"array","insertSnippet":"flags = []","documentation":"array of { glob, cflags, cxxflags, asmflags, defines } tables"},{"label":"include_dirs","kind":"template","detail":"array","insertSnippet":"include_dirs = []"},{"label":"include_dirs_after","kind":"template","detail":"array","insertSnippet":"include_dirs_after = []"},{"label":"ios_deployment_target","kind":"template","detail":"string","insertSnippet":"ios_deployment_target = \"\""},{"label":"jobs","kind":"template","detail":"enum: auto","insertSnippet":"jobs = \"auto\"","documentation":"a positive integer is accepted as well; the type is either"},{"label":"ldflags","kind":"template","detail":"array","insertSnippet":"ldflags = []"},{"label":"macos_deployment_target","kind":"template","detail":"string","insertSnippet":"macos_deployment_target = \"\""},{"label":"module_extensions","kind":"template","detail":"array","insertSnippet":"module_extensions = []"},{"label":"platform-dependencies","kind":"template","detail":"string","insertSnippet":"platform-dependencies = \"\"","documentation":"names a platform"},{"label":"private_include_dirs","kind":"template","detail":"array","insertSnippet":"private_include_dirs = []"},{"label":"profile","kind":"template","detail":"string","insertSnippet":"profile = \"\"","documentation":"accepted alias of default-profile"},{"label":"sources","kind":"template","detail":"array","insertSnippet":"sources = []"},{"label":"static_stdlib","kind":"template","detail":"boolean","insertSnippet":"static_stdlib = true","documentation":"replaced by [build] cxx_runtime Legacy key."},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"target","kind":"template","detail":"string","insertSnippet":"target = \"\"","documentation":"default target triple when no --target is passed"}]],"hovers":[{"title":"[pack]","body":"plane: `unclassified`\n\nkeys: 11","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/10-pack-and-release.md#configuration"},{"title":"[targets]","body":"plane: `artifact`\n\nkeys: 15","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"type: `enum`\n\nvalues: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"\"","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"standard","body":"enum value of `standard`\n\none of: `c++20` | `c++23` | `c++26` | `c++2a` | `c++2c` | `gnu++20` | `gnu++23` | `gnu++26` | `c++latest` | `c++fly`\n\ndefault: `\"c++23\"`\n\nc++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"kind","body":"type: `enum`\n\nvalues: `bin` | `lib` | `shared` | `app` | `entry`\n\nsince: 2026.9.12.3\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"\"b","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"kind","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"b","body":"enum value of `kind`\n\none of: `bin` | `lib` | `shared` | `app` | `entry`\n\n\"app\" requires mcpp 2026.9.12.3+ and \"entry\" 2026.10.26.2+; library/binary/dylib/so/shlib are accepted aliases","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"main","body":"type: `string`\n\na file, or { from = \"/\" } naming a dependency's kind = \"entry\" target (2026.10.26.2+)","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#22-targetsname--build-targets"},{"title":"[build]","body":"plane: `compile`\n\nkeys: 31","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"bmi_schedule","body":"type: `enum`\n\nvalues: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"au","body":"enum value of `bmi_schedule`\n\none of: `auto` | `on` | `off`\n\ndefault: `\"auto\"`\n\nmodule-edge scheduling; \"auto\" currently means off","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"},{"title":"cxxflags","body":"type: `array`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#23-build--build-configuration"}],"document":{"nodes":[{"type":"section","segments":[{"name":"pack","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":5}}],"isArray":false,"open":true,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":5},"line":0},{"type":"section","segments":[{"name":"targets","quoted":false,"range":{"startLine":1,"startCharacter":1,"endLine":1,"endCharacter":8}}],"isArray":false,"open":true,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":9},"line":1},{"type":"section","segments":[],"isArray":true,"open":true,"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":2},"line":2},{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":3,"startCharacter":1,"endLine":3,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":3,"startCharacter":0,"endLine":3,"endCharacter":9},"line":3},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":4}}],"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":13},"value":{"kind":"string","range":{"startLine":4,"startCharacter":7,"endLine":4,"endCharacter":13},"open":false,"text":"edit","quote":"\"","multiline":false,"contentRange":{"startLine":4,"startCharacter":8,"endLine":4,"endCharacter":12}}},{"type":"keyValue","keyPath":[{"name":"standard","quoted":false,"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":8}}],"range":{"startLine":5,"startCharacter":0,"endLine":5,"endCharacter":12},"value":{"kind":"string","range":{"startLine":5,"startCharacter":11,"endLine":5,"endCharacter":12},"open":true,"text":"","quote":"\"","multiline":false,"contentRange":{"startLine":5,"startCharacter":12,"endLine":5,"endCharacter":12}}},{"type":"keyValue","keyPath":[{"name":"kind","quoted":false,"range":{"startLine":6,"startCharacter":0,"endLine":6,"endCharacter":4}}],"range":{"startLine":6,"startCharacter":0,"endLine":6,"endCharacter":7}},{"type":"section","segments":[{"name":"targets","quoted":false,"range":{"startLine":7,"startCharacter":1,"endLine":7,"endCharacter":8}},{"name":"app","quoted":false,"range":{"startLine":7,"startCharacter":9,"endLine":7,"endCharacter":12}}],"isArray":false,"open":false,"range":{"startLine":7,"startCharacter":0,"endLine":7,"endCharacter":13},"line":7},{"type":"keyValue","keyPath":[{"name":"kind","quoted":false,"range":{"startLine":8,"startCharacter":0,"endLine":8,"endCharacter":4}}],"range":{"startLine":8,"startCharacter":0,"endLine":8,"endCharacter":9},"value":{"kind":"string","range":{"startLine":8,"startCharacter":7,"endLine":8,"endCharacter":9},"open":true,"text":"b","quote":"\"","multiline":false,"contentRange":{"startLine":8,"startCharacter":8,"endLine":8,"endCharacter":9}}},{"type":"keyValue","keyPath":[{"name":"main","quoted":false,"range":{"startLine":9,"startCharacter":0,"endLine":9,"endCharacter":4}}],"range":{"startLine":9,"startCharacter":0,"endLine":9,"endCharacter":7}},{"type":"section","segments":[{"name":"build","quoted":false,"range":{"startLine":10,"startCharacter":1,"endLine":10,"endCharacter":6}}],"isArray":false,"open":false,"range":{"startLine":10,"startCharacter":0,"endLine":10,"endCharacter":7},"line":10},{"type":"keyValue","keyPath":[{"name":"bmi_schedule","quoted":false,"range":{"startLine":11,"startCharacter":0,"endLine":11,"endCharacter":12}}],"range":{"startLine":11,"startCharacter":0,"endLine":11,"endCharacter":17},"value":{"kind":"unknown","range":{"startLine":11,"startCharacter":15,"endLine":11,"endCharacter":17},"open":false,"text":"au"}},{"type":"keyValue","keyPath":[{"name":"cxxflags","quoted":false,"range":{"startLine":12,"startCharacter":0,"endLine":12,"endCharacter":8}}],"range":{"startLine":12,"startCharacter":0,"endLine":25,"endCharacter":0},"value":{"kind":"array","range":{"startLine":12,"startCharacter":11,"endLine":25,"endCharacter":0},"open":true,"elements":[{"kind":"string","range":{"startLine":12,"startCharacter":12,"endLine":12,"endCharacter":17},"open":false,"text":"-O2","quote":"\"","multiline":false,"contentRange":{"startLine":12,"startCharacter":13,"endLine":12,"endCharacter":16}},{"kind":"array","range":{"startLine":13,"startCharacter":0,"endLine":13,"endCharacter":14},"open":false,"elements":[{"kind":"unknown","range":{"startLine":13,"startCharacter":1,"endLine":13,"endCharacter":13},"open":false,"text":"dependencies"}]},{"kind":"unknown","range":{"startLine":14,"startCharacter":0,"endLine":14,"endCharacter":10},"open":false,"text":"zlib = \"1."},{"kind":"unknown","range":{"startLine":15,"startCharacter":0,"endLine":15,"endCharacter":5},"open":false,"text":"fmt ="},{"kind":"inlineTable","range":{"startLine":15,"startCharacter":6,"endLine":25,"endCharacter":0},"open":true,"entries":[{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":15,"startCharacter":8,"endLine":15,"endCharacter":15}}],"range":{"startLine":15,"startCharacter":8,"endLine":15,"endCharacter":20},"value":{"kind":"string","range":{"startLine":15,"startCharacter":18,"endLine":15,"endCharacter":20},"open":true,"text":"1","quote":"\"","multiline":false,"contentRange":{"startLine":15,"startCharacter":19,"endLine":15,"endCharacter":20}}},{"type":"keyValue","keyPath":[{"name":"foo","quoted":false,"range":{"startLine":16,"startCharacter":0,"endLine":16,"endCharacter":3}}],"range":{"startLine":16,"startCharacter":0,"endLine":25,"endCharacter":0},"value":{"kind":"inlineTable","range":{"startLine":16,"startCharacter":6,"endLine":25,"endCharacter":0},"open":true,"entries":[{"type":"keyValue","keyPath":[{"name":"git","quoted":false,"range":{"startLine":16,"startCharacter":8,"endLine":16,"endCharacter":11}}],"range":{"startLine":16,"startCharacter":8,"endLine":16,"endCharacter":17},"value":{"kind":"string","range":{"startLine":16,"startCharacter":14,"endLine":16,"endCharacter":17},"open":false,"text":"x","quote":"\"","multiline":false,"contentRange":{"startLine":16,"startCharacter":15,"endLine":16,"endCharacter":16}}},{"type":"keyValue","keyPath":[{"name":"tag","quoted":false,"range":{"startLine":16,"startCharacter":19,"endLine":16,"endCharacter":22}}],"range":{"startLine":16,"startCharacter":19,"endLine":16,"endCharacter":26},"value":{"kind":"string","range":{"startLine":16,"startCharacter":25,"endLine":16,"endCharacter":26},"open":true,"text":"","quote":"\"","multiline":false,"contentRange":{"startLine":16,"startCharacter":26,"endLine":16,"endCharacter":26}}},{"type":"keyValue","keyPath":[{"name":"bar","quoted":false,"range":{"startLine":17,"startCharacter":0,"endLine":17,"endCharacter":3}}],"range":{"startLine":17,"startCharacter":0,"endLine":25,"endCharacter":0},"value":{"kind":"inlineTable","range":{"startLine":17,"startCharacter":6,"endLine":25,"endCharacter":0},"open":true,"entries":[{"type":"keyValue","keyPath":[{"name":"features","quoted":false,"range":{"startLine":17,"startCharacter":8,"endLine":17,"endCharacter":16}}],"range":{"startLine":17,"startCharacter":8,"endLine":25,"endCharacter":0},"value":{"kind":"array","range":{"startLine":17,"startCharacter":19,"endLine":25,"endCharacter":0},"open":true,"elements":[{"kind":"string","range":{"startLine":17,"startCharacter":20,"endLine":17,"endCharacter":21},"open":true,"text":"","quote":"\"","multiline":false,"contentRange":{"startLine":17,"startCharacter":21,"endLine":17,"endCharacter":21}},{"kind":"unknown","range":{"startLine":18,"startCharacter":1,"endLine":18,"endCharacter":13},"open":false,"text":"tabbed = \"v\""},{"kind":"unknown","range":{"startLine":19,"startCharacter":0,"endLine":19,"endCharacter":18},"open":false,"text":"名字 = \"unicode key\""},{"kind":"unknown","range":{"startLine":20,"startCharacter":0,"endLine":20,"endCharacter":8},"open":false,"text":"x = \"名字\""},{"kind":"array","range":{"startLine":21,"startCharacter":0,"endLine":21,"endCharacter":10},"open":false,"elements":[{"kind":"unknown","range":{"startLine":21,"startCharacter":1,"endLine":21,"endCharacter":9},"open":false,"text":"features"}]},{"kind":"unknown","range":{"startLine":22,"startCharacter":0,"endLine":22,"endCharacter":6},"open":false,"text":"simd ="},{"kind":"inlineTable","range":{"startLine":22,"startCharacter":7,"endLine":25,"endCharacter":0},"open":true,"entries":[{"type":"keyValue","keyPath":[{"name":"defines","quoted":false,"range":{"startLine":22,"startCharacter":9,"endLine":22,"endCharacter":16}}],"range":{"startLine":22,"startCharacter":9,"endLine":22,"endCharacter":24},"value":{"kind":"array","range":{"startLine":22,"startCharacter":19,"endLine":22,"endCharacter":24},"open":false,"elements":[{"kind":"string","range":{"startLine":22,"startCharacter":20,"endLine":22,"endCharacter":23},"open":false,"text":"A","quote":"\"","multiline":false,"contentRange":{"startLine":22,"startCharacter":21,"endLine":22,"endCharacter":22}}]}},{"type":"keyValue","keyPath":[{"name":"sources","quoted":false,"range":{"startLine":22,"startCharacter":26,"endLine":22,"endCharacter":33}}],"range":{"startLine":22,"startCharacter":26,"endLine":25,"endCharacter":0},"value":{"kind":"array","range":{"startLine":22,"startCharacter":36,"endLine":25,"endCharacter":0},"open":true,"elements":[{"kind":"inlineTable","range":{"startLine":22,"startCharacter":38,"endLine":25,"endCharacter":0},"open":true,"entries":[{"type":"keyValue","keyPath":[{"name":"dependencies","quoted":false,"range":{"startLine":23,"startCharacter":1,"endLine":23,"endCharacter":13}},{"name":"ns","quoted":false,"range":{"startLine":23,"startCharacter":14,"endLine":23,"endCharacter":16}}],"range":{"startLine":23,"startCharacter":1,"endLine":23,"endCharacter":16}}]}]}}]}]}}]}}]}}]}]}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"1:0":3,"1:1":1,"1:2":4,"1:3":4,"1:4":4,"1:5":4,"1:6":4,"1:7":4,"1:8":4,"1:9":5,"1:10":5,"1:11":5,"2:0":6,"2:1":7,"2:2":8,"2:3":8,"2:4":8,"3:0":0,"3:1":1,"3:2":4,"3:3":4,"3:4":4,"3:5":4,"3:6":4,"3:7":4,"3:8":4,"3:9":9,"3:10":9,"3:11":9,"4:0":10,"4:1":11,"4:2":11,"4:3":11,"4:4":11,"4:5":12,"4:6":13,"4:7":14,"4:8":15,"4:9":15,"4:10":15,"4:11":15,"4:12":15,"4:13":16,"4:14":16,"4:15":16,"5:0":10,"5:1":17,"5:2":17,"5:3":17,"5:4":17,"5:5":17,"5:6":17,"5:7":17,"5:8":17,"5:9":18,"5:10":19,"5:11":20,"5:12":21,"5:13":21,"5:14":21,"6:0":10,"6:1":11,"6:2":11,"6:3":11,"6:4":11,"6:5":22,"6:6":23,"6:7":24,"6:8":24,"6:9":24,"7:0":10,"7:1":1,"7:2":4,"7:3":4,"7:4":4,"7:5":4,"7:6":4,"7:7":4,"7:8":4,"7:9":5,"7:10":25,"7:11":25,"7:12":25,"7:13":26,"7:14":26,"7:15":26,"8:0":6,"8:1":27,"8:2":27,"8:3":27,"8:4":27,"8:5":28,"8:6":29,"8:7":30,"8:8":31,"8:9":31,"8:10":31,"8:11":31,"9:0":6,"9:1":27,"9:2":27,"9:3":27,"9:4":27,"9:5":32,"9:6":33,"9:7":34,"9:8":34,"9:9":34,"10:0":6,"10:1":1,"10:2":35,"10:3":35,"10:4":35,"10:5":35,"10:6":35,"10:7":36,"10:8":36,"10:9":36,"11:0":37,"11:1":38,"11:2":38,"11:3":38,"11:4":38,"11:5":38,"11:6":38,"11:7":38,"11:8":38,"11:9":38,"11:10":38,"11:11":38,"11:12":38,"11:13":39,"11:14":40,"11:15":40,"11:16":40,"11:17":40,"11:18":40,"11:19":40,"12:0":37,"12:1":41,"12:2":41,"12:3":41,"12:4":41,"12:5":41,"12:6":41,"12:7":41,"12:8":41,"12:9":42,"12:10":43,"12:11":44,"12:12":45,"12:13":46,"12:14":46,"12:15":46,"12:16":46,"12:17":47,"12:18":48,"12:19":49,"12:20":49,"12:21":49,"13:0":50,"13:1":51,"13:2":52,"13:3":52,"13:4":52,"13:5":52,"13:6":52,"13:7":52,"13:8":52,"13:9":52,"13:10":52,"13:11":52,"13:12":52,"13:13":52,"13:14":53,"13:15":53,"13:16":53,"14:0":50,"14:1":54,"14:2":54,"14:3":54,"14:4":54,"14:5":54,"14:6":54,"14:7":54,"14:8":54,"14:9":54,"14:10":54,"14:11":54,"14:12":54,"15:0":50,"15:1":55,"15:2":55,"15:3":55,"15:4":55,"15:5":55,"15:6":56,"15:7":57,"15:8":58,"15:9":59,"15:10":59,"15:11":59,"15:12":59,"15:13":59,"15:14":59,"15:15":59,"15:16":60,"15:17":61,"15:18":62,"15:19":63,"15:20":63,"15:21":63,"15:22":63,"16:0":64,"16:1":65,"16:2":65,"16:3":65,"16:4":66,"16:5":67,"16:6":68,"16:7":69,"16:8":70,"16:9":71,"16:10":71,"16:11":71,"16:12":72,"16:13":73,"16:14":74,"16:15":75,"16:16":75,"16:17":76,"16:18":77,"16:19":78,"16:20":79,"16:21":79,"16:22":79,"16:23":80,"16:24":81,"16:25":82,"16:26":83,"16:27":83,"16:28":83,"17:0":84,"17:1":85,"17:2":85,"17:3":85,"17:4":86,"17:5":87,"17:6":88,"17:7":89,"17:8":90,"17:9":91,"17:10":91,"17:11":91,"17:12":91,"17:13":91,"17:14":91,"17:15":91,"17:16":91,"17:17":92,"17:18":93,"17:19":94,"17:20":95,"17:21":96,"17:22":96,"17:23":96,"18:0":97,"18:1":98,"18:2":99,"18:3":99,"18:4":99,"18:5":99,"18:6":99,"18:7":99,"18:8":99,"18:9":99,"18:10":99,"18:11":99,"18:12":99,"18:13":99,"18:14":99,"18:15":99,"19:0":97,"19:1":100,"19:2":100,"19:3":100,"19:4":100,"19:5":100,"19:6":100,"19:7":100,"19:8":100,"19:9":100,"19:10":100,"19:11":100,"19:12":100,"19:13":100,"19:14":100,"19:15":100,"19:16":100,"19:17":100,"19:18":100,"19:19":100,"19:20":100,"20:0":97,"20:1":101,"20:2":101,"20:3":101,"20:4":101,"20:5":101,"20:6":101,"20:7":101,"20:8":101,"20:9":102,"20:10":103,"20:11":104,"20:12":105,"20:13":106,"20:14":106,"20:15":106,"21:0":97,"21:1":98,"21:2":107,"21:3":107,"21:4":107,"21:5":107,"21:6":107,"21:7":107,"21:8":107,"21:9":107,"21:10":103,"21:11":103,"21:12":103,"22:0":97,"22:1":108,"22:2":108,"22:3":108,"22:4":108,"22:5":108,"22:6":108,"22:7":109,"22:8":110,"22:9":111,"22:10":112,"22:11":112,"22:12":112,"22:13":112,"22:14":112,"22:15":112,"22:16":112,"22:17":113,"22:18":114,"22:19":115,"22:20":116,"22:21":117,"22:22":117,"22:23":118,"22:24":119,"22:25":120,"22:26":121,"22:27":122,"22:28":122,"22:29":122,"22:30":122,"22:31":122,"22:32":122,"22:33":122,"22:34":123,"22:35":124,"22:36":125,"22:37":126,"22:38":127,"22:39":128,"22:40":129,"22:41":129,"22:42":129,"23:0":130,"23:1":131,"23:2":132,"23:3":132,"23:4":132,"23:5":132,"23:6":132,"23:7":132,"23:8":132,"23:9":132,"23:10":132,"23:11":132,"23:12":132,"23:13":132,"23:14":133,"23:15":134,"23:16":134,"23:17":134,"23:18":134,"24:0":130,"24:1":130,"24:2":130,"25:0":130,"25:1":130,"25:2":130},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"pack"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["standard"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["standard"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["standard"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["standard"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["kind"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["kind"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["kind"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":["kind"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["kind"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":8,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":["main"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["main"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"known","group":"targets"},"keyPath":["main"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":12}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":["bmi_schedule"],"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["bmi_schedule"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":["cxxflags"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags"],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":15}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags"],"keyPrefix":["version"],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","version"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":19,"endCharacter":20}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags"],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags"],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":3}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags"],"keyPrefix":["foo"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":11}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":["git"],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","git"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","git"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","git"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","git"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":19,"endCharacter":22}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":["tag"],"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","tag"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":25,"endCharacter":26}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","tag"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":25,"endCharacter":25}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","tag"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":26,"endCharacter":26}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":3}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo"],"keyPrefix":["bar"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar"],"keyPrefix":[],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar"],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar"],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar"],"keyPrefix":["features"],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":20,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"valueKind":"unknown","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":6}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":16}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":["defines"],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","defines"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","defines"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","defines"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":20,"endCharacter":20}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","defines"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":22}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","defines"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":[],"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":[],"replaceRange":{"startCharacter":25,"endCharacter":25}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":[],"replaceRange":{"startCharacter":26,"endCharacter":26}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":[],"replaceRange":{"startCharacter":26,"endCharacter":33}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features"],"keyPrefix":["sources"],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":35,"endCharacter":35}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":36,"endCharacter":36}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":37,"endCharacter":37}},{"kind":"value","section":{"kind":"known","group":"build"},"keyPath":["cxxflags","foo","bar","features","sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":38,"endCharacter":38}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features","sources"],"keyPrefix":[],"replaceRange":{"startCharacter":39,"endCharacter":39}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features","sources"],"keyPrefix":[],"replaceRange":{"startCharacter":40,"endCharacter":40}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features","sources"],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features","sources"],"keyPrefix":[],"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features","sources"],"keyPrefix":[],"replaceRange":{"startCharacter":1,"endCharacter":13}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features","sources"],"keyPrefix":["dependencies"],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":["cxxflags","foo","bar","features","sources"],"keyPrefix":["dependencies"],"replaceRange":{"startCharacter":14,"endCharacter":16}}]} diff --git a/tests/rules/golden/empty.toml.json b/tests/rules/golden/empty.toml.json index 8690ddf..a71b43f 100644 --- a/tests/rules/golden/empty.toml.json +++ b/tests/rules/golden/empty.toml.json @@ -1 +1 @@ -{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,0,0],"0:2":[0,0,0]},"hover":{},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}]],"hovers":[]} +{"diagnostics":{"default":[],"quiet":[]},"completion":{"0:0":[0,0,0],"0:1":[0,0,0],"0:2":[0,0,0]},"hover":{},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}]],"hovers":[],"document":{"nodes":[]},"context":{"0:0":0,"0:1":0,"0:2":0,"1:0":0,"1:1":0,"1:2":0},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}}]} diff --git a/tests/rules/golden/snippets.json b/tests/rules/golden/snippets.json new file mode 100644 index 0000000..ec7e8c7 --- /dev/null +++ b/tests/rules/golden/snippets.json @@ -0,0 +1 @@ +[{"lines":["[package]"],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}}]},{"lines":["[targets.myapp]"],"document":{"nodes":[{"type":"section","segments":[{"name":"targets","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}},{"name":"myapp","quoted":false,"range":{"startLine":0,"startCharacter":9,"endLine":0,"endCharacter":14}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":15},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":4,"0:11":4,"0:12":4,"0:13":4,"0:14":4,"0:15":5,"0:16":5,"0:17":5,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"targets"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}}]},{"lines":["[target.'cfg(windows)'.build]"],"document":{"nodes":[{"type":"section","segments":[{"name":"target","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":7}},{"name":"cfg(windows)","quoted":true,"range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":22}},{"name":"build","quoted":false,"range":{"startLine":0,"startCharacter":23,"endLine":0,"endCharacter":28}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":29},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":3,"0:9":4,"0:10":4,"0:11":4,"0:12":4,"0:13":4,"0:14":4,"0:15":4,"0:16":4,"0:17":4,"0:18":4,"0:19":4,"0:20":4,"0:21":4,"0:22":4,"0:23":5,"0:24":6,"0:25":6,"0:26":6,"0:27":6,"0:28":6,"0:29":7,"0:30":7,"0:31":7,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":7}},{"kind":"section-header","segments":["target"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"section-header","segments":["target"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":22}},{"kind":"section-header","segments":["target","cfg(windows)"],"isArray":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"section-header","segments":["target","cfg(windows)"],"isArray":false,"replaceRange":{"startCharacter":23,"endCharacter":28}},{"kind":"key","section":{"kind":"known","group":"build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":29,"endCharacter":29}}]},{"lines":["[runtime.\"opengl.glx.driver\"]"],"document":{"nodes":[{"type":"section","segments":[{"name":"runtime","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}},{"name":"opengl.glx.driver","quoted":true,"range":{"startLine":0,"startCharacter":9,"endLine":0,"endCharacter":28}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":29},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":4,"0:11":4,"0:12":4,"0:13":4,"0:14":4,"0:15":4,"0:16":4,"0:17":4,"0:18":4,"0:19":4,"0:20":4,"0:21":4,"0:22":4,"0:23":4,"0:24":4,"0:25":4,"0:26":4,"0:27":4,"0:28":4,"0:29":5,"0:30":5,"0:31":5,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"section-header","segments":["runtime"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"section-header","segments":["runtime"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":28}},{"kind":"key","section":{"kind":"known","group":"runtime.capability"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":29,"endCharacter":29}}]},{"lines":["[[build.flags]]"],"document":{"nodes":[{"type":"section","segments":[{"name":"build","quoted":false,"range":{"startLine":0,"startCharacter":2,"endLine":0,"endCharacter":7}},{"name":"flags","quoted":false,"range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":13}}],"isArray":true,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":15},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":3,"0:5":3,"0:6":3,"0:7":3,"0:8":4,"0:9":5,"0:10":5,"0:11":5,"0:12":5,"0:13":5,"0:14":6,"0:15":7,"0:16":7,"0:17":7,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":2,"endCharacter":7}},{"kind":"section-header","segments":["build"],"isArray":true,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"section-header","segments":["build"],"isArray":true,"replaceRange":{"startCharacter":8,"endCharacter":13}},{"kind":"key","section":{"kind":"unknown","segments":["build","flags"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"unknown","segments":["build","flags"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}}]},{"lines":["[dep","name = \"x\""],"document":{"nodes":[{"type":"section","segments":[{"name":"dep","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":4}}],"isArray":false,"open":true,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4},"line":0},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":10},"value":{"kind":"string","range":{"startLine":1,"startCharacter":7,"endLine":1,"endCharacter":10},"open":false,"text":"x","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":8,"endLine":1,"endCharacter":9}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"1:0":3,"1:1":4,"1:2":4,"1:3":4,"1:4":4,"1:5":5,"1:6":6,"1:7":7,"1:8":8,"1:9":8,"1:10":9,"1:11":9,"1:12":9,"2:0":3,"2:1":4,"2:2":4},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":4}},{"kind":"key","section":{"kind":"unknown","segments":["dep"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"unknown","segments":["dep"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"unknown","segments":["dep"]},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"unknown","segments":["dep"]},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":10}},{"kind":"value","section":{"kind":"unknown","segments":["dep"]},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"unknown","segments":["dep"]},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":9}},{"kind":"value","section":{"kind":"unknown","segments":["dep"]},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}}]},{"lines":["[package]","name = \"demo\""],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":13},"value":{"kind":"string","range":{"startLine":1,"startCharacter":7,"endLine":1,"endCharacter":13},"open":false,"text":"demo","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":8,"endLine":1,"endCharacter":12}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"1:0":4,"1:1":5,"1:2":5,"1:3":5,"1:4":5,"1:5":6,"1:6":7,"1:7":8,"1:8":9,"1:9":9,"1:10":9,"1:11":9,"1:12":9,"1:13":10,"1:14":10,"1:15":10,"2:0":4,"2:1":5,"2:2":5},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}}]},{"lines":["\"chriskohlhoff.asio\" = \"1.28\""],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"chriskohlhoff.asio","quoted":true,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":20}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":29},"value":{"kind":"string","range":{"startLine":0,"startCharacter":23,"endLine":0,"endCharacter":29},"open":false,"text":"1.28","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":24,"endLine":0,"endCharacter":28}}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":1,"0:6":1,"0:7":1,"0:8":1,"0:9":1,"0:10":1,"0:11":1,"0:12":1,"0:13":1,"0:14":1,"0:15":1,"0:16":1,"0:17":1,"0:18":1,"0:19":1,"0:20":1,"0:21":2,"0:22":3,"0:23":4,"0:24":5,"0:25":5,"0:26":5,"0:27":5,"0:28":5,"0:29":6,"0:30":6,"0:31":6,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":20}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["chriskohlhoff.asio"],"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"value","section":{"kind":"top"},"keyPath":["chriskohlhoff.asio"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":29}},{"kind":"value","section":{"kind":"top"},"keyPath":["chriskohlhoff.asio"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"value","section":{"kind":"top"},"keyPath":["chriskohlhoff.asio"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":28}},{"kind":"value","section":{"kind":"top"},"keyPath":["chriskohlhoff.asio"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":29,"endCharacter":29}}]},{"lines":["capi.lua = \"1.0\""],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"capi","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4}},{"name":"lua","quoted":false,"range":{"startLine":0,"startCharacter":5,"endLine":0,"endCharacter":8}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":16},"value":{"kind":"string","range":{"startLine":0,"startCharacter":11,"endLine":0,"endCharacter":16},"open":false,"text":"1.0","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":12,"endLine":0,"endCharacter":15}}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":2,"0:6":3,"0:7":3,"0:8":3,"0:9":4,"0:10":5,"0:11":6,"0:12":7,"0:13":7,"0:14":7,"0:15":7,"0:16":8,"0:17":8,"0:18":8,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["capi"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["capi"],"replaceRange":{"startCharacter":5,"endCharacter":8}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["capi","lua"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["capi","lua"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":16}},{"kind":"value","section":{"kind":"top"},"keyPath":["capi","lua"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"top"},"keyPath":["capi","lua"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":15}},{"kind":"value","section":{"kind":"top"},"keyPath":["capi","lua"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}}]},{"lines":["opt = 2"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"opt","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":3}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":7},"value":{"kind":"integer","range":{"startLine":0,"startCharacter":6,"endLine":0,"endCharacter":7},"open":false,"text":"2"}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":2,"0:5":3,"0:6":3,"0:7":3,"0:8":3,"0:9":3,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":3}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["opt"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"value","section":{"kind":"top"},"keyPath":["opt"],"valueKind":"integer","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":7}}]},{"lines":["debug = true"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"debug","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":5}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":12},"value":{"kind":"boolean","range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":12},"open":false,"text":"true"}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":1,"0:6":2,"0:7":3,"0:8":3,"0:9":3,"0:10":3,"0:11":3,"0:12":3,"0:13":3,"0:14":3,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["debug"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"top"},"keyPath":["debug"],"valueKind":"boolean","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":12}}]},{"lines":["sources = ["," \"a\","," \"b\",","]"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"sources","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":7}}],"range":{"startLine":0,"startCharacter":0,"endLine":3,"endCharacter":1},"value":{"kind":"array","range":{"startLine":0,"startCharacter":10,"endLine":3,"endCharacter":1},"open":false,"elements":[{"kind":"string","range":{"startLine":1,"startCharacter":2,"endLine":1,"endCharacter":5},"open":false,"text":"a","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":3,"endLine":1,"endCharacter":4}},{"kind":"string","range":{"startLine":2,"startCharacter":2,"endLine":2,"endCharacter":5},"open":false,"text":"b","quote":"\"","multiline":false,"contentRange":{"startLine":2,"startCharacter":3,"endLine":2,"endCharacter":4}}]}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":1,"0:6":1,"0:7":1,"0:8":2,"0:9":3,"0:10":4,"0:11":5,"0:12":5,"0:13":5,"1:0":6,"1:1":7,"1:2":8,"1:3":9,"1:4":9,"1:5":10,"1:6":11,"1:7":11,"1:8":11,"2:0":6,"2:1":7,"2:2":8,"2:3":9,"2:4":9,"2:5":10,"2:6":11,"2:7":11,"2:8":11,"3:0":6,"3:1":12,"3:2":12,"3:3":12,"4:0":6,"4:1":12,"4:2":12},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["sources"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":3,"endCharacter":4}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":1,"endCharacter":1}}]},{"lines":["flags = [ { glob = \"x\" },"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"flags","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":5}}],"range":{"startLine":0,"startCharacter":0,"endLine":1,"endCharacter":0},"value":{"kind":"array","range":{"startLine":0,"startCharacter":8,"endLine":1,"endCharacter":0},"open":true,"elements":[{"kind":"inlineTable","range":{"startLine":0,"startCharacter":10,"endLine":0,"endCharacter":24},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"glob","quoted":false,"range":{"startLine":0,"startCharacter":12,"endLine":0,"endCharacter":16}}],"range":{"startLine":0,"startCharacter":12,"endLine":0,"endCharacter":22},"value":{"kind":"string","range":{"startLine":0,"startCharacter":19,"endLine":0,"endCharacter":22},"open":false,"text":"x","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":20,"endLine":0,"endCharacter":21}}}]}]}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":1,"0:6":2,"0:7":3,"0:8":4,"0:9":5,"0:10":6,"0:11":7,"0:12":8,"0:13":9,"0:14":9,"0:15":9,"0:16":9,"0:17":10,"0:18":11,"0:19":12,"0:20":13,"0:21":13,"0:22":14,"0:23":15,"0:24":16,"0:25":17,"0:26":17,"0:27":17,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["flags"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":16}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":["glob"],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags","glob"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":22}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags","glob"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags","glob"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":20,"endCharacter":21}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags","glob"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":[],"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":25,"endCharacter":25}}]},{"lines":["dep = { version = \"1\", opts = { a = 1 } }"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"dep","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":3}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":41},"value":{"kind":"inlineTable","range":{"startLine":0,"startCharacter":6,"endLine":0,"endCharacter":41},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":15}}],"range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":21},"value":{"kind":"string","range":{"startLine":0,"startCharacter":18,"endLine":0,"endCharacter":21},"open":false,"text":"1","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":19,"endLine":0,"endCharacter":20}}},{"type":"keyValue","keyPath":[{"name":"opts","quoted":false,"range":{"startLine":0,"startCharacter":23,"endLine":0,"endCharacter":27}}],"range":{"startLine":0,"startCharacter":23,"endLine":0,"endCharacter":39},"value":{"kind":"inlineTable","range":{"startLine":0,"startCharacter":30,"endLine":0,"endCharacter":39},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"a","quoted":false,"range":{"startLine":0,"startCharacter":32,"endLine":0,"endCharacter":33}}],"range":{"startLine":0,"startCharacter":32,"endLine":0,"endCharacter":38},"value":{"kind":"integer","range":{"startLine":0,"startCharacter":36,"endLine":0,"endCharacter":37},"open":false,"text":"1"}}]}}]}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":2,"0:5":3,"0:6":4,"0:7":5,"0:8":6,"0:9":7,"0:10":7,"0:11":7,"0:12":7,"0:13":7,"0:14":7,"0:15":7,"0:16":8,"0:17":9,"0:18":10,"0:19":11,"0:20":11,"0:21":12,"0:22":13,"0:23":14,"0:24":15,"0:25":15,"0:26":15,"0:27":15,"0:28":16,"0:29":17,"0:30":18,"0:31":19,"0:32":20,"0:33":21,"0:34":22,"0:35":23,"0:36":23,"0:37":23,"0:38":24,"0:39":25,"0:40":26,"0:41":27,"0:42":27,"0:43":27,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":3}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["dep"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":15}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":["version"],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":21}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":20}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":22,"endCharacter":22}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":23,"endCharacter":27}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":["opts"],"replaceRange":{"startCharacter":28,"endCharacter":28}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep","opts"],"keyPrefix":[],"replaceRange":{"startCharacter":29,"endCharacter":29}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep","opts"],"keyPrefix":[],"replaceRange":{"startCharacter":30,"endCharacter":30}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep","opts"],"keyPrefix":[],"replaceRange":{"startCharacter":31,"endCharacter":31}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep","opts"],"keyPrefix":[],"replaceRange":{"startCharacter":32,"endCharacter":32}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep","opts"],"keyPrefix":[],"replaceRange":{"startCharacter":32,"endCharacter":33}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep","opts"],"keyPrefix":["a"],"replaceRange":{"startCharacter":34,"endCharacter":34}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","opts","a"],"valueKind":"integer","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":36,"endCharacter":37}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep","opts"],"keyPrefix":[],"replaceRange":{"startCharacter":38,"endCharacter":38}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":39,"endCharacter":39}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":40,"endCharacter":40}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":41,"endCharacter":41}}]},{"lines":["dep = { version = \"1\""],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"dep","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":3}}],"range":{"startLine":0,"startCharacter":0,"endLine":1,"endCharacter":0},"value":{"kind":"inlineTable","range":{"startLine":0,"startCharacter":6,"endLine":1,"endCharacter":0},"open":true,"entries":[{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":15}}],"range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":21},"value":{"kind":"string","range":{"startLine":0,"startCharacter":18,"endLine":0,"endCharacter":21},"open":false,"text":"1","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":19,"endLine":0,"endCharacter":20}}}]}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":2,"0:5":3,"0:6":4,"0:7":5,"0:8":6,"0:9":7,"0:10":7,"0:11":7,"0:12":7,"0:13":7,"0:14":7,"0:15":7,"0:16":8,"0:17":9,"0:18":10,"0:19":11,"0:20":11,"0:21":12,"0:22":12,"0:23":12,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":3}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["dep"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":15}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":["version"],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":21}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":20}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":21,"endCharacter":21}}]},{"lines":["# 顶部注释","name = \"a # b\" # 行尾注释"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":14},"value":{"kind":"string","range":{"startLine":1,"startCharacter":7,"endLine":1,"endCharacter":14},"open":false,"text":"a # b","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":8,"endLine":1,"endCharacter":13}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":4,"0:5":5,"0:6":6,"0:7":6,"0:8":6,"1:0":0,"1:1":7,"1:2":7,"1:3":7,"1:4":7,"1:5":8,"1:6":9,"1:7":10,"1:8":11,"1:9":11,"1:10":11,"1:11":11,"1:12":11,"1:13":11,"1:14":12,"1:15":13,"1:16":14,"1:17":15,"1:18":16,"1:19":17,"1:20":18,"1:21":19,"1:22":19,"1:23":19,"2:0":0,"2:1":7,"2:2":7},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":3,"endCharacter":3}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":14}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":13}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":14,"endCharacter":14}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":20,"endCharacter":20}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":21,"endCharacter":21}}]},{"lines":["desc = \"a \\\"b\\\" c\""],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"desc","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":18},"value":{"kind":"string","range":{"startLine":0,"startCharacter":7,"endLine":0,"endCharacter":18},"open":false,"text":"a \\\"b\\\" c","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":17}}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":2,"0:6":3,"0:7":4,"0:8":5,"0:9":5,"0:10":5,"0:11":5,"0:12":6,"0:13":6,"0:14":6,"0:15":7,"0:16":7,"0:17":7,"0:18":8,"0:19":8,"0:20":8,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["desc"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["desc"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":18}},{"kind":"value","section":{"kind":"top"},"keyPath":["desc"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"top"},"keyPath":["desc"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":11}},{"kind":"value","section":{"kind":"top"},"keyPath":["desc"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":14}},{"kind":"value","section":{"kind":"top"},"keyPath":["desc"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":15,"endCharacter":17}},{"kind":"value","section":{"kind":"top"},"keyPath":["desc"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}}]},{"lines":["text = \"\"\"","hello","\"\"\""],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"text","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4}}],"range":{"startLine":0,"startCharacter":0,"endLine":2,"endCharacter":3},"value":{"kind":"string","range":{"startLine":0,"startCharacter":7,"endLine":2,"endCharacter":3},"open":false,"text":"\nhello\n","quote":"\"","multiline":true,"contentRange":{"startLine":0,"startCharacter":10,"endLine":2,"endCharacter":0}}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":2,"0:6":3,"0:7":4,"0:8":5,"0:9":6,"0:10":7,"0:11":7,"0:12":7,"1:0":8,"1:1":8,"1:2":8,"1:3":8,"1:4":8,"1:5":8,"1:6":8,"1:7":8,"2:0":9,"2:1":10,"2:2":11,"2:3":12,"2:4":12,"2:5":12,"3:0":9,"3:1":10,"3:2":11},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["text"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"top"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":3,"endCharacter":3}}]},{"lines":["name = \"hel"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":11},"value":{"kind":"string","range":{"startLine":0,"startCharacter":7,"endLine":0,"endCharacter":11},"open":true,"text":"hel","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":11}}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":2,"0:6":3,"0:7":4,"0:8":5,"0:9":5,"0:10":5,"0:11":5,"0:12":5,"0:13":5,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":11}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":8,"endCharacter":11}}]},{"lines":["[package]\r","name = \"demo\"\r"],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":13},"value":{"kind":"string","range":{"startLine":1,"startCharacter":7,"endLine":1,"endCharacter":13},"open":false,"text":"demo","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":8,"endLine":1,"endCharacter":12}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"0:12":3,"1:0":4,"1:1":5,"1:2":5,"1:3":5,"1:4":5,"1:5":6,"1:6":7,"1:7":8,"1:8":9,"1:9":9,"1:10":9,"1:11":9,"1:12":9,"1:13":10,"1:14":10,"1:15":10,"1:16":10,"2:0":4,"2:1":5,"2:2":5},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":13}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}}]},{"lines":["!!garbage!!","[package]"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"garbage","quoted":false,"range":{"startLine":0,"startCharacter":2,"endLine":0,"endCharacter":9}}],"range":{"startLine":0,"startCharacter":2,"endLine":0,"endCharacter":9}},{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":1,"startCharacter":1,"endLine":1,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":9},"line":1}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":3,"0:5":3,"0:6":3,"0:7":3,"0:8":3,"0:9":3,"0:10":4,"0:11":5,"0:12":5,"0:13":5,"1:0":0,"1:1":6,"1:2":7,"1:3":7,"1:4":7,"1:5":7,"1:6":7,"1:7":7,"1:8":7,"1:9":8,"1:10":8,"1:11":8,"2:0":0,"2:1":6,"2:2":7},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":2,"endCharacter":9}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}}]},{"lines":["[tar"],"document":{"nodes":[{"type":"section","segments":[{"name":"tar","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":4}}],"isArray":false,"open":true,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":4}}]},{"lines":["[targets."],"document":{"nodes":[{"type":"section","segments":[{"name":"targets","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":true,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"section-header","segments":["targets"],"isArray":false,"replaceRange":{"startCharacter":9,"endCharacter":9}}]},{"lines":["[["],"document":{"nodes":[{"type":"section","segments":[],"isArray":true,"open":true,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":2},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":true,"replaceRange":{"startCharacter":2,"endCharacter":2}}]},{"lines":["[package]",""],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"1:0":4,"1:1":4,"1:2":4,"2:0":4,"2:1":4,"2:2":4},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}}]},{"lines":[""],"document":{"nodes":[]},"context":{"0:0":0,"0:1":0,"0:2":0,"1:0":0,"1:1":0,"1:2":0},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}}]},{"lines":["[custom]",""],"document":{"nodes":[{"type":"section","segments":[{"name":"custom","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":7}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":8},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":3,"0:9":3,"0:10":3,"1:0":4,"1:1":4,"1:2":4,"2:0":4,"2:1":4,"2:2":4},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":7}},{"kind":"key","section":{"kind":"unknown","segments":["custom"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"unknown","segments":["custom"]},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}}]},{"lines":["capi.la"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"capi","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4}},{"name":"la","quoted":false,"range":{"startLine":0,"startCharacter":5,"endLine":0,"endCharacter":7}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":7}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":2,"0:6":3,"0:7":3,"0:8":3,"0:9":3,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["capi"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["capi"],"replaceRange":{"startCharacter":5,"endCharacter":7}}]},{"lines":["kind = \"bin\""],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"kind","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":12},"value":{"kind":"string","range":{"startLine":0,"startCharacter":7,"endLine":0,"endCharacter":12},"open":false,"text":"bin","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":11}}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":2,"0:6":3,"0:7":4,"0:8":5,"0:9":5,"0:10":5,"0:11":5,"0:12":6,"0:13":6,"0:14":6,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["kind"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":12}},{"kind":"value","section":{"kind":"top"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"top"},"keyPath":["kind"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":11}},{"kind":"value","section":{"kind":"top"},"keyPath":["kind"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}}]},{"lines":["kind = "],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"kind","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":7}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":2,"0:6":3,"0:7":4,"0:8":4,"0:9":4,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["kind"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["kind"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"top"},"keyPath":["kind"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}}]},{"lines":["sources = [ "],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"sources","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":7}}],"range":{"startLine":0,"startCharacter":0,"endLine":1,"endCharacter":0},"value":{"kind":"array","range":{"startLine":0,"startCharacter":10,"endLine":1,"endCharacter":0},"open":true,"elements":[]}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":1,"0:6":1,"0:7":1,"0:8":2,"0:9":3,"0:10":4,"0:11":5,"0:12":6,"0:13":6,"0:14":6,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["sources"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"value","section":{"kind":"top"},"keyPath":["sources"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":12,"endCharacter":12}}]},{"lines":["[features]","simd = { flags = [ { "],"document":{"nodes":[{"type":"section","segments":[{"name":"features","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":9}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":10},"line":0},{"type":"keyValue","keyPath":[{"name":"simd","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":2,"endCharacter":0},"value":{"kind":"inlineTable","range":{"startLine":1,"startCharacter":7,"endLine":2,"endCharacter":0},"open":true,"entries":[{"type":"keyValue","keyPath":[{"name":"flags","quoted":false,"range":{"startLine":1,"startCharacter":9,"endLine":1,"endCharacter":14}}],"range":{"startLine":1,"startCharacter":9,"endLine":2,"endCharacter":0},"value":{"kind":"array","range":{"startLine":1,"startCharacter":17,"endLine":2,"endCharacter":0},"open":true,"elements":[{"kind":"inlineTable","range":{"startLine":1,"startCharacter":19,"endLine":2,"endCharacter":0},"open":true,"entries":[]}]}}]}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":2,"0:10":3,"0:11":3,"0:12":3,"1:0":4,"1:1":5,"1:2":5,"1:3":5,"1:4":5,"1:5":6,"1:6":7,"1:7":8,"1:8":9,"1:9":10,"1:10":11,"1:11":11,"1:12":11,"1:13":11,"1:14":11,"1:15":12,"1:16":13,"1:17":14,"1:18":15,"1:19":16,"1:20":17,"1:21":18,"1:22":18,"1:23":18,"2:0":4,"2:1":5,"2:2":5},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":[],"keyPrefix":["simd"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":14}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd"],"keyPrefix":["flags"],"replaceRange":{"startCharacter":15,"endCharacter":15}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"known","group":"features"},"keyPath":["simd","flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd","flags"],"keyPrefix":[],"replaceRange":{"startCharacter":20,"endCharacter":20}},{"kind":"key","section":{"kind":"known","group":"features"},"containerPath":["simd","flags"],"keyPrefix":[],"replaceRange":{"startCharacter":21,"endCharacter":21}}]},{"lines":["dep = { version = \"1.0\" }"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"dep","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":3}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":25},"value":{"kind":"inlineTable","range":{"startLine":0,"startCharacter":6,"endLine":0,"endCharacter":25},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"version","quoted":false,"range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":15}}],"range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":23},"value":{"kind":"string","range":{"startLine":0,"startCharacter":18,"endLine":0,"endCharacter":23},"open":false,"text":"1.0","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":19,"endLine":0,"endCharacter":22}}}]}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":2,"0:5":3,"0:6":4,"0:7":5,"0:8":6,"0:9":7,"0:10":7,"0:11":7,"0:12":7,"0:13":7,"0:14":7,"0:15":7,"0:16":8,"0:17":9,"0:18":10,"0:19":11,"0:20":11,"0:21":11,"0:22":11,"0:23":12,"0:24":13,"0:25":14,"0:26":14,"0:27":14,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":3}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["dep"],"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":8,"endCharacter":15}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":["version"],"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":23}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":18,"endCharacter":18}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":22}},{"kind":"value","section":{"kind":"top"},"keyPath":["dep","version"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"key","section":{"kind":"top"},"containerPath":["dep"],"keyPrefix":[],"replaceRange":{"startCharacter":24,"endCharacter":24}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":25,"endCharacter":25}}]},{"lines":["flags = [ { glob = \"src/**\" } ]"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"flags","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":5}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":31},"value":{"kind":"array","range":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":31},"open":false,"elements":[{"kind":"inlineTable","range":{"startLine":0,"startCharacter":10,"endLine":0,"endCharacter":29},"open":false,"entries":[{"type":"keyValue","keyPath":[{"name":"glob","quoted":false,"range":{"startLine":0,"startCharacter":12,"endLine":0,"endCharacter":16}}],"range":{"startLine":0,"startCharacter":12,"endLine":0,"endCharacter":27},"value":{"kind":"string","range":{"startLine":0,"startCharacter":19,"endLine":0,"endCharacter":27},"open":false,"text":"src/**","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":20,"endLine":0,"endCharacter":26}}}]}]}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":1,"0:6":2,"0:7":3,"0:8":4,"0:9":5,"0:10":6,"0:11":7,"0:12":8,"0:13":9,"0:14":9,"0:15":9,"0:16":9,"0:17":10,"0:18":11,"0:19":12,"0:20":13,"0:21":13,"0:22":13,"0:23":13,"0:24":13,"0:25":13,"0:26":13,"0:27":14,"0:28":15,"0:29":16,"0:30":17,"0:31":18,"0:32":18,"0:33":18,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["flags"],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":[],"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":12}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":[],"replaceRange":{"startCharacter":12,"endCharacter":16}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":["glob"],"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags","glob"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":27}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags","glob"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags","glob"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":20,"endCharacter":26}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags","glob"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":27,"endCharacter":27}},{"kind":"key","section":{"kind":"top"},"containerPath":["flags"],"keyPrefix":[],"replaceRange":{"startCharacter":28,"endCharacter":28}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":29,"endCharacter":29}},{"kind":"value","section":{"kind":"top"},"keyPath":["flags"],"insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":30,"endCharacter":30}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":31,"endCharacter":31}}]},{"lines":["cmdline = true"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"cmdline","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":7}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":14},"value":{"kind":"boolean","range":{"startLine":0,"startCharacter":10,"endLine":0,"endCharacter":14},"open":false,"text":"true"}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":1,"0:6":1,"0:7":1,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"0:12":3,"0:13":3,"0:14":3,"0:15":3,"0:16":3,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["cmdline"],"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["cmdline"],"valueKind":"boolean","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":14}}]},{"lines":["name = \"demo\""],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":4}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":13},"value":{"kind":"string","range":{"startLine":0,"startCharacter":7,"endLine":0,"endCharacter":13},"open":false,"text":"demo","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":8,"endLine":0,"endCharacter":12}}}]},"context":{"0:0":0,"0:1":1,"0:2":1,"0:3":1,"0:4":1,"0:5":2,"0:6":3,"0:7":4,"0:8":5,"0:9":5,"0:10":5,"0:11":5,"0:12":5,"0:13":6,"0:14":6,"0:15":6,"1:0":0,"1:1":1,"1:2":1},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":13}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":12}},{"kind":"value","section":{"kind":"top"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":13,"endCharacter":13}}]},{"lines":["s = 'a\"b'"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"s","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":1}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"value":{"kind":"string","range":{"startLine":0,"startCharacter":4,"endLine":0,"endCharacter":9},"open":false,"text":"a\"b","quote":"'","multiline":false,"contentRange":{"startLine":0,"startCharacter":5,"endLine":0,"endCharacter":8}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":4,"0:5":5,"0:6":5,"0:7":5,"0:8":5,"0:9":6,"0:10":6,"0:11":6,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":1}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["s"],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"top"},"keyPath":["s"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":4,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["s"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"value","section":{"kind":"top"},"keyPath":["s"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":5,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["s"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}}]},{"lines":["s = \"a'b\""],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"s","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":1}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"value":{"kind":"string","range":{"startLine":0,"startCharacter":4,"endLine":0,"endCharacter":9},"open":false,"text":"a'b","quote":"\"","multiline":false,"contentRange":{"startLine":0,"startCharacter":5,"endLine":0,"endCharacter":8}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":4,"0:5":5,"0:6":5,"0:7":5,"0:8":5,"0:9":6,"0:10":6,"0:11":6,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":1}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["s"],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"top"},"keyPath":["s"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":4,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["s"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":4,"endCharacter":4}},{"kind":"value","section":{"kind":"top"},"keyPath":["s"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":5,"endCharacter":8}},{"kind":"value","section":{"kind":"top"},"keyPath":["s"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}}]},{"lines":["x = 1 [dep"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"x","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":1}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":6},"value":{"kind":"integer","range":{"startLine":0,"startCharacter":4,"endLine":0,"endCharacter":5},"open":false,"text":"1"}},{"type":"section","segments":[{"name":"dep","quoted":false,"range":{"startLine":0,"startCharacter":7,"endLine":0,"endCharacter":10}}],"isArray":false,"open":true,"range":{"startLine":0,"startCharacter":6,"endLine":0,"endCharacter":10},"line":0}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":3,"0:5":3,"0:6":4,"0:7":5,"0:8":6,"0:9":6,"0:10":6,"0:11":6,"0:12":6,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":1}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["x"],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"top"},"keyPath":["x"],"valueKind":"integer","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":4,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":7,"endCharacter":10}}]},{"lines":["y = 2 {k = 1}"],"document":{"nodes":[{"type":"keyValue","keyPath":[{"name":"y","quoted":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":1}}],"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":6},"value":{"kind":"integer","range":{"startLine":0,"startCharacter":4,"endLine":0,"endCharacter":5},"open":false,"text":"2"}},{"type":"keyValue","keyPath":[{"name":"k","quoted":false,"range":{"startLine":0,"startCharacter":7,"endLine":0,"endCharacter":8}}],"range":{"startLine":0,"startCharacter":7,"endLine":0,"endCharacter":12},"value":{"kind":"integer","range":{"startLine":0,"startCharacter":11,"endLine":0,"endCharacter":12},"open":false,"text":"1"}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":3,"0:4":3,"0:5":3,"0:6":4,"0:7":5,"0:8":6,"0:9":7,"0:10":8,"0:11":8,"0:12":8,"0:13":9,"0:14":9,"0:15":9,"1:0":0,"1:1":1,"1:2":2},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":1}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["y"],"replaceRange":{"startCharacter":2,"endCharacter":2}},{"kind":"value","section":{"kind":"top"},"keyPath":["y"],"valueKind":"integer","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":4,"endCharacter":5}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":7,"endCharacter":8}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":["k"],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"top"},"keyPath":["k"],"valueKind":"integer","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":11,"endCharacter":12}},{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":13,"endCharacter":13}}]},{"lines":["[workspace.package]","","[target.x86_64-openkal.link]","","[platform.build.'cfg(arch = \"aarch64\")']","","[features.mt.requires_abi]",""],"document":{"nodes":[{"type":"section","segments":[{"name":"workspace","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":10}},{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":11,"endLine":0,"endCharacter":18}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":19},"line":0},{"type":"section","segments":[{"name":"target","quoted":false,"range":{"startLine":2,"startCharacter":1,"endLine":2,"endCharacter":7}},{"name":"x86_64-openkal","quoted":false,"range":{"startLine":2,"startCharacter":8,"endLine":2,"endCharacter":22}},{"name":"link","quoted":false,"range":{"startLine":2,"startCharacter":23,"endLine":2,"endCharacter":27}}],"isArray":false,"open":false,"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":28},"line":2},{"type":"section","segments":[{"name":"platform","quoted":false,"range":{"startLine":4,"startCharacter":1,"endLine":4,"endCharacter":9}},{"name":"build","quoted":false,"range":{"startLine":4,"startCharacter":10,"endLine":4,"endCharacter":15}},{"name":"cfg(arch = \"aarch64\")","quoted":true,"range":{"startLine":4,"startCharacter":16,"endLine":4,"endCharacter":39}}],"isArray":false,"open":false,"range":{"startLine":4,"startCharacter":0,"endLine":4,"endCharacter":40},"line":4},{"type":"section","segments":[{"name":"features","quoted":false,"range":{"startLine":6,"startCharacter":1,"endLine":6,"endCharacter":9}},{"name":"mt","quoted":false,"range":{"startLine":6,"startCharacter":10,"endLine":6,"endCharacter":12}},{"name":"requires_abi","quoted":false,"range":{"startLine":6,"startCharacter":13,"endLine":6,"endCharacter":25}}],"isArray":false,"open":false,"range":{"startLine":6,"startCharacter":0,"endLine":6,"endCharacter":26},"line":6}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":2,"0:10":2,"0:11":3,"0:12":4,"0:13":4,"0:14":4,"0:15":4,"0:16":4,"0:17":4,"0:18":4,"0:19":5,"0:20":5,"0:21":5,"1:0":6,"1:1":6,"1:2":6,"2:0":6,"2:1":1,"2:2":7,"2:3":7,"2:4":7,"2:5":7,"2:6":7,"2:7":7,"2:8":8,"2:9":9,"2:10":9,"2:11":9,"2:12":9,"2:13":9,"2:14":9,"2:15":9,"2:16":9,"2:17":9,"2:18":9,"2:19":9,"2:20":9,"2:21":9,"2:22":9,"2:23":10,"2:24":11,"2:25":11,"2:26":11,"2:27":11,"2:28":12,"2:29":12,"2:30":12,"3:0":13,"3:1":13,"3:2":13,"4:0":13,"4:1":1,"4:2":14,"4:3":14,"4:4":14,"4:5":14,"4:6":14,"4:7":14,"4:8":14,"4:9":14,"4:10":15,"4:11":16,"4:12":16,"4:13":16,"4:14":16,"4:15":16,"4:16":17,"4:17":18,"4:18":18,"4:19":18,"4:20":18,"4:21":18,"4:22":18,"4:23":18,"4:24":18,"4:25":18,"4:26":18,"4:27":18,"4:28":18,"4:29":18,"4:30":18,"4:31":18,"4:32":18,"4:33":18,"4:34":18,"4:35":18,"4:36":18,"4:37":18,"4:38":18,"4:39":18,"4:40":19,"4:41":19,"4:42":19,"5:0":20,"5:1":20,"5:2":20,"6:0":20,"6:1":1,"6:2":14,"6:3":14,"6:4":14,"6:5":14,"6:6":14,"6:7":14,"6:8":14,"6:9":14,"6:10":21,"6:11":22,"6:12":22,"6:13":23,"6:14":24,"6:15":24,"6:16":24,"6:17":24,"6:18":24,"6:19":24,"6:20":24,"6:21":24,"6:22":24,"6:23":24,"6:24":24,"6:25":24,"6:26":25,"6:27":25,"6:28":25,"7:0":26,"7:1":26,"7:2":26,"8:0":26,"8:1":26,"8:2":26},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":10}},{"kind":"section-header","segments":["workspace"],"isArray":false,"replaceRange":{"startCharacter":11,"endCharacter":11}},{"kind":"section-header","segments":["workspace"],"isArray":false,"replaceRange":{"startCharacter":11,"endCharacter":18}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":19,"endCharacter":19}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":7}},{"kind":"section-header","segments":["target"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"section-header","segments":["target"],"isArray":false,"replaceRange":{"startCharacter":8,"endCharacter":22}},{"kind":"section-header","segments":["target","x86_64-openkal"],"isArray":false,"replaceRange":{"startCharacter":23,"endCharacter":23}},{"kind":"section-header","segments":["target","x86_64-openkal"],"isArray":false,"replaceRange":{"startCharacter":23,"endCharacter":27}},{"kind":"key","section":{"kind":"known","group":"platform.link"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":28,"endCharacter":28}},{"kind":"key","section":{"kind":"known","group":"platform.link"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":9}},{"kind":"section-header","segments":["platform"],"isArray":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"section-header","segments":["platform"],"isArray":false,"replaceRange":{"startCharacter":10,"endCharacter":15}},{"kind":"section-header","segments":["platform","build"],"isArray":false,"replaceRange":{"startCharacter":16,"endCharacter":16}},{"kind":"section-header","segments":["platform","build"],"isArray":false,"replaceRange":{"startCharacter":16,"endCharacter":39}},{"kind":"key","section":{"kind":"known","group":"platform.build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":40,"endCharacter":40}},{"kind":"key","section":{"kind":"known","group":"platform.build"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":["features"],"isArray":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"section-header","segments":["features"],"isArray":false,"replaceRange":{"startCharacter":10,"endCharacter":12}},{"kind":"section-header","segments":["features","mt"],"isArray":false,"replaceRange":{"startCharacter":13,"endCharacter":13}},{"kind":"section-header","segments":["features","mt"],"isArray":false,"replaceRange":{"startCharacter":13,"endCharacter":25}},{"kind":"key","section":{"kind":"known","group":"requires_abi"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":26,"endCharacter":26}},{"kind":"key","section":{"kind":"known","group":"requires_abi"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}}]}] diff --git a/tests/rules/golden/unclosed.toml.json b/tests/rules/golden/unclosed.toml.json index 9165b01..d422edc 100644 --- a/tests/rules/golden/unclosed.toml.json +++ b/tests/rules/golden/unclosed.toml.json @@ -1 +1 @@ -{"diagnostics":{"default":[{"code":"mcpp.manifest.unknownKey","message":"unknown key text in [package]","severity":"warning","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"text"}],"quiet":[{"code":"mcpp.manifest.unknownKey","message":"unknown key text in [package]","severity":"info","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"text"}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"2:0":[3,0,0],"2:1":[3,0,4],"2:2":[3,0,4],"2:3":[3,0,4],"2:4":[3,0,4],"2:5":[3,5,5],"2:6":[2],"2:7":[2],"2:8":[2],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"3:0":[2],"3:1":[2],"3:2":[2],"3:3":[2],"3:4":[2],"3:5":[2],"3:6":[2],"3:7":[2],"3:8":[2],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"4:0":[2],"4:1":[2],"4:2":[2],"4:3":[2],"4:4":[2],"4:5":[2],"4:6":[2],"4:7":[2],"4:8":[2],"4:9":[2],"5:0":[2],"5:1":[2],"5:2":[2],"5:3":[2],"5:4":[2],"5:5":[2],"5:6":[2],"5:7":[2],"5:8":[2],"5:9":[2],"5:10":[2],"5:11":[2],"5:12":[2],"5:13":[2],"5:14":[2],"5:15":[2],"5:16":[2],"5:17":[2],"5:18":[2],"5:19":[2],"6:0":[2],"6:1":[2],"6:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"}]} +{"diagnostics":{"default":[{"code":"mcpp.manifest.unknownKey","message":"unknown key text in [package]","severity":"warning","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"text"}],"quiet":[{"code":"mcpp.manifest.unknownKey","message":"unknown key text in [package]","severity":"info","line":3,"startCharacter":1,"endCharacter":5,"relatedKey":"text"}]},"completion":{"0:0":[0,0,0],"0:1":[0,0,1],"0:2":[0,0,8],"0:3":[0,0,8],"0:4":[0,0,8],"0:5":[0,0,8],"0:6":[0,0,8],"0:7":[0,0,8],"0:8":[0,0,8],"0:9":[1,9,9],"0:10":[1,9,9],"0:11":[1,9,9],"1:0":[1,0,0],"1:1":[1,0,4],"1:2":[1,0,4],"1:3":[1,0,4],"1:4":[1,0,4],"1:5":[1,5,5],"1:6":[2],"1:7":[2],"1:8":[2],"1:9":[2],"1:10":[2],"1:11":[2],"1:12":[2],"2:0":[3,0,0],"2:1":[3,0,4],"2:2":[3,0,4],"2:3":[3,0,4],"2:4":[3,0,4],"2:5":[3,5,5],"2:6":[2],"2:7":[2],"2:8":[2],"2:9":[2],"2:10":[2],"2:11":[2],"2:12":[2],"3:0":[2],"3:1":[2],"3:2":[2],"3:3":[2],"3:4":[2],"3:5":[2],"3:6":[2],"3:7":[2],"3:8":[2],"3:9":[2],"3:10":[2],"3:11":[2],"3:12":[2],"3:13":[2],"3:14":[2],"4:0":[2],"4:1":[2],"4:2":[2],"4:3":[2],"4:4":[2],"4:5":[2],"4:6":[2],"4:7":[2],"4:8":[2],"4:9":[2],"5:0":[2],"5:1":[2],"5:2":[2],"5:3":[2],"5:4":[2],"5:5":[2],"5:6":[2],"5:7":[2],"5:8":[2],"5:9":[2],"5:10":[2],"5:11":[2],"5:12":[2],"5:13":[2],"5:14":[2],"5:15":[2],"5:16":[2],"5:17":[2],"5:18":[2],"5:19":[2],"6:0":[2],"6:1":[2],"6:2":[2]},"hover":{"0:2":0,"0:3":0,"0:4":0,"0:5":0,"0:6":0,"0:7":0,"0:8":0,"1:1":1,"1:2":1,"1:3":1,"1:4":1,"1:5":1},"definition":{},"lists":[[{"label":"[target.'cfg(...)'.abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.abi]"},{"label":"[build]","kind":"section","detail":"Build configuration","insertSnippet":"[build]"},{"label":"[build-dependencies]","kind":"section","detail":"Build-time dependencies (pulled at build time only, invisible at run time)","insertSnippet":"[build-dependencies]"},{"label":"[c-abi]","kind":"section","detail":"plane: identity","insertSnippet":"[c-abi]"},{"label":"[c-abi-absent]","kind":"section","detail":"plane: unclassified","insertSnippet":"[c-abi-absent]"},{"label":"[capabilities]","kind":"section","detail":"Capability bindings (provider selection)","insertSnippet":"[capabilities]"},{"label":"[dependencies]","kind":"section","detail":"Runtime dependencies","insertSnippet":"[dependencies]"},{"label":"[dev-dependencies]","kind":"section","detail":"Development/test dependencies","insertSnippet":"[dev-dependencies]"},{"label":"[feature-deps.]","kind":"section","detail":"Optional dependencies pulled in by a feature","insertSnippet":"[feature-deps.${1:name}]"},{"label":"[target.'cfg(...)'.feature-requires-abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.feature-requires-abi]"},{"label":"[feature-xlings]","kind":"section","detail":"plane: gate","insertSnippet":"[feature-xlings]"},{"label":"[features]","kind":"section","detail":"Feature definitions","insertSnippet":"[features]"},{"label":"[generated_files]","kind":"section","detail":"Generated files (path → content)","insertSnippet":"[generated_files]"},{"label":"[hooks]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks]"},{"label":"[hooks.build_failed]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_failed]"},{"label":"[hooks.build_finished]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_finished]"},{"label":"[hooks.build_start]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.build_start]"},{"label":"[hooks.during_build]","kind":"section","detail":"plane: lifecycle","insertSnippet":"[hooks.during_build]"},{"label":"[host]","kind":"section","detail":"plane: condition","insertSnippet":"[host]"},{"label":"[indices]","kind":"section","detail":"Project-level index redirection","insertSnippet":"[indices]"},{"label":"[kernel-abi]","kind":"section","detail":"plane: unclassified","insertSnippet":"[kernel-abi]"},{"label":"[language]","kind":"section","detail":"Legacy compatibility field; new projects should use [package].standard","insertSnippet":"[language]","documentation":"Deprecated; use `[package].standard`."},{"label":"[lib]","kind":"section","detail":"Library root-module convention","insertSnippet":"[lib]"},{"label":"[modules]","kind":"section","detail":"plane: unclassified","insertSnippet":"[modules]"},{"label":"[pack]","kind":"section","detail":"mcpp pack packaging configuration","insertSnippet":"[pack]"},{"label":"[pack.bundle-project]","kind":"section","detail":"Fine-tuning of the vendored filtering policy","insertSnippet":"[pack.bundle-project]"},{"label":"[package]","kind":"section","detail":"Package metadata","insertSnippet":"[package]"},{"label":"[platform]","kind":"section","detail":"plane: platform","insertSnippet":"[platform]"},{"label":"[platform.build]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.build]"},{"label":"[platform.carrier]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.carrier]"},{"label":"[platform.hosted]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.hosted]"},{"label":"[platform.layers]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.layers]"},{"label":"[platform.link]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.link]"},{"label":"[[platform.link.inputs]]","kind":"section","detail":"plane: platform","insertSnippet":"[[platform.link.inputs]]"},{"label":"[platform.runners]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.runners]"},{"label":"[platform.tools]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.tools]"},{"label":"[platform.versions]","kind":"section","detail":"plane: platform","insertSnippet":"[platform.versions]"},{"label":"[profile.]","kind":"section","detail":"Build profiles","insertSnippet":"[profile.${1:name}]"},{"label":"[target.'cfg(...)'.requires_abi]","kind":"section","detail":"plane: condition","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.requires_abi]"},{"label":"[resources]","kind":"section","detail":"Metadata and assets compiled into the product (PE targets only)","insertSnippet":"[resources]"},{"label":"[resources.version-info]","kind":"section","detail":"plane: metadata","insertSnippet":"[resources.version-info]"},{"label":"[runtime]","kind":"section","detail":"Host runtime capabilities","insertSnippet":"[runtime]"},{"label":"[[runtime.artifacts]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.artifacts]]"},{"label":"[[runtime.deploy]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.deploy]]"},{"label":"[[runtime.requirements]]","kind":"section","detail":"plane: metadata","insertSnippet":"[[runtime.requirements]]"},{"label":"[scan_overrides]","kind":"section","detail":"plane: unclassified","insertSnippet":"[scan_overrides]"},{"label":"[target.]","kind":"section","detail":"Configuration per target triple","insertSnippet":"[target.${1:x86_64-linux-gnu}]"},{"label":"[targets.]","kind":"section","detail":"Build targets","insertSnippet":"[targets.${1:name}]"},{"label":"[test]","kind":"section","detail":"plane: unclassified","insertSnippet":"[test]"},{"label":"[toolchain]","kind":"section","detail":"Compiler toolchain shorthand","insertSnippet":"[toolchain]"},{"label":"[tools]","kind":"section","detail":"plane: unclassified","insertSnippet":"[tools]"},{"label":"[tools.overrides]","kind":"section","detail":"Host tool binary overrides","insertSnippet":"[tools.overrides]"},{"label":"[workspace]","kind":"section","detail":"Workspace member declarations","insertSnippet":"[workspace]"},{"label":"[xlings]","kind":"section","detail":"Build environment (supplied by xlings)","insertSnippet":"[xlings]"},{"label":"[xlings.overrides]","kind":"section","detail":"plane: tool","insertSnippet":"[xlings.overrides]"},{"label":"[xlings.workspace]","kind":"section","detail":"Pin tool versions","insertSnippet":"[xlings.workspace]"},{"label":"[workspace.dependencies]","kind":"section","detail":"Declare dependency versions centrally; members inherit them with workspace = true","insertSnippet":"[workspace.dependencies]"},{"label":"[dependencies.]","kind":"section","detail":"Dependencies of one index namespace","insertSnippet":"[dependencies.${1:mcpplibs}]"},{"label":"[features.]","kind":"section","detail":"A feature as a table","insertSnippet":"[features.${1:name}]"},{"label":"[target.'cfg(...)'.build]","kind":"section","detail":"Build settings for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.build]"},{"label":"[target.'cfg(...)'.dependencies]","kind":"section","detail":"Dependencies for the targets a selector matches","insertSnippet":"[target.'cfg(${1:os = \"linux\"})'.dependencies]"}],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"name","kind":"template","detail":"string","insertSnippet":"name = \"\""},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}],[],[{"label":"accelerators","kind":"template","detail":"array","insertSnippet":"accelerators = []","documentation":"accelerator backends the package supports (docs/04 §2.12b)"},{"label":"authors","kind":"template","detail":"array","insertSnippet":"authors = []"},{"label":"c-environment","kind":"template","detail":"string","insertSnippet":"c-environment = \"\""},{"label":"description","kind":"template","detail":"string","insertSnippet":"description = \"\""},{"label":"exclusive","kind":"template","detail":"array","insertSnippet":"exclusive = []"},{"label":"license","kind":"template","detail":"string","insertSnippet":"license = \"\""},{"label":"mcpp","kind":"template","detail":"string","insertSnippet":"mcpp = \"\"","documentation":"release floor, only the \">=\" form is accepted (docs/04 §2.1) Since 2026.9.28.3."},{"label":"metadata","kind":"template","detail":"string","insertSnippet":"metadata = \"\"","documentation":"table keyed by tool name; mcpp keeps it and does not interpret it"},{"label":"namespace","kind":"template","detail":"string","insertSnippet":"namespace = \"\""},{"label":"platforms","kind":"template","detail":"enum: linux | macos | windows | ios | android | emscripten","insertSnippet":"platforms = \"linux\"","documentation":"array of platform names (docs/04 §2.12)"},{"label":"provides","kind":"template","detail":"array","insertSnippet":"provides = []"},{"label":"repo","kind":"template","detail":"string","insertSnippet":"repo = \"\""},{"label":"requires","kind":"template","detail":"array","insertSnippet":"requires = []"},{"label":"requires_abi","kind":"template","detail":"table","insertSnippet":"requires_abi = { }","documentation":"{ threads, exceptions }: the artefact's ABI switches this package requires Since 2026.9.12.2."},{"label":"standard","kind":"template","detail":"enum: c++20 | c++23 | c++26 | c++2a | c++2c | gnu++20 | gnu++23 | gnu++26 | c++latest | c++fly (default: \"c++23\")","insertSnippet":"standard = \"c++20\"","documentation":"c++2a/c++2c are aliases; gnu++NN, c++latest and c++fly are documented too"},{"label":"std-compat-module","kind":"template","detail":"string","insertSnippet":"std-compat-module = \"\""},{"label":"std-module","kind":"template","detail":"string","insertSnippet":"std-module = \"\""},{"label":"std-module-flags","kind":"template","detail":"array","insertSnippet":"std-module-flags = []"},{"label":"version","kind":"template","detail":"string","insertSnippet":"version = \"\""}]],"hovers":[{"title":"[package]","body":"plane: `identity`\n\nkeys: 20","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"},{"title":"name","body":"type: `string`","documentation":"https://github.com/mcpp-community/mcpp/blob/main/docs/04-mcpp-toml.md#21-package--package-metadata"}],"document":{"nodes":[{"type":"section","segments":[{"name":"package","quoted":false,"range":{"startLine":0,"startCharacter":1,"endLine":0,"endCharacter":8}}],"isArray":false,"open":false,"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":9},"line":0},{"type":"keyValue","keyPath":[{"name":"name","quoted":false,"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":4}}],"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":10},"value":{"kind":"string","range":{"startLine":1,"startCharacter":7,"endLine":1,"endCharacter":10},"open":false,"text":"u","quote":"\"","multiline":false,"contentRange":{"startLine":1,"startCharacter":8,"endLine":1,"endCharacter":9}}},{"type":"keyValue","keyPath":[{"name":"text","quoted":false,"range":{"startLine":2,"startCharacter":0,"endLine":2,"endCharacter":4}}],"range":{"startLine":2,"startCharacter":0,"endLine":7,"endCharacter":0},"value":{"kind":"string","range":{"startLine":2,"startCharacter":7,"endLine":7,"endCharacter":0},"open":true,"text":"\nnever closed\n[build]\ncxxflags = [\"-O2\"\n","quote":"\"","multiline":true,"contentRange":{"startLine":2,"startCharacter":10,"endLine":7,"endCharacter":0}}}]},"context":{"0:0":0,"0:1":1,"0:2":2,"0:3":2,"0:4":2,"0:5":2,"0:6":2,"0:7":2,"0:8":2,"0:9":3,"0:10":3,"0:11":3,"1:0":4,"1:1":5,"1:2":5,"1:3":5,"1:4":5,"1:5":6,"1:6":7,"1:7":8,"1:8":9,"1:9":9,"1:10":10,"1:11":10,"1:12":10,"2:0":4,"2:1":5,"2:2":5,"2:3":5,"2:4":5,"2:5":11,"2:6":12,"2:7":13,"2:8":14,"2:9":15,"2:10":16,"2:11":16,"2:12":16,"3:0":17,"3:1":17,"3:2":17,"3:3":17,"3:4":17,"3:5":17,"3:6":17,"3:7":17,"3:8":17,"3:9":17,"3:10":17,"3:11":17,"3:12":17,"3:13":17,"3:14":17,"4:0":18,"4:1":18,"4:2":18,"4:3":18,"4:4":18,"4:5":18,"4:6":18,"4:7":18,"4:8":18,"4:9":18,"5:0":17,"5:1":17,"5:2":17,"5:3":17,"5:4":17,"5:5":17,"5:6":17,"5:7":17,"5:8":17,"5:9":17,"5:10":17,"5:11":17,"5:12":17,"5:13":19,"5:14":19,"5:15":19,"5:16":19,"5:17":20,"5:18":20,"5:19":20,"6:0":21,"6:1":21,"6:2":21,"7:0":21,"7:1":21,"7:2":21},"contexts":[{"kind":"key","section":{"kind":"top"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":1}},{"kind":"section-header","segments":[],"isArray":false,"replaceRange":{"startCharacter":1,"endCharacter":8}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":0}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":[],"replaceRange":{"startCharacter":0,"endCharacter":4}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["name"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":true,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["name"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"key","section":{"kind":"known","group":"package"},"containerPath":[],"keyPrefix":["text"],"replaceRange":{"startCharacter":5,"endCharacter":5}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":6,"endCharacter":6}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":7,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":8,"endCharacter":8}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":false,"stringOpen":false,"replaceRange":{"startCharacter":9,"endCharacter":9}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":10,"endCharacter":10}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":0,"endCharacter":12}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":0,"endCharacter":7}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":13,"endCharacter":16}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":17,"endCharacter":17}},{"kind":"value","section":{"kind":"known","group":"package"},"keyPath":["text"],"valueKind":"string","insideString":true,"stringOpen":true,"replaceRange":{"startCharacter":0,"endCharacter":0}}]} diff --git a/tests/support.hpp b/tests/support.hpp index 4d57adc..39b72ad 100644 --- a/tests/support.hpp +++ b/tests/support.hpp @@ -19,6 +19,11 @@ namespace testsupport { // The repository, from this file's own path. inline std::filesystem::path repo() { return std::filesystem::path(__FILE__).parent_path().parent_path(); } +// A path as the `file:` URI an LSP client names it by. +inline std::string file_uri(const std::filesystem::path& p) { + const auto g = p.generic_string(); + return "file://" + std::string(g.starts_with("/") ? "" : "/") + g; +} inline std::filesystem::path fixtures() { return repo() / "protocol" / "fixtures"; } inline std::string read(const std::filesystem::path& p) { std::ifstream in(p, std::ios::binary); diff --git a/tests/test_mcppls_status.cpp b/tests/test_mcppls_status.cpp index 2fc3522..f56590b 100644 --- a/tests/test_mcppls_status.cpp +++ b/tests/test_mcppls_status.cpp @@ -86,13 +86,13 @@ int main() { return answer["result"]; }; const Json candidates = Json::array({payload.generic_string()}); - result("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "jetbrains"}, {"trusted", true}, - {"languageService", {{"candidates", candidates}}}, - {"workspaceFolders", Json::array({root.generic_string()})}}); + result("initialize", Json{{"clientInfo", {{"name", "test"}}}, {"workspaceFolders", Json::array({Json{{"uri", testsupport::file_uri(root)}}})}, + {"initializationOptions", Json{{"editor", "jetbrains"}, {"trusted", true}, + {"languageService", {{"candidates", candidates}}}}}}); // 0.0.19: no `status`; the actions are the frozen table's, and the workspace is not known. std::ofstream(payload / "mock-version") << "0.0.19\n"; - auto old = result("languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); + auto old = result("mcpp-ide/languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); CHECK(old["state"] == "ok" && old["version"] == "0.0.19"); CHECK(old["statusSource"] == "frozen" && old["workspace"].is_null()); CHECK(old["serverActions"] == ls::frozen_actions()); @@ -100,31 +100,31 @@ int main() { // 0.0.20: the status document. std::ofstream(payload / "mock-version") << "0.0.20\n"; std::ofstream(root / ".mock-mcppls-state") << "starting\n"; - auto now = result("languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); + auto now = result("mcpp-ide/languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); CHECK(now["statusSource"] == "status"); CHECK(now["workspace"]["state"] == "starting"); CHECK(now["capabilities"] == Json::array({"status"})); CHECK(now["serverActions"].size() == 3); // The commands each editor is offered: mcppls's action under that editor's id, or not at all. - auto jetbrains = result("commands/list", Json{{"editor", "jetbrains"}}); + auto jetbrains = result("mcpp-ide/commands/list", Json{{"editor", "jetbrains"}}); CHECK(command(jetbrains, "mcpp.languageServer.restart")["action"]["command"] == "community.mcpp.mcppls.RestartServer"); CHECK(command(jetbrains, "mcpp.languageServer.restartEngine").is_null()); - auto nvim = result("commands/list", Json{{"editor", "nvim"}}); + auto nvim = result("mcpp-ide/commands/list", Json{{"editor", "nvim"}}); CHECK(command(nvim, "mcpp.languageServer.restart")["action"]["command"] == "McpplsRestart"); - auto vscode = result("commands/list", Json{{"editor", "vscode"}}); + auto vscode = result("mcpp-ide/commands/list", Json{{"editor", "vscode"}}); CHECK(command(vscode, "mcpp.languageServer.openLogFolder")["action"]["arguments"] == Json::array({"logs"})); CHECK(command(vscode, "mcpp.languageServer.restartEngine")["action"]["command"] == "mcppls.restartClangd"); - auto cli = result("commands/list", Json{{"editor", "cli"}}); + auto cli = result("mcpp-ide/commands/list", Json{{"editor", "cli"}}); CHECK(std::ranges::none_of(cli["commands"], [](const Json& c) { return c["action"].value("kind", "") == "forward"; })); // A change of state is told once; asking again with nothing changed tells nothing. - const int before = wire.count("languageService/changed"); + const int before = wire.count("mcpp-ide/languageService/changed"); std::ofstream(root / ".mock-mcppls-state") << "ready\n"; - auto ready = result("languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); + auto ready = result("mcpp-ide/languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); CHECK(ready["workspace"]["state"] == "ready"); - (void)result("languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); - CHECK(wire.count("languageService/changed") == before + 1); + (void)result("mcpp-ide/languageService/status", Json{{"candidates", candidates}, {"root", root.generic_string()}}); + CHECK(wire.count("mcpp-ide/languageService/changed") == before + 1); std::error_code ignored; std::filesystem::remove_all(base, ignored); diff --git a/tests/test_probe.cpp b/tests/test_probe.cpp index 789b222..9dc84a5 100644 --- a/tests/test_probe.cpp +++ b/tests/test_probe.cpp @@ -76,30 +76,30 @@ int main() { CHECK(answer.contains("result")); return answer["result"]; }; - result("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "vscode"}, {"trusted", true}, - {"settings", {{"mcpp.path", mock.generic_string()}}}, - {"workspaceFolders", Json::array({base.generic_string()})}}); + result("initialize", Json{{"clientInfo", {{"name", "test"}}}, {"workspaceFolders", Json::array({Json{{"uri", testsupport::file_uri(base)}}})}, + {"initializationOptions", Json{{"editor", "vscode"}, {"trusted", true}, + {"settings", {{"mcpp.path", mock.generic_string()}}}}}}); // Before features: the frozen table, by the version the project's mcpp answers. - auto a = result("project/mcpp", Json{{"root", old.generic_string()}}); + auto a = result("mcpp-ide/project/mcpp", Json{{"root", old.generic_string()}}); CHECK(a["state"] == "ok" && a["version"] == "2026.10.29.1"); CHECK(a["features"].empty() && a["featuresFrozen"] == true); CHECK(a["fromSetting"] == true); - auto b = result("project/mcpp", Json{{"root", sets.generic_string()}}); + auto b = result("mcpp-ide/project/mcpp", Json{{"root", sets.generic_string()}}); CHECK(b["version"] == "2026.10.30.1" && b["featuresFrozen"] == true); CHECK(b["features"] == Json::array({"target-sets", "pkg-format-list"})); // Stated: taken at mcpp's word, a name the table does not hold included. - auto c = result("project/mcpp", Json{{"root", stated.generic_string()}}); + auto c = result("mcpp-ide/project/mcpp", Json{{"root", stated.generic_string()}}); CHECK(c["version"] == "2026.10.31.1" && c["featuresFrozen"] == false); CHECK(c["features"] == Json::array({"target-sets", "pkg-format-list", "polyglot"})); // A pinned version that is not installed: the shim's words and its install command. - auto d = result("project/mcpp", Json{{"root", missing.generic_string()}}); + auto d = result("mcpp-ide/project/mcpp", Json{{"root", missing.generic_string()}}); CHECK(d["state"] == "pinned-missing"); CHECK(d["install"] == "xlings install mcpp@2026.8.17.1"); CHECK(d["message"].get().find("not found") != std::string::npos); // Why a feature is not offered: the party, the capability and the version that has it. - auto why = result("capabilities/explain", Json{{"root", old.generic_string()}}); + auto why = result("mcpp-ide/capabilities/explain", Json{{"root", old.generic_string()}}); bool explained = false; for (const auto& f : why["features"]) if (f["id"] == "targets.sets") { @@ -121,26 +121,26 @@ int main() { // A job's envelope names another version: the project is asked again, and the editor told. std::ofstream(old / ".mock-mcpp-version") << "2026.10.31.1\n"; - auto job = result("job/start", Json{{"kind", "build"}, {"root", old.generic_string()}}); - for (int i = 0; i < 2000 && !wire.saw("project/mcppChanged"); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5)); - CHECK(wire.saw("project/mcppChanged")); - auto again = result("project/mcpp", Json{{"root", old.generic_string()}}); + auto job = result("mcpp-ide/job/start", Json{{"kind", "build"}, {"root", old.generic_string()}}); + for (int i = 0; i < 2000 && !wire.saw("mcpp-ide/project/mcppChanged"); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK(wire.saw("mcpp-ide/project/mcppChanged")); + auto again = result("mcpp-ide/project/mcpp", Json{{"root", old.generic_string()}}); CHECK(again["version"] == "2026.10.31.1"); (void)job; // The report names every project asked, with no secret in it. - auto report = result("report/collect", Json::object()); + auto report = result("mcpp-ide/report/collect", Json::object()); CHECK(report["kind"] == "mcpp-ide.report" && report["projects"].size() >= 4); // An untrusted workspace: the pinned program is not run. Wire untrusted; server::Session second(untrusted.server); second.install(); - auto answer = untrusted.call("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "vscode"}, {"trusted", false}, - {"settings", {{"mcpp.path", mock.generic_string()}}}, - {"workspaceFolders", Json::array({base.generic_string()})}}); + auto answer = untrusted.call("initialize", Json{{"clientInfo", {{"name", "test"}}}, {"workspaceFolders", Json::array({Json{{"uri", testsupport::file_uri(base)}}})}, + {"initializationOptions", Json{{"editor", "vscode"}, {"trusted", false}, + {"settings", {{"mcpp.path", mock.generic_string()}}}}}}); CHECK(answer.contains("result")); - auto e = untrusted.call("project/mcpp", Json{{"root", missing.generic_string()}}); + auto e = untrusted.call("mcpp-ide/project/mcpp", Json{{"root", missing.generic_string()}}); CHECK(e.contains("result") && e["result"]["state"] == "not-probed"); std::error_code ignored; diff --git a/tests/test_protocol.cpp b/tests/test_protocol.cpp index 84bce97..d62c4c2 100644 --- a/tests/test_protocol.cpp +++ b/tests/test_protocol.cpp @@ -113,26 +113,54 @@ int main() { CHECK(conforms(answer["result"], def, protocol, method)); return answer["result"]; }; - auto init = result_of("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "vscode"}, - {"settings", {{"mcpp.path", mock.generic_string()}}}, - {"workspaceFolders", Json::array({root.generic_string()})}}); + auto init = result_of("initialize", Json{{"clientInfo", {{"name", "test"}}}, {"workspaceFolders", Json::array({Json{{"uri", testsupport::file_uri(root)}}})}, + {"initializationOptions", Json{{"editor", "vscode"}, {"settings", {{"mcpp.path", mock.generic_string()}}}}}}); CHECK(init["projects"].size() == 1); CHECK(init["mcpp"].is_object() && init["mcpp"]["contract"] == true); + CHECK(init["capabilities"]["textDocumentSync"]["change"] == 1 && init["capabilities"]["hoverProvider"] == true); + + // The rules layer as LSP: a manifest opened has its diagnostics published (0-based, UTF-16), and + // completion, hover and the outline answer for it. + const auto tomlUri = testsupport::file_uri(root / "mcpp.toml"); + const std::string toml = "[package]\nname = \"app\"\ncolour = 1\n[build]\nbmi_schedule = \"\"\n"; + wire.server.dispatch(Json{{"jsonrpc", "2.0"}, {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", {{"uri", tomlUri}, {"languageId", "mcpp-toml"}, {"version", 1}, {"text", toml}}}}}}.dump()); + auto published = wire.notifications("textDocument/publishDiagnostics"); + CHECK(!published.empty() && published.back()["uri"] == tomlUri && published.back()["diagnostics"].size() == 1); + CHECK(published.back()["diagnostics"][0]["range"]["start"] == Json({{"line", 2}, {"character", 0}})); + CHECK(published.back()["diagnostics"][0]["code"] == "mcpp.manifest.unknownKey"); + auto at = [&](int line, int character) { + return Json{{"textDocument", {{"uri", tomlUri}}}, {"position", {{"line", line}, {"character", character}}}}; + }; + auto list = result_of("textDocument/completion", at(4, 16)); + std::set labels; + for (const auto& i : list["items"]) labels.insert(i["label"].get()); + CHECK(labels.contains("auto") && labels.contains("on") && labels.contains("off")); + auto hover = result_of("textDocument/hover", at(4, 2)); + CHECK(hover["contents"]["value"].get().find("bmi_schedule") != std::string::npos); + auto outline = result_of("textDocument/documentSymbol", Json{{"textDocument", {{"uri", tomlUri}}}}); + CHECK(outline.size() == 2 && outline[1]["name"] == "[build]" && outline[1]["children"].size() == 1); + // A change is the whole text; closing clears the document's diagnostics. + wire.server.dispatch(Json{{"jsonrpc", "2.0"}, {"method", "textDocument/didChange"}, + {"params", {{"textDocument", {{"uri", tomlUri}, {"version", 2}}}, {"contentChanges", Json::array({{{"text", "[package]\n"}}})}}}}.dump()); + CHECK(wire.notifications("textDocument/publishDiagnostics").back()["diagnostics"].empty()); + wire.server.dispatch(Json{{"jsonrpc", "2.0"}, {"method", "textDocument/didClose"}, {"params", {{"textDocument", {{"uri", tomlUri}}}}}}.dump()); + CHECK(result_of("textDocument/hover", at(0, 2)).is_null()); const Json rooted{{"root", root.generic_string()}}; - result_of("project/summary", rooted); - auto tests = result_of("test/list", rooted); + result_of("mcpp-ide/project/summary", rooted); + auto tests = result_of("mcpp-ide/test/list", rooted); CHECK(tests["tests"].size() == 1 && tests["tests"][0]["test"] == "test_smoke"); - auto targets = result_of("targets/list", rooted); + auto targets = result_of("mcpp-ide/targets/list", rooted); CHECK(!targets["rows"].empty()); - auto cmds = result_of("commands/list", Json{{"editor", "jetbrains"}}); + auto cmds = result_of("mcpp-ide/commands/list", Json{{"editor", "jetbrains"}}); for (const auto& c : cmds["commands"]) CHECK(c["action"]["kind"] != "forward"); // Neovim's table: no forward to mcppls's commands (mcppls-nvim has its own), no panel it has no // text for, its own quickfix command, and each row's subcommand (`:Mcpp `). - const Json nvimInit{{"editor", "nvim"}, {"locale", "en_US.UTF-8"}, {"trusted", true}, - {"workspaceFolders", Json::array({root.generic_string()})}, {"settings", Json::object()}}; + const Json nvimInit{{"clientInfo", {{"name", "Neovim"}}}, {"workspaceFolders", Json::array({Json{{"uri", testsupport::file_uri(root)}}})}, + {"initializationOptions", Json{{"editor", "nvim"}, {"locale", "en_US.UTF-8"}, {"trusted", true}, {"settings", Json::object()}}}}; CHECK(conforms(nvimInit, protocol["methods"]["initialize"]["params"], protocol, "initialize params (nvim)")); std::map nvim; - for (const auto& c : result_of("commands/list", Json{{"editor", "nvim"}})["commands"]) { + for (const auto& c : result_of("mcpp-ide/commands/list", Json{{"editor", "nvim"}})["commands"]) { const auto kind = c["action"]["kind"].get(); CHECK(kind != "forward" && kind != "editor"); CHECK(!(kind == "panel" && c["action"]["panel"] == "settings")); @@ -140,7 +168,7 @@ int main() { } CHECK(nvim["cancel"] == "mcpp.cancelJob" && nvim["build"] == "mcpp.build" && nvim["problems"] == "mcpp.problems"); CHECK(nvim.contains("showMenu") && nvim.contains("library.search") && !nvim.contains("openSettings")); - for (const auto& c : result_of("commands/list", Json{{"editor", "vscode"}})["commands"]) CHECK(c["id"] != "mcpp.problems"); + for (const auto& c : result_of("mcpp-ide/commands/list", Json{{"editor", "vscode"}})["commands"]) CHECK(c["id"] != "mcpp.problems"); // A row that leaves Neovim out says why. for (const auto& c : load(testsupport::repo() / "spec" / "commands.json")["commands"]) { if (!c.contains("editors") || std::ranges::find(c["editors"], Json("nvim")) != c["editors"].end()) continue; @@ -149,58 +177,58 @@ int main() { if (reason.empty()) std::println(std::cerr, "{} leaves nvim out without saying why", c["id"].get()); CHECK(!reason.empty()); } - result_of("menu/quick", rooted); - result_of("languageService/status", Json{{"candidates", Json::array()}}); + result_of("mcpp-ide/menu/quick", rooted); + result_of("mcpp-ide/languageService/status", Json{{"candidates", Json::array()}}); // The pack panel's choices: the closures SPEC-014 names, and linkages only where mcpp takes them. - auto pack = result_of("pack/formats", rooted); + auto pack = result_of("mcpp-ide/pack/formats", rooted); CHECK(pack["modes"].size() == 4); const bool helpNamesLinkage = testsupport::read(testsupport::fixtures() / "pack-help.txt").find("--linkage") != std::string::npos; CHECK(pack["linkages"].empty() != helpNamesLinkage); - result_of("graph/packages", rooted); - result_of("graph/modules", rooted); - result_of("manifest/check", rooted); - result_of("settings/registry", Json::object()); - result_of("selfCheck", Json::object()); - result_of("cache/inventory", Json::object()); - auto launch = result_of("run/launch", rooted); + result_of("mcpp-ide/graph/packages", rooted); + result_of("mcpp-ide/graph/modules", rooted); + result_of("mcpp-ide/manifest/check", rooted); + result_of("mcpp-ide/settings/registry", Json::object()); + result_of("mcpp-ide/selfCheck", Json::object()); + result_of("mcpp-ide/cache/inventory", Json::object()); + auto launch = result_of("mcpp-ide/run/launch", rooted); CHECK(!launch["argv"].empty()); - result_of("debug/configuration", Json{{"root", root.generic_string()}, {"available", Json::array({"lldb"})}}); + result_of("mcpp-ide/debug/configuration", Json{{"root", root.generic_string()}, {"available", Json::array({"lldb"})}}); // What mcpp-vscode's own parsers read: one command's output as it printed it, and a project's // files with its source count. - auto exec = result_of("mcpp/exec", Json{{"args", Json::array({"--version"})}, {"cwd", root.generic_string()}}); + auto exec = result_of("mcpp-ide/mcpp/exec", Json{{"args", Json::array({"--version"})}, {"cwd", root.generic_string()}}); CHECK(exec["exitCode"] == 0 && exec["stdout"].get().find("mcpp") != std::string::npos); CHECK(exec["timedOut"] == false && exec["truncated"] == false); - auto bounded = result_of("mcpp/exec", Json{{"args", Json::array({"--version"})}, {"maxOutputBytes", 3}}); + auto bounded = result_of("mcpp-ide/mcpp/exec", Json{{"args", Json::array({"--version"})}, {"maxOutputBytes", 3}}); CHECK(bounded["truncated"] == true && bounded["stdout"].get().size() <= 3); - CHECK(wire.call("mcpp/exec", Json{{"args", "--version"}}).contains("error")); + CHECK(wire.call("mcpp-ide/mcpp/exec", Json{{"args", "--version"}}).contains("error")); // mcppls's own commands run as the command the status found (here a program that answers --version). - result_of("languageService/status", Json{{"candidates", Json::array({mock.generic_string()})}}); - auto ls = result_of("languageService/exec", Json{{"args", Json::array({"--version"})}}); + result_of("mcpp-ide/languageService/status", Json{{"candidates", Json::array({mock.generic_string()})}}); + auto ls = result_of("mcpp-ide/languageService/exec", Json{{"args", Json::array({"--version"})}}); CHECK(ls["exitCode"] == 0 && !ls["stdout"].get().empty()); std::ofstream(root / "src" / "main.cpp") << "int main() {}\n"; std::ofstream(root / "src" / "notes.txt") << "not a source\n"; std::filesystem::create_directories(root / "target" / "obj"); std::ofstream(root / "target" / "obj" / "gen.cpp") << "// build output\n"; - auto files = result_of("project/files", rooted); + auto files = result_of("mcpp-ide/project/files", rooted); CHECK(files["manifest"].get().starts_with("[package]") && files["lock"].is_null()); CHECK(files["sourceFiles"] == 1); - result_of("job/start", Json{{"kind", "build"}, {"root", root.generic_string()}}); - result_of("job/start", Json{{"kind", "test"}, {"root", root.generic_string()}}); - for (int i = 0; i < 2000 && wire.notifications("job/finished").size() < 2; ++i) + result_of("mcpp-ide/job/start", Json{{"kind", "build"}, {"root", root.generic_string()}}); + result_of("mcpp-ide/job/start", Json{{"kind", "test"}, {"root", root.generic_string()}}); + for (int i = 0; i < 2000 && wire.notifications("mcpp-ide/job/finished").size() < 2; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5)); - CHECK(wire.notifications("job/finished").size() == 2); + CHECK(wire.notifications("mcpp-ide/job/finished").size() == 2); for (const auto& name : server::kNotifications) { const auto& def = protocol["notifications"][std::string(name)]["params"]; for (const auto& n : wire.notifications(name)) CHECK(conforms(n, def, protocol, name)); } - CHECK(!wire.notifications("job/plan").empty()); - CHECK(!wire.notifications("job/artifact").empty()); - const auto results = wire.notifications("test/result"); + CHECK(!wire.notifications("mcpp-ide/job/plan").empty()); + CHECK(!wire.notifications("mcpp-ide/job/artifact").empty()); + const auto results = wire.notifications("mcpp-ide/test/result"); CHECK(results.size() == 1 && results[0]["status"] == "passed"); - CHECK(wire.notifications("test/summary").size() == 1); + CHECK(wire.notifications("mcpp-ide/test/summary").size() == 1); // The plan a build sent is the project's configuration from then on. - auto after = result_of("project/summary", rooted); + auto after = result_of("mcpp-ide/project/summary", rooted); CHECK(after.contains("configuration")); std::filesystem::remove_all(root); std::println("test_protocol: ok"); diff --git a/tests/test_rules.cpp b/tests/test_rules.cpp index 1b9ef51..b768353 100644 --- a/tests/test_rules.cpp +++ b/tests/test_rules.cpp @@ -1,13 +1,15 @@ // The rules layer answers what mcpp-vscode 0.9's TypeScript answered (design 2026-10-30 part 2 §8): // over each file of tests/rules/corpus, at every position of every line (and past them), the -// completions, hovers, jumps, version contexts and diagnostics equal tests/rules/golden, which that -// implementation wrote. A difference is printed with its file and position. +// scanner's structure and cursor contexts, the completions, hovers, jumps, version contexts and +// diagnostics equal tests/rules/golden, which that implementation wrote (with the documents of its +// parser tests, golden/snippets.json). A difference is printed with its file and position. #include "support.hpp" import std; import nlohmann.json; import mcppide.text; import mcppide.manifest; import mcppide.buildscript; +import mcppide.toml; using namespace mcppide; using Json = nlohmann::json; @@ -18,8 +20,16 @@ int failures = 0; void expect(const Json& got, const Json& want, const std::string& where) { if (got == want) return; - if (++failures <= 25) - std::println(std::cerr, "MISMATCH {}\n got: {}\n want: {}", where, got.dump().substr(0, 1500), want.dump().substr(0, 1500)); + if (++failures > 25) return; + // Two lists: the first entry that differs. + if (got.is_array() && want.is_array()) + for (std::size_t i = 0; i < std::max(got.size(), want.size()); ++i) + if (i >= got.size() || i >= want.size() || got[i] != want[i]) { + std::println(std::cerr, "MISMATCH {} [{} of {}/{}]\n got: {}\n want: {}", where, i, got.size(), want.size(), + i < got.size() ? got[i].dump() : "-", i < want.size() ? want[i].dump() : "-"); + return; + } + std::println(std::cerr, "MISMATCH {}\n got: {}\n want: {}", where, got.dump().substr(0, 1500), want.dump().substr(0, 1500)); } Json opt(const std::optional& s) { return s ? Json(*s) : Json(); } @@ -28,6 +38,111 @@ void put(Json& j, std::string_view key, const std::optional& v) { if (v) j[std::string(key)] = *v; } +// ── the scanner, in the shapes mcpp-vscode 0.9's parser answered ── + +std::vector stripped(std::vector lines) { + for (auto& l : lines) + if (!l.empty() && l.back() == '\r') l.pop_back(); + if (lines.empty()) lines.emplace_back(); + return lines; +} + +int col16(const std::vector& lines, int line, int byte) { + if (line < 0 || line >= static_cast(lines.size())) return byte; + return text::utf16_column(lines[line], static_cast(byte)); +} + +Json range_json(const std::vector& lines, const toml::Range& r) { + return Json{{"startLine", r.startLine}, {"startCharacter", col16(lines, r.startLine, r.startCol)}, + {"endLine", r.endLine}, {"endCharacter", col16(lines, r.endLine, r.endCol)}}; +} + +Json segments_json(const std::vector& lines, const std::vector& segments) { + Json out = Json::array(); + for (const auto& s : segments) out.push_back(Json{{"name", s.name}, {"quoted", s.quoted}, {"range", range_json(lines, s.range)}}); + return out; +} + +Json key_value_json(const std::vector& lines, const toml::KeyValue& kv); + +Json value_json(const std::vector& lines, const toml::Value& v) { + Json j{{"kind", toml::name_of(v.kind)}, {"range", range_json(lines, v.range)}, {"open", v.open}}; + if (v.text) j["text"] = *v.text; + if (v.kind == toml::ValueKind::string) { + j["quote"] = std::string(1, v.quote); + j["multiline"] = v.multiline; + if (v.contentRange) j["contentRange"] = range_json(lines, *v.contentRange); + } + if (v.kind == toml::ValueKind::array) { + j["elements"] = Json::array(); + for (const auto& e : v.elements) j["elements"].push_back(value_json(lines, e)); + } + if (v.kind == toml::ValueKind::inlineTable) { + j["entries"] = Json::array(); + for (const auto& e : v.entries) j["entries"].push_back(key_value_json(lines, e)); + } + return j; +} + +Json key_value_json(const std::vector& lines, const toml::KeyValue& kv) { + Json j{{"type", "keyValue"}, {"keyPath", segments_json(lines, kv.keyPath)}, {"range", range_json(lines, kv.range)}}; + if (kv.value) j["value"] = value_json(lines, *kv.value); + return j; +} + +Json document_json(const std::vector& lines) { + Json nodes = Json::array(); + for (const auto& node : toml::parse(lines).nodes) { + if (const auto* s = std::get_if(&node)) + nodes.push_back(Json{{"type", "section"}, {"segments", segments_json(lines, s->segments)}, {"isArray", s->isArray}, + {"open", s->open}, {"range", range_json(lines, s->range)}, {"line", s->line}}); + else + nodes.push_back(key_value_json(lines, std::get(node))); + } + return Json{{"nodes", nodes}}; +} + +Json context_json(const std::vector& lines, int line, int character) { + const auto c = toml::context_at(lines, line, character); + const int cl = std::clamp(line, 0, static_cast(lines.size()) - 1); + const Json replace{{"startCharacter", col16(lines, cl, c.replace.start)}, {"endCharacter", col16(lines, cl, c.replace.end)}}; + auto section = [&] { + switch (c.section.kind) { + case toml::Resolution::Kind::top: return Json{{"kind", "top"}}; + case toml::Resolution::Kind::unknown: return Json{{"kind", "unknown"}, {"segments", c.section.segments}}; + case toml::Resolution::Kind::known: return Json{{"kind", "known"}, {"group", c.section.group}}; + } + return Json(); + }; + switch (c.kind) { + case toml::Context::Kind::header: + return Json{{"kind", "section-header"}, {"segments", c.segments}, {"isArray", c.isArray}, {"replaceRange", replace}}; + case toml::Context::Kind::key: + return Json{{"kind", "key"}, {"section", section()}, {"containerPath", c.containerPath}, {"keyPrefix", c.keyPrefix}, + {"replaceRange", replace}}; + case toml::Context::Kind::value: { + Json j{{"kind", "value"}, {"section", section()}, {"keyPath", c.keyPath}, {"insideString", c.insideString}, + {"stringOpen", c.stringOpen}, {"replaceRange", replace}}; + if (c.valueKind) j["valueKind"] = toml::name_of(*c.valueKind); + return j; + } + } + return Json(); +} + +void check_scanner(const std::vector& given, const Json& golden, const std::string& name) { + const auto lines = stripped(given); + expect(document_json(given), golden["document"], name + " document"); + const int n = static_cast(given.size()); + for (int l = 0; l <= n; ++l) { + const int len = l < n ? text::utf16_length(lines[std::min(l, static_cast(lines.size()) - 1)]) : 0; + for (int c = 0; c <= len + 2; ++c) { + const auto at = std::format("{}:{}", l, c); + expect(context_json(lines, l, c), golden["contexts"][golden["context"][at].get()], std::format("{} {} context", name, at)); + } + } +} + Json toml_diagnostics(const std::vector& ds) { Json out = Json::array(); for (const auto& d : ds) { @@ -42,6 +157,7 @@ Json toml_diagnostics(const std::vector& ds) { void check_manifest(const std::filesystem::path& file, const Json& golden) { const auto name = file.filename().string(); const auto lines = text::lines_of(testsupport::read(file)); + check_scanner(lines, golden, name); manifest::DiagnosticSettings quiet; quiet.syntax = "info"; quiet.unknownSection = "off"; @@ -170,6 +286,10 @@ int main() { else check_buildscript(f, golden); } check_catalogue(Json::parse(testsupport::read(root / "golden" / "catalogue.json"))); + // The documents mcpp-vscode 0.9's parser tests read: their structure and every cursor's context. + int k = 0; + for (const auto& snippet : Json::parse(testsupport::read(root / "golden" / "snippets.json"))) + check_scanner(snippet["lines"].get>(), snippet, std::format("snippet {}", k++)); // Past the goldens: the catalogue's versions, newest first, a package's own only. const Json envelope{{"data", {{"packages", Json::array({ diff --git a/tests/test_testing.cpp b/tests/test_testing.cpp index 2211c89..96cdd80 100644 --- a/tests/test_testing.cpp +++ b/tests/test_testing.cpp @@ -164,15 +164,15 @@ TEST(Math, CHECK(!answers.empty() && answers.back().contains("result")); return answers.back()["result"]; }; - (void)call("initialize", Json{{"client", {{"name", "test"}}}, {"editor", "vscode"}, {"trusted", false}, - {"settings", {{"mcpp.path", "/no/such/mcpp"}}}, - {"workspaceFolders", Json::array({base.generic_string()})}}); - auto program = call("source/entries", Json{{"file", (base / "src" / "main.cpp").generic_string()}}); + (void)call("initialize", Json{{"clientInfo", {{"name", "test"}}}, {"workspaceFolders", Json::array({Json{{"uri", testsupport::file_uri(base)}}})}, + {"initializationOptions", Json{{"editor", "vscode"}, {"trusted", false}, + {"settings", {{"mcpp.path", "/no/such/mcpp"}}}}}}); + auto program = call("mcpp-ide/source/entries", Json{{"file", (base / "src" / "main.cpp").generic_string()}}); CHECK(program["kind"] == "program" && program["main"]["line"] == 2 && program["main"]["approximate"] == true); - CHECK(call("source/entries", Json{{"file", (base / "src" / "lib.cpp").generic_string()}})["kind"] == "none"); - auto test = call("source/entries", Json{{"file", (base / "tests" / "unit" / "math.cpp").generic_string()}}); + CHECK(call("mcpp-ide/source/entries", Json{{"file", (base / "src" / "lib.cpp").generic_string()}})["kind"] == "none"); + auto test = call("mcpp-ide/source/entries", Json{{"file", (base / "tests" / "unit" / "math.cpp").generic_string()}}); CHECK(test["kind"] == "test" && test["test"] == "unit/math" && !test.contains("framework")); - auto cases = call("test/cases", Json{{"root", base.generic_string()}, {"test", "unit/math"}}); + auto cases = call("mcpp-ide/test/cases", Json{{"root", base.generic_string()}, {"test", "unit/math"}}); CHECK(cases["framework"].is_null() && cases["cases"].empty()); std::error_code ignored; std::filesystem::remove_all(base, ignored); From 59d65d02b9e67b477e1d9c7b20db967ebe857022 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 23:21:06 +0900 Subject: [PATCH 098/119] CI uses the released mcpp 2026.10.31.1 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index d7e42f7..379b561 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,14 +1,14 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. -# The released mcpp CI installs and uses: the control contract the fixtures were captured from -# (protocol/fixtures/version.txt) and the platforms (openkal-platform 0.18.0) the backend's ok images -# are built on. -MCPP_VERSION=2026.10.27.1 +# The released mcpp CI installs and uses: it states its features (`features`, the project's mcpp of +# part 2 §4.3) and builds the backend's ok images on openkal-platform 0.18.0. The mock replays the +# control contract the fixtures were captured from (protocol/fixtures/version.txt). +MCPP_VERSION=2026.10.31.1 # A ref of mcpp-community/mcpp CI builds mcpp from and uses instead, for a contract not yet released. # Empty: the release above is used. -MCPP_SOURCE_REF=release/2026.10.30.1 +MCPP_SOURCE_REF= # The LLVM the backend builds with on macOS and Windows (the targets mcpp reaches there with it). LLVM_VERSION=23.1.3 -# The xlings mcpp 2026.10.27.1 bundles, which reads the current index. +# The xlings mcpp 2026.10.31.1 bundles, which reads the current index. XLINGS_VERSION=v2026.10.26.1 # The oldest VS Code the extension supports (engines.vscode), which its tests run in. VSCODE_VERSION=1.91.0 From 87fc148439417772a3333fe376bab491f8b02f04 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 23:48:19 +0900 Subject: [PATCH 099/119] =?UTF-8?q?checkpoint=206:=20CLion=20in=20its=20ow?= =?UTF-8?q?n=20components=20(mcpp=20design=202026-10-30=20part=202=20?= =?UTF-8?q?=C2=A79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool window's Project, Libraries and Cache tabs are native trees and lists over the backend's view models; a graph is the one page in JCEF (webui/graph.html, mermaid over graph/packages and graph/modules, tested in Chromium over a real backend); Settings | Tools | mcpp is a form generated from settings/registry with Apply, Reset and a search. The workbench, its vscode shim and its bridge are removed. The views' models and the settings form are tested headless. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 10 + README.md | 17 +- README.zh-CN.md | 14 +- docs/architecture.md | 46 +- mide-plugins/clion/README.md | 19 +- mide-plugins/clion/build.gradle.kts | 19 +- .../mcppclion/actions/Dispatch.kt | 4 +- .../mcppclion/settings/McppConfigurable.kt | 203 +++++++- .../mcppcommunity/mcppclion/ui/CacheView.kt | 114 ++++ .../mcppcommunity/mcppclion/ui/GraphPage.kt | 107 ++++ .../mcppcommunity/mcppclion/ui/LibraryView.kt | 188 +++++++ .../mcppclion/ui/McppToolWindow.kt | 109 ++++ .../mcppcommunity/mcppclion/ui/ProjectView.kt | 201 ++++++++ .../mcppclion/webui/McppPanels.kt | 54 -- .../mcppclion/webui/Workbench.kt | 485 ------------------ .../src/main/resources/META-INF/plugin.xml | 4 +- .../github/mcppcommunity/mcppclion/UiTest.kt | 98 ++++ mide-plugins/vscode/package.json | 2 +- mide-plugins/vscode/tools/bundle-webui.mjs | 39 +- spec/i18n/runtime.zh-cn.json | 30 +- webui/graph.html | 21 + webui/index.html | 12 - webui/src/bridge.ts | 106 ---- webui/src/graph.ts | 100 ++++ webui/src/main.ts | 85 --- webui/src/node/buffer.ts | 2 - webui/src/node/child_process.ts | 9 - webui/src/node/crypto.ts | 31 -- webui/src/node/fs.ts | 118 ----- webui/src/node/os.ts | 9 - webui/src/node/path.ts | 110 ---- webui/src/node/process.ts | 10 - webui/src/node/url.ts | 15 - webui/src/node/util.ts | 7 - webui/src/vscode.ts | 422 --------------- webui/src/workbench.ts | 424 --------------- webui/tests/graph.test.mjs | 106 ++++ webui/tests/workbench.test.mjs | 199 ------- webui/tsconfig.json | 27 +- webui/workbench.css | 38 -- 41 files changed, 1346 insertions(+), 2270 deletions(-) create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/CacheView.kt create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/GraphPage.kt create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/LibraryView.kt create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/McppToolWindow.kt create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/ProjectView.kt delete mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/McppPanels.kt delete mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt create mode 100644 mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/UiTest.kt create mode 100644 webui/graph.html delete mode 100644 webui/index.html delete mode 100644 webui/src/bridge.ts create mode 100644 webui/src/graph.ts delete mode 100644 webui/src/main.ts delete mode 100644 webui/src/node/buffer.ts delete mode 100644 webui/src/node/child_process.ts delete mode 100644 webui/src/node/crypto.ts delete mode 100644 webui/src/node/fs.ts delete mode 100644 webui/src/node/os.ts delete mode 100644 webui/src/node/path.ts delete mode 100644 webui/src/node/process.ts delete mode 100644 webui/src/node/url.ts delete mode 100644 webui/src/node/util.ts delete mode 100644 webui/src/vscode.ts delete mode 100644 webui/src/workbench.ts create mode 100644 webui/tests/graph.test.mjs delete mode 100644 webui/tests/workbench.test.mjs delete mode 100644 webui/workbench.css diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6282c83..9d40006 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -431,7 +431,7 @@ jobs: echo "MCPP_IDE_OKB=$GITHUB_WORKSPACE/backend/mcpp-ide.okb" echo "MCPP_IDE_OKRUN=$GITHUB_WORKSPACE/backend/okrun" } >> "$GITHUB_ENV" - - name: The workbench in Chromium, over the backend + - name: The graph page in Chromium, over the backend working-directory: mide-plugins/vscode run: | npx playwright install --with-deps chromium diff --git a/CHANGELOG.md b/CHANGELOG.md index a855892..2659cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,16 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 buffers. While C++ Modules serves build.mcpp (`mcpplsServes`) it is left the unknownSymbol rule and the module names after `import`. One snapshot of each: `spec/toml-schema.json`, `spec/buildscript-api.json` (mcpp 2026.10.29.1's directives). +- **CLion in its own components (§9).** The "mcpp" tool window's Project, Libraries and Cache tabs + are native trees and lists over the backend's view models (each project's mcpp, targets, tests, + libraries, last build, artifacts and platform, and what C++ Modules says; the indexes' packages, a + package's page and adding it to the project; what mcpp keeps and the command table's cleanups with + their confirmations). A graph of the packages or the modules is the one page in JCEF + (`webui/graph.html`, mermaid over `graph/packages` and `graph/modules`; a module opens its source), + tested in Chromium over a real backend. Settings | Tools | mcpp is a form generated from + `settings/registry`, with Apply, Reset and a search. The workbench that ran mcpp-vscode's pages in + JCEF, its `vscode` shim and its bridge are removed; the views' models and the settings form are + tested headless. - **The declared degraded mode (§8, F15).** Where the package's okrun does not run (macOS x86_64, Windows arm64, Linux armhf) VS Code says the backend does not run on this platform and goes on with build, run, test and the project pages, running mcpp's queries itself; an Extension Host run diff --git a/README.md b/README.md index 41f141f..277385d 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,12 @@ different tool. mcpp-ide makes them once. Its backend, `mcpp-ide serve`, is a C++ program that reads only mcpp's machine contract (event streams, result records, launch descriptors, JSON queries) and answers the -editors over JSON-RPC. The VS Code extension's views and pages are mcpp-vscode 0.9.0's; the CLion -plugin shows the same pages in a workbench of its own (JCEF), and the Neovim plugin maps the answers -onto buffers, floating windows and the quickfix list. The editor plugins are `mide-plugins/` of this repository. Language features of -C++ files come from [mcppls](https://github.com/Sunrisepeak/mcpp-language-server); mcpp-ide -registers none. +editors as an LSP server (JSON-RPC, protocol 2). The VS Code extension's views and pages are +mcpp-vscode 0.9.0's; the CLion plugin shows the same information in CLion's own trees, lists and +settings form (a graph in JCEF), and the Neovim plugin maps the answers onto buffers, floating +windows and the quickfix list. The editor plugins are `mide-plugins/` of this repository. Language +features of C++ files come from [mcppls](https://github.com/Sunrisepeak/mcpp-language-server); +those of mcpp.toml and build.mcpp come from the backend ([docs/rules.md](docs/rules.md)). ## Features @@ -29,8 +30,8 @@ registers none. | Test tree, re-run one test | Testing view | IDE test runner | `:Mcpp test ` | | Debug | CodeLLDB, C/C++ (gdb, lldb), lldb-dap | CLion's GDB or LLDB | nvim-dap (codelldb, lldb-dap, cppdbg, gdb) | | Diagnostics | Problems, from mcpp's located diagnostics | Console links to the reported location | `vim.diagnostic`, `:Mcpp problems` in the quickfix list | -| mcpp.toml | Problems from mcpp; completion, hover, outline from mcpp's key registry | Problems from mcpp | Problems from mcpp | -| Libraries, a package's page, graphs, cache, settings | mcpp-vscode's views and pages | The same pages, in a JCEF workbench | Text in floating windows, `vim.ui.select`; settings in `setup()` | +| mcpp.toml, build.mcpp | Completion, hover, jumps and the rules' diagnostics from the backend (language client); Problems from mcpp | The same, through CLion's completion, documentation, annotations and go-to | The same, through `vim.lsp`; mcpp's problems in `vim.diagnostic` | +| Libraries, a package's page, graphs, cache, settings | mcpp-vscode's views and pages | Native trees and lists, a settings form from the registry, the graphs in JCEF | Text in floating windows, `vim.ui.select`; settings in `setup()` | | Targets (target sets among them), pack | Quick picks | Popups | `vim.ui.select` | | Language features | mcppls (installed with the extension) | mcppls CLion plugin (a dependency) | mcppls-nvim (`xlings install mcppls-nvim`) | @@ -71,7 +72,7 @@ Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it mcpp build # the backend, the mock mcpp, mcpp-ide-dev mcpp test # the backend's tests cd mide-plugins/vscode && npm ci && npm test # the extension's tests, the parity gates against 0.9.0 among them -npm run test:webui # the workbench in Chromium, over the backend +npm run test:webui # CLion's graph page in Chromium, over the backend npm run package # dist/mcpp-vscode-.vsix cd ../clion && gradle buildPlugin verifyPlugin # build/distributions/mcpp-clion-.zip cd ../.. && nvim --headless --clean -l mide-plugins/nvim/tests/smoke.lua # the Neovim plugin's tests diff --git a/README.zh-CN.md b/README.zh-CN.md index 94d0e43..a918837 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,9 +12,11 @@ 每个编辑器各写一遍,这些决定就会分歧;从一个编辑器换到另一个,用户遇到的就是另一个工具。 mcpp-ide 只做一次。它的后端 `mcpp-ide serve` 是一个 C++ 程序,只读取 mcpp 的机器契约(事件流、结果记录、启动描述、 -JSON 查询),通过 JSON-RPC 回答编辑器。VS Code 扩展的视图与页面即 mcpp-vscode 0.9.0 的视图与页面;CLion 插件在自己的 -工作台(JCEF)中显示同一套页面;Neovim 插件把这些回答映射到缓冲区、浮动窗口与 quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。 -C++ 文件的语言功能来自 [mcppls](https://github.com/Sunrisepeak/mcpp-language-server);mcpp-ide 不注册任何语言服务。 +JSON 查询),以 LSP 服务器(JSON-RPC,协议 2)回答编辑器。VS Code 扩展的视图与页面即 mcpp-vscode 0.9.0 的视图与页面; +CLion 插件以 CLion 自己的树、列表与设置表单显示同样的信息(图在 JCEF 中);Neovim 插件把这些回答映射到缓冲区、浮动窗口与 +quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文件的语言功能来自 +[mcppls](https://github.com/Sunrisepeak/mcpp-language-server);mcpp.toml 与 build.mcpp 的语言功能来自后端 +([docs/rules.md](docs/rules.md))。 ## 功能 @@ -24,8 +26,8 @@ C++ 文件的语言功能来自 [mcppls](https://github.com/Sunrisepeak/mcpp-lan | 测试树、单个测试重跑 | 测试视图 | IDE 测试运行器 | `:Mcpp test ` | | 调试 | CodeLLDB、C/C++(gdb、lldb)、lldb-dap | CLion 的 GDB 或 LLDB | nvim-dap(codelldb、lldb-dap、cppdbg、gdb) | | 诊断 | 问题面板,来自 mcpp 带位置的诊断 | 控制台链接到报告的位置 | `vim.diagnostic`,`:Mcpp problems` 填入 quickfix 列表 | -| mcpp.toml | 问题来自 mcpp;补全、悬停、大纲来自 mcpp 的键注册表 | 问题来自 mcpp | 问题来自 mcpp | -| 库、包页面、依赖图、缓存、设置 | mcpp-vscode 的视图与页面 | 同一套页面,位于 JCEF 工作台 | 浮动窗口中的文本、`vim.ui.select`;设置在 `setup()` 中 | +| mcpp.toml、build.mcpp | 补全、悬停、跳转与规则诊断来自后端(语言客户端);问题来自 mcpp | 同上,经 CLion 的补全、文档、标注与跳转 | 同上,经 `vim.lsp`;mcpp 的问题在 `vim.diagnostic` | +| 库、包页面、依赖图、缓存、设置 | mcpp-vscode 的视图与页面 | 原生的树与列表、由设置注册表生成的设置表单、JCEF 中的图 | 浮动窗口中的文本、`vim.ui.select`;设置在 `setup()` 中 | | 目标(含目标集合)、打包 | 快速选择 | 弹出列表 | `vim.ui.select` | | 语言功能 | mcppls(随扩展一并安装) | mcppls CLion 插件(依赖) | mcppls-nvim(`xlings install mcppls-nvim`) | @@ -64,7 +66,7 @@ x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 mcpp build # 后端、mock mcpp、mcpp-ide-dev mcpp test # 后端测试 cd mide-plugins/vscode && npm ci && npm test # 扩展的测试,含对 0.9.0 的一致性门 -npm run test:webui # 工作台在 Chromium 中、经后端运行 +npm run test:webui # CLion 的图页面在 Chromium 中、经后端运行 npm run package # dist/mcpp-vscode-.vsix cd ../clion && gradle buildPlugin verifyPlugin # build/distributions/mcpp-clion-.zip cd ../.. && nvim --headless --clean -l mide-plugins/nvim/tests/smoke.lua # Neovim 插件的测试 diff --git a/docs/architecture.md b/docs/architecture.md index fbedd30..d74ae17 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,40 +10,30 @@ ## The pages -The views and pages a user sees -- the project tree, the libraries and a package's page, the cache, -the settings page, the dependency and module graphs -- are mcpp-vscode's -(`mide-plugins/vscode/src`), as mcpp-vscode 0.9.0 drew them; `registerPages` registers them. They -read projects and the index from disk and run mcpp through the backend's `mcpp/exec` and mcppls -through its `languageService/exec`; the extension starts no other process for either. - -The workbench (`webui/`) runs the same modules in a browser host. `webui/src` answers what they ask -of VS Code (the `vscode` module: settings, commands, dialogs, views, webviews) and of Node (`fs`, -`path`, `os`, `crypto`) through a bridge to the IDE; `tools/bundle-webui.mjs` bundles them into one -script. Views are tabs in the manifest's order and names; a webview is an iframe with the page's -own document, its CSP and nonce, and the IDE's colours as the `--vscode-*` variables. The bridge -carries requests through `mcppHostSend`/`mcppHostReceive`, and a synchronous file read as a -`prompt` the host answers. mcpp-clion hosts the workbench in JCEF (`webui/Workbench.kt`). - -| Bridge action | What it stands for in VS Code | What mcpp-clion answers (Workbench.kt) | -|---|---|---| -| `rpc` | the backend's method | the backend's method | -| `settings.read`, `settings.write` | the configuration | the plugin's settings store | -| `message`, `input`, `pick`, `saveDialog`, `progress.*` | notifications, input box, quick pick, save dialog, progress | notifications or dialogs, input dialog, list popup, file saver, background task | -| `openFile`, `openText`, `clipboard`, `openExternal` | editors, the clipboard, the browser | editors, the clipboard, the browser | -| `command` | the command | the command table's action | -| `fs.*`, `findFiles` | the file system | the file system | - -The parity gates hold the pages to 0.9.0: the manifest's contributions and strings contain 0.9.0's -exactly; every page's document, rendered by the extension's generators, is byte-identical to the -snapshot 0.9.0's generators render (`test/parity`); a Chromium test runs the workbench over a real -backend (`webui/tests`). +The views and pages of VS Code -- the project tree, the libraries and a package's page, the cache, +the settings page, the dependency and module graphs -- are mcpp-vscode's (`mide-plugins/vscode/src`), +as mcpp-vscode 0.9.0 drew them; `registerPages` registers them. They read projects and the index +from disk and run mcpp through the backend's `mcpp-ide/mcpp/exec` and mcppls through its +`mcpp-ide/languageService/exec`; the extension starts no other process for either. The parity gates +hold them to 0.9.0: the manifest's contributions and strings contain 0.9.0's exactly, and every +page's document, rendered by the extension's generators, is byte-identical to the snapshot 0.9.0's +generators render (`test/parity`). + +CLion has the same information in its own components (design 2026-10-30 part 2 §9): the "mcpp" +tool window's Project, Libraries and Cache tabs are native trees and lists over the backend's view +models (`projects/list`, `project/summary`, `project/mcpp`, `library/search`, `cache/inventory`), +a package's page is HTML in the Libraries tab, the cleanups are the command table's with their +confirmations, and Settings | Tools | mcpp is a form generated from `settings/registry`. A graph is +the one page in JCEF: `webui/graph.html` draws the backend's `graph/packages` or `graph/modules` +with mermaid and asks the IDE to open a module's source; a Chromium test runs it over a real +backend (`webui/tests`). The strings are the backend's (`i18n/bundle`), the ones VS Code shows. ## The adapters | Directory | Plugin | `editor` | Declares | Holds | |---|---|---|---|---| | `mide-plugins/vscode` | mcpp-vscode | `vscode` | package.json's contributes, from `mcpp-ide-dev gen vscode` | the pages, tasks, the Testing view, debug configurations, Problems | -| `mide-plugins/clion` | mcpp-clion | `jetbrains` (the IntelliJ platform) | actions and message bundles, from `mcpp-ide-dev gen jetbrains` | run configurations, the test tree, CLion's debugger, the workbench in JCEF | +| `mide-plugins/clion` | mcpp-clion | `jetbrains` (the IntelliJ platform) | actions and message bundles, from `mcpp-ide-dev gen jetbrains` | run configurations, the test tree, CLion's debugger, the tool window's native views, the settings form, the graph page in JCEF | | `mide-plugins/nvim` | mcpp-nvim | `nvim` | nothing: `:Mcpp` reads the command table from the backend | jobs in a buffer, a terminal for the program, `vim.diagnostic` and the quickfix list, the view models as text, nvim-dap | `spec/commands.json` says which editors offer a command (`editors`, every editor when absent; a diff --git a/mide-plugins/clion/README.md b/mide-plugins/clion/README.md index 57cdf88..f44542b 100644 --- a/mide-plugins/clion/README.md +++ b/mide-plugins/clion/README.md @@ -8,9 +8,12 @@ mcpp's project features in CLion 2026.1 and later, over the mcpp-ide backend: - **debugging** under CLion's own GDB or LLDB (the default toolchain's); an ok image runs under `okrun`, whose images reach GDB through the JIT interface; - **console links** to `file:line:column`; -- the **mcpp tool window**: mcpp-vscode's views and pages -- the project tree, the libraries and a - package's page, the cache, the dependency and module graphs -- in a workbench (JCEF) whose tabs - are the views; the settings page is Settings | Tools | mcpp; +- the **mcpp tool window** in CLion's own components: the Project tab (each project's mcpp, + targets, tests, libraries, last build, artifacts, platform, and what C++ Modules says), the + Libraries tab (the indexes' packages, a package's page, adding it to the project), the Cache tab + (what mcpp keeps, and the cleanups with their confirmations), and a graph of the packages or the + modules as a page in JCEF; **Settings | Tools | mcpp** is a form generated from the backend's + settings registry, with a search; - **Select the Build Target** (target sets among the rows) and **Pack** as popups, packing as a job; - the **Tools | mcpp** menu, generated from `spec/commands.json`. @@ -23,7 +26,7 @@ come from the backend over the session's connection (mcpp-ide's `docs/rules.md`) ```bash xlings install gradle # Gradle and its JDK mcpp build # in the repository root: the backend and mcpp-ide-dev -(cd ../vscode && npm ci) # the extension's dependencies: the build bundles the workbench with them +(cd ../vscode && npm ci) # the extension's dependencies: the build bundles the graph page with them gradle buildPlugin # build/distributions/mcpp-clion-.zip gradle verifyPlugin # CLion 2026.1 and the version in gradle.properties ``` @@ -35,7 +38,7 @@ docs/architecture.md, Distribution. Without them the plugin finds the backend th ## Tests -`gradle test` runs the wire's tests, and a session over the mock mcpp when `MCPP_IDE_BACKEND` and -`MCPP_IDE_MOCK_MCPP` name the programs `mcpp build` produced. `npm run test:webui` in -`../vscode` runs the workbench in Chromium over the backend, answering its bridge as Workbench.kt -does. +`gradle test` runs the wire's tests, a session over the mock mcpp when `MCPP_IDE_BACKEND` and +`MCPP_IDE_MOCK_MCPP` name the programs `mcpp build` produced, and the views' models and the +settings form headless. `npm run test:webui` in `../vscode` runs the graph page in Chromium over the +backend. diff --git a/mide-plugins/clion/build.gradle.kts b/mide-plugins/clion/build.gradle.kts index 8f50af4..cb4046c 100644 --- a/mide-plugins/clion/build.gradle.kts +++ b/mide-plugins/clion/build.gradle.kts @@ -1,8 +1,8 @@ // mcpp-clion: mcpp's project features in CLion -- run configurations, the test tree, debugging, the -// console's links, and mcpp-vscode's pages in a workbench (../../webui) -- over the mcpp-ide backend (`mcpp-ide serve`). +// console's links, native views of the project, the libraries and the cache, the graphs in JCEF (../../webui), +// and mcpp.toml's and build.mcpp's rules -- over the mcpp-ide backend (`mcpp-ide serve`). // -// Language features are mcppls's: this plugin depends on the mcppls CLion plugin and registers no -// language service. The declarations generated from ../../spec (actions, message bundles) are written +// C++ language features are mcppls's: this plugin depends on the mcppls CLion plugin. The declarations generated from ../../spec (actions, message bundles) are written // by the backend's `mcpp-ide-dev gen jetbrains` before the resources are processed; nothing // generated is in the repository. // @@ -89,21 +89,20 @@ val generateDeclarations = tasks.register("generateDeclarations") { if (dev != null) commandLine(dev, "gen", "jetbrains", "--out", out.absolutePath) else commandLine("mcpp", "run", "mcpp-ide-dev", "--", "gen", "jetbrains", "--out", out.absolutePath) } -// The workbench JCEF loads: mcpp-vscode's pages over ../../webui, bundled by the extension's -// tools/bundle-webui.mjs (Node; `npm ci` in ../vscode first), with webui/files.txt naming its files. +// The graph page JCEF loads (../../webui, mcpp design 2026-10-30 part 2 §9): webui/src/graph.ts and +// mermaid, bundled by the extension's tools/bundle-webui.mjs (Node; `npm ci` in ../vscode first), +// with webui/files.txt naming its files. val webuiOut = layout.buildDirectory.dir("generated/webui/webui") val bundleWebui = tasks.register("bundleWebui") { val extension = repositoryRoot.resolve("mide-plugins/vscode") inputs.dir(repositoryRoot.resolve("webui/src")) - inputs.files(repositoryRoot.resolve("webui/index.html"), repositoryRoot.resolve("webui/workbench.css")) - inputs.dir(extension.resolve("src")) - inputs.dir(extension.resolve("media")) - inputs.file(extension.resolve("package.json")) + inputs.file(repositoryRoot.resolve("webui/graph.html")) + inputs.file(extension.resolve("tools/bundle-webui.mjs")) outputs.dir(webuiOut) workingDir = extension doFirst { if (!extension.resolve("node_modules/esbuild").isDirectory) - throw GradleException("the workbench is bundled with the extension's dependencies: run `npm ci` in ${extension}") + throw GradleException("the graph page is bundled with the extension's dependencies: run `npm ci` in ${extension}") } commandLine("node", "tools/bundle-webui.mjs", "--out", webuiOut.get().asFile.absolutePath) } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt index 5b760aa..fb3cc83 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/actions/Dispatch.kt @@ -32,7 +32,7 @@ import io.github.mcppcommunity.mcppclion.McppSession import io.github.mcppcommunity.mcppclion.run.McppRunConfiguration import io.github.mcppcommunity.mcppclion.run.McppRunConfigurationType import io.github.mcppcommunity.mcppclion.settings.McppSettings -import io.github.mcppcommunity.mcppclion.webui.McppPanels +import io.github.mcppcommunity.mcppclion.ui.McppPanels import java.io.File import java.nio.file.Paths @@ -156,7 +156,7 @@ object Dispatch { } /** A job whose narration is not a console's: its end is a notification. */ - private fun job(project: Project, method: String, params: JsonObject) { + fun job(project: Project, method: String, params: JsonObject) { val session = McppSession.get(project) session.request(method, params).thenAccept { r -> val id = r.asJsonObject["jobId"]?.asString ?: return@thenAccept diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/settings/McppConfigurable.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/settings/McppConfigurable.kt index 84dbef2..72f1a84 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/settings/McppConfigurable.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/settings/McppConfigurable.kt @@ -1,30 +1,203 @@ -// Settings | Tools | mcpp: mcpp-vscode's settings page, in the workbench (Workbench.kt). +// Settings | Tools | mcpp, in CLion's own form (mcpp design 2026-10-30 part 2 §9): one field per +// setting of the backend's `settings/registry` -- its title, its description and the values it may +// hold, in the session's language -- under the registry's groups, the advanced ones last. Apply +// stores what changed (a value equal to the default is removed) and the session tells the backend; +// Reset shows what is stored. The search field narrows the form by key, title and description. package io.github.mcppcommunity.mcppclion.settings -import com.intellij.openapi.Disposable -import com.intellij.openapi.options.Configurable +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.options.SearchableConfigurable import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import io.github.mcppcommunity.mcppclion.webui.Workbench +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.SearchTextField +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import io.github.mcppcommunity.mcppclion.McppSession +import java.awt.BorderLayout +import javax.swing.DefaultComboBoxModel import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.event.DocumentEvent -class McppConfigurable(private val project: Project) : Configurable { - private var disposable: Disposable? = null +/** One setting's field: what it shows, how it reads and writes a JSON value. */ +class SettingField(val entry: JsonObject) { + val key: String = entry["key"].asString + val type: String = entry["type"]?.asString ?: "string" + val default: JsonElement = entry["default"] ?: JsonNull.INSTANCE + private val enumValues: List = entry["enum"]?.takeIf { it.isJsonArray }?.asJsonArray?.toList() ?: emptyList() + val title: String = entry["title"]?.asString ?: key + val description: String = entry["description"]?.asString ?: "" + val component: JComponent = when { + type == "boolean" -> JBCheckBox(title) + enumValues.isNotEmpty() -> ComboBox(DefaultComboBoxModel(enumValues.map { label(it) }.toTypedArray())) + type == "array" -> JBTextArea(2, 40).apply { lineWrap = false } + else -> JBTextField(30) + } + val label: JBLabel? = if (type == "boolean") null else JBLabel("$title:") + val comment: JBLabel = JBLabel("${escape(description)}
      $key").apply { + foreground = UIUtil.getContextHelpForeground() + font = JBUI.Fonts.smallFont() + } + + private fun label(v: JsonElement) = if (v.isJsonPrimitive && v.asJsonPrimitive.isString) v.asString else v.toString() + + /** Shows a stored value, else the default. */ + fun show(value: JsonElement?) { + val v = value?.takeIf { !it.isJsonNull } ?: default + when (val c = component) { + is JBCheckBox -> c.isSelected = v.isJsonPrimitive && v.asBoolean + is ComboBox<*> -> c.selectedItem = label(v) + is JBTextArea -> c.text = if (v.isJsonArray) v.asJsonArray.joinToString("\n") { label(it) } else "" + is JBTextField -> c.text = if (v.isJsonNull) "" else label(v) + } + } + + /** The value the field holds, as the registry types it; null when it is not one. */ + fun value(): JsonElement? = when (val c = component) { + is JBCheckBox -> JsonPrimitive(c.isSelected) + is ComboBox<*> -> enumValues.firstOrNull { label(it) == c.selectedItem } ?: default + is JBTextArea -> JsonArray().apply { c.text.lines().map { it.trim() }.filter { it.isNotEmpty() }.forEach { add(it) } } + is JBTextField -> if (type == "number") c.text.trim().toDoubleOrNull()?.let { d -> + if (d == Math.floor(d) && !d.isInfinite()) JsonPrimitive(d.toLong()) else JsonPrimitive(d) + } else JsonPrimitive(c.text) + else -> null + } + + fun matches(filter: String): Boolean = filter.isEmpty() || + listOf(key, title, description).any { it.contains(filter, ignoreCase = true) } + + fun setVisible(visible: Boolean) { + component.isVisible = visible + label?.isVisible = visible + comment.isVisible = visible + } + + private fun escape(s: String) = s.replace("&", "&").replace("<", "<").replace(">", ">") +} + +/** The form from a registry. Pure but for Swing: the tests build it from the backend's answer. */ +class SettingsForm(registry: JsonObject, t: (String) -> String, private val stored: (String) -> JsonElement?) { + val fields: List + val component: JComponent + private val search = SearchTextField(false) + private val separators = mutableListOf>>() + + init { + val entries = registry.getAsJsonArray("settings")?.map { it.asJsonObject }?.filter { it["deprecated"]?.asBoolean != true } ?: emptyList() + val groups = registry.getAsJsonArray("groups")?.map { it.asJsonObject }?.sortedBy { it["order"]?.asInt ?: 0 } ?: emptyList() + val builder = FormBuilder.createFormBuilder() + val all = mutableListOf() + // The public settings by group, then the advanced ones by group. + for (advanced in listOf(false, true)) { + for (g in groups) { + val id = g["id"].asString + val inGroup = entries.filter { it["group"]?.asString == id && (it["tier"]?.asString == "advanced") == advanced } + if (inGroup.isEmpty()) continue + val title = g["title"]?.asString ?: id + val separator = TitledSeparator(if (advanced) "$title · ${t("Advanced")}" else title) + builder.addComponent(separator) + val fields = inGroup.map { SettingField(it) } + for (f in fields) { + if (f.label != null) builder.addLabeledComponent(f.label, f.component) else builder.addComponent(f.component) + builder.addComponentToRightColumn(f.comment) + } + separators += separator to fields + all += fields + } + } + fields = all + val form = builder.addComponentFillVertically(JPanel(), 0).panel + search.addDocumentListener(object : com.intellij.ui.DocumentAdapter() { + override fun textChanged(e: DocumentEvent) = filter(search.text.trim()) + }) + component = JPanel(BorderLayout(0, JBUI.scale(6))).apply { + add(search, BorderLayout.NORTH) + add(JBScrollPane(form).apply { border = JBUI.Borders.empty() }, BorderLayout.CENTER) + } + reset() + } + + fun filter(text: String) { + for ((separator, fields) in separators) { + var any = false + for (f in fields) { + val shown = f.matches(text) + f.setVisible(shown) + any = any || shown + } + separator.isVisible = any + } + } + + /** The stored value a field stands for: stored, else its default. */ + private fun current(f: SettingField): JsonElement = stored(f.key)?.takeIf { !it.isJsonNull } ?: f.default + + fun isModified(): Boolean = fields.any { f -> f.value()?.let { it != current(f) } ?: true } + + /** What changed, each value to store (null: back to the default). */ + fun changes(): Map = fields.mapNotNull { f -> + val v = f.value() ?: return@mapNotNull null + if (v == current(f)) null else f.key to (if (v == f.default) null else v) + }.toMap() + + fun reset() = fields.forEach { it.show(stored(it.key)) } +} + +class McppConfigurable(private val project: Project) : SearchableConfigurable { + private var form: SettingsForm? = null + private val holder = JPanel(BorderLayout()) + + override fun getId(): String = "io.github.mcppcommunity.mcppclion.settings" override fun getDisplayName(): String = "mcpp" override fun createComponent(): JComponent { - val d = Disposer.newDisposable("mcpp settings") - disposable = d - return Workbench(project, "settings", d).component + val session = McppSession.get(project) + holder.removeAll() + holder.add(JBLabel(session.t("Reading the settings…")), BorderLayout.NORTH) + session.start().thenCompose { session.request("settings/registry") }.whenComplete { registry, error -> + ApplicationManager.getApplication().invokeLater { + holder.removeAll() + if (error != null || registry == null || !registry.isJsonObject) { + holder.add(JBLabel(session.t("The settings come from the mcpp-ide backend, which is not running: {0}", (error?.cause ?: error)?.message ?: "")), BorderLayout.NORTH) + } else { + val f = SettingsForm(registry.asJsonObject, { key -> session.t(key) }) { key -> McppSettings.get().get(key) } + form = f + holder.add(f.component, BorderLayout.CENTER) + } + holder.revalidate() + holder.repaint() + } + } + return holder } - // Every change is stored as it is made (the page writes through the workbench). - override fun isModified(): Boolean = false - override fun apply() {} + override fun isModified(): Boolean = form?.isModified() ?: false + + override fun apply() { + val f = form ?: return + val settings = McppSettings.get() + for ((key, value) in f.changes()) settings.set(key, value) + } + + override fun reset() { + form?.reset() + } override fun disposeUIResources() { - disposable?.let { Disposer.dispose(it) } - disposable = null + form = null + holder.removeAll() } } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/CacheView.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/CacheView.kt new file mode 100644 index 0000000..72cdd48 --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/CacheView.kt @@ -0,0 +1,114 @@ +// The cache: what mcpp keeps (`cache/inventory` over `mcpp cache list`) -- the total, each kind and +// its size, the largest labels, the ages -- and the cleanups of the command table, each planned, +// confirmed at its danger level and applied as a job (Dispatch.cleanup). +package io.github.mcppcommunity.mcppclion.ui + +import com.google.gson.JsonObject +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.SimpleToolWindowPanel +import com.intellij.openapi.util.Disposer +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.treeStructure.Tree +import com.intellij.util.ui.tree.TreeUtil +import io.github.mcppcommunity.mcppclion.McppSession +import io.github.mcppcommunity.mcppclion.actions.Dispatch +import javax.swing.JComponent +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel + +/** The cache's rows, from the inventory. Pure: the tests build it. */ +object CacheModel { + fun rows(inventory: JsonObject, t: (String, Array) -> String): List { + fun tr(key: String, vararg a: Any?) = t(key, a) + val out = mutableListOf() + out += RowNode(Row(tr("{0} in {1} entries", inventory["totalText"]?.asString ?: "", inventory["totalEntries"]?.asString ?: "0"), + inventory["root"]?.asString ?: "", AllIcons.Nodes.Folder)) + val kinds = inventory["byKind"].objects() + if (kinds.isNotEmpty()) + out += RowNode(Row(tr("By kind"), icon = AllIcons.Nodes.Folder), kinds.map { k -> + RowNode(Row(k["kind"].asString, tr("{0} · {1} entries", k["bytesText"]?.asString ?: "", k["entries"]?.asString ?: "0"), AllIcons.Nodes.DataTables)) + }) + val top = inventory["topLabels"].objects() + if (top.isNotEmpty()) + out += RowNode(Row(tr("Largest"), icon = AllIcons.Nodes.Folder), top.map { l -> + RowNode(Row(l["label"].asString.ifEmpty { "—" }, tr("{0} · {1} entries", l["bytesText"]?.asString ?: "", l["entries"]?.asString ?: "0"), AllIcons.Nodes.Package)) + }) + val ages = inventory["ageBuckets"].objects().filter { (it["entries"]?.asLong ?: 0) > 0 } + if (ages.isNotEmpty()) + out += RowNode(Row(tr("By last use"), icon = AllIcons.Nodes.Folder), ages.map { b -> + val from = b["fromDays"]?.asInt ?: 0 + val span = b["toDays"]?.asInt?.let { tr("{0}–{1} days", from, it) } ?: tr("{0} days or more", from) + RowNode(Row(span, tr("{0} · {1} entries", b["bytesText"]?.asString ?: "", b["entries"]?.asString ?: "0"), AllIcons.Vcs.History)) + }) + val incomplete = inventory["incomplete"].objects() + if (incomplete.isNotEmpty()) + out += RowNode(Row(tr("{0} incomplete entries", incomplete.size), icon = AllIcons.General.Warning, error = true)) + return out + } +} + +class CacheView(private val project: Project, parent: Disposable) : McppView, Disposable { + override val id = "cache" + private val session = McppSession.get(project) + private val tree = Tree(DefaultTreeModel(DefaultMutableTreeNode())) + private val panel = SimpleToolWindowPanel(true, true) + private val cleanups = DefaultActionGroup() + val component: JComponent get() = panel + private var unsubscribe: () -> Unit = {} + + init { + Disposer.register(parent, this) + tree.isRootVisible = false + tree.showsRootHandles = true + tree.cellRenderer = RowRenderer() + val group = DefaultActionGroup().apply { + add(object : DumbAwareAction(session.t("Refresh"), null, AllIcons.Actions.Refresh) { + override fun actionPerformed(e: AnActionEvent) = refresh() + }) + addSeparator() + add(cleanups) + } + val toolbar = ActionManager.getInstance().createActionToolbar("McppCacheView", group, true) + toolbar.targetComponent = panel + panel.toolbar = toolbar.component + panel.setContent(ScrollPaneFactory.createScrollPane(tree)) + unsubscribe = session.on("job/finished") { p -> if (p.asJsonObject["kind"]?.asString in setOf("cache", "clean", "command")) refresh() } + } + + /** The cleanups the command table offers here, under their titles. */ + private fun fillCleanups() { + cleanups.removeAll() + for (c in session.commands) { + val action = c["action"]?.asJsonObject ?: continue + if (action["kind"]?.asString != "cache") continue + val plan = action["plan"]?.asString ?: continue + cleanups.add(object : DumbAwareAction(c["title"]?.asString ?: plan, null, AllIcons.Actions.GC) { + override fun actionPerformed(e: AnActionEvent) = Dispatch.cleanup(project, plan) + }) + } + } + + override fun refresh() { + if (!session.running) return + session.request("cache/inventory", JsonObject().apply { session.root?.let { addProperty("root", it) } }).whenComplete { r, error -> + ApplicationManager.getApplication().invokeLater { + if (project.isDisposed) return@invokeLater + fillCleanups() + val root = DefaultMutableTreeNode() + if (error != null) root.add(RowNode(Row(session.t("mcpp: {0}", (error.cause ?: error).message ?: ""), icon = AllIcons.General.Error, error = true))) + else CacheModel.rows(r.asJsonObject) { key, args -> session.t(key, *args) }.forEach { root.add(it) } + tree.model = DefaultTreeModel(root) + TreeUtil.expand(tree, 1) + } + } + } + + override fun dispose() = unsubscribe() +} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/GraphPage.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/GraphPage.kt new file mode 100644 index 0000000..1d82a18 --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/GraphPage.kt @@ -0,0 +1,107 @@ +// A graph of the project -- its packages or its modules -- as the page webui/graph.html draws it in +// JCEF (mcpp design 2026-10-30 part 2 §9). The page receives the backend's `graph/packages` or +// `graph/modules` and the strings it shows, and asks the IDE to open a module's source; it reaches +// nothing else. Without JCEF the tab says so. +package io.github.mcppcommunity.mcppclion.ui + +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBLabel +import com.intellij.ui.jcef.JBCefApp +import com.intellij.ui.jcef.JBCefBrowser +import com.intellij.ui.jcef.JBCefBrowserBase +import com.intellij.ui.jcef.JBCefJSQuery +import io.github.mcppcommunity.mcppclion.McppSession +import org.cef.browser.CefBrowser +import org.cef.browser.CefFrame +import org.cef.handler.CefLoadHandlerAdapter +import java.io.File +import javax.swing.JComponent +import javax.swing.SwingConstants + +class GraphPage(private val project: Project, private val kind: String) : McppView, Disposable { + override val id = "graph:$kind" + private val session = McppSession.get(project) + private val browser: JBCefBrowser? = if (JBCefApp.isSupported()) JBCefBrowser() else null + private val query: JBCefJSQuery? = browser?.let { JBCefJSQuery.create(it as JBCefBrowserBase) } + @Volatile private var loaded = false + private var pending: String? = null + val component: JComponent = browser?.component ?: JBLabel(session.t("This IDE runs without JCEF, which draws the graph."), SwingConstants.CENTER) + + init { + if (browser != null && query != null) { + Disposer.register(this, browser) + Disposer.register(this, query) + query.addHandler { text -> open(text); null } + browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { + override fun onLoadEnd(b: CefBrowser, frame: CefFrame, status: Int) { + if (!frame.isMain) return + b.executeJavaScript("window.mcppHostSend = function (text) { ${query.inject("text")} };", b.url, 0) + ApplicationManager.getApplication().invokeLater { + loaded = true + pending?.let { render(it) } + pending = null + } + } + }, browser.cefBrowser) + browser.loadURL(page().toURI().toString()) + } + } + + /** The page's files, written once per build of the plugin where the browser can load them. */ + private fun page(): File { + val jar = runCatching { File(GraphPage::class.java.protectionDomain.codeSource.location.toURI()) }.getOrNull() + val version = jar?.takeIf { it.isFile }?.let { "${it.nameWithoutExtension}-${it.lastModified()}" } ?: "dev" + val dir = File(PathManager.getSystemPath(), "mcpp-clion/webui-$version") + val index = javaClass.getResource("/webui/files.txt")?.readText() ?: error("the graph page is not in the plugin (webui/files.txt)") + for (f in index.lines().filter { it.isNotBlank() }) { + val target = File(dir, f) + if (target.isFile && version != "dev") continue + target.parentFile.mkdirs() + javaClass.getResourceAsStream("/webui/$f")?.use { input -> target.outputStream().use { input.copyTo(it) } } + } + return File(dir, "graph.html") + } + + override fun refresh() { + val root = session.root ?: return + session.request("graph/$kind", JsonObject().apply { addProperty("root", root) }).whenComplete { graph, error -> + val page = JsonObject().apply { + addProperty("kind", kind) + add("graph", graph?.takeIf { it.isJsonObject } ?: JsonObject().apply { add("nodes", com.google.gson.JsonArray()); add("edges", com.google.gson.JsonArray()) }) + addProperty("title", if (kind == "modules") session.t("Module Graph") else session.t("Dependency Graph")) + addProperty("empty", error?.let { session.t("mcpp: {0}", (it.cause ?: it).message ?: "") } ?: session.t("Nothing to draw yet: build the project first.")) + addProperty("truncated", session.t("Only the first {0} nodes are drawn.")) + addProperty("dark", !JBColor.isBright()) + } + ApplicationManager.getApplication().invokeLater { + if (loaded) render(page.toString()) else pending = page.toString() + } + } + } + + private fun render(page: String) { + val b = browser ?: return + b.cefBrowser.executeJavaScript("window.mcppRender && window.mcppRender($page);", b.cefBrowser.url, 0) + } + + /** `{"open": ""}`: the module's source in an editor. */ + private fun open(text: String) { + val source = runCatching { JsonParser.parseString(text).asJsonObject["open"]?.asString }.getOrNull() ?: return + val root = session.root ?: return + val path = File(source).let { if (it.isAbsolute) it else File(root, source) }.path + ApplicationManager.getApplication().invokeLater { + LocalFileSystem.getInstance().refreshAndFindFileByPath(path)?.let { OpenFileDescriptor(project, it).navigate(true) } + } + } + + override fun dispose() {} +} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/LibraryView.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/LibraryView.kt new file mode 100644 index 0000000..a0c103b --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/LibraryView.kt @@ -0,0 +1,188 @@ +// The libraries: the packages of mcpp's indexes (`library/search`, a keyword and an index), and the +// page of the one selected -- its description, versions, licences and repository -- with the action +// that adds it to the project (`library/add`, `mcpp add` as a job). +package io.github.mcppcommunity.mcppclion.ui + +import com.google.gson.JsonObject +import com.intellij.icons.AllIcons +import com.intellij.ide.BrowserUtil +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep +import com.intellij.openapi.util.Disposer +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.JBSplitter +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.SearchTextField +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.components.JBList +import com.intellij.util.Alarm +import com.intellij.util.ui.HTMLEditorKitBuilder +import com.intellij.util.ui.JBUI +import io.github.mcppcommunity.mcppclion.McppSession +import java.awt.BorderLayout +import java.awt.FlowLayout +import javax.swing.DefaultComboBoxModel +import javax.swing.DefaultListModel +import javax.swing.JButton +import javax.swing.JComponent +import javax.swing.JEditorPane +import javax.swing.JList +import javax.swing.JPanel +import javax.swing.event.DocumentEvent +import javax.swing.event.HyperlinkEvent + +/** A package's page as HTML. Pure: the tests read it. */ +object LibraryPage { + private fun esc(s: String) = s.replace("&", "&").replace("<", "<").replace(">", ">") + + fun html(p: JsonObject, t: (String) -> String): String { + val versions = p.getAsJsonArray("versions")?.map { it.asString } ?: emptyList() + val licenses = p.getAsJsonArray("licenses")?.joinToString(", ") { it.asString } ?: "" + val repo = p["repo"]?.takeIf { it.isJsonPrimitive }?.asString ?: "" + return buildString { + append("") + append("

      ${esc(p["id"]?.asString ?: "")}

      ") + p["description"]?.takeIf { it.isJsonPrimitive }?.asString?.trim()?.takeIf { it.isNotEmpty() }?.let { append("

      ${esc(it)}

      ") } + append("
      ") + fun row(k: String, v: String) { if (v.isNotEmpty()) append("") } + row("Index", esc(p["index"]?.asString ?: "")) + row("In mcpp.toml", "${esc(p["dependencyName"]?.asString ?: "")}") + row("Latest", esc(p["latest"]?.takeIf { it.isJsonPrimitive }?.asString ?: "")) + row("Versions", esc(versions.joinToString(", "))) + row("License", esc(licenses)) + if (repo.isNotEmpty()) row("Repository", "${esc(repo)}") + append("
      ${esc(t(k))}$v
      ") + } + } +} + +class LibraryView(private val project: Project, parent: Disposable) : McppView, Disposable { + override val id = "libraries" + private val session = McppSession.get(project) + private val search = SearchTextField(true) + private val index = ComboBox() + private val packages = DefaultListModel() + private val list = JBList(packages) + private val page = JEditorPane().apply { + isEditable = false + editorKit = HTMLEditorKitBuilder.simple() + addHyperlinkListener { e -> if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) e.url?.let { BrowserUtil.browse(it) } } + } + private val add = JButton(session.t("Add to the project"), AllIcons.General.Add) + private val status = com.intellij.ui.components.JBLabel() + private val alarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, this) + private var wanted: String? = null + private val panel = JPanel(BorderLayout()) + val component: JComponent get() = panel + + init { + Disposer.register(parent, this) + index.model = DefaultComboBoxModel(arrayOf("")) + index.renderer = com.intellij.ui.SimpleListCellRenderer.create("") { it.ifEmpty { session.t("All indexes") } } + list.cellRenderer = object : ColoredListCellRenderer() { + override fun customizeCellRenderer(list: JList, value: JsonObject?, index: Int, selected: Boolean, hasFocus: Boolean) { + value ?: return + icon = AllIcons.Nodes.PpLib + append(value["id"]?.asString ?: "") + value["latest"]?.takeIf { it.isJsonPrimitive }?.let { append(" ${it.asString}", SimpleTextAttributes.GRAYED_ATTRIBUTES) } + value["description"]?.takeIf { it.isJsonPrimitive }?.asString?.trim()?.takeIf { it.isNotEmpty() }?.let { + append(" — " + it.take(100), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES) + } + } + } + list.addListSelectionListener { if (!it.valueIsAdjusting) show(list.selectedValue) } + search.addDocumentListener(object : com.intellij.ui.DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + alarm.cancelAllRequests() + alarm.addRequest({ refresh() }, 300) + } + }) + index.addActionListener { refresh() } + add.isEnabled = false + add.addActionListener { list.selectedValue?.let { addToProject(it) } } + val top = JPanel(BorderLayout(JBUI.scale(4), 0)).apply { + border = JBUI.Borders.empty(4) + add(search, BorderLayout.CENTER) + add(index, BorderLayout.EAST) + } + val detail = JPanel(BorderLayout()).apply { + add(ScrollPaneFactory.createScrollPane(page), BorderLayout.CENTER) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { add(add) }, BorderLayout.SOUTH) + } + val splitter = JBSplitter(true, 0.55f).apply { + firstComponent = ScrollPaneFactory.createScrollPane(list) + secondComponent = detail + } + panel.add(top, BorderLayout.NORTH) + panel.add(splitter, BorderLayout.CENTER) + panel.add(status.apply { border = JBUI.Borders.empty(2, 6) }, BorderLayout.SOUTH) + } + + override fun focus(params: JsonObject) { + params["id"]?.asString?.let { id -> + wanted = id + search.text = id.substringAfter(':') + } + refresh() + } + + override fun refresh() { + if (!session.running) return + val params = JsonObject().apply { + addProperty("keyword", search.text.trim()) + (index.selectedItem as? String)?.takeIf { it.isNotEmpty() }?.let { addProperty("index", it) } + addProperty("limit", 500) + session.root?.let { addProperty("root", it) } + } + status.text = session.t("Searching…") + session.request("library/search", params).whenComplete { r, error -> + ApplicationManager.getApplication().invokeLater { + if (project.isDisposed) return@invokeLater + if (error != null) { status.text = session.t("mcpp: {0}", (error.cause ?: error).message ?: ""); return@invokeLater } + val o = r.asJsonObject + val selected = wanted ?: list.selectedValue?.get("id")?.asString + packages.clear() + o.getAsJsonArray("packages").forEach { packages.addElement(it.asJsonObject) } + val known = (index.model as DefaultComboBoxModel) + if (known.size <= 1) o.getAsJsonArray("indexes")?.forEach { known.addElement(it.asString) } + status.text = session.t("{0} packages", o["total"]?.asString ?: packages.size().toString()) + val at = (0 until packages.size()).firstOrNull { packages[it]["id"]?.asString == selected } + if (at != null) { list.selectedIndex = at; list.ensureIndexIsVisible(at) } else show(null) + wanted = null + } + } + } + + private fun show(p: JsonObject?) { + add.isEnabled = p != null + page.text = p?.let { LibraryPage.html(it) { key -> session.t(key) } } ?: "" + page.caretPosition = 0 + } + + /** `mcpp add [@version]`: the version chosen among the package's, the latest first. */ + private fun addToProject(p: JsonObject) { + val root = session.root ?: return + val name = p["dependencyName"]?.asString ?: return + val versions = listOf("") + (p.getAsJsonArray("versions")?.map { it.asString } ?: emptyList()) + val step = object : BaseListPopupStep(session.t("Add {0}", name), versions) { + override fun getTextFor(value: String): String = value.ifEmpty { session.t("The latest version") } + override fun onChosen(value: String, finalChoice: Boolean): PopupStep<*>? { + // A job: its end is said, and the project view reads the manifest again. + io.github.mcppcommunity.mcppclion.actions.Dispatch.job(project, "library/add", JsonObject().apply { + addProperty("root", root) + addProperty("dependency", name) + if (value.isNotEmpty()) addProperty("version", value) + }) + return FINAL_CHOICE + } + } + JBPopupFactory.getInstance().createListPopup(step).showUnderneathOf(add) + } + + override fun dispose() {} +} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/McppToolWindow.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/McppToolWindow.kt new file mode 100644 index 0000000..afc1cad --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/McppToolWindow.kt @@ -0,0 +1,109 @@ +// The "mcpp" tool window in CLion's own components (mcpp design 2026-10-30 part 2 §9): the project, +// the libraries and the cache as native trees and lists over the backend's view models, and a graph +// (a project's packages or modules) as a page of its own in JCEF. VS Code's information structure, +// CLion's components; the strings are the backend's (`i18n/bundle`), the ones VS Code shows. +package io.github.mcppcommunity.mcppclion.ui + +import com.google.gson.JsonObject +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.ui.content.Content +import com.intellij.ui.content.ContentFactory +import io.github.mcppcommunity.mcppclion.McppSession +import io.github.mcppcommunity.mcppclion.actions.Dispatch +import io.github.mcppcommunity.mcppclion.settings.McppConfigurable +import java.io.File + +/** A view of the tool window: what it is called, and how it is asked to show something. */ +interface McppView { + val id: String + fun refresh() + /** Shows what `params` names (a package of the libraries); the view itself when nothing. */ + fun focus(params: JsonObject) {} +} + +object McppPanels { + const val TOOL_WINDOW = "mcpp" + private val VIEW = Key.create("mcpp.view") + + /** The command table's `panel` actions: a view brought on screen, a graph opened, settings shown. */ + fun show(project: Project, panel: String, params: JsonObject = JsonObject()) { + when (panel) { + "targets" -> { Dispatch.selectTarget(project); return } + "pack" -> { Dispatch.pack(project); return } + "settings" -> { ShowSettingsUtil.getInstance().showSettingsDialog(project, McppConfigurable::class.java); return } + } + val window = ToolWindowManager.getInstance(project).getToolWindow(TOOL_WINDOW) ?: return + window.activate { + when (panel) { + "graph" -> openGraph(project, window, params["graph"]?.asString ?: "packages") + "project" -> select(window, "project", params) + "cache" -> select(window, "cache", params) + "libraries", "library" -> select(window, "libraries", params) + } + } + } + + private fun select(window: ToolWindow, id: String, params: JsonObject) { + val content = window.contentManager.contents.firstOrNull { it.getUserData(VIEW)?.id == id } ?: return + window.contentManager.setSelectedContent(content, true) + content.getUserData(VIEW)?.focus(params) + } + + /** A graph is a tab of its own, closeable; one per kind, refreshed when it is asked again. */ + private fun openGraph(project: Project, window: ToolWindow, kind: String) { + val id = "graph:$kind" + window.contentManager.contents.firstOrNull { it.getUserData(VIEW)?.id == id }?.let { + window.contentManager.setSelectedContent(it, true) + it.getUserData(VIEW)?.refresh() + return + } + val session = McppSession.get(project) + val page = GraphPage(project, kind) + val title = if (kind == "modules") session.t("Module Graph") else session.t("Dependency Graph") + val content = ContentFactory.getInstance().createContent(page.component, title, false) + content.isCloseable = true + content.putUserData(VIEW, page) + Disposer.register(content, page) + window.contentManager.addContent(content) + window.contentManager.setSelectedContent(content, true) + page.refresh() + } + + class Factory : ToolWindowFactory, DumbAware { + override suspend fun isApplicableAsync(project: Project): Boolean = project.basePath?.let { File(it, "mcpp.toml").isFile } ?: false + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val session = McppSession.get(project) + fun add(view: McppView, component: javax.swing.JComponent, title: String): Content { + val content = ContentFactory.getInstance().createContent(component, title, false) + content.isCloseable = false + content.putUserData(VIEW, view) + toolWindow.contentManager.addContent(content) + return content + } + val projectView = ProjectView(project, toolWindow.disposable) + val libraryView = LibraryView(project, toolWindow.disposable) + val cacheView = CacheView(project, toolWindow.disposable) + add(projectView, projectView.component, session.t("Project")) + add(libraryView, libraryView.component, session.t("Libraries")) + add(cacheView, cacheView.component, session.t("Cache")) + // The strings and the data arrive with the backend. + session.whenStarted { + com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater { + if (project.isDisposed) return@invokeLater + val titles = mapOf("project" to "Project", "libraries" to "Libraries", "cache" to "Cache") + for (c in toolWindow.contentManager.contents) c.getUserData(VIEW)?.id?.let { id -> titles[id]?.let { c.displayName = session.t(it) } } + projectView.refresh() + } + } + projectView.refresh() + } + } +} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/ProjectView.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/ProjectView.kt new file mode 100644 index 0000000..f4f3fa0 --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/ProjectView.kt @@ -0,0 +1,201 @@ +// The project view: each mcpp project of the window, its mcpp, its targets, tests and libraries, its +// last build and what it produced, and what C++ Modules says about it -- a tree over the backend's +// `projects/list`, `project/summary`, `project/mcpp` and `languageService/status`. +package io.github.mcppcommunity.mcppclion.ui + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.SimpleToolWindowPanel +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.ui.ColoredTreeCellRenderer +import com.intellij.ui.DoubleClickListener +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.treeStructure.Tree +import com.intellij.util.ui.tree.TreeUtil +import io.github.mcppcommunity.mcppclion.McppSession +import io.github.mcppcommunity.mcppclion.actions.Dispatch +import java.awt.event.MouseEvent +import java.util.concurrent.CompletableFuture +import javax.swing.Icon +import javax.swing.JComponent +import javax.swing.JTree +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel + +/** A row of a tree: its text, a quieter detail, an icon, and a file it opens or a command it runs. */ +data class Row(val text: String, val detail: String = "", val icon: Icon? = null, val file: String? = null, + val command: String? = null, val root: String? = null, val error: Boolean = false) + +/** A row with its children. */ +class RowNode(row: Row, children: List = emptyList()) : DefaultMutableTreeNode(row) { + init { children.forEach { add(it) } } + val row: Row get() = userObject as Row +} + +/** What the project view shows for one project, from the backend's answers. Pure: the tests build it. */ +object ProjectModel { + fun project(p: JsonObject, summary: JsonObject?, mcpp: JsonObject?, t: (String, Array) -> String): RowNode { + fun tr(key: String, vararg a: Any?) = t(key, a) + val root = p["root"].asString + val children = mutableListOf() + mcpp?.let { m -> + val state = m["state"]?.asString ?: "not-probed" + val text = if (state == "ok") "mcpp ${m["version"]?.asString ?: ""}" else "mcpp: ${m["message"]?.asString ?: state}" + children += RowNode(Row(text, m["program"]?.asString ?: "", AllIcons.Nodes.Plugin, error = state != "ok")) + } + summary?.let { s -> + s["error"]?.takeIf { it.isJsonPrimitive }?.let { children += RowNode(Row(tr("The build database could not be read: {0}", it.asString), icon = AllIcons.General.Warning, error = true)) } + fun group(key: String, title: String, icon: Icon, command: String?) { + val rows = s[key]?.takeIf { it.isJsonArray }?.asJsonArray ?: return + if (rows.isEmpty) return + children += RowNode(Row(title, rows.size().toString(), AllIcons.Nodes.Folder), rows.map { r -> + val o = r.asJsonObject + RowNode(Row(o["name"].asString, o["configuration"]?.asString ?: "", icon, command = command, root = root)) + }) + } + group("targets", tr("Targets"), AllIcons.RunConfigurations.Application, "mcpp.run") + group("tests", tr("Tests"), AllIcons.RunConfigurations.Junit, "mcpp.test") + group("libraries", tr("Libraries"), AllIcons.Nodes.PpLib, null) + s["lastBuild"]?.takeIf { it.isJsonObject }?.asJsonObject?.let { b -> + val status = b["status"]?.asString ?: "" + children += RowNode(Row(tr("Last build: {0}", status), b["command"]?.asString ?: "", + if (status == "ok") AllIcons.RunConfigurations.TestPassed else AllIcons.RunConfigurations.TestFailed, error = status != "ok")) + } + s["artifacts"]?.takeIf { it.isJsonArray }?.asJsonArray?.takeIf { !it.isEmpty }?.let { artifacts -> + children += RowNode(Row(tr("Artifacts"), artifacts.size().toString(), AllIcons.Nodes.Artifact), artifacts.mapNotNull { a -> + val path = when { + a.isJsonPrimitive -> a.asString + a.isJsonObject -> a.asJsonObject["path"]?.asString + else -> null + } ?: return@mapNotNull null + RowNode(Row(path.substringAfterLast('/'), path, AllIcons.FileTypes.Any_type, file = path)) + }) + } + s["platformText"]?.takeIf { it.isJsonPrimitive && it.asString.isNotEmpty() }?.let { + children += RowNode(Row(tr("Platform: {0}", it.asString), icon = AllIcons.Nodes.Module)) + } + } + return RowNode(Row(p["name"]?.asString ?: root.substringAfterLast('/'), root, AllIcons.Nodes.Module, file = "$root/mcpp.toml"), children) + } + + fun languageService(ls: JsonObject?, t: (String, Array) -> String): RowNode? { + ls ?: return null + val ok = ls["state"]?.asString == "ok" + val workspace = ls["workspace"]?.takeIf { it.isJsonObject }?.asJsonObject + val state = workspace?.get("state")?.asString + val text = t("C++ Modules", emptyArray()) + (state?.let { ": $it" } ?: "") + return RowNode(Row(text, ls["message"]?.asString ?: "", AllIcons.Nodes.Plugin, error = !ok)) + } +} + +class ProjectView(private val project: Project, parent: Disposable) : McppView, Disposable { + override val id = "project" + private val session = McppSession.get(project) + private val tree = Tree(DefaultTreeModel(DefaultMutableTreeNode())) + private val panel = SimpleToolWindowPanel(true, true) + val component: JComponent get() = panel + private var unsubscribe: List<() -> Unit> = emptyList() + + init { + Disposer.register(parent, this) + tree.isRootVisible = false + tree.showsRootHandles = true + tree.cellRenderer = RowRenderer() + object : DoubleClickListener() { + override fun onDoubleClick(event: MouseEvent): Boolean { + val row = (tree.lastSelectedPathComponent as? RowNode)?.row ?: return false + open(row) + return true + } + }.installOn(tree) + val group = DefaultActionGroup().apply { + add(action("Refresh", AllIcons.Actions.Refresh) { refresh() }) + add(action("Build", AllIcons.Actions.Compile) { Dispatch.run(project, "mcpp.build", JsonObject()) }) + add(action("Test", AllIcons.RunConfigurations.Junit) { Dispatch.run(project, "mcpp.test", JsonObject()) }) + addSeparator() + add(action("Dependency Graph", AllIcons.Graph.Layout) { McppPanels.show(project, "graph", JsonObject().apply { addProperty("graph", "packages") }) }) + add(action("Module Graph", AllIcons.Nodes.Module) { McppPanels.show(project, "graph", JsonObject().apply { addProperty("graph", "modules") }) }) + } + val toolbar = ActionManager.getInstance().createActionToolbar("McppProjectView", group, true) + toolbar.targetComponent = panel + panel.toolbar = toolbar.component + panel.setContent(ScrollPaneFactory.createScrollPane(tree)) + // What changes the tree: a build ended, a manifest or a result record changed, mcppls answered. + unsubscribe = listOf("job/finished", "result/changed", "manifest/changed", "languageService/changed", "project/mcppChanged") + .map { m -> session.on(m) { refresh() } } + } + + private fun action(text: String, icon: Icon, run: () -> Unit) = object : DumbAwareAction(session.t(text), null, icon) { + override fun actionPerformed(e: AnActionEvent) = run() + } + + private fun open(row: Row) { + row.file?.let { path -> + LocalFileSystem.getInstance().refreshAndFindFileByPath(path)?.let { OpenFileDescriptor(project, it).navigate(true) } + return + } + val command = row.command ?: return + // A target runs as its program (`bin`), a test as the one the filter names. + Dispatch.run(project, command, JsonObject().apply { + row.root?.let { addProperty("root", it) } + addProperty(if (command == "mcpp.run") "bin" else "filter", row.text) + }) + } + + override fun refresh() { + if (!session.running) return + session.request("projects/list").thenCompose { list -> + val projects = list.takeIf { it.isJsonArray }?.asJsonArray?.map { it.asJsonObject } ?: emptyList() + val asked = projects.map { p -> + val root = JsonObject().apply { addProperty("root", p["root"].asString) } + val summary = session.request("project/summary", root).handle { v, _ -> v } + val mcpp = session.request("project/mcpp", root).handle { v, _ -> v } + summary.thenCombine(mcpp) { s, m -> Triple(p, s?.takeIf { it.isJsonObject }?.asJsonObject, m?.takeIf { it.isJsonObject }?.asJsonObject) } + } + CompletableFuture.allOf(*asked.toTypedArray()).thenCombine(session.languageServiceStatus().handle { v, _ -> v }) { _, ls -> + asked.map { it.join() } to ls?.takeIf { it.isJsonObject }?.asJsonObject + } + }.thenAccept { (rows, ls) -> + val t: (String, Array) -> String = { key, args -> session.t(key, *args) } + val root = DefaultMutableTreeNode() + for ((p, s, m) in rows) root.add(ProjectModel.project(p, s, m, t)) + ProjectModel.languageService(ls, t)?.let { root.add(it) } + ApplicationManager.getApplication().invokeLater { + if (project.isDisposed) return@invokeLater + val state = TreeUtil.collectExpandedPaths(tree) + tree.model = DefaultTreeModel(root) + if (state.isEmpty()) TreeUtil.expand(tree, 2) else TreeUtil.restoreExpandedPaths(tree, state) + } + } + } + + override fun dispose() { + unsubscribe.forEach { it() } + } +} + +/** A row's text, then its detail in grey; an error in the error colour. */ +class RowRenderer : ColoredTreeCellRenderer() { + override fun customizeCellRenderer(tree: JTree, value: Any?, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean) { + val r = (value as? RowNode)?.row ?: return + icon = r.icon + append(r.text, if (r.error) SimpleTextAttributes.ERROR_ATTRIBUTES else SimpleTextAttributes.REGULAR_ATTRIBUTES) + if (r.detail.isNotEmpty()) append(" " + r.detail, SimpleTextAttributes.GRAYED_ATTRIBUTES) + } +} + +/** The values of a JSON array as objects. */ +internal fun JsonElement?.objects(): List = + (this as? JsonArray)?.mapNotNull { it.takeIf { e -> e.isJsonObject }?.asJsonObject } ?: emptyList() diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/McppPanels.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/McppPanels.kt deleted file mode 100644 index 91644d8..0000000 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/McppPanels.kt +++ /dev/null @@ -1,54 +0,0 @@ -// The "mcpp" tool window: the workbench (Workbench.kt), whose tabs are mcpp-vscode's views -- the -// project tree, the libraries, the cache -- and the panels they open. A request for a panel (the -// command table's `panel` actions) brings it on screen there; the targets and packing are the IDE's -// own popups, as VS Code's are its quick picks. -package io.github.mcppcommunity.mcppclion.webui - -import com.google.gson.JsonObject -import com.google.gson.JsonPrimitive -import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Key -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowFactory -import com.intellij.openapi.wm.ToolWindowManager -import com.intellij.ui.content.ContentFactory -import io.github.mcppcommunity.mcppclion.actions.Dispatch -import java.io.File - -object McppPanels { - const val TOOL_WINDOW = "mcpp" - private val WORKBENCH = Key.create("mcpp.workbench") - - fun show(project: Project, panel: String, params: JsonObject = JsonObject()) { - when (panel) { - "targets" -> { Dispatch.selectTarget(project); return } - "pack" -> { Dispatch.pack(project); return } - } - val window = ToolWindowManager.getInstance(project).getToolWindow(TOOL_WINDOW) ?: return - window.activate { - val workbench = window.contentManager.contents.firstNotNullOfOrNull { it.getUserData(WORKBENCH) } ?: return@activate - when (panel) { - "project" -> workbench.show("mcpp.project") - "cache" -> workbench.show("mcpp.cache") - "settings" -> workbench.show("settings") - "graph" -> workbench.show("graph:${params["graph"]?.asString ?: "packages"}") - "libraries", "library" -> params["id"]?.asString?.let { workbench.show("library", JsonPrimitive(it)) } ?: workbench.show("mcpp.library") - else -> workbench.show(panel) - } - } - } - - class Factory : ToolWindowFactory, DumbAware { - override suspend fun isApplicableAsync(project: Project): Boolean = project.basePath?.let { File(it, "mcpp.toml").isFile } ?: false - - override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val content = ContentFactory.getInstance().createContent(null, "", false) - val workbench = Workbench(project, "window", toolWindow.disposable) - content.component = workbench.component - content.isCloseable = false - content.putUserData(WORKBENCH, workbench) - toolWindow.contentManager.addContent(content) - } - } -} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt deleted file mode 100644 index 82d8e47..0000000 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/webui/Workbench.kt +++ /dev/null @@ -1,485 +0,0 @@ -// The workbench (../../webui): mcpp-vscode's pages -- the project tree, the libraries and a package's -// page, the cache, the settings, the graphs -- in a JCEF browser, as the same modules VS Code runs. -// What the pages ask of VS Code is answered here, in the IDE's own terms: the backend's methods, the -// settings store, dialogs and popups, editors, the clipboard, the browser, files, mcpp's commands. -// -// Asynchronous requests arrive through `window.mcppHostSend` (a JBCefJSQuery) and are answered -// through `window.mcppHostReceive`; a synchronous one (a page's synchronous file read) is a -// `prompt("mcpp-host:...")` answered by the JS dialog handler, the one synchronous channel a page has. -package io.github.mcppcommunity.mcppclion.webui - -import com.google.gson.Gson -import com.google.gson.JsonArray -import com.google.gson.JsonElement -import com.google.gson.JsonNull -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import com.google.gson.JsonPrimitive -import com.intellij.ide.BrowserUtil -import com.intellij.ide.trustedProjects.TrustedProjects -import com.intellij.notification.NotificationAction -import com.intellij.notification.NotificationGroupManager -import com.intellij.notification.NotificationType -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.PathManager -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.fileChooser.FileChooserFactory -import com.intellij.openapi.fileChooser.FileSaverDescriptor -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.fileEditor.FileDocumentManagerListener -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.FileEditorManagerEvent -import com.intellij.openapi.fileEditor.FileEditorManagerListener -import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.ide.CopyPasteManager -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.ui.popup.PopupStep -import com.intellij.openapi.ui.popup.util.BaseListPopupStep -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.openapi.vfs.newvfs.BulkFileListener -import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent -import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent -import com.intellij.openapi.vfs.newvfs.events.VFileEvent -import com.intellij.testFramework.LightVirtualFile -import com.intellij.ui.JBColor -import com.intellij.ui.jcef.JBCefApp -import com.intellij.ui.jcef.JBCefBrowser -import com.intellij.ui.jcef.JBCefBrowserBase -import com.intellij.ui.jcef.JBCefJSQuery -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import io.github.mcppcommunity.mcppclion.McppSession -import io.github.mcppcommunity.mcppclion.actions.Dispatch -import io.github.mcppcommunity.mcppclion.settings.McppSettings -import org.cef.browser.CefBrowser -import org.cef.browser.CefFrame -import org.cef.callback.CefJSDialogCallback -import org.cef.handler.CefJSDialogHandler -import org.cef.handler.CefJSDialogHandlerAdapter -import org.cef.handler.CefLoadHandlerAdapter -import org.cef.misc.BoolRef -import java.awt.Color -import java.awt.datatransfer.StringSelection -import java.io.File -import java.nio.file.FileSystems -import java.nio.file.Files -import java.nio.file.LinkOption -import java.nio.file.Path -import java.nio.file.attribute.BasicFileAttributes -import java.util.Base64 -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicInteger -import javax.swing.JComponent -import javax.swing.JLabel -import javax.swing.SwingConstants - -/** - * One workbench. [mode] `window` shows the views as tabs (the "mcpp" tool window); `settings` opens - * the settings panel alone (Settings | Tools | mcpp). - */ -class Workbench(private val project: Project, private val mode: String, parent: Disposable) : Disposable { - private val session = McppSession.get(project) - private val browser: JBCefBrowser? = if (JBCefApp.isSupported()) JBCefBrowser() else null - private val query: JBCefJSQuery? = browser?.let { JBCefJSQuery.create(it as JBCefBrowserBase) } - private val unsubscribe = mutableListOf<() -> Unit>() - private var loaded = false - private val watched = Regex("/(mcpp\\.toml|mcpp\\.lock|[^/]+\\.(cpp|cc|cxx|cppm|ixx|mpp|ccm))$") - private val waiting = mutableListOf() - - val component: JComponent = browser?.component - ?: JLabel(session.t("This IDE runs without JCEF; the mcpp panels need it."), SwingConstants.CENTER) - - init { - Disposer.register(parent, this) - unsubscribe += session.on("*") { m -> notify(m.asJsonObject["method"].asString, m.asJsonObject["params"] ?: JsonNull.INSTANCE) } - if (browser != null && query != null) { - query.addHandler { text -> receive(text); null } - browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { - override fun onLoadEnd(b: CefBrowser, frame: CefFrame, status: Int) { - if (!frame.isMain) return - b.executeJavaScript("window.mcppHostSend = function (text) { ${query.inject("text")} }; window.mcppHostAttached && window.mcppHostAttached();", b.url, 0) - ApplicationManager.getApplication().invokeLater { loaded = true; waiting.forEach(::post); waiting.clear() } - } - }, browser.cefBrowser) - browser.jbCefClient.addJSDialogHandler(object : CefJSDialogHandlerAdapter() { - override fun onJSDialog(b: CefBrowser, origin: String, type: CefJSDialogHandler.JSDialogType, message: String, defaultPrompt: String, - callback: CefJSDialogCallback, suppress: BoolRef): Boolean { - if (type != CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_PROMPT || !message.startsWith("mcpp-host:")) return false - callback.Continue(true, sync(message.removePrefix("mcpp-host:")).toString()) - return true - } - }, browser.cefBrowser) - val bus = project.messageBus.connect(this) - bus.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { - override fun selectionChanged(event: FileEditorManagerEvent) { - notify("activeEditor", JsonObject().apply { event.newFile?.takeIf { it.isInLocalFileSystem }?.let { addProperty("path", it.path) } }) - } - }) - bus.subscribe(FileDocumentManagerListener.TOPIC, object : FileDocumentManagerListener { - override fun beforeDocumentSaving(document: com.intellij.openapi.editor.Document) { - FileDocumentManager.getInstance().getFile(document)?.let { f -> notify("documentSaved", JsonObject().apply { addProperty("path", f.path) }) } - } - }) - bus.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener { - override fun after(events: List) { - for (e in events) { - // What the pages watch: the manifests and the module sources (the graphs). - val path = e.path - if (!watched.containsMatchIn(path)) continue - val kind = when (e) { is VFileCreateEvent -> "created"; is VFileDeleteEvent -> "deleted"; else -> "changed" } - notify("fileChanged", JsonObject().apply { addProperty("path", path); addProperty("kind", kind) }) - } - } - }) - ApplicationManager.getApplication().messageBus.connect(this).subscribe(McppSettings.CHANGED, McppSettings.Listener { key -> - notify("settingsChanged", JsonObject().apply { add("keys", JsonArray().apply { add(key) }); add("levels", levels()) }) - }) - browser.loadURL(page().toURI().toString()) - } - } - - /** The workbench's files, written once per build of the plugin where the browser can load them. */ - private fun page(): File { - val jar = runCatching { File(Workbench::class.java.protectionDomain.codeSource.location.toURI()) }.getOrNull() - val version = jar?.takeIf { it.isFile }?.let { "${it.nameWithoutExtension}-${it.lastModified()}" } ?: "dev" - val dir = File(PathManager.getSystemPath(), "mcpp-clion/workbench-$version") - val index = javaClass.getResource("/webui/files.txt")?.readText() ?: error("the workbench is not in the plugin (webui/files.txt)") - for (f in index.lines().filter { it.isNotBlank() }) { - val target = File(dir, f) - if (target.isFile && version != "dev") continue - target.parentFile.mkdirs() - javaClass.getResourceAsStream("/webui/$f")?.use { input -> target.outputStream().use { input.copyTo(it) } } - } - return File(dir, "index.html") - } - - /** Asks the workbench to bring a view or a panel on screen (McppPanels.show). */ - fun show(view: String, args: JsonElement? = null) { - notify("show", JsonObject().apply { addProperty("view", view); args?.let { add("args", it) } }) - } - - private fun notify(method: String, params: JsonElement) { - post(JsonObject().apply { addProperty("type", "notify"); addProperty("method", method); add("params", params) }) - } - - private fun post(message: JsonObject) { - val b = browser ?: return - ApplicationManager.getApplication().invokeLater { - if (!loaded) { waiting += message; return@invokeLater } - b.cefBrowser.executeJavaScript("window.mcppHostReceive && window.mcppHostReceive($message);", b.cefBrowser.url, 0) - } - } - - private fun answer(id: JsonElement, result: CompletableFuture) { - result.whenComplete { value, error -> - post(JsonObject().apply { - add("id", id) - if (error == null) { addProperty("type", "result"); add("result", value ?: JsonNull.INSTANCE) } - else { addProperty("type", "error"); add("error", JsonObject().apply { addProperty("message", (error.cause ?: error).message ?: "failed") }) } - }) - } - } - - private fun receive(text: String) { - val m = runCatching { JsonParser.parseString(text).asJsonObject }.getOrNull() ?: return - if (m["type"]?.asString != "request") return - val action = m["action"]?.asString ?: return - val params = m["params"]?.takeIf { it.isJsonObject }?.asJsonObject ?: JsonObject() - answer(m["id"], handle(action, params)) - } - - // ── synchronous requests: files ────────────────────────────────────────────────────────── - - private fun sync(text: String): JsonObject = runCatching { - val m = JsonParser.parseString(text).asJsonObject - val params = m["params"]?.takeIf { it.isJsonObject }?.asJsonObject ?: JsonObject() - JsonObject().apply { addProperty("ok", true); add("result", files(m["action"].asString, params)) } - }.getOrElse { e -> JsonObject().apply { addProperty("ok", false); addProperty("error", e.message ?: "failed") } } - - private fun stat(path: Path, follow: Boolean): JsonElement { - val options = if (follow) arrayOf() else arrayOf(LinkOption.NOFOLLOW_LINKS) - val a = runCatching { Files.readAttributes(path, BasicFileAttributes::class.java, *options) }.getOrNull() ?: return JsonNull.INSTANCE - return JsonObject().apply { - addProperty("type", when { a.isSymbolicLink -> "symlink"; a.isDirectory -> "directory"; a.isRegularFile -> "file"; else -> "other" }) - addProperty("size", a.size()) - addProperty("mtimeMs", a.lastModifiedTime().toMillis()) - addProperty("ino", (a.fileKey()?.hashCode() ?: path.hashCode()).toLong() and 0xffffffffL) - } - } - - private fun files(action: String, p: JsonObject): JsonElement { - val path = Path.of(p["path"].asString) - return when (action) { - "fs.stat" -> stat(path, p["follow"]?.asBoolean ?: true) - "fs.read" -> if (Files.isRegularFile(path)) JsonPrimitive(Files.readString(path)) else JsonNull.INSTANCE - "fs.list" -> if (!Files.isDirectory(path)) JsonNull.INSTANCE else JsonArray().also { out -> - Files.list(path).use { stream -> - stream.forEach { child -> - val s = stat(child, false) - if (s.isJsonObject) out.add(s.asJsonObject.apply { addProperty("name", child.fileName.toString()) }) - } - } - } - "fs.realpath" -> if (Files.exists(path)) JsonPrimitive(path.toRealPath().toString()) else JsonNull.INSTANCE - "fs.write" -> { - val text = p["text"].asString - Files.createDirectories(path.parent) - if (p["base64"]?.asBoolean == true) Files.write(path, Base64.getDecoder().decode(text)) else Files.writeString(path, text) - LocalFileSystem.getInstance().refreshAndFindFileByPath(path.toString()) - JsonPrimitive(true) - } - else -> error("unknown file action $action") - } - } - - // ── asynchronous requests ──────────────────────────────────────────────────────────────── - - private fun done(value: JsonElement = JsonNull.INSTANCE) = CompletableFuture.completedFuture(value) - - private fun onEdt(block: (CompletableFuture) -> Unit): CompletableFuture { - val f = CompletableFuture() - ApplicationManager.getApplication().invokeLater { runCatching { block(f) }.onFailure { f.completeExceptionally(it) } } - return f - } - - private fun levels(): JsonObject = JsonObject().apply { - add("user", McppSettings.get().values()) - add("workspace", JsonObject()) - } - - private fun handle(action: String, p: JsonObject): CompletableFuture = when (action) { - "init" -> done(JsonObject().apply { - add("info", info()) - add("theme", Theme.variables()) - addProperty("themeKind", if (JBColor.isBright()) "vscode-light" else "vscode-dark") - addProperty("mode", mode) - }) - "rpc" -> session.request(p["method"].asString, p["params"] ?: JsonObject()) - "settings.read" -> done(levels()) - "settings.write" -> { - McppSettings.get().set(p["key"].asString, p["value"]?.takeIf { !it.isJsonNull }) - done(levels()) - } - "message" -> message(p) - "input" -> onEdt { f -> - val value = Messages.showInputDialog(project, p["prompt"]?.asString ?: "", p["title"]?.asString ?: "mcpp", null, p["value"]?.asString ?: "", null) - f.complete(value?.let { JsonPrimitive(it) } ?: JsonNull.INSTANCE) - } - "pick" -> onEdt { f -> pick(p, f) } - "saveDialog" -> onEdt { f -> - val filters = p["filters"]?.takeIf { it.isJsonObject }?.asJsonObject?.entrySet().orEmpty() - val start = p["path"]?.asString?.let { File(it) } - val descriptor = FileSaverDescriptor(p["title"]?.asString ?: "mcpp", "") - // VS Code's filters (label -> extensions) as the chooser's one filter: the first label, every extension. - if (filters.isNotEmpty()) - descriptor.withExtensionFilter(filters.first().key, *filters.flatMap { e -> e.value.asJsonArray.map { it.asString } }.toTypedArray()) - val dialog = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, project) - val chosen = dialog.save(start?.parentFile?.toPath(), start?.name) - f.complete(chosen?.file?.path?.let { JsonPrimitive(it) } ?: JsonNull.INSTANCE) - } - "progress.start" -> progressStart(p["title"]?.asString ?: "mcpp") - "progress.report" -> { progress[p["id"].asInt]?.text2 = p["message"]?.asString; done() } - "progress.end" -> { progressEnd[p["id"].asInt]?.complete(Unit); done() } - "openFile" -> onEdt { f -> - val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(p["path"].asString) - if (file != null) OpenFileDescriptor(project, file, maxOf(0, (p["line"]?.takeIf { !it.isJsonNull }?.asInt ?: 1) - 1), 0).navigate(true) - f.complete(JsonPrimitive(file != null)) - } - "openText" -> onEdt { f -> - val file = LightVirtualFile(p["name"].asString, p["text"].asString).apply { isWritable = false } - FileEditorManager.getInstance(project).openFile(file, true) - f.complete(JsonPrimitive(true)) - } - "clipboard" -> { CopyPasteManager.getInstance().setContents(StringSelection(p["text"].asString)); done() } - "openExternal" -> { BrowserUtil.browse(p["url"].asString); done(JsonPrimitive(true)) } - "log" -> { session.log(p["text"].asString); done() } - "log.show" -> done() - "command" -> onEdt { f -> - val id = p["id"].asString - if (session.commands.any { it["id"].asString == id }) { - val args = p["args"]?.takeIf { it.isJsonArray }?.asJsonArray - val first = args?.firstOrNull() - Dispatch.run(project, id, if (first != null && first.isJsonObject) first.asJsonObject.deepCopy() else JsonObject()) - } - f.complete(JsonNull.INSTANCE) - } - "watch" -> done() - "findFiles" -> CompletableFuture.supplyAsync { findFiles(p) } - "fs.read", "fs.list", "fs.stat", "fs.realpath", "fs.write" -> CompletableFuture.supplyAsync { files(action, p) } - else -> CompletableFuture.failedFuture(IllegalArgumentException("unknown workbench action $action")) - } - - private fun info(): JsonObject = JsonObject().apply { - val os = System.getProperty("os.name").lowercase() - addProperty("platform", when { os.startsWith("windows") -> "win32"; os.startsWith("mac") -> "darwin"; else -> "linux" }) - addProperty("arch", if (System.getProperty("os.arch").lowercase().let { it == "aarch64" || it == "arm64" }) "arm64" else "x64") - addProperty("language", com.intellij.DynamicBundle.getLocale().toLanguageTag().lowercase().let { if (it.startsWith("zh")) "zh-cn" else it }) - addProperty("home", System.getProperty("user.home")) - addProperty("tmp", System.getProperty("java.io.tmpdir")) - add("env", Gson().toJsonTree(System.getenv())) - addProperty("extensionUri", page().parentFile.toURI().toString()) - add("workspaceFolders", JsonArray().apply { project.basePath?.let { add(it) } }) - addProperty("trusted", TrustedProjects.isProjectTrusted(project)) - } - - private fun message(p: JsonObject): CompletableFuture = onEdt { f -> - val items = p["items"]?.asJsonArray?.map { it.asString } ?: emptyList() - val text = listOfNotNull(p["message"]?.asString, p["detail"]?.takeIf { !it.isJsonNull }?.asString).joinToString("\n\n") - val modal = p["modal"]?.asBoolean == true - if (modal || items.isNotEmpty() && p["level"]?.asString == "warning") { - val icon = when (p["level"]?.asString) { "error" -> Messages.getErrorIcon(); "warning" -> Messages.getWarningIcon(); else -> Messages.getInformationIcon() } - val buttons = (items + session.t("Cancel")).toTypedArray() - val chosen = Messages.showDialog(project, text, "mcpp", buttons, items.size, icon) - f.complete(if (chosen in items.indices) JsonPrimitive(chosen) else JsonNull.INSTANCE) - return@onEdt - } - val type = when (p["level"]?.asString) { "error" -> NotificationType.ERROR; "warning" -> NotificationType.WARNING; else -> NotificationType.INFORMATION } - val n = NotificationGroupManager.getInstance().getNotificationGroup("mcpp").createNotification(text, type) - items.forEachIndexed { i, title -> - n.addAction(NotificationAction.createSimpleExpiring(title) { f.complete(JsonPrimitive(i)) }) - } - n.whenExpired { f.complete(JsonNull.INSTANCE) } - n.notify(project) - if (items.isEmpty()) f.complete(JsonNull.INSTANCE) - } - - private fun pick(p: JsonObject, f: CompletableFuture) { - val rows = p["items"].asJsonArray.map { it.asJsonObject } - val labels = rows.map { r -> listOfNotNull(r["label"]?.asString, r["description"]?.takeIf { !it.isJsonNull }?.asString).joinToString(" · ") } - val step = object : BaseListPopupStep(p["title"]?.takeIf { !it.isJsonNull }?.asString ?: p["placeHolder"]?.takeIf { !it.isJsonNull }?.asString, labels) { - override fun onChosen(selectedValue: String, finalChoice: Boolean): PopupStep<*>? { - f.complete(JsonPrimitive(labels.indexOf(selectedValue))) - return FINAL_CHOICE - } - override fun isSpeedSearchEnabled(): Boolean = true - override fun canceled() { f.complete(JsonNull.INSTANCE) } - } - JBPopupFactory.getInstance().createListPopup(step).showCenteredInCurrentWindow(project) - } - - private val progress = ConcurrentHashMap() - private val progressEnd = ConcurrentHashMap>() - private val nextProgress = AtomicInteger(1) - - private fun progressStart(title: String): CompletableFuture { - val id = nextProgress.getAndIncrement() - val end = CompletableFuture() - progressEnd[id] = end - ApplicationManager.getApplication().invokeLater { - ProgressManager.getInstance().run(object : Task.Backgroundable(project, title, false) { - override fun run(indicator: ProgressIndicator) { - progress[id] = indicator - indicator.isIndeterminate = true - runCatching { end.get() } - progress.remove(id) - progressEnd.remove(id) - } - }) - } - return done(JsonPrimitive(id)) - } - - private fun findFiles(p: JsonObject): JsonElement { - val base = Path.of(p["base"]?.takeIf { !it.isJsonNull }?.asString ?: project.basePath ?: return JsonArray()) - val matcher = FileSystems.getDefault().getPathMatcher("glob:${p["pattern"].asString}") - val skip = Regex("(^|/)(node_modules|target|\\.git|\\.mcpp|build)(/|$)") - val max = p["max"]?.takeIf { !it.isJsonNull }?.asInt ?: 10000 - val out = JsonArray() - if (!Files.isDirectory(base)) return out - Files.walk(base).use { stream -> - stream.filter { Files.isRegularFile(it) }.filter { !skip.containsMatchIn(base.relativize(it).toString().replace('\\', '/')) } - .filter { matcher.matches(base.relativize(it)) }.limit(max.toLong()).forEach { out.add(it.toString()) } - } - return out - } - - override fun dispose() { - unsubscribe.forEach { it() } - progressEnd.values.forEach { it.complete(Unit) } - query?.let { Disposer.dispose(it) } - browser?.let { Disposer.dispose(it) } - } -} - -/** - * The IDE's theme as the `--vscode-*` variables the pages' stylesheets read: VS Code's names, the - * IDE's colours and fonts. - */ -object Theme { - private fun css(c: Color) = if (c.alpha == 255) "rgb(${c.red}, ${c.green}, ${c.blue})" else "rgba(${c.red}, ${c.green}, ${c.blue}, ${c.alpha / 255.0})" - private fun named(key: String, light: Int, dark: Int) = JBColor.namedColor(key, JBColor(light, dark)) - private fun pick(light: String, dark: String) = if (JBColor.isBright()) light else dark - - fun variables(): JsonObject = JsonObject().apply { - val fg = UIUtil.getLabelForeground() - val muted = UIUtil.getContextHelpForeground() - val border = JBColor.border() - val scheme = EditorColorsManager.getInstance().globalScheme - val editorBg = scheme.defaultBackground - val panel = UIUtil.getPanelBackground() - val inputBg = UIUtil.getTextFieldBackground() - val inputFg = UIUtil.getTextFieldForeground() - val accent = named("Button.default.startBackground", 0x3574F0, 0x3574F0) - val accentFg = named("Button.default.foreground", 0xFFFFFF, 0xFFFFFF) - val secondary = named("Button.startBackground", 0xEBECF0, 0x3C3F41) - val link = JBUI.CurrentTheme.Link.Foreground.ENABLED - val hover = JBUI.CurrentTheme.List.Hover.background(true) - val selection = UIUtil.getListSelectionBackground(true) - val selectionFg = UIUtil.getListSelectionForeground(true) - val popup = named("PopupMenu.background", 0xFFFFFF, 0x2B2D30) - val warning = named("Component.warningFocusColor", 0xE2A53A, 0xE2A53A) - val error = named("Label.errorForeground", 0xC7222D, 0xF75464) - val font = UIUtil.getLabelFont() - fun v(name: String, value: String) = addProperty("--vscode-$name", value) - v("foreground", css(fg)); v("editorForeground", css(fg)); v("icon-foreground", css(fg)) - v("descriptionForeground", css(muted)); v("editorCodeLens-foreground", css(muted)); v("input-placeholderForeground", css(muted)) - v("editor-background", css(editorBg)); v("sideBar-background", css(panel)); v("editorWidget-background", css(popup)) - v("textCodeBlock-background", css(ColorUtilCompat.blend(editorBg, fg, 0.06))) - for (b in listOf("panel-border", "contrastBorder", "widget-border", "menu-border", "sideBarSectionHeader-border", "input-border", "dropdown-border")) v(b, css(border)) - v("focusBorder", css(JBUI.CurrentTheme.Focus.focusColor())) - v("textLink-foreground", css(link)); v("textLink-activeForeground", css(link)) - v("input-background", css(inputBg)); v("input-foreground", css(inputFg)) - v("dropdown-background", css(inputBg)); v("dropdown-foreground", css(inputFg)) - v("checkbox-background", css(inputBg)); v("checkbox-selectBackground", css(inputBg)) - v("inputOption-activeBackground", css(ColorUtilCompat.blend(inputBg, accent, 0.25))); v("inputOption-activeBorder", css(accent)); v("inputOption-activeForeground", css(fg)) - v("inputValidation-warningBackground", css(ColorUtilCompat.blend(inputBg, warning, 0.2))); v("inputValidation-warningBorder", css(warning)) - v("inputValidation-infoBackground", css(ColorUtilCompat.blend(inputBg, accent, 0.15))); v("inputValidation-infoBorder", css(accent)) - v("button-background", css(accent)); v("button-foreground", css(accentFg)); v("button-hoverBackground", css(ColorUtilCompat.blend(accent, Color.BLACK, 0.1))) - v("button-secondaryBackground", css(secondary)); v("button-secondaryForeground", css(fg)); v("button-secondaryHoverBackground", css(ColorUtilCompat.blend(secondary, fg, 0.08))) - v("button-border", "transparent"); v("button-separator", "transparent") - v("list-hoverBackground", css(hover)); v("list-activeSelectionBackground", css(selection)); v("list-activeSelectionForeground", css(selectionFg)) - v("list-inactiveSelectionBackground", css(UIUtil.getListSelectionBackground(false))) - v("toolbar-hoverBackground", css(JBUI.CurrentTheme.ActionButton.hoverBackground())); v("toolbar-activeBackground", css(JBUI.CurrentTheme.ActionButton.pressedBackground())) - v("badge-background", css(named("Counter.background", 0xDFE1E5, 0x4E5157))); v("badge-foreground", css(named("Counter.foreground", 0x000000, 0xDFE1E5))) - v("menu-background", css(popup)); v("menu-foreground", css(fg)) - v("menu-selectionBackground", css(selection)); v("menu-selectionForeground", css(selectionFg)) - v("sideBarSectionHeader-background", css(panel)); v("sideBarSectionHeader-foreground", css(fg)) - v("widget-shadow", "rgba(0, 0, 0, 0.36)") - v("errorForeground", css(error)); v("editorWarning-foreground", css(warning)) - v("charts-red", pick("#e51400", "#f14c4c")); v("charts-blue", pick("#1a85ff", "#3794ff")); v("charts-yellow", pick("#bf8803", "#cca700")) - v("charts-orange", pick("#d18616", "#d18616")); v("charts-green", pick("#388a34", "#89d185")); v("charts-purple", pick("#652d90", "#b180d7")) - v("testing-iconPassed", pick("#388a34", "#73c991")) - v("symbolIcon-keywordForeground", css(fg)); v("symbolIcon-interfaceForeground", pick("#1a85ff", "#75beff")); v("symbolIcon-operatorForeground", css(fg)) - v("mcpp-cacheOkForeground", pick("#388a34", "#89d185")); v("mcpp-cacheStaleForeground", pick("#bf8803", "#e2c08d")) - v("font-family", "\"${font.family}\", system-ui, sans-serif"); v("font-size", "${font.size}px") - v("editor-font-family", "\"${scheme.editorFontName}\", monospace") - } -} - -/** Colour arithmetic the theme needs (a mix of two colours). */ -private object ColorUtilCompat { - fun blend(a: Color, b: Color, t: Double): Color = Color( - (a.red + (b.red - a.red) * t).toInt().coerceIn(0, 255), - (a.green + (b.green - a.green) * t).toInt().coerceIn(0, 255), - (a.blue + (b.blue - a.blue) * t).toInt().coerceIn(0, 255), - ) -} diff --git a/mide-plugins/clion/src/main/resources/META-INF/plugin.xml b/mide-plugins/clion/src/main/resources/META-INF/plugin.xml index a16a802..8a73fde 100644 --- a/mide-plugins/clion/src/main/resources/META-INF/plugin.xml +++ b/mide-plugins/clion/src/main/resources/META-INF/plugin.xml @@ -21,7 +21,7 @@ com.intellij.modules.clion com.intellij.modules.cidr.debugger io.github.sunrisepeak.mcppls - com.intellij.modules.jcef @@ -37,7 +37,7 @@ + factoryClass="io.github.mcppcommunity.mcppclion.ui.McppPanels$Factory"/> diff --git a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/UiTest.kt b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/UiTest.kt new file mode 100644 index 0000000..9dfd88c --- /dev/null +++ b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/UiTest.kt @@ -0,0 +1,98 @@ +// CLion's own views over the backend's view models (mcpp design 2026-10-30 part 2 §9), headless: the +// project tree and the cache's rows from the answers the backend gives, a package's page, and the +// settings form from a registry -- its fields, its search, Apply's changes and Reset. +package io.github.mcppcommunity.mcppclion + +import com.google.gson.JsonElement +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBTextField +import io.github.mcppcommunity.mcppclion.settings.SettingsForm +import io.github.mcppcommunity.mcppclion.ui.CacheModel +import io.github.mcppcommunity.mcppclion.ui.LibraryPage +import io.github.mcppcommunity.mcppclion.ui.ProjectModel +import io.github.mcppcommunity.mcppclion.ui.RowNode + +class UiTest : BasePlatformTestCase() { + private val t: (String, Array) -> String = { key, args -> + var text = key + args.forEachIndexed { i, a -> text = text.replace("{$i}", a.toString()) } + text + } + + private fun json(text: String) = JsonParser.parseString(text).asJsonObject + + private fun texts(node: RowNode): List = (0 until node.childCount).map { (node.getChildAt(it) as RowNode).row.text } + + fun testTheProjectTreeFromTheBackendsAnswers() { + val project = json("""{"root": "/w/app", "name": "app"}""") + val summary = json("""{"targets": [{"name": "app", "kind": "executable", "configuration": "dev"}], + "tests": [{"name": "test_smoke", "kind": "test"}], "libraries": [], + "lastBuild": {"status": "ok", "command": "mcpp build"}, "artifacts": [{"path": "/w/app/target/bin/app"}], + "platformText": "openkal-platform 0.19.0 (built-in)"}""") + val mcpp = json("""{"state": "ok", "version": "2026.10.31.2", "program": "/x/mcpp"}""") + val node = ProjectModel.project(project, summary, mcpp, t) + assertEquals("app", node.row.text) + assertEquals("/w/app/mcpp.toml", node.row.file) + assertEquals(listOf("mcpp 2026.10.31.2", "Targets", "Tests", "Last build: ok", "Artifacts", "Platform: openkal-platform 0.19.0 (built-in)"), texts(node)) + val targets = node.getChildAt(1) as RowNode + assertEquals("mcpp.run", (targets.getChildAt(0) as RowNode).row.command) + val artifacts = node.getChildAt(4) as RowNode + assertEquals("/w/app/target/bin/app", (artifacts.getChildAt(0) as RowNode).row.file) + // A project whose mcpp is pinned and missing says so, as an error. + val missing = ProjectModel.project(project, null, json("""{"state": "pinned-missing", "message": "mcpp 2026.9.1.1 is pinned"}"""), t) + assertTrue((missing.getChildAt(0) as RowNode).row.error) + } + + fun testTheCacheRowsFromTheInventory() { + val inventory = json("""{"root": "/home/u/.mcpp", "totalBytes": 3, "totalEntries": 2, "totalText": "1.5 GiB", + "byKind": [{"kind": "bmi", "entries": 2, "bytes": 3, "bytesText": "1.5 GiB"}], + "topLabels": [{"label": "std", "entries": 1, "bytes": 2, "bytesText": "1.0 GiB"}], + "ageBuckets": [{"fromDays": 0, "toDays": 7, "entries": 2, "bytes": 3, "bytesText": "1.5 GiB"}, {"fromDays": 7, "entries": 0, "bytes": 0, "bytesText": "0 B"}], + "incomplete": []}""") + val rows = CacheModel.rows(inventory, t) + assertEquals(listOf("1.5 GiB in 2 entries", "By kind", "Largest", "By last use"), rows.map { it.row.text }) + assertEquals("0–7 days", (rows[3].getChildAt(0) as RowNode).row.text) + assertEquals(1, rows[3].childCount) // an empty bucket is not a row + } + + fun testAPackagesPage() { + val html = LibraryPage.html(json("""{"id": "mcpplibs:os", "description": "the OS ", "index": "mcpplibs", + "dependencyName": "mcpplibs.os", "latest": "0.2.4", "versions": ["0.2.4", "0.2.3"], "licenses": ["Apache-2.0"], + "repo": "https://github.com/mcpplibs/os"}""")) { it } + assertTrue(html.contains("mcpplibs:os")) + assertTrue(html.contains("the OS <API>")) + assertTrue(html.contains("mcpplibs.os")) + assertTrue(html.contains("href=\"https://github.com/mcpplibs/os\"")) + } + + fun testTheSettingsFormFromARegistry() { + val registry = json("""{"groups": [{"id": "project", "order": 10, "title": "Project"}, {"id": "build", "order": 20, "title": "Build"}], + "settings": [ + {"key": "mcpp.path", "type": "string", "default": "", "group": "project", "tier": "public", "title": "mcpp path", "description": "the mcpp to run"}, + {"key": "mcpp.build.onSave", "type": "boolean", "default": false, "group": "build", "tier": "public", "title": "Build on save", "description": "a save builds"}, + {"key": "mcpp.build.jobs", "type": "number", "default": 0, "group": "build", "tier": "advanced", "title": "Jobs", "description": "parallel jobs"}, + {"key": "mcpp.build.profile", "type": "string", "enum": ["dev", "release"], "default": "dev", "group": "build", "tier": "public", "title": "Profile", "description": ""}, + {"key": "mcpp.old", "type": "string", "default": "", "group": "build", "tier": "public", "deprecated": true, "title": "old", "description": ""}]}""") + val stored = mutableMapOf("mcpp.path" to JsonPrimitive("/opt/mcpp")) + val form = SettingsForm(registry, { it }) { stored[it] } + assertEquals(listOf("mcpp.path", "mcpp.build.onSave", "mcpp.build.profile", "mcpp.build.jobs"), form.fields.map { it.key }) + assertFalse(form.isModified()) + assertEquals("/opt/mcpp", (form.fields[0].component as JBTextField).text) + // A change is what Apply stores; a value back at its default is removed. + (form.fields[1].component as JBCheckBox).isSelected = true + (form.fields[0].component as JBTextField).text = "" + (form.fields[3].component as JBTextField).text = "8" + assertTrue(form.isModified()) + assertEquals(mapOf("mcpp.path" to null, "mcpp.build.onSave" to JsonPrimitive(true), "mcpp.build.jobs" to JsonPrimitive(8L)), form.changes()) + form.reset() + assertFalse(form.isModified()) + // The search narrows the form. + form.filter("save") + assertEquals(listOf(false, true, false, false), form.fields.map { it.component.isVisible }) + form.filter("") + assertTrue(form.fields.all { it.component.isVisible }) + } +} diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json index 417a9a5..34fd4d0 100644 --- a/mide-plugins/vscode/package.json +++ b/mide-plugins/vscode/package.json @@ -1446,7 +1446,7 @@ "test:parity": "npm run compile && node --test \"dist/test/parity/*.test.js\"", "bundle:webui": "node tools/bundle-webui.mjs", "check:webui": "tsc -p ../../webui/tsconfig.json", - "test:webui": "npm run compile && node tools/bundle-webui.mjs --out .webui-test && node --test ../../webui/tests/workbench.test.mjs" + "test:webui": "node tools/bundle-webui.mjs --out .webui-test && node --test ../../webui/tests/graph.test.mjs" }, "devDependencies": { "@mermaid-js/tiny": "12.2.0", diff --git a/mide-plugins/vscode/tools/bundle-webui.mjs b/mide-plugins/vscode/tools/bundle-webui.mjs index ae4bb41..4120317 100644 --- a/mide-plugins/vscode/tools/bundle-webui.mjs +++ b/mide-plugins/vscode/tools/bundle-webui.mjs @@ -1,9 +1,8 @@ /** - * The workbench mcpp-clion shows (webui/): mcpp-vscode's page modules, unchanged, with `vscode` and - * Node's built-ins answered by webui/src, bundled into one script, beside the shell page, its - * stylesheet, the codicons, and the files the pages load (media/, dist/vendor/), into - * webui/dist (or the directory named by --out), with files.txt listing them (what mcpp-clion - * extracts from its jar). Nothing it writes is kept in the repository. + * The graph page mcpp-clion shows in JCEF (webui/, mcpp design 2026-10-30 part 2 §9): webui/src/graph.ts + * bundled into graph.js, beside graph.html and mermaid, into webui/dist (or the directory named by + * --out), with files.txt listing them (what mcpp-clion extracts from its jar). Nothing it writes is + * kept in the repository. */ import { build } from "esbuild"; import fs from "node:fs"; @@ -14,39 +13,23 @@ const extension = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".. const webui = path.resolve(extension, "..", "..", "webui"); const at = process.argv.indexOf("--out"); const out = at > 0 ? path.resolve(process.argv[at + 1]) : path.join(webui, "dist"); -const shim = (name) => path.join(webui, "src", ...name.split("/")); fs.rmSync(out, { recursive: true, force: true }); -fs.mkdirSync(out, { recursive: true }); +fs.mkdirSync(path.join(out, "vendor"), { recursive: true }); await build({ - entryPoints: [shim("main.ts")], + entryPoints: [path.join(webui, "src", "graph.ts")], bundle: true, format: "iife", platform: "browser", target: "chrome120", - outfile: path.join(out, "workbench.js"), - alias: { - vscode: shim("vscode.ts"), - "node:fs": shim("node/fs.ts"), - "node:path": shim("node/path.ts"), - "node:os": shim("node/os.ts"), - "node:crypto": shim("node/crypto.ts"), - "node:url": shim("node/url.ts"), - "node:util": shim("node/util.ts"), - "node:child_process": shim("node/child_process.ts"), - }, - inject: [shim("node/process.ts"), shim("node/buffer.ts")], - nodePaths: [path.join(extension, "node_modules")], + outfile: path.join(out, "graph.js"), logLevel: "warning", legalComments: "eof", }); -for (const file of ["index.html", "workbench.css"]) fs.copyFileSync(path.join(webui, file), path.join(out, file)); -const codicons = path.join(extension, "node_modules", "@vscode", "codicons", "dist"); -for (const file of ["codicon.css", "codicon.ttf"]) fs.copyFileSync(path.join(codicons, file), path.join(out, file)); -fs.cpSync(path.join(extension, "media"), path.join(out, "media"), { recursive: true }); -fs.mkdirSync(path.join(out, "dist", "vendor"), { recursive: true }); -fs.copyFileSync(path.join(extension, "node_modules", "@mermaid-js", "tiny", "dist", "mermaid.tiny.js"), path.join(out, "dist", "vendor", "mermaid.js")); -fs.copyFileSync(path.join(extension, "node_modules", "@mermaid-js", "tiny", "LICENSE"), path.join(out, "dist", "vendor", "MERMAID-LICENSE")); +fs.copyFileSync(path.join(webui, "graph.html"), path.join(out, "graph.html")); +const mermaid = path.join(extension, "node_modules", "@mermaid-js", "tiny"); +fs.copyFileSync(path.join(mermaid, "dist", "mermaid.tiny.js"), path.join(out, "vendor", "mermaid.js")); +fs.copyFileSync(path.join(mermaid, "LICENSE"), path.join(out, "vendor", "MERMAID-LICENSE")); const listed = []; const list = (dir) => { for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { diff --git a/spec/i18n/runtime.zh-cn.json b/spec/i18n/runtime.zh-cn.json index eea5c27..d824f34 100644 --- a/spec/i18n/runtime.zh-cn.json +++ b/spec/i18n/runtime.zh-cn.json @@ -775,5 +775,33 @@ "A feature as a table": "以表形式写出的特性", "Build settings for the targets a selector matches": "选择器匹配的目标的构建设置", "Dependencies for the targets a selector matches": "选择器匹配的目标的依赖", - "Dependencies of one index namespace": "一个索引命名空间的依赖" + "Dependencies of one index namespace": "一个索引命名空间的依赖", + "Add to the project": "添加到项目", + "Add {0}": "添加 {0}", + "Advanced": "高级", + "All indexes": "全部索引", + "Artifacts": "产物", + "C++ Modules": "C++ Modules", + "Dependency Graph": "依赖图", + "Largest": "最大", + "Last build: {0}": "上次构建:{0}", + "Module Graph": "模块图", + "No toolchain is set up in Settings | Build, Execution, Deployment | Toolchains; CLion's debugger comes from one.": "设置 | 构建、执行、部署 | 工具链 中没有工具链;CLion 的调试器来自工具链。", + "Nothing to draw yet: build the project first.": "还没有可绘制的内容:请先构建项目。", + "Only the first {0} nodes are drawn.": "只绘制前 {0} 个节点。", + "Platform: {0}": "平台:{0}", + "Reading the settings…": "正在读取设置…", + "Remove it": "删除它", + "The build database could not be read: {0}": "无法读取构建数据库:{0}", + "The latest version": "最新版本", + "The settings come from the mcpp-ide backend, which is not running: {0}": "设置来自 mcpp-ide 后端,后端未运行:{0}", + "This IDE runs without JCEF, which draws the graph.": "此 IDE 没有 JCEF,无法绘制图。", + "This program runs through {0}. LLDB reads its symbols once `settings set plugin.jit-loader.gdb.enable on` is in ~/.lldbinit; GDB reads them as it is.": "此程序经 {0} 运行。~/.lldbinit 中有 `settings set plugin.jit-loader.gdb.enable on` 时 LLDB 读取其符号;GDB 直接读取。", + "compile_commands.json at the project root was written by an earlier mcpp. CLion may open the project as a compilation database instead of leaving its C++ to mcppls; mcpp now keeps the database under target/.": "项目根目录的 compile_commands.json 由较早的 mcpp 写入。CLion 可能把项目当作编译数据库打开,而不是把 C++ 交给 mcppls;mcpp 现在把数据库放在 target/ 下。", + "mcpp: building for the debugger…": "mcpp:正在为调试器构建…", + "unknown command {0}": "未知命令 {0}", + "{0} days or more": "{0} 天及以上", + "{0} in {1} entries": "{0},共 {1} 项", + "{0} incomplete entries": "{0} 项不完整", + "{0} · {1} entries": "{0} · {1} 项" } diff --git a/webui/graph.html b/webui/graph.html new file mode 100644 index 0000000..e656395 --- /dev/null +++ b/webui/graph.html @@ -0,0 +1,21 @@ + + + + +mcpp graph + + + + + +
      +
      +
      + + diff --git a/webui/index.html b/webui/index.html deleted file mode 100644 index 064ad3e..0000000 --- a/webui/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/webui/src/bridge.ts b/webui/src/bridge.ts deleted file mode 100644 index 61554f2..0000000 --- a/webui/src/bridge.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * The bridge to the IDE that hosts the workbench (mcpp-clion's JCEF browser). - * - * Asynchronous requests travel through `window.mcppHostSend(text)` (a JBCefJSQuery the plugin - * injects) and come back through `window.mcppHostReceive(message)`; the IDE's notifications arrive - * the same way. A synchronous request -- what a synchronous file read of the page code needs -- is - * a `prompt()` the plugin answers through its JS dialog handler, which is the one synchronous - * channel a Chromium page has to its host. - */ - -type Pending = { resolve: (value: unknown) => void; reject: (error: Error) => void }; - -const pending = new Map(); -const listeners = new Map void>>(); -const queue: string[] = []; -let nextId = 1; - -declare global { - interface Window { - mcppHostSend?: (text: string) => void; - mcppHostReceive?: (message: unknown) => void; - mcppHostAttached?: () => void; - } -} - -function send(message: unknown): void { - const text = JSON.stringify(message); - if (typeof window.mcppHostSend === "function") window.mcppHostSend(text); - else queue.push(text); -} - -window.mcppHostAttached = () => { - while (queue.length > 0) window.mcppHostSend?.(queue.shift()!); -}; - -window.mcppHostReceive = (raw: unknown) => { - let message = raw as { type?: string; id?: number; result?: unknown; error?: { message?: string; code?: number; data?: unknown }; method?: string; params?: unknown }; - if (typeof raw === "string") { - try { message = JSON.parse(raw); } catch { return; } - } - if (!message || typeof message !== "object") return; - if (message.type === "result" || message.type === "error") { - const p = pending.get(message.id ?? -1); - pending.delete(message.id ?? -1); - if (!p) return; - if (message.type === "result") p.resolve(message.result); - else p.reject(Object.assign(new Error(message.error?.message ?? "failed"), { code: message.error?.code, data: message.error?.data })); - } else if (message.type === "notify" && typeof message.method === "string") { - for (const fn of listeners.get(message.method) ?? []) fn(message.params); - for (const fn of listeners.get("*") ?? []) fn({ method: message.method, params: message.params }); - } -}; - -/** An action of the IDE: `rpc` reaches the backend, everything else the IDE itself. */ -export function request(action: string, params: unknown = {}): Promise { - const id = nextId++; - return new Promise((resolve, reject) => { - pending.set(id, { resolve: resolve as (value: unknown) => void, reject }); - send({ type: "request", id, action, params }); - }); -} - -/** A method of the backend (`mcpp-ide serve`). */ -export function rpc(method: string, params: unknown = {}): Promise { - return request("rpc", { method, params }); -} - -/** The IDE's answer, now: for the synchronous file reads of the page code. */ -export function requestSync(action: string, params: unknown = {}): T { - const answer = window.prompt(`mcpp-host:${JSON.stringify({ action, params })}`, ""); - if (answer === null) throw new Error(`the IDE did not answer ${action}`); - const parsed = JSON.parse(answer) as { ok: boolean; result?: T; error?: string; code?: string }; - if (!parsed.ok) throw Object.assign(new Error(parsed.error ?? `${action} failed`), { code: parsed.code }); - return parsed.result as T; -} - -/** The IDE's notifications (`*` for every one): the backend's, and the IDE's own. */ -export function on(method: string, fn: (params: any) => void): { dispose(): void } { - if (!listeners.has(method)) listeners.set(method, new Set()); - listeners.get(method)!.add(fn); - return { dispose: () => listeners.get(method)?.delete(fn) }; -} - -/** What the IDE told the workbench when it loaded. */ -export interface HostInfo { - /** "linux" | "darwin" | "win32" */ - platform: string; - arch: string; - /** The IDE's language: "en" | "zh-cn" | ... */ - language: string; - home: string; - tmp: string; - env: Record; - /** The workbench's own files (media/, dist/vendor/) as a URL. */ - extensionUri: string; - workspaceFolders: string[]; - trusted: boolean; -} - -export let info: HostInfo = { - platform: "linux", arch: "x64", language: "en", home: "/", tmp: "/tmp", env: {}, extensionUri: "", workspaceFolders: [], trusted: false, -}; - -export function setInfo(next: HostInfo): void { - info = next; -} diff --git a/webui/src/graph.ts b/webui/src/graph.ts new file mode 100644 index 0000000..7ffdb25 --- /dev/null +++ b/webui/src/graph.ts @@ -0,0 +1,100 @@ +// The graph page mcpp-clion shows in JCEF (mcpp design 2026-10-30 part 2 §9): the backend's +// `mcpp-ide/graph/packages` or `mcpp-ide/graph/modules`, drawn with mermaid. The page decides +// nothing: the IDE hands it the backend's answer and its strings (`mcppRender`), and a click on a +// module whose source is known asks the IDE to open it (`mcppHostSend`). + +interface GraphNode { + id: string; + kind?: string; + family?: string; + units?: number; + set?: string; + source?: string; +} + +interface GraphEdge { + from: string; + to: string; +} + +export interface GraphPage { + kind: "packages" | "modules"; + graph: { nodes: GraphNode[]; edges: GraphEdge[] }; + title: string; + /** Said when the graph has no node. */ + empty: string; + /** Said when the graph was cut to `limit` nodes; `{0}` is the limit. */ + truncated: string; + dark: boolean; + limit?: number; +} + +declare const mermaid: { + initialize(options: object): void; + render(id: string, text: string): Promise<{ svg: string }>; +}; + +declare global { + interface Window { + mcppRender(page: GraphPage): Promise; + mcppHostSend?: (text: string) => void; + } +} + +/** A label as mermaid reads it inside `"…"`. */ +function label(text: string): string { + return text.replace(/"/g, "#quot;").replace(//g, "#gt;"); +} + +/** The flowchart, and which mermaid id stands for which node. */ +export function flowchart(page: GraphPage): { text: string; nodes: Map; cut: boolean } { + const limit = page.limit ?? 300; + const shown = page.graph.nodes.slice(0, limit); + const idOf = new Map(); + const nodes = new Map(); + const lines = ["flowchart LR"]; + shown.forEach((node, i) => { + const id = `n${i}`; + idOf.set(node.id, id); + nodes.set(id, node); + const detail = page.kind === "packages" && node.kind ? `
      ${label(node.kind)}` : ""; + lines.push(` ${id}["${label(node.id)}${detail}"]`); + if (node.kind === "external") lines.push(` class ${id} external`); + else if (node.kind === "test") lines.push(` class ${id} test`); + }); + for (const edge of page.graph.edges) { + const from = idOf.get(edge.from), to = idOf.get(edge.to); + if (from && to) lines.push(` ${from} --> ${to}`); + } + lines.push(" classDef external stroke-dasharray: 4 3"); + lines.push(" classDef test stroke-width: 2px"); + return { text: lines.join("\n"), nodes, cut: page.graph.nodes.length > shown.length }; +} + +let renders = 0; + +window.mcppRender = async (page: GraphPage): Promise => { + const title = document.getElementById("title")!; + const note = document.getElementById("note")!; + const target = document.getElementById("graph")!; + title.textContent = page.title; + document.body.classList.toggle("dark", page.dark); + if (page.graph.nodes.length === 0) { + note.textContent = page.empty; + target.innerHTML = ""; + return; + } + const { text, nodes, cut } = flowchart(page); + note.textContent = cut ? page.truncated.replace("{0}", String(page.limit ?? 300)) : ""; + mermaid.initialize({ startOnLoad: false, theme: page.dark ? "dark" : "default", securityLevel: "strict", maxEdges: 5000 }); + const { svg } = await mermaid.render(`graph${++renders}`, text); + target.innerHTML = svg; + // A module's source opens in the IDE. + for (const element of Array.from(target.querySelectorAll("g.node"))) { + const match = /-(n\d+)-/.exec(element.id); + const node = match ? nodes.get(match[1]) : undefined; + if (!node?.source) continue; + element.style.cursor = "pointer"; + element.addEventListener("click", () => window.mcppHostSend?.(JSON.stringify({ open: node.source }))); + } +}; diff --git a/webui/src/main.ts b/webui/src/main.ts deleted file mode 100644 index 7f8df15..0000000 --- a/webui/src/main.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * The workbench's start: the IDE says where it is (its system, language, folders, trust, theme), - * mcpp and mcppls are routed through the backend as the extension routes them, and the pages are - * registered by mcpp-vscode's own `registerPages` -- the same modules, wired the same way. - */ -import manifest from "../../mide-plugins/vscode/package.json"; -import { useMcppExecutor, useMcpplsExecutor } from "../../mide-plugins/vscode/src/cli/process"; -import { setLanguagePreference, type LanguagePreference } from "../../mide-plugins/vscode/src/i18n/t"; -import { read } from "../../mide-plugins/vscode/src/config/access"; -import { onDidChange } from "../../mide-plugins/vscode/src/config/access"; -import { registerPages } from "../../mide-plugins/vscode/src/pages"; -import { on, request, rpc, setInfo, type HostInfo } from "./bridge"; -import { configure as configurePath } from "./node/path"; -import { Uri, commands, loadSettings, window } from "./vscode"; -import { workbench } from "./workbench"; - -interface Init { - info: HostInfo; - theme: Record; - themeKind: "vscode-light" | "vscode-dark" | "vscode-high-contrast"; - /** `window`: the views as tabs; `settings`: the settings page alone (the IDE's settings dialog). */ - mode?: "window" | "settings"; -} - -interface Exec { exitCode: number; stdout: string; stderr: string; timedOut: boolean } - -async function start(): Promise { - const init = await request("init"); - setInfo(init.info); - configurePath(); - workbench.setTheme(init.theme, init.themeKind); - on("theme", (t: { theme: Record; themeKind: Init["themeKind"] }) => workbench.setTheme(t.theme, t.themeKind)); - await loadSettings(); - const language = () => setLanguagePreference(read("mcpp.ui.language")); - language(); - onDidChange(language); - - const exec = (method: string) => async (args: string[], cwd?: string, options: { timeoutMs?: number; maxBufferMiB?: number } = {}) => { - try { - const answer = await rpc(method, { - args, ...(cwd ? { cwd } : {}), ...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}), - maxOutputBytes: Math.max(1, options.maxBufferMiB ?? 16) * 1024 * 1024, - }); - return { exitCode: answer.timedOut && answer.exitCode === 0 ? 1 : answer.exitCode, stdout: answer.stdout, stderr: answer.stderr }; - } catch (error) { - return { exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error) }; - } - }; - useMcppExecutor((_executable, args, cwd, options) => exec("mcpp/exec")(args, cwd, options)); - useMcpplsExecutor(exec("languageService/exec")); - - const context = { - subscriptions: [] as Array<{ dispose(): unknown }>, - extensionUri: Uri.parse(init.info.extensionUri), - extension: { packageJSON: manifest }, - }; - registerPages(context as never, { - output: window.createOutputChannel("mcpp"), - // The backend runs the mcpp it located; the name is only what a message would say. - mcppExecutable: () => "mcpp", - }); - if (init.mode === "settings") { - document.body.classList.add("settings-only"); - void commands.executeCommand("mcpp.openSettings"); - } else { - workbench.showFirst(); - } - on("show", (p: { view: string; args?: unknown }) => { - // A panel the IDE asks for: the command that opens it, as the menus run it. - const open: Record = { - settings: "mcpp.openSettings", - "graph:packages": "mcpp.project.showDependencyGraph", - "graph:modules": "mcpp.project.showModuleDependencyGraph", - library: "mcpp.library.openDetail", - }; - const command = open[p.view]; - if (command) void commands.executeCommand(command, ...(p.args === undefined ? [] : [p.args])); - else workbench.focus(p.view); - }); -} - -start().catch((error) => { - document.body.textContent = `mcpp: ${error instanceof Error ? error.message : String(error)}`; - request("log", { text: `workbench: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n` }).catch(() => undefined); -}); diff --git a/webui/src/node/buffer.ts b/webui/src/node/buffer.ts deleted file mode 100644 index 98f5619..0000000 --- a/webui/src/node/buffer.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** The `Buffer` global the page code uses (the graph's exports). */ -export { Buffer } from "buffer"; diff --git a/webui/src/node/child_process.ts b/webui/src/node/child_process.ts deleted file mode 100644 index 37c1af9..0000000 --- a/webui/src/node/child_process.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * `node:child_process` for the page code. The workbench starts no program itself: mcpp and mcppls - * run through the backend (the process module's executors), and anything else is refused. - */ -export function execFile(file: string, _args: unknown, _options: unknown, callback?: (error: Error) => void): void { - const done = typeof _options === "function" ? (_options as (error: Error) => void) : callback; - done?.(new Error(`the workbench does not start programs (${file})`)); -} -export default { execFile }; diff --git a/webui/src/node/crypto.ts b/webui/src/node/crypto.ts deleted file mode 100644 index 2c63397..0000000 --- a/webui/src/node/crypto.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** `node:crypto` for the page code: SHA-256 digests and random bytes. */ -import { sha256 } from "@noble/hashes/sha256"; -import { Buffer } from "buffer"; - -export function randomBytes(size: number): Buffer { - const bytes = new Uint8Array(size); - globalThis.crypto.getRandomValues(bytes); - return Buffer.from(bytes); -} - -class Hash { - private readonly chunks: Uint8Array[] = []; - constructor(algorithm: string) { - if (algorithm !== "sha256") throw new Error(`the workbench has no ${algorithm} digest`); - } - update(data: string | Uint8Array): this { - this.chunks.push(typeof data === "string" ? new TextEncoder().encode(data) : data); - return this; - } - digest(encoding?: "hex" | "base64"): string | Buffer { - const all = Buffer.concat(this.chunks); - const out = Buffer.from(sha256(all)); - return encoding ? out.toString(encoding) : out; - } -} - -export function createHash(algorithm: string): Hash { - return new Hash(algorithm); -} - -export default { randomBytes, createHash }; diff --git a/webui/src/node/fs.ts b/webui/src/node/fs.ts deleted file mode 100644 index e1384e3..0000000 --- a/webui/src/node/fs.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * `node:fs` for the page code: the IDE reads the files. A synchronous call is answered through the - * bridge's synchronous channel, a promise through its asynchronous one; a directory listing brings - * each entry's status along, so the `lstatSync` that follows a `readdirSync` (the walk of target/) - * costs nothing more. - */ -import { request, requestSync } from "../bridge"; - -interface StatJson { - type: "file" | "directory" | "symlink" | "other"; - size: number; - mtimeMs: number; - ino: number; -} - -interface EntryJson extends StatJson { - name: string; -} - -class Stats { - readonly size: number; - readonly mtimeMs: number; - readonly mtime: Date; - readonly ino: number | bigint; - readonly mtimeNs: bigint | number; - constructor(private readonly s: StatJson, bigint = false) { - this.size = s.size; - this.mtimeMs = s.mtimeMs; - this.mtime = new Date(s.mtimeMs); - this.ino = bigint ? BigInt(s.ino) : s.ino; - this.mtimeNs = bigint ? BigInt(Math.round(s.mtimeMs)) * 1000000n : s.mtimeMs * 1e6; - } - isFile(): boolean { return this.s.type === "file"; } - isDirectory(): boolean { return this.s.type === "directory"; } - isSymbolicLink(): boolean { return this.s.type === "symlink"; } -} - -class Dirent { - constructor(readonly name: string, private readonly type: StatJson["type"]) {} - isFile(): boolean { return this.type === "file"; } - isDirectory(): boolean { return this.type === "directory"; } - isSymbolicLink(): boolean { return this.type === "symlink"; } -} - -/** The statuses a listing brought, by full name: `lstatSync` reads them first. */ -const listed = new Map(); - -function enoent(path: string): Error { - return Object.assign(new Error(`ENOENT: no such file or directory, '${path}'`), { code: "ENOENT", path }); -} - -function childName(dir: string, name: string): string { - return /[\\/]$/.test(dir) ? `${dir}${name}` : `${dir}${dir.includes("\\") && !dir.includes("/") ? "\\" : "/"}${name}`; -} - -export function existsSync(path: string): boolean { - try { return requestSync("fs.stat", { path, follow: true }) !== null; } catch { return false; } -} - -export function statSync(path: string, options?: { bigint?: boolean }): Stats { - const s = requestSync("fs.stat", { path, follow: true }); - if (s === null) throw enoent(path); - return new Stats(s, options?.bigint === true); -} - -export function lstatSync(path: string, options?: { bigint?: boolean }): Stats { - const known = listed.get(path); - if (known) return new Stats(known, options?.bigint === true); - const s = requestSync("fs.stat", { path, follow: false }); - if (s === null) throw enoent(path); - return new Stats(s, options?.bigint === true); -} - -export function readFileSync(path: string, _encoding?: unknown): string { - const text = requestSync("fs.read", { path }); - if (text === null) throw enoent(path); - return text; -} - -export function readdirSync(path: string, options?: { withFileTypes?: boolean }): Array { - const entries = requestSync("fs.list", { path }); - if (entries === null) throw enoent(path); - for (const e of entries) listed.set(childName(path, e.name), e); - return options?.withFileTypes ? entries.map((e) => new Dirent(e.name, e.type)) : entries.map((e) => e.name); -} - -export const promises = { - async readFile(path: string, _encoding?: unknown): Promise { - const text = await request("fs.read", { path }); - if (text === null) throw enoent(path); - return text; - }, - async writeFile(path: string, data: string | Uint8Array): Promise { - const base64 = typeof data !== "string"; - const text = typeof data === "string" ? data : btoa(String.fromCharCode(...data)); - await request("fs.write", { path, text, base64 }); - }, - async readdir(path: string, options?: { withFileTypes?: boolean }): Promise> { - const entries = await request("fs.list", { path }); - if (entries === null) throw enoent(path); - return options?.withFileTypes ? entries.map((e) => new Dirent(e.name, e.type)) : entries.map((e) => e.name); - }, - async stat(path: string): Promise { - const s = await request("fs.stat", { path, follow: true }); - if (s === null) throw enoent(path); - return new Stats(s); - }, - async access(path: string): Promise { - if ((await request("fs.stat", { path, follow: true })) === null) throw enoent(path); - }, - async realpath(path: string): Promise { - const real = await request("fs.realpath", { path }); - if (real === null) throw enoent(path); - return real; - }, -}; - -export default { existsSync, statSync, lstatSync, readFileSync, readdirSync, promises }; diff --git a/webui/src/node/os.ts b/webui/src/node/os.ts deleted file mode 100644 index 6195b09..0000000 --- a/webui/src/node/os.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** `node:os` for the page code: the IDE's machine. */ -import { info } from "../bridge"; - -export function homedir(): string { return info.home; } -export function tmpdir(): string { return info.tmp; } -export function platform(): string { return info.platform; } -export function arch(): string { return info.arch; } -export const EOL = "\n"; -export default { homedir, tmpdir, platform, arch, EOL }; diff --git a/webui/src/node/path.ts b/webui/src/node/path.ts deleted file mode 100644 index bf6c3fc..0000000 --- a/webui/src/node/path.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * `node:path` for the page code in the workbench: the conventions of the system the IDE runs on - * (Windows names with `\` and a drive, POSIX names otherwise), as Node's own module answers them on - * that system, for the functions the page code calls. - */ -import { info } from "../bridge"; - -const windows = (): boolean => info.platform === "win32"; - -function splitRoot(p: string): [root: string, rest: string] { - if (!windows()) return p.startsWith("/") ? ["/", p.slice(1)] : ["", p]; - const drive = /^[A-Za-z]:/.exec(p); - if (drive) { - const after = p.slice(2); - return /^[\\/]/.test(after) ? [`${drive[0]}\\`, after.slice(1)] : [drive[0], after]; - } - const unc = /^[\\/]{2}[^\\/]+[\\/][^\\/]+/.exec(p); - if (unc) return [`${unc[0].replace(/\//g, "\\")}\\`, p.slice(unc[0].length + 1)]; - return /^[\\/]/.test(p) ? ["\\", p.slice(1)] : ["", p]; -} - -function parts(rest: string): string[] { - return rest.split(windows() ? /[\\/]+/ : /\/+/).filter((s) => s.length > 0); -} - -export function normalize(p: string): string { - if (p.length === 0) return "."; - const [root, rest] = splitRoot(p); - const out: string[] = []; - for (const part of parts(rest)) { - if (part === ".") continue; - if (part === "..") { - if (out.length > 0 && out[out.length - 1] !== "..") out.pop(); - else if (!root) out.push(".."); - continue; - } - out.push(part); - } - const trailing = /[\\/]$/.test(p) && out.length > 0 ? sep : ""; - const body = out.join(sep); - if (!root) return (body || ".") + trailing; - return root + body + trailing; -} - -export function isAbsolute(p: string): boolean { - if (!windows()) return p.startsWith("/"); - return /^[A-Za-z]:[\\/]/.test(p) || /^[\\/]/.test(p); -} - -export function join(...segments: string[]): string { - const joined = segments.filter((s) => s.length > 0).join(sep); - return joined.length === 0 ? "." : normalize(joined); -} - -export function resolve(...segments: string[]): string { - let resolved = ""; - for (let i = segments.length - 1; i >= 0 && !isAbsolute(resolved); i--) { - const s = segments[i]; - if (s.length === 0) continue; - resolved = resolved ? `${s}${sep}${resolved}` : s; - } - if (!isAbsolute(resolved)) resolved = `${info.workspaceFolders[0] ?? info.home}${sep}${resolved}`; - const n = normalize(resolved); - return n.length > 1 && /[\\/]$/.test(n) && !/^[A-Za-z]:\\$/.test(n) ? n.slice(0, -1) : n; -} - -export function relative(from: string, to: string): string { - const a = resolve(from), b = resolve(to); - const fold = (s: string) => (windows() ? s.toLowerCase() : s); - const [ra, xa] = splitRoot(a), [rb, xb] = splitRoot(b); - if (fold(ra) !== fold(rb)) return b; - const pa = parts(xa), pb = parts(xb); - let i = 0; - while (i < pa.length && i < pb.length && fold(pa[i]) === fold(pb[i])) i++; - return [...pa.slice(i).map(() => ".."), ...pb.slice(i)].join(sep); -} - -export function dirname(p: string): string { - const [root, rest] = splitRoot(p); - const ps = parts(rest); - if (ps.length <= 1) return root || "."; - return root + ps.slice(0, -1).join(sep); -} - -export function basename(p: string, ext?: string): string { - const [, rest] = splitRoot(p); - const ps = parts(rest); - const base = ps.length > 0 ? ps[ps.length - 1] : ""; - return ext && base.endsWith(ext) && base !== ext ? base.slice(0, -ext.length) : base; -} - -export function extname(p: string): string { - const base = basename(p); - const dot = base.lastIndexOf("."); - return dot <= 0 ? "" : base.slice(dot); -} - -export let sep = "/"; -export let delimiter = ":"; - -/** Called once the IDE has said which system it runs on. */ -export function configure(): void { - sep = windows() ? "\\" : "/"; - delimiter = windows() ? ";" : ":"; - api.sep = sep; - api.delimiter = delimiter; -} - -const api = { normalize, isAbsolute, join, resolve, relative, dirname, basename, extname, sep, delimiter }; -export default api; diff --git a/webui/src/node/process.ts b/webui/src/node/process.ts deleted file mode 100644 index fa18662..0000000 --- a/webui/src/node/process.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** The `process` global the page code reads: the IDE's system and environment. */ -import { info } from "../bridge"; - -export const process = { - get platform(): string { return info.platform; }, - get arch(): string { return info.arch; }, - get env(): Record { return info.env; }, - cwd: (): string => info.workspaceFolders[0] ?? info.home, - versions: {} as Record, -}; diff --git a/webui/src/node/url.ts b/webui/src/node/url.ts deleted file mode 100644 index dd20551..0000000 --- a/webui/src/node/url.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** `node:url` for the page code: file URLs. */ -import { info } from "../bridge"; - -export function fileURLToPath(url: string | URL): string { - const u = typeof url === "string" ? new URL(url) : url; - const p = decodeURIComponent(u.pathname); - return info.platform === "win32" ? p.replace(/^\/([A-Za-z]:)/, "$1").replace(/\//g, "\\") : p; -} - -export function pathToFileURL(p: string): URL { - const forward = p.replace(/\\/g, "/"); - return new URL(`file://${forward.startsWith("/") ? "" : "/"}${encodeURI(forward)}`); -} - -export default { fileURLToPath, pathToFileURL }; diff --git a/webui/src/node/util.ts b/webui/src/node/util.ts deleted file mode 100644 index dfe04ba..0000000 --- a/webui/src/node/util.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** `node:util` for the page code: promisify, which the process module applies at load. */ -export function promisify void>(fn: T): (...args: any[]) => Promise { - return (...args: any[]) => new Promise((resolve, reject) => fn(...args, (error: unknown, ...values: unknown[]) => { - if (error) reject(error); else resolve(values.length > 1 ? values : values[0]); - })); -} -export default { promisify }; diff --git a/webui/src/vscode.ts b/webui/src/vscode.ts deleted file mode 100644 index b96d13d..0000000 --- a/webui/src/vscode.ts +++ /dev/null @@ -1,422 +0,0 @@ -/** - * The part of VS Code's extension API that mcpp-vscode's pages use, for the workbench in an IDE that - * is not VS Code (mcpp-clion's JCEF browser). The pages are mcpp-vscode's own modules, unchanged; - * what they ask of VS Code is answered here: views and panels by the workbench (workbench.ts), - * dialogs, files, settings, the clipboard and the IDE's own commands by the IDE (bridge.ts). - */ -import zhCn from "../../mide-plugins/vscode/l10n/bundle.l10n.zh-cn.json"; -import { info, on, request } from "./bridge"; -import { workbench } from "./workbench"; - -// ── events and disposables ────────────────────────────────────────────────────────────────────── - -export class Disposable { - constructor(private readonly fn: () => void) {} - static from(...items: Array<{ dispose(): unknown }>): Disposable { - return new Disposable(() => items.forEach((i) => i.dispose())); - } - dispose(): void { - this.fn(); - } -} - -export type Event = (listener: (e: T) => unknown) => Disposable; - -export class EventEmitter { - private readonly listeners = new Set<(e: T) => unknown>(); - readonly event: Event = (listener) => { - this.listeners.add(listener); - return new Disposable(() => this.listeners.delete(listener)); - }; - fire(value: T): void { - for (const l of [...this.listeners]) { - try { l(value); } catch (e) { console.error(e); } - } - } - dispose(): void { - this.listeners.clear(); - } -} - -// ── values ────────────────────────────────────────────────────────────────────────────────────── - -export class Uri { - private constructor(readonly scheme: string, readonly authority: string, readonly path: string, readonly query: string, readonly fragment: string) {} - - static parse(text: string): Uri { - const m = /^([a-zA-Z][\w+.-]*):(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/.exec(text); - if (!m) return new Uri("file", "", text, "", ""); - return new Uri(m[1], m[2] ?? "", decodeURIComponent(m[3] ?? ""), m[4] ?? "", m[5] ?? ""); - } - - static file(fsPath: string): Uri { - let p = fsPath.replace(/\\/g, "/"); - if (/^[A-Za-z]:/.test(p)) p = `/${p}`; - return new Uri("file", "", p, "", ""); - } - - static joinPath(base: Uri, ...segments: string[]): Uri { - const joined = [base.path.replace(/\/+$/, ""), ...segments.map((s) => s.replace(/^\/+|\/+$/g, ""))].join("/"); - return base.with({ path: joined }); - } - - get fsPath(): string { - const p = /^\/[A-Za-z]:/.test(this.path) ? this.path.slice(1) : this.path; - return info.platform === "win32" ? p.replace(/\//g, "\\") : p; - } - - with(change: { scheme?: string; path?: string; query?: string; fragment?: string }): Uri { - return new Uri(change.scheme ?? this.scheme, this.authority, change.path ?? this.path, change.query ?? this.query, change.fragment ?? this.fragment); - } - - toString(): string { - const authority = this.scheme === "file" || this.authority ? `//${this.authority}` : ""; - const path = this.path.split("/").map(encodeURIComponent).join("/").replace(/%3A/g, ":"); - return `${this.scheme}:${authority}${path}${this.query ? `?${this.query}` : ""}${this.fragment ? `#${this.fragment}` : ""}`; - } - - toJSON(): string { - return this.toString(); - } -} - -export class ThemeColor { - constructor(readonly id: string) {} -} - -export class ThemeIcon { - constructor(readonly id: string, readonly color?: ThemeColor) {} -} - -export enum TreeItemCollapsibleState { None = 0, Collapsed = 1, Expanded = 2 } - -export class TreeItem { - id?: string; - description?: string; - tooltip?: string; - iconPath?: ThemeIcon; - contextValue?: string; - command?: { command: string; title: string; arguments?: unknown[] }; - accessibilityInformation?: { label: string }; - constructor(public label: string, public collapsibleState: TreeItemCollapsibleState = TreeItemCollapsibleState.None) {} -} - -export class RelativePattern { - readonly baseUri: Uri; - readonly base: string; - constructor(base: Uri | string | { uri: Uri }, readonly pattern: string) { - this.baseUri = typeof base === "string" ? Uri.file(base) : base instanceof Uri ? base : base.uri; - this.base = this.baseUri.fsPath; - } -} - -export enum ViewColumn { Active = -1, Beside = -2, One = 1 } -export enum ProgressLocation { SourceControl = 1, Window = 10, Notification = 15 } -export enum ConfigurationTarget { Global = 1, Workspace = 2, WorkspaceFolder = 3 } -export enum StatusBarAlignment { Left = 1, Right = 2 } - -// ── settings ──────────────────────────────────────────────────────────────────────────────────── - -/** Every mcpp.* value, by level: the IDE's own store, read once and kept current. */ -interface SettingLevels { user: Record; workspace: Record } -let levels: SettingLevels = { user: {}, workspace: {} }; -const configurationChanged = new EventEmitter<{ affectsConfiguration(section: string): boolean }>(); - -export async function loadSettings(): Promise { - levels = await request("settings.read"); -} - -on("settingsChanged", (changed: { keys: string[]; levels: SettingLevels }) => { - levels = changed.levels; - configurationChanged.fire({ affectsConfiguration: (section) => changed.keys.some((k) => k === section || k.startsWith(`${section}.`)) }); -}); - -function configuration(section?: string) { - const full = (key: string) => (section ? `${section}.${key}` : key); - return { - get(key: string, fallback?: T): T | undefined { - const k = full(key); - return (levels.workspace[k] ?? levels.user[k] ?? fallback) as T | undefined; - }, - has(key: string): boolean { - const k = full(key); - return k in levels.workspace || k in levels.user; - }, - inspect(key: string) { - const k = full(key); - return { key: k, globalValue: levels.user[k] as T | undefined, workspaceValue: levels.workspace[k] as T | undefined, workspaceFolderValue: undefined }; - }, - async update(key: string, value: unknown, target?: ConfigurationTarget | boolean): Promise { - const k = full(key); - const level = target === ConfigurationTarget.Global || target === true ? "user" : "workspace"; - levels = await request("settings.write", { key: k, value: value ?? null, level }); - configurationChanged.fire({ affectsConfiguration: (s) => k === s || k.startsWith(`${s}.`) }); - }, - }; -} - -// ── documents ─────────────────────────────────────────────────────────────────────────────────── - -const contentProviders = new Map }>(); -const saved = new EventEmitter<{ uri: Uri; fileName: string; languageId: string }>(); -on("documentSaved", (p: { path: string }) => saved.fire({ uri: Uri.file(p.path), fileName: p.path, languageId: "" })); - -class TextDocument { - constructor(readonly uri: Uri, private readonly text: string) {} - get fileName(): string { return this.uri.fsPath; } - getText(): string { return this.text; } -} - -async function openTextDocument(target: Uri | string): Promise { - const uri = typeof target === "string" ? Uri.file(target) : target; - const provider = contentProviders.get(uri.scheme); - if (provider) return new TextDocument(uri, await provider.provideTextDocumentContent(uri)); - return new TextDocument(uri, (await request("fs.read", { path: uri.fsPath })) ?? ""); -} - -async function showTextDocument(target: Uri | TextDocument, options?: { selection?: { start: { line: number } } }): Promise { - const doc = target instanceof TextDocument ? target : undefined; - const uri = doc ? doc.uri : (target as Uri); - const line = options?.selection?.start?.line; - if (uri.scheme === "file") return request("openFile", { path: uri.fsPath, line: line === undefined ? undefined : line + 1 }); - const text = doc ? doc.getText() : (await openTextDocument(uri)).getText(); - return request("openText", { name: uri.path.split("/").pop() ?? uri.path, text }); -} - -// ── file watchers ─────────────────────────────────────────────────────────────────────────────── - -/** A glob over `/`-separated names: `**`, `*`, `?` and `{a,b}`. */ -/** A glob as VS Code reads one: `**`, `*`, `?`, and `{a,b}` whose alternatives are globs again. */ -function globSource(glob: string): string { - let re = ""; - for (let i = 0; i < glob.length; i++) { - const c = glob[i]; - if (c === "*" && glob[i + 1] === "*") { - i++; - if (glob[i + 1] === "/") { re += "(?:.*/)?"; i++; } else re += ".*"; - } - else if (c === "*") re += "[^/]*"; - else if (c === "?") re += "[^/]"; - else if (c === "{") { const end = glob.indexOf("}", i); re += `(?:${glob.slice(i + 1, end).split(",").map(globSource).join("|")})`; i = end; } - else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&"); - } - return re; -} - -function globToRegExp(glob: string): RegExp { - return new RegExp(`^${globSource(glob)}$`); -} - -const watchers = new Set<{ match(path: string): boolean; created: EventEmitter; changed: EventEmitter; deleted: EventEmitter }>(); -on("fileChanged", (p: { path: string; kind: "created" | "changed" | "deleted" }) => { - for (const w of watchers) { - if (!w.match(p.path)) continue; - (p.kind === "created" ? w.created : p.kind === "deleted" ? w.deleted : w.changed).fire(Uri.file(p.path)); - } -}); - -function createFileSystemWatcher(pattern: string | RelativePattern) { - const slash = (s: string) => s.replace(/\\/g, "/"); - const base = typeof pattern === "string" ? "" : slash(pattern.base).replace(/\/$/, ""); - const re = globToRegExp(typeof pattern === "string" ? pattern : pattern.pattern); - const w = { - match: (path: string) => { - const p = slash(path); - if (base && !p.startsWith(`${base}/`)) return false; - const rel = base ? p.slice(base.length + 1) : p; - return re.test(rel) || (!base && re.test(p.split("/").slice(-rel.split("/").length).join("/"))) || re.test(p.replace(/^.*\//, "")); - }, - created: new EventEmitter(), changed: new EventEmitter(), deleted: new EventEmitter(), - }; - watchers.add(w); - request("watch", { base: base || undefined, pattern: typeof pattern === "string" ? pattern : pattern.pattern }).catch(() => undefined); - return { - onDidCreate: w.created.event, onDidChange: w.changed.event, onDidDelete: w.deleted.event, - dispose: () => watchers.delete(w), - }; -} - -// ── commands ──────────────────────────────────────────────────────────────────────────────────── - -const registry = new Map unknown>(); - -export const commands = { - registerCommand(id: string, fn: (...args: unknown[]) => unknown): Disposable { - registry.set(id, fn); - return new Disposable(() => registry.delete(id)); - }, - async executeCommand(id: string, ...args: unknown[]): Promise { - const own = registry.get(id); - if (own) return (await own(...args)) as T; - if (id === "setContext") { workbench.setContext(String(args[0]), args[1]); return undefined; } - if (id.endsWith(".focus")) { workbench.focus(id.slice(0, -".focus".length)); return undefined; } - if (id === "workbench.action.openSettings") { return (await commands.executeCommand("mcpp.openSettings")) as T; } - // Everything else is the IDE's: mcpp's commands (spec/commands.json), and the editor's own. - return request("command", { id, args }); - }, - async getCommands(): Promise { - return [...registry.keys()]; - }, -}; - -// ── window ────────────────────────────────────────────────────────────────────────────────────── - -type MessageItem = string | { title: string; isCloseAffordance?: boolean }; - -async function showMessage(level: "info" | "warning" | "error", message: string, ...rest: unknown[]): Promise { - const options = rest.length > 0 && typeof rest[0] === "object" && rest[0] !== null && !("title" in (rest[0] as object)) ? (rest.shift() as { modal?: boolean; detail?: string }) : {}; - const items = rest.filter((i) => i !== undefined) as MessageItem[]; - const titles = items.map((i) => (typeof i === "string" ? i : i.title)); - const chosen = await request("message", { level, message, detail: options.detail, modal: options.modal === true, items: titles }); - return chosen === null || chosen < 0 ? undefined : items[chosen]; -} - -const activeEditorChanged = new EventEmitter(); -let activeEditorPath: string | undefined; -on("activeEditor", (p: { path?: string }) => { - activeEditorPath = p.path; - activeEditorChanged.fire(window.activeTextEditor); -}); - -export const window = { - showInformationMessage: (message: string, ...rest: unknown[]) => showMessage("info", message, ...rest), - showWarningMessage: (message: string, ...rest: unknown[]) => showMessage("warning", message, ...rest), - showErrorMessage: (message: string, ...rest: unknown[]) => showMessage("error", message, ...rest), - async showInputBox(options: { title?: string; prompt?: string; value?: string; placeHolder?: string; validateInput?: (v: string) => unknown } = {}): Promise { - for (;;) { - const value = await request("input", { title: options.title, prompt: options.prompt, value: options.value, placeHolder: options.placeHolder }); - if (value === null) return undefined; - const problem = options.validateInput ? await options.validateInput(value) : undefined; - if (!problem) return value; - await showMessage("warning", typeof problem === "string" ? problem : String((problem as { message?: string }).message ?? problem)); - options = { ...options, value }; - } - }, - async showQuickPick(items: readonly T[] | Promise, options: { title?: string; placeHolder?: string } = {}): Promise { - const list = await items; - const rows = list.map((i) => (typeof i === "string" ? { label: i } : { label: i.label, description: i.description, detail: i.detail })); - const chosen = await request("pick", { title: options.title, placeHolder: options.placeHolder, items: rows }); - return chosen === null || chosen < 0 ? undefined : list[chosen]; - }, - async showSaveDialog(options: { defaultUri?: Uri; filters?: Record; title?: string } = {}): Promise { - const path = await request("saveDialog", { path: options.defaultUri?.fsPath, filters: options.filters, title: options.title }); - return path === null ? undefined : Uri.file(path); - }, - async withProgress(options: { location: ProgressLocation; title?: string; cancellable?: boolean }, task: (progress: { report(v: { message?: string; increment?: number }): void }, token: { isCancellationRequested: boolean; onCancellationRequested: Event }) => Thenable | Promise): Promise { - const id = await request("progress.start", { title: options.title ?? "" }); - const cancelled = new EventEmitter(); - try { - return await task( - { report: (v) => { if (v.message) request("progress.report", { id, message: v.message }).catch(() => undefined); } }, - { isCancellationRequested: false, onCancellationRequested: cancelled.event }, - ); - } finally { - request("progress.end", { id }).catch(() => undefined); - } - }, - showTextDocument, - get activeTextEditor(): { document: { uri: Uri; fileName: string } } | undefined { - return activeEditorPath ? { document: { uri: Uri.file(activeEditorPath), fileName: activeEditorPath } } : undefined; - }, - onDidChangeActiveTextEditor: activeEditorChanged.event, - createStatusBarItem() { - // The IDE's status bar is its own; the workbench shows none. - return { text: "", tooltip: "", command: undefined as unknown, show() {}, hide() {}, dispose() {} }; - }, - createOutputChannel(name: string) { - return { - name, - append: (text: string) => { request("log", { text }).catch(() => undefined); }, - appendLine: (text: string) => { request("log", { text: `${text}\n` }).catch(() => undefined); }, - show: () => { request("log.show").catch(() => undefined); }, - clear() {}, hide() {}, dispose() {}, replace() {}, - }; - }, - registerWebviewViewProvider: (id: string, provider: { resolveWebviewView(view: unknown, context: unknown, token: unknown): unknown }) => - workbench.registerView(id, provider), - createWebviewPanel: (viewType: string, title: string, _column: unknown, options: unknown) => workbench.createPanel(viewType, title, options), - createTreeView: (id: string, options: { treeDataProvider: unknown }) => - workbench.registerTree(id, options.treeDataProvider, (command, args) => commands.executeCommand(command, ...args)), - registerTreeDataProvider: (id: string, provider: unknown) => - workbench.registerTree(id, provider, (command, args) => commands.executeCommand(command, ...args)), -}; - -// ── workspace ─────────────────────────────────────────────────────────────────────────────────── - -const trustGranted = new EventEmitter(); -const foldersChanged = new EventEmitter(); -on("trust", (p: { trusted: boolean }) => { info.trusted = p.trusted; if (p.trusted) trustGranted.fire(); }); - -function folders() { - return info.workspaceFolders.map((f, index) => ({ uri: Uri.file(f), name: f.split(/[\\/]/).pop() ?? f, index })); -} - -export const workspace = { - get isTrusted(): boolean { return info.trusted; }, - get workspaceFolders() { return folders(); }, - getWorkspaceFolder(uri: Uri) { - const p = uri.fsPath.replace(/\\/g, "/"); - return folders().find((f) => p === f.uri.fsPath.replace(/\\/g, "/") || p.startsWith(`${f.uri.fsPath.replace(/\\/g, "/")}/`)); - }, - getConfiguration: (section?: string, _resource?: unknown) => configuration(section), - onDidChangeConfiguration: configurationChanged.event, - onDidSaveTextDocument: saved.event, - onDidGrantWorkspaceTrust: trustGranted.event, - onDidChangeWorkspaceFolders: foldersChanged.event, - openTextDocument, - registerTextDocumentContentProvider(scheme: string, provider: { provideTextDocumentContent(uri: Uri): string | Promise }): Disposable { - contentProviders.set(scheme, provider); - return new Disposable(() => contentProviders.delete(scheme)); - }, - createFileSystemWatcher, - async findFiles(pattern: string | RelativePattern, exclude?: string, max?: number): Promise { - const base = typeof pattern === "string" ? info.workspaceFolders[0] : pattern.base; - const glob = typeof pattern === "string" ? pattern : pattern.pattern; - const found = await request("findFiles", { base, pattern: glob, exclude, max }); - return found.map((p) => Uri.file(p)); - }, - fs: { - async writeFile(uri: Uri, bytes: Uint8Array): Promise { - let text = ""; - for (let i = 0; i < bytes.length; i += 0x8000) text += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); - await request("fs.write", { path: uri.fsPath, text: btoa(text), base64: true }); - }, - async readFile(uri: Uri): Promise { - return new TextEncoder().encode((await request("fs.read", { path: uri.fsPath })) ?? ""); - }, - }, -}; - -// ── the rest ──────────────────────────────────────────────────────────────────────────────────── - -export const env = { - get language(): string { return info.language; }, - clipboard: { writeText: (text: string) => request("clipboard", { text }).then(() => undefined) }, - openExternal: (uri: Uri) => request("openExternal", { url: uri.toString() }), -}; - -export const extensions = { - // The C++ Modules language service is the IDE's (the mcppls plugin); no VS Code extension is. - getExtension: (_id: string) => undefined, - onDidChange: new EventEmitter().event, -}; - -export const tasks = { - // A build in the IDE ends in the backend's result record: the pages refresh as on a task's end. - onDidEndTaskProcess: (listener: (e: unknown) => unknown) => on("result/changed", () => listener({ execution: { task: { definition: { type: "mcpp" } } }, exitCode: 0 })), -}; - -function format(template: string, args: unknown[]): string { - return args.reduce((text, a, i) => text.split(`{${i}}`).join(String(a)), template); -} - -export const l10n = { - t(message: string | { message: string; args?: unknown[] }, ...args: unknown[]): string { - const text = typeof message === "string" ? message : message.message; - const values = typeof message === "string" ? args : (message.args ?? []); - const bundle = info.language.toLowerCase().startsWith("zh") ? (zhCn as Record) : {}; - return format(bundle[text] ?? text, values); - }, -}; - -export const version = "1.91.0"; diff --git a/webui/src/workbench.ts b/webui/src/workbench.ts deleted file mode 100644 index ba609f5..0000000 --- a/webui/src/workbench.ts +++ /dev/null @@ -1,424 +0,0 @@ -/** - * The workbench: what VS Code's own window does for mcpp-vscode's pages, in one browser page. - * - * Views (the project tree, the libraries, the cache) and panels (the settings, a package's page, - * the graphs) are tabs. A webview's document is an iframe whose page is the document the page code - * assigned, with `acquireVsCodeApi()` and the theme's `--vscode-*` variables put in front of it, so - * the page runs as it runs in VS Code: messages both ways, state across reloads. A tree view renders - * the provider's items as VS Code's tree does, with the inline actions the manifest names. - */ -import manifest from "../../mide-plugins/vscode/package.json"; -import nlsEn from "../../mide-plugins/vscode/package.nls.json"; -import nlsZh from "../../mide-plugins/vscode/package.nls.zh-cn.json"; -import { info, request } from "./bridge"; - -type Listener = (e: T) => unknown; - -class Emitter { - private readonly ls = new Set>(); - event = (l: Listener) => { this.ls.add(l); return { dispose: () => this.ls.delete(l) }; }; - fire(v: T): void { for (const l of [...this.ls]) { try { l(v); } catch (e) { console.error(e); } } } -} - -/** A manifest string (`%key%`) in the IDE's language. */ -function nls(text: string | undefined): string { - if (!text) return ""; - const m = /^%(.+)%$/.exec(text); - if (!m) return text; - const zh = info.language.toLowerCase().startsWith("zh"); - return ((zh ? (nlsZh as Record)[m[1]] : undefined) ?? (nlsEn as Record)[m[1]] ?? text); -} - -// ── context keys and `when` ───────────────────────────────────────────────────────────────────── - -const context = new Map(); -const contextChanged = new Emitter(); - -/** The `when` forms the manifest uses: `a`, `!a`, `a == b`, `a != b`, `a =~ /re/`, `&&`, `||`. */ -function when(expression: string | undefined, extra: Record = {}): boolean { - if (!expression) return true; - const value = (name: string): unknown => (name in extra ? extra[name] : context.get(name)); - return expression.split("||").some((alternative) => alternative.split("&&").every((raw) => { - const term = raw.trim(); - let m = /^(\S+)\s*=~\s*\/(.*)\/([a-z]*)$/.exec(term); - if (m) return new RegExp(m[2], m[3]).test(String(value(m[1]) ?? "")); - m = /^(\S+)\s*(==|!=)\s*(.+)$/.exec(term); - if (m) { - const right = m[3].trim().replace(/^'(.*)'$/, "$1"); - const equal = String(value(m[1])) === right; - return m[2] === "==" ? equal : !equal; - } - if (term.startsWith("!")) return !value(term.slice(1).trim()); - return Boolean(value(term)); - })); -} - -// ── the frame of a webview ────────────────────────────────────────────────────────────────────── - -let theme: Record = {}; -let themeKind: "vscode-light" | "vscode-dark" | "vscode-high-contrast" = "vscode-light"; -const frames = new Map(); -let nextFrame = 1; - -window.addEventListener("message", (e: MessageEvent) => { - const data = e.data as { __mcppWebview?: string; message?: unknown; state?: unknown; kind?: string }; - if (!data || typeof data.__mcppWebview !== "string") return; - const webview = frames.get(data.__mcppWebview); - if (!webview) return; - if (data.kind === "state") webview.state = data.state; - else webview.received.fire(data.message); -}); - -class Webview { - readonly id = `w${nextFrame++}`; - readonly received = new Emitter(); - readonly onDidReceiveMessage = this.received.event; - options: unknown = {}; - state: unknown = undefined; - private document = ""; - constructor(readonly frame: HTMLIFrameElement) { - frames.set(this.id, this); - } - - /** Where the page's own files are, as a CSP source: `file:` (a scheme) for the files the IDE extracted. */ - get cspSource(): string { - const url = new URL(info.extensionUri); - return url.protocol === "file:" ? "file:" : url.origin; - } - - asWebviewUri(uri: { toString(): string }): { toString(): string } { - return uri; - } - - get html(): string { - return this.document; - } - - set html(document: string) { - this.document = document; - const nonce = /'nonce-([^']+)'/.exec(document)?.[1] ?? ""; - const vars = Object.entries(theme).map(([k, v]) => `${k}: ${v};`).join(" "); - const prelude = `` - + ``; - const withPrelude = /]*>/i.test(document) ? document.replace(/]*>/i, (h) => `${h}${prelude}`) : `${prelude}${document}`; - this.frame.srcdoc = withPrelude; - } - - async postMessage(message: unknown): Promise { - this.frame.contentWindow?.postMessage(message, "*"); - return true; - } -} - -// ── tabs ──────────────────────────────────────────────────────────────────────────────────────── - -interface Tab { - key: string; - title: string; - closable: boolean; - /** A view of the manifest: shown while its `when` holds. */ - when?: string; - body: HTMLElement; - button?: HTMLButtonElement; - shown(): void; - hidden(): void; - closed(): void; -} - -const tabs: Tab[] = []; -let current: Tab | undefined; -let strip: HTMLElement; -let stage: HTMLElement; - -function layout(): void { - if (strip) return; - strip = document.createElement("nav"); - strip.className = "tabs"; - stage = document.createElement("main"); - stage.className = "stage"; - document.body.append(strip, stage); -} - -function renderStrip(): void { - layout(); - strip.replaceChildren(); - for (const tab of tabs) { - if (!when(tab.when)) continue; - const b = document.createElement("button"); - b.className = `tab${tab === current ? " active" : ""}`; - b.textContent = tab.title; - b.onclick = () => select(tab); - if (tab.closable) { - const x = document.createElement("span"); - x.className = "codicon codicon-close close"; - x.title = "Close"; - x.onclick = (e) => { e.stopPropagation(); close(tab); }; - b.append(x); - } - tab.button = b; - strip.append(b); - } -} - -function select(tab: Tab): void { - layout(); - if (current === tab) { renderStrip(); return; } - current?.hidden(); - if (current) current.body.hidden = true; - current = tab; - tab.body.hidden = false; - tab.shown(); - renderStrip(); -} - -function add(tab: Tab): void { - layout(); - tab.body.hidden = true; - stage.append(tab.body); - tabs.push(tab); - renderStrip(); -} - -function close(tab: Tab): void { - const i = tabs.indexOf(tab); - if (i < 0) return; - tabs.splice(i, 1); - tab.body.remove(); - tab.closed(); - if (current === tab) { - current = undefined; - const next = tabs[Math.max(0, i - 1)]; - if (next) select(next); - } - renderStrip(); -} - -contextChanged.event(() => { - renderStrip(); - if (current && !when(current.when)) { - const next = tabs.find((t) => when(t.when)); - if (next) select(next); - } -}); - -/** The views the manifest contributes, in its order, with their titles and `when`. */ -const declaredViews: Array<{ id: string; name?: string; when?: string; type?: string }> = - ((manifest as { contributes?: { views?: Record> } }).contributes?.views?.mcpp) ?? []; - -function viewTab(id: string, body: HTMLElement, shown: () => void, hidden: () => void): Tab { - const declared = declaredViews.find((v) => v.id === id); - const tab: Tab = { key: id, title: nls(declared?.name) || id, closable: false, when: declared?.when, body, shown, hidden, closed() {} }; - // The manifest's order, whatever order the pages registered in. - const order = (key: string) => { const i = declaredViews.findIndex((v) => v.id === key); return i < 0 ? 1000 : i; }; - const at = tabs.findIndex((t) => !t.closable && order(t.key) > order(id)); - layout(); - body.hidden = true; - stage.append(body); - if (at < 0) tabs.splice(tabs.filter((t) => !t.closable).length, 0, tab); else tabs.splice(at, 0, tab); - renderStrip(); - return tab; -} - -// ── tree views ────────────────────────────────────────────────────────────────────────────────── - -interface TreeItemLike { - label: string; id?: string; description?: string; tooltip?: string; contextValue?: string; collapsibleState?: number; - iconPath?: { id: string; color?: { id: string } }; - command?: { command: string; arguments?: unknown[] }; -} -interface Provider { - getChildren(element?: T): T[] | Promise; - getTreeItem(element: T): TreeItemLike | Promise; - onDidChangeTreeData?: (l: () => unknown) => unknown; -} - -const inlineActions: Array<{ command: string; when?: string; group?: string }> = - ((manifest as { contributes?: { menus?: Record> } }).contributes?.menus?.["view/item/context"]) ?? []; -const commandIcons = new Map( - ((manifest as { contributes?: { commands?: Array<{ command: string; title: string; icon?: string }> } }).contributes?.commands ?? []) - .map((c) => [c.command, { title: nls(c.title), icon: c.icon }]), -); - -function treeView(id: string, provider: Provider, run: (command: string, args: unknown[]) => void) { - const body = document.createElement("section"); - body.className = "tree"; - const expanded = new Map(); - const changedVisibility = new Emitter<{ visible: boolean }>(); - let visible = false; - const draw = async (): Promise => { - const rows: HTMLElement[] = []; - const walk = async (element: T | undefined, depth: number): Promise => { - for (const child of await provider.getChildren(element)) { - const item = await provider.getTreeItem(child); - const key = item.id ?? `${depth}:${item.label}`; - const collapsible = (item.collapsibleState ?? 0) !== 0; - const open = expanded.get(key) ?? item.collapsibleState === 2; - const row = document.createElement("div"); - row.className = "row"; - row.style.paddingLeft = `${8 + depth * 16}px`; - row.title = item.tooltip ?? ""; - row.setAttribute("role", "treeitem"); - if (collapsible) row.setAttribute("aria-expanded", String(open)); - const twistie = document.createElement("span"); - twistie.className = `twistie codicon ${collapsible ? (open ? "codicon-chevron-down" : "codicon-chevron-right") : ""}`; - row.append(twistie); - if (item.iconPath?.id) { - const icon = document.createElement("span"); - icon.className = `icon codicon codicon-${item.iconPath.id}`; - if (item.iconPath.color) icon.style.color = `var(--vscode-${item.iconPath.color.id.replace(/\./g, "-")})`; - row.append(icon); - } - const label = document.createElement("span"); - label.className = "label"; - label.textContent = item.label; - row.append(label); - if (item.description) { - const d = document.createElement("span"); - d.className = "description"; - d.textContent = item.description; - row.append(d); - } - for (const action of inlineActions) { - if (!(action.group ?? "").startsWith("inline")) continue; - if (!when(action.when, { view: id, viewItem: item.contextValue ?? "" })) continue; - const declared = commandIcons.get(action.command); - const b = document.createElement("span"); - const icon = /^\$\((.+)\)$/.exec(declared?.icon ?? "")?.[1]; - b.className = `action codicon ${icon ? `codicon-${icon}` : "codicon-arrow-right"}`; - b.title = declared?.title ?? action.command; - b.onclick = (e) => { e.stopPropagation(); run(action.command, [child]); }; - row.append(b); - } - row.onclick = () => { - if (collapsible) { expanded.set(key, !open); void draw(); return; } - if (item.command) run(item.command.command, item.command.arguments ?? []); - }; - rows.push(row); - if (collapsible && open) await walk(child, depth + 1); - } - }; - await walk(undefined, 0); - body.replaceChildren(...rows); - }; - provider.onDidChangeTreeData?.(() => { if (visible) void draw(); }); - const tab = viewTab(id, body, () => { visible = true; changedVisibility.fire({ visible: true }); void draw(); }, () => { visible = false; changedVisibility.fire({ visible: false }); }); - return { - get visible() { return visible; }, - onDidChangeVisibility: changedVisibility.event, - reveal: async () => select(tab), - dispose: () => close(tab), - title: tab.title, - description: undefined as string | undefined, - message: undefined as string | undefined, - badge: undefined as unknown, - }; -} - -// ── webview views and panels ──────────────────────────────────────────────────────────────────── - -function frame(): HTMLIFrameElement { - const f = document.createElement("iframe"); - f.className = "webview"; - return f; -} - -export const workbench = { - /** The IDE's theme, as the `--vscode-*` variables the pages read, and the theme's kind. */ - setTheme(vars: Record, kind: typeof themeKind): void { - theme = vars; - themeKind = kind; - document.documentElement.setAttribute("style", Object.entries(vars).map(([k, v]) => `${k}: ${v}`).join("; ")); - document.body.classList.remove("vscode-light", "vscode-dark", "vscode-high-contrast"); - document.body.classList.add(kind); - }, - - setContext(key: string, value: unknown): void { - context.set(key, value); - contextChanged.fire(); - }, - - /** Brings a view or a panel on screen: the IDE's request, or a page's `.focus`. */ - focus(key: string): void { - const tab = tabs.find((t) => t.key === key) ?? tabs.find((t) => t.key.endsWith(`.${key}`)); - if (tab) select(tab); - }, - - /** The first view whose `when` holds, when nothing is on screen yet. */ - showFirst(): void { - if (current) return; - const tab = tabs.find((t) => when(t.when)); - if (tab) select(tab); - }, - - registerView(id: string, provider: { resolveWebviewView(view: unknown, context: unknown, token: unknown): unknown }) { - const body = document.createElement("section"); - body.className = "view"; - const f = frame(); - body.append(f); - const webview = new Webview(f); - const changed = new Emitter(); - const disposed = new Emitter(); - let visible = false; - let resolved = false; - const view = { - webview, - get visible() { return visible; }, - onDidChangeVisibility: changed.event, - onDidDispose: disposed.event, - show: () => workbench.focus(id), - title: undefined as string | undefined, - description: undefined as string | undefined, - }; - viewTab(id, body, () => { - visible = true; - if (!resolved) { resolved = true; void provider.resolveWebviewView(view, { state: undefined }, { isCancellationRequested: false }); } - changed.fire(); - }, () => { visible = false; changed.fire(); }); - return { dispose() {} }; - }, - - createPanel(viewType: string, title: string, options: unknown) { - const body = document.createElement("section"); - body.className = "panel"; - const f = frame(); - body.append(f); - const webview = new Webview(f); - webview.options = options; - const disposed = new Emitter(); - const viewState = new Emitter<{ webviewPanel: unknown }>(); - let visible = false; - let isDisposed = false; - const panel = { - viewType, - webview, - get title() { return tab.title; }, - set title(t: string) { tab.title = t; renderStrip(); }, - get visible() { return visible; }, - get active() { return visible; }, - iconPath: undefined as unknown, - onDidDispose: disposed.event, - onDidChangeViewState: viewState.event, - reveal: () => select(tab), - dispose: () => { if (!isDisposed) close(tab); }, - }; - const tab: Tab = { - key: `${viewType}#${webview.id}`, title, closable: true, body, - shown: () => { visible = true; viewState.fire({ webviewPanel: panel }); }, - hidden: () => { visible = false; viewState.fire({ webviewPanel: panel }); }, - closed: () => { isDisposed = true; visible = false; frames.delete(webview.id); disposed.fire(); }, - }; - add(tab); - select(tab); - return panel; - }, - - /** A tree view; a row's command goes through `run`, where the page's own commands go. */ - registerTree(id: string, provider: unknown, run: (command: string, args: unknown[]) => Promise) { - return treeView(id, provider as Provider, (command, args) => { - run(command, args).catch((e) => request("log", { text: `${e}\n` })); - }); - }, -}; diff --git a/webui/tests/graph.test.mjs b/webui/tests/graph.test.mjs new file mode 100644 index 0000000..5bf27cd --- /dev/null +++ b/webui/tests/graph.test.mjs @@ -0,0 +1,106 @@ +// The graph page as mcpp-clion shows it, in Chromium (the engine of JCEF): a real `mcpp-ide serve` +// answers `mcpp-ide/graph/packages` and `mcpp-ide/graph/modules` for the mock mcpp's build database, +// and the page draws what it is handed -- every node, the edges, an external module dashed -- says +// so when there is nothing to draw, and asks the IDE to open a module's source when it is clicked. +// +// Run from mide-plugins/vscode: npm run test:webui (it bundles the page first). MCPP_IDE_BACKEND and +// MCPP_IDE_MOCK_MCPP name the programs; else the newest of the repository's target/ are taken. +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { createRequire } from "node:module"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repo = path.resolve(here, "..", ".."); +const extension = path.join(repo, "mide-plugins", "vscode"); +const require = createRequire(path.join(extension, "package.json")); +const { chromium } = require("playwright"); +const rpc = require("vscode-jsonrpc/node"); + +function built(name) { + const env = name === "mcpp-ide" ? process.env.MCPP_IDE_BACKEND : process.env.MCPP_IDE_MOCK_MCPP; + if (env) return env; + const exe = process.platform === "win32" ? `${name}.exe` : name; + let best, bestTime = 0; + const target = path.join(repo, "target"); + if (!fs.existsSync(target)) return undefined; + for (const triple of fs.readdirSync(target, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)) + for (const config of fs.readdirSync(path.join(target, triple), { withFileTypes: true })) { + if (!config.isDirectory()) continue; + const p = path.join(target, triple, config.name, "bin", exe); + if (fs.existsSync(p) && fs.statSync(p).mtimeMs > bestTime) { best = p; bestTime = fs.statSync(p).mtimeMs; } + } + return best; +} + +const page = path.join(extension, ".webui-test", "graph.html"); + +test("the graph page draws the backend's graphs", async (t) => { + const backend = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); + if (!backend || !mock) { t.skip("build the backend first (mcpp build)"); return; } + assert.ok(fs.existsSync(page), "bundle the page first (npm run test:webui)"); + const root = fs.mkdtempSync(path.join(os.tmpdir(), "mcpp-graph-")); + fs.mkdirSync(path.join(root, "src")); + fs.writeFileSync(path.join(root, "mcpp.toml"), '[package]\nname = "app"\nversion = "0.1.0"\n'); + + const child = spawn(backend, ["serve"], { stdio: ["pipe", "pipe", "inherit"] }); + const connection = rpc.createMessageConnection(new rpc.StreamMessageReader(child.stdout), new rpc.StreamMessageWriter(child.stdin)); + connection.listen(); + const browser = await chromium.launch(); + try { + await connection.sendRequest("initialize", { + processId: process.pid, + workspaceFolders: [{ uri: pathToFileURL(root).href, name: "app" }], + capabilities: {}, + initializationOptions: { editor: "jetbrains", settings: { "mcpp.path": mock } }, + }); + const packages = await connection.sendRequest("mcpp-ide/graph/packages", { root }); + const modules = await connection.sendRequest("mcpp-ide/graph/modules", { root }); + assert.ok(packages.nodes.length > 0 && modules.nodes.length > 0, JSON.stringify({ packages, modules }).slice(0, 400)); + + const tab = await browser.newPage(); + const opened = []; + await tab.exposeFunction("mcppHostSend", (text) => opened.push(JSON.parse(text))); + await tab.goto(pathToFileURL(page).href); + const draw = (graph, kind) => tab.evaluate(([g, k]) => window.mcppRender({ + kind: k, graph: g, title: `${k} graph`, empty: "nothing to draw", truncated: "the first {0} nodes", dark: false, + }), [graph, kind]); + + await draw(packages, "packages"); + const drawn = await tab.locator("#graph svg").innerHTML(); + for (const node of packages.nodes) assert.ok(drawn.includes(node.id), `${node.id} is not drawn`); + assert.equal(await tab.locator("#title").textContent(), "packages graph"); + + await draw(modules, "modules"); + assert.equal(await tab.locator("#graph g.node").count(), modules.nodes.length); + assert.equal(await tab.locator("#graph g.node.external").count(), modules.nodes.filter((n) => n.kind === "external").length); + const withSource = modules.nodes.findIndex((n) => n.source); + if (withSource >= 0) { + await tab.locator(`#graph g.node[id*="-n${withSource}-"]`).click(); + await tab.waitForTimeout(200); + assert.deepEqual(opened, [{ open: modules.nodes[withSource].source }]); + } + + await draw({ nodes: [], edges: [] }, "modules"); + assert.equal(await tab.locator("#note").textContent(), "nothing to draw"); + assert.equal(await tab.locator("#graph svg").count(), 0); + + // A large graph is cut, and says where. + const many = { nodes: Array.from({ length: 5 }, (_, i) => ({ id: `m${i}` })), edges: [] }; + await tab.evaluate((g) => window.mcppRender({ kind: "modules", graph: g, title: "t", empty: "", truncated: "the first {0} nodes", dark: true, limit: 3 }), many); + assert.equal(await tab.locator("#note").textContent(), "the first 3 nodes"); + assert.equal(await tab.locator("#graph g.node").count(), 3); + } finally { + await browser.close(); + const ended = new Promise((resolve) => child.on("exit", resolve)); + await connection.sendRequest("shutdown").catch(() => undefined); + await connection.sendNotification("exit").catch(() => undefined); + await ended; + connection.dispose(); + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/webui/tests/workbench.test.mjs b/webui/tests/workbench.test.mjs deleted file mode 100644 index ca021a2..0000000 --- a/webui/tests/workbench.test.mjs +++ /dev/null @@ -1,199 +0,0 @@ -// The workbench as mcpp-clion shows it, in Chromium (the engine of JCEF), with a host that answers -// the bridge as Workbench.kt does -- the backend's methods over a real `mcpp-ide serve` answering for -// the mock mcpp, files from disk, settings in memory, dialogs recorded -- and the synchronous channel -// as the JS dialog handler answers it. mcpp-vscode's pages must run in it unchanged: the views as -// tabs, the project tree, the cache page over `mcpp cache list`, the libraries over an index and a -// package's page, the settings page writing a setting, and the graph page. -// -// Run from mide-plugins/vscode: npm run test:webui (it bundles the workbench and compiles the -// extension first). MCPP_IDE_BACKEND and MCPP_IDE_MOCK_MCPP name the programs; else the newest of -// the repository's target/ are taken. -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; -import { createRequire } from "node:module"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -const here = path.dirname(fileURLToPath(import.meta.url)); -const repo = path.resolve(here, "..", ".."); -const extension = path.join(repo, "mide-plugins", "vscode"); -const require = createRequire(path.join(extension, "package.json")); -const { chromium } = require("playwright"); -const { Backend } = require(path.join(extension, "dist", "src", "ide", "backend", "client.js")); - -function built(name) { - const env = name === "mcpp-ide" ? process.env.MCPP_IDE_BACKEND : process.env.MCPP_IDE_MOCK_MCPP; - if (env) return env; - const exe = process.platform === "win32" ? `${name}.exe` : name; - let best, bestTime = 0; - const walk = (dir, depth) => { - if (depth > 4) return; - let entries = []; - try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } - for (const e of entries) { - const p = path.join(dir, e.name); - if (e.isDirectory()) walk(p, depth + 1); - else if (e.name === exe && path.basename(dir) === "bin") { const t = fs.statSync(p).mtimeMs; if (t > bestTime) { best = p; bestTime = t; } } - } - }; - walk(path.join(repo, "target"), 0); - return best; -} - -/** A project, a home with one index descriptor, and a target/ with artifacts to measure. */ -function fixture() { - const base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "mcpp-workbench-"))); - const root = path.join(base, "app"); - fs.mkdirSync(path.join(root, "src"), { recursive: true }); - fs.writeFileSync(path.join(root, "mcpp.toml"), '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies.compat]\nargparse = "3.2"\n'); - fs.writeFileSync(path.join(root, "src", "main.cpp"), "int main() {}\n"); - fs.mkdirSync(path.join(root, "target", "x86_64-linux-gnu", "obj"), { recursive: true }); - fs.writeFileSync(path.join(root, "target", "x86_64-linux-gnu", "obj", "main.o"), Buffer.alloc(4096)); - const home = path.join(base, "home"); - fs.mkdirSync(path.join(home, "indices", "mcpplibs", "pkgs", "c"), { recursive: true }); - fs.writeFileSync(path.join(home, "indices", "mcpplibs", "pkgs", "c", "compat.argparse.lua"), - 'package = { name = "argparse", namespace = "compat", description = "Argument parser", xpm = { linux = { ["3.2"] = {} }, macosx = { ["3.2"] = {} }, windows = { ["3.2"] = {} } } }'); - return { base, root, home }; -} - -function statJson(p, follow) { - try { - const s = follow ? fs.statSync(p) : fs.lstatSync(p); - return { type: s.isSymbolicLink() ? "symlink" : s.isDirectory() ? "directory" : s.isFile() ? "file" : "other", size: s.size, mtimeMs: s.mtimeMs, ino: Number(s.ino % 4294967296) }; - } catch { return null; } -} - -/** What Workbench.kt answers, for the bridge's actions. */ -function host({ backend, root, home, dist, recorded }) { - const settings = { user: { "mcpp.library.indexPath": "" }, workspace: {} }; - const files = (action, p) => { - switch (action) { - case "fs.stat": return statJson(p.path, p.follow !== false); - case "fs.read": try { return fs.readFileSync(p.path, "utf8"); } catch { return null; } - case "fs.list": try { return fs.readdirSync(p.path).map((name) => ({ name, ...statJson(path.join(p.path, name), false) })); } catch { return null; } - case "fs.realpath": try { return fs.realpathSync(p.path); } catch { return null; } - case "fs.write": fs.mkdirSync(path.dirname(p.path), { recursive: true }); fs.writeFileSync(p.path, p.base64 ? Buffer.from(p.text, "base64") : p.text); return true; - } - throw new Error(`unknown file action ${action}`); - }; - return { - files, - async handle(action, p) { - recorded.push({ action, params: p }); - switch (action) { - case "init": - return { - info: { - platform: process.platform, arch: process.arch, language: "en", home: os.homedir(), tmp: os.tmpdir(), - env: { MCPP_HOME: home }, extensionUri: `${pathToFileURL(dist).href}/`, workspaceFolders: [root], trusted: true, - }, - theme: { "--vscode-foreground": "#333", "--vscode-editor-background": "#fff", "--vscode-font-family": "sans-serif", "--vscode-font-size": "13px" }, - themeKind: "vscode-light", - mode: "window", - }; - case "rpc": return backend.request(p.method, p.params); - case "settings.read": return settings; - case "settings.write": settings[p.level === "user" ? "user" : "workspace"][p.key] = p.value; if (p.value === null) delete settings[p.level === "user" ? "user" : "workspace"][p.key]; return settings; - case "progress.start": return 1; - case "message": case "input": case "pick": case "saveDialog": return null; - case "findFiles": return []; - default: - if (action.startsWith("fs.")) return files(action, p); - return null; - } - }, - }; -} - -test("mcpp-vscode's pages run in the workbench, as mcpp-clion shows them", { timeout: 180_000 }, async (t) => { - const program = built("mcpp-ide"), mock = built("mcpp-ide-mock-mcpp"); - if (!program || !mock) { t.skip("build the backend first (mcpp build)"); return; } - const dist = process.env.MCPP_WEBUI_DIST ?? path.join(extension, ".webui-test"); - assert.ok(fs.existsSync(path.join(dist, "workbench.js")), "bundle the workbench first (npm run bundle:webui -- --out .webui-test)"); - const { base, root, home } = fixture(); - const backend = new Backend({ program, env: { ...process.env, HOME: home } }); - await backend.request("initialize", { editor: "jetbrains", settings: { "mcpp.path": mock }, workspaceFolders: [root], trusted: true }); - const recorded = []; - const h = host({ backend, root, home, dist, recorded }); - const browser = await chromium.launch(); - const page = await browser.newPage(); - const errors = []; - const consoleLines = []; - page.on("pageerror", (e) => errors.push(String(e))); - page.on("console", (m) => consoleLines.push(`${m.type()}: ${m.text()}`)); - page.on("dialog", async (dialog) => { - const message = dialog.message(); - if (dialog.type() !== "prompt" || !message.startsWith("mcpp-host:")) { await dialog.dismiss(); return; } - const { action, params } = JSON.parse(message.slice("mcpp-host:".length)); - let answer; - try { answer = { ok: true, result: h.files(action, params) }; } catch (e) { answer = { ok: false, error: String(e) }; } - await dialog.accept(JSON.stringify(answer)); - }); - await page.exposeBinding("mcppHostSend", async (_source, text) => { - const m = JSON.parse(text); - let reply; - try { reply = { type: "result", id: m.id, result: (await h.handle(m.action, m.params)) ?? null }; } - catch (e) { reply = { type: "error", id: m.id, error: { message: e instanceof Error ? e.message : String(e) } }; } - await page.evaluate((r) => window.mcppHostReceive(r), reply).catch(() => undefined); - }); - const notify = (method, params) => page.evaluate(([m, p]) => window.mcppHostReceive({ type: "notify", method: m, params: p }), [method, params]); - try { - await page.goto(pathToFileURL(path.join(dist, "index.html")).href); - // The views, in the manifest's order and names. - await page.waitForSelector(".tabs .tab"); - assert.deepEqual(await page.$$eval(".tabs .tab", (tabs) => tabs.map((t) => t.textContent.trim())), ["Project", "Libraries", "Cache"]); - - // The project tree: 0.9.0's rows for this project. - await page.waitForSelector(".tree .row"); - const rows = await page.$$eval(".tree .row .label", (labels) => labels.map((l) => l.textContent)); - assert.ok(rows.length > 0, "the project tree has rows"); - - // The cache page, over `mcpp cache list` through the backend. - await page.click(".tabs .tab:nth-child(3)"); - const cacheFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); - await page.waitForFunction((f) => f.contentDocument?.body?.textContent?.includes("Global build cache"), cacheFrame, { timeout: 30_000 }); - const cacheText = await cacheFrame.evaluate((f) => f.contentDocument.body.textContent); - assert.match(cacheText, /Project cache/); - - // The libraries page, over the index under MCPP_HOME. - await page.click(".tabs .tab:nth-child(2)"); - const libraryFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); - await page.waitForFunction((f) => f.contentDocument?.querySelector("[data-row]")?.textContent?.includes("argparse"), libraryFrame, { timeout: 30_000 }); - - // A package's page: the row opens it as a panel, a tab of its own. - await libraryFrame.evaluate((f) => f.contentDocument.querySelector("[data-row]").click()); - await page.waitForFunction(() => document.querySelectorAll(".tabs .tab .close").length === 1, null, { timeout: 15_000 }); - const detailFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); - await page.waitForFunction((f) => f.contentDocument?.body?.textContent?.includes("Argument parser"), detailFrame, { timeout: 30_000 }); - - // The settings page; a change it makes reaches the IDE's store. - await notify("show", { view: "settings" }); - await page.waitForFunction(() => [...document.querySelectorAll(".tabs .tab")].some((t) => t.textContent.includes("settings")), null, { timeout: 15_000 }); - const settingsFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); - await page.waitForFunction((f) => f.contentDocument?.querySelector("input[type=checkbox][data-key]"), settingsFrame, { timeout: 30_000 }); - const writesBefore = recorded.filter((r) => r.action === "settings.write").length; - await settingsFrame.evaluate((f) => f.contentDocument.querySelector("input[type=checkbox][data-key]").click()); - const deadline = Date.now() + 10_000; - while (recorded.filter((r) => r.action === "settings.write").length === writesBefore && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100)); - assert.ok(recorded.filter((r) => r.action === "settings.write").length > writesBefore, "the settings page wrote a setting"); - - // The dependency graph page opens and names the project. - await notify("show", { view: "graph:packages" }); - const graphFrame = await page.waitForSelector(".stage > section:not([hidden]) iframe.webview"); - await page.waitForFunction((f) => f.contentDocument?.querySelector("#project-name")?.textContent === "app", graphFrame, { timeout: 30_000 }); - - assert.deepEqual(errors, [], "the workbench raised no error"); - assert.deepEqual(consoleLines.filter((l) => l.startsWith("error:")), [], "no page logged an error (a CSP refusal among them)"); - } catch (error) { - const body = await page.evaluate(() => document.body.innerText).catch(() => ""); - const logs = recorded.filter((r) => r.action === "log").map((r) => r.params.text); - console.error(`-- page --\n${body}\n-- errors --\n${errors.join("\n")}\n-- console --\n${consoleLines.join("\n")}\n-- log --\n${logs.join("")}`); - throw error; - } finally { - await browser.close(); - await backend.stop(); - fs.rmSync(base, { recursive: true, force: true }); - } -}); diff --git a/webui/tsconfig.json b/webui/tsconfig.json index 89fe55f..7ce9774 100644 --- a/webui/tsconfig.json +++ b/webui/tsconfig.json @@ -3,32 +3,11 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", - "lib": [ - "ES2022", - "DOM" - ], + "lib": ["ES2022", "DOM"], "strict": true, "noEmit": true, - "resolveJsonModule": true, - "esModuleInterop": true, "skipLibCheck": true, - "types": [ - "node" - ], - "baseUrl": ".", - "paths": { - "@noble/hashes/*": [ - "../mide-plugins/vscode/node_modules/@noble/hashes/*" - ], - "buffer": [ - "../mide-plugins/vscode/node_modules/buffer" - ] - }, - "typeRoots": [ - "../mide-plugins/vscode/node_modules/@types" - ] + "types": [] }, - "include": [ - "src/**/*.ts" - ] + "include": ["src/**/*.ts"] } diff --git a/webui/workbench.css b/webui/workbench.css deleted file mode 100644 index ebb8141..0000000 --- a/webui/workbench.css +++ /dev/null @@ -1,38 +0,0 @@ -/* The workbench's own chrome: a tab per view and panel, as the IDE's tool window shows tabs, in the - theme's colours (the --vscode-* variables the IDE sets on the root). */ -html, body { height: 100%; margin: 0; } -body { - display: flex; flex-direction: column; overflow: hidden; - color: var(--vscode-foreground); background: var(--vscode-sideBar-background, var(--vscode-editor-background)); - font-family: var(--vscode-font-family); font-size: var(--vscode-font-size); -} -.tabs { - display: flex; flex: none; gap: 2px; overflow-x: auto; padding: 0 4px; - border-bottom: 1px solid var(--vscode-panel-border); -} -.tab { - display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border: 0; border-bottom: 2px solid transparent; - background: none; color: var(--vscode-descriptionForeground); font: inherit; cursor: pointer; white-space: nowrap; -} -.tab:hover { color: var(--vscode-foreground); } -.tab.active { color: var(--vscode-foreground); border-bottom-color: var(--vscode-focusBorder); } -.tab .close { font-size: 12px; opacity: 0.6; } -.tab .close:hover { opacity: 1; } -.stage { flex: 1; min-height: 0; position: relative; } -.stage > section { position: absolute; inset: 0; overflow: auto; } -.stage > section[hidden] { display: none; } -iframe.webview { width: 100%; height: 100%; border: 0; display: block; background: var(--vscode-editor-background); } -.tree { padding: 4px 0; } -.tree .row { - display: flex; align-items: center; gap: 4px; height: 22px; padding-right: 8px; cursor: pointer; white-space: nowrap; -} -.tree .row:hover { background: var(--vscode-list-hoverBackground); } -.tree .twistie { width: 16px; flex: none; } -.tree .icon { flex: none; } -.tree .label { overflow: hidden; text-overflow: ellipsis; } -.tree .description { color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; margin-left: 4px; font-size: 0.92em; } -.tree .action { margin-left: auto; opacity: 0; } -.tree .row:hover .action { opacity: 0.8; } -.tree .action:hover { opacity: 1; } -/* The IDE's settings dialog shows the settings page alone. */ -body.settings-only .tabs { display: none; } From 8fc8eb58b2c352eb7779d555b3b1fc93c9008c69 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 00:25:33 +0900 Subject: [PATCH 100/119] the workspace's mcpp is the one CI installs, 2026.10.31.1 (.github/versions.env) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .xlings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.xlings.json b/.xlings.json index 8eff4f4..e042017 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.10.27.1" + "mcpp": "2026.10.31.1" } } From 9f064bc712f7d8d2c1d7fc06fa355bdf115aa082 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 00:44:01 +0900 Subject: [PATCH 101/119] CI: the manifest check's script holds no apostrophe, which closed its single quotes Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d40006..90047ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,7 +189,7 @@ jobs: const fail = (message) => { console.error(message); process.exit(1); }; if (p.extensionDependencies?.[0] !== "sunrisepeak.mcpp-language-server") fail("the dependency moved"); if (p.extensionDependencies.some((id) => id.includes("clangd"))) fail("clangd is back in the dependency list"); - // The backend's language client is bundled into dist/vendor; nothing is a runtime dependency. + // The language client of the backend is bundled into dist/vendor; nothing is a runtime dependency. if (Object.keys(p.dependencies ?? {}).length) fail("a runtime dependency crept in: the VSIX carries no node_modules"); if ((p.activationEvents ?? []).some((event) => event.startsWith("onCommand:"))) fail("onCommand activations are derived since 1.74"); if (!p.contributes?.viewsContainers?.activitybar?.length) fail("the view container disappeared"); From c58a5299e3c42410ab49c0a00d2448ae42b143ce Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 01:09:31 +0900 Subject: [PATCH 102/119] CLion: the UI tests run over the application alone (ApplicationRule, EDT), not a light project on temp://, where CLion's bundled STM32 startup activity fails Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../github/mcppcommunity/mcppclion/UiTest.kt | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/UiTest.kt b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/UiTest.kt index 9dfd88c..a43d5c9 100644 --- a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/UiTest.kt +++ b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/UiTest.kt @@ -1,12 +1,16 @@ // CLion's own views over the backend's view models (mcpp design 2026-10-30 part 2 §9), headless: the // project tree and the cache's rows from the answers the backend gives, a package's page, and the -// settings form from a registry -- its fields, its search, Apply's changes and Reset. +// settings form from a registry -- its fields, its search, Apply's changes and Reset. They need the +// application (icons, Swing) and no project: a light project lives on temp://, where a bundled +// plugin's startup activity (CLion's STM32) fails. package io.github.mcppcommunity.mcppclion import com.google.gson.JsonElement import com.google.gson.JsonParser import com.google.gson.JsonPrimitive -import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.ApplicationRule +import com.intellij.testFramework.EdtRule +import com.intellij.testFramework.RunsInEdt import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBTextField import io.github.mcppcommunity.mcppclion.settings.SettingsForm @@ -14,8 +18,21 @@ import io.github.mcppcommunity.mcppclion.ui.CacheModel import io.github.mcppcommunity.mcppclion.ui.LibraryPage import io.github.mcppcommunity.mcppclion.ui.ProjectModel import io.github.mcppcommunity.mcppclion.ui.RowNode +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.ClassRule +import org.junit.Rule +import org.junit.Test + +@RunsInEdt +class UiTest { + companion object { + @ClassRule @JvmField val application = ApplicationRule() + } + + @get:Rule val edt = EdtRule() -class UiTest : BasePlatformTestCase() { private val t: (String, Array) -> String = { key, args -> var text = key args.forEachIndexed { i, a -> text = text.replace("{$i}", a.toString()) } @@ -26,7 +43,8 @@ class UiTest : BasePlatformTestCase() { private fun texts(node: RowNode): List = (0 until node.childCount).map { (node.getChildAt(it) as RowNode).row.text } - fun testTheProjectTreeFromTheBackendsAnswers() { + @Test + fun theProjectTreeFromTheBackendsAnswers() { val project = json("""{"root": "/w/app", "name": "app"}""") val summary = json("""{"targets": [{"name": "app", "kind": "executable", "configuration": "dev"}], "tests": [{"name": "test_smoke", "kind": "test"}], "libraries": [], @@ -46,7 +64,8 @@ class UiTest : BasePlatformTestCase() { assertTrue((missing.getChildAt(0) as RowNode).row.error) } - fun testTheCacheRowsFromTheInventory() { + @Test + fun theCacheRowsFromTheInventory() { val inventory = json("""{"root": "/home/u/.mcpp", "totalBytes": 3, "totalEntries": 2, "totalText": "1.5 GiB", "byKind": [{"kind": "bmi", "entries": 2, "bytes": 3, "bytesText": "1.5 GiB"}], "topLabels": [{"label": "std", "entries": 1, "bytes": 2, "bytesText": "1.0 GiB"}], @@ -58,7 +77,8 @@ class UiTest : BasePlatformTestCase() { assertEquals(1, rows[3].childCount) // an empty bucket is not a row } - fun testAPackagesPage() { + @Test + fun aPackagesPage() { val html = LibraryPage.html(json("""{"id": "mcpplibs:os", "description": "the OS ", "index": "mcpplibs", "dependencyName": "mcpplibs.os", "latest": "0.2.4", "versions": ["0.2.4", "0.2.3"], "licenses": ["Apache-2.0"], "repo": "https://github.com/mcpplibs/os"}""")) { it } @@ -68,7 +88,8 @@ class UiTest : BasePlatformTestCase() { assertTrue(html.contains("href=\"https://github.com/mcpplibs/os\"")) } - fun testTheSettingsFormFromARegistry() { + @Test + fun theSettingsFormFromARegistry() { val registry = json("""{"groups": [{"id": "project", "order": 10, "title": "Project"}, {"id": "build", "order": 20, "title": "Build"}], "settings": [ {"key": "mcpp.path", "type": "string", "default": "", "group": "project", "tier": "public", "title": "mcpp path", "description": "the mcpp to run"}, From 9400c3f637ceb824d028aa737d85a8f8d14b0429 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 01:34:55 +0900 Subject: [PATCH 103/119] CLion: whether the mcppls plugin is enabled through PluginManager.findEnabledPlugin, not PluginManagerCore's internal getPlugin (verifier: INTERNAL_API_USAGES on 2026.2); the index box's renderer by its customizer Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt | 2 +- .../io/github/mcppcommunity/mcppclion/ui/LibraryView.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index f5fb4e6..bef7ab0 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -121,7 +121,7 @@ class McppSession(private val project: Project) : Disposable { */ fun mcpplsServes(): Boolean = runCatching { val id = com.intellij.openapi.extensions.PluginId.getId(MCPPLS_PLUGIN_ID) - com.intellij.ide.plugins.PluginManagerCore.getPlugin(id) != null && !com.intellij.ide.plugins.PluginManagerCore.isDisabled(id) + com.intellij.ide.plugins.PluginManager.getInstance().findEnabledPlugin(id) != null }.getOrDefault(false) /** The plugin's directory: the parent of the lib/ holding this class's jar. */ diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/LibraryView.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/LibraryView.kt index a0c103b..d8d8722 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/LibraryView.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/LibraryView.kt @@ -83,7 +83,9 @@ class LibraryView(private val project: Project, parent: Disposable) : McppView, init { Disposer.register(parent, this) index.model = DefaultComboBoxModel(arrayOf("")) - index.renderer = com.intellij.ui.SimpleListCellRenderer.create("") { it.ifEmpty { session.t("All indexes") } } + index.renderer = com.intellij.ui.SimpleListCellRenderer.create { label, value, _ -> + label.text = (value ?: "").ifEmpty { session.t("All indexes") } + } list.cellRenderer = object : ColoredListCellRenderer() { override fun customizeCellRenderer(list: JList, value: JsonObject?, index: Int, selected: Boolean, hasFocus: Boolean) { value ?: return From 56919eddaa9b4380d12e572c657d542d566285a4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 01:54:17 +0900 Subject: [PATCH 104/119] CLion: the mcppls plugin serves when PluginManagerCore.isLoaded says so, the public question; findEnabledPlugin is internal on 2026.2 too Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index bef7ab0..e820d75 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -121,7 +121,7 @@ class McppSession(private val project: Project) : Disposable { */ fun mcpplsServes(): Boolean = runCatching { val id = com.intellij.openapi.extensions.PluginId.getId(MCPPLS_PLUGIN_ID) - com.intellij.ide.plugins.PluginManager.getInstance().findEnabledPlugin(id) != null + com.intellij.ide.plugins.PluginManagerCore.isLoaded(id) }.getOrDefault(false) /** The plugin's directory: the parent of the lib/ holding this class's jar. */ From 280b8e6ee2127665df20d3f11d5751cc2d807fe6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 03:22:10 +0900 Subject: [PATCH 105/119] CI and the workspace use mcpp 2026.10.31.2, which states test-launch Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 9 +++++---- .xlings.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index 379b561..910d306 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,14 +1,15 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. # The released mcpp CI installs and uses: it states its features (`features`, the project's mcpp of -# part 2 §4.3) and builds the backend's ok images on openkal-platform 0.18.0. The mock replays the -# control contract the fixtures were captured from (protocol/fixtures/version.txt). -MCPP_VERSION=2026.10.31.1 +# part 2 §4.3), test-launch among them (2026.10.31.2), and builds the backend's ok images on +# openkal-platform 0.18.0. The mock replays the control contract the fixtures were captured from +# (protocol/fixtures/version.txt). +MCPP_VERSION=2026.10.31.2 # A ref of mcpp-community/mcpp CI builds mcpp from and uses instead, for a contract not yet released. # Empty: the release above is used. MCPP_SOURCE_REF= # The LLVM the backend builds with on macOS and Windows (the targets mcpp reaches there with it). LLVM_VERSION=23.1.3 -# The xlings mcpp 2026.10.31.1 bundles, which reads the current index. +# The xlings mcpp 2026.10.31.2 bundles, which reads the current index. XLINGS_VERSION=v2026.10.26.1 # The oldest VS Code the extension supports (engines.vscode), which its tests run in. VSCODE_VERSION=1.91.0 diff --git a/.xlings.json b/.xlings.json index e042017..5616698 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.10.31.1" + "mcpp": "2026.10.31.2" } } From d0e0535ee3149030950952d667e1d55e76d7e93c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 03:49:11 +0900 Subject: [PATCH 106/119] mcpplibs.os 0.2.4 and openkal-platform 0.19.0 for the ok images (openkal 0.18, whose okrun maps the image it runs), mcpp >= 2026.10.31.2; CI's macOS runs on macos-15 -- GitHub retires macos-14 on 2026-11-02 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 12 +++--- CHANGELOG.md | 7 ++-- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.lock | 49 +++++++++++----------- mcpp.toml | 13 ++++-- mide-plugins/vscode/test/artifacts.test.ts | 2 +- 7 files changed, 47 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90047ad..170c9f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,9 @@ jobs: include: - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', pack: true } - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - - { os: macos-14, platform: darwin-arm64, llvm: 'true' } + - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.18.0' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.19.0' } defaults: run: shell: bash @@ -95,7 +95,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-14, windows-latest] + os: [ubuntu-latest, macos-15, windows-latest] runs-on: ${{ matrix.os }} timeout-minutes: 30 defaults: @@ -256,7 +256,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-14, windows-latest] + os: [ubuntu-latest, macos-15, windows-latest] runs-on: ${{ matrix.os }} timeout-minutes: 45 defaults: @@ -310,7 +310,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-14, windows-latest] + os: [ubuntu-latest, macos-15, windows-latest] runs-on: ${{ matrix.os }} timeout-minutes: 30 defaults: @@ -530,7 +530,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-24.04, ubuntu-24.04-arm, macos-14, windows-2022] + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, windows-2022] defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 2659cbd..dbea2f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,10 +110,11 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 sessions through `XDebuggerManager.newSessionBuilder`, the debugger driver through `createDriverConfiguration(project, CPPEnvironment(toolchain))`; packing is a job over the backend's `pack/formats`. -- **mcpp 2026.10.30.1.** One pack of the target set `openkal` writes the editor packages' tree and +- **mcpp 2026.10.31.2.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads - `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`. The backend is on mcpplibs.os 0.2.3 and - openkal-platform 0.18.0 (openkal-linux 0.18.3, mcpp#857). + `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`, built over a platform other than mcpp's own + openkal rows'. The backend is on mcpplibs.os 0.2.4 and openkal-platform 0.19.0, ok images included + (openkal 0.18, whose okrun maps the image it runs). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. diff --git a/README.md b/README.md index 277385d..d0155be 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.18.0) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.19.0) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index a918837..0b05b11 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.18.0 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.19.0 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.lock b/mcpp.lock index 7c6a938..9545f48 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -15,62 +15,63 @@ targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal"] namespace = "mcpplibs" -version = "0.17.1" -source = "index+mcpplibs@0.17.1" -hash = "fnv1a:d9a6fd1043f0f322" +version = "0.18.0" +source = "index+mcpplibs@0.18.0" +hash = "fnv1a:49329d1082d25374" targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."openkal-ld"] namespace = "mcpplibs" -version = "0.1.3" -source = "index+mcpplibs@0.1.3" -hash = "fnv1a:c45ba7726c74fc44" +version = "0.1.5" +source = "index+mcpplibs@0.1.5" +hash = "fnv1a:c45ba5726c74f8de" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-linux"] namespace = "mcpplibs" -version = "0.18.3" -source = "index+mcpplibs@0.18.3" -hash = "fnv1a:2947b44ddbcf8660" +version = "0.19.0" +source = "index+mcpplibs@0.19.0" +hash = "fnv1a:20ab944dd6f2acb2" [package."openkal-llvm-runtime"] namespace = "mcpplibs" -version = "0.16.6" -source = "index+mcpplibs@0.16.6" -hash = "fnv1a:1310f13360b2df3f" +version = "0.16.10" +source = "index+mcpplibs@0.16.10" +hash = "fnv1a:189ed74d4fe89362" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-musl"] namespace = "mcpplibs" -version = "0.22.6" -source = "index+mcpplibs@0.22.6" -hash = "fnv1a:de2d14df84a5a777" +version = "0.23.0" +source = "index+mcpplibs@0.23.0" +hash = "fnv1a:d4b76fdf7f0ffb0a" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-platform"] namespace = "mcpplibs" -version = "0.17.3" -source = "index+mcpplibs@0.17.3" -hash = "fnv1a:d470e612205b7986" +version = "0.19.0" +source = "index+mcpplibs@0.19.0" +hash = "fnv1a:e4c15312294f983d" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-tools"] namespace = "mcpplibs" -version = "0.1.3" -source = "index+mcpplibs@0.1.3" -hash = "fnv1a:3a0aa473e0da7eaf" +version = "0.1.5" +source = "index+mcpplibs@0.1.5" +hash = "fnv1a:3a0aa273e0da7b49" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."mcpplibs.os"] namespace = "mcpplibs" -version = "0.2.3" -source = "index+mcpplibs@0.2.3" -hash = "fnv1a:d0be95a500330418" +version = "0.2.4" +source = "index+mcpplibs@0.2.4" +hash = "fnv1a:d0be9ca500330ffd" +targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."nlohmann.json"] namespace = "nlohmann" diff --git a/mcpp.toml b/mcpp.toml index e946e25..6a610cc 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -14,7 +14,7 @@ description = "The IDE backend for mcpp: sessions, jobs, records and view models license = "Apache-2.0" authors = ["mcpp-community"] repo = "https://github.com/mcpp-community/mcpp-ide" -mcpp = ">=2026.10.27.1" +mcpp = ">=2026.10.31.2" [language] standard = "c++23" @@ -33,6 +33,12 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" +# The platform of the ok images the editor packages carry: 0.19.0's version group is openkal 0.18's, +# which mcpplibs.os 0.2.4 is written against and whose okrun maps the image it runs; mcpp's own +# openkal rows pin 0.18.0. +[target.'cfg(os = "openkal")'] +platform = "openkal-platform@0.19.0" + # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] toolchain = "llvm@23.1.3" @@ -68,10 +74,9 @@ kind = "bin" main = "src/bin/dev.cpp" [dependencies.mcpplibs] -# os 0.2.3: openkal-linux 0.18.3 (a program is found in a directory the process may search but not -# read, mcpp#857). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, +# os 0.2.4: openkal 0.18 (openkal-linux 0.19.0, openkal-macos 0.16.0, openkal-windows 0.14.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, # so the package declares no openkal-tools of its own beside the platform's. -os = "0.2.3" +os = "0.2.4" [dependencies.nlohmann] json = "3.12.0" diff --git a/mide-plugins/vscode/test/artifacts.test.ts b/mide-plugins/vscode/test/artifacts.test.ts index 1bb1771..a5ba7d9 100644 --- a/mide-plugins/vscode/test/artifacts.test.ts +++ b/mide-plugins/vscode/test/artifacts.test.ts @@ -679,7 +679,7 @@ test("CI runs the gates, the package checks, the drift check and every e2e varia assert.match(workflow, /NODE_VERSION: 22/); // The unit gates, the Extension Host e2e and the isolated install all run on Linux, macOS and // Windows, where the spawn and path handling actually differ. - const osMatrixes = workflow.match(/os: \[ubuntu-latest, macos-14, windows-latest\]/g) ?? []; + const osMatrixes = workflow.match(/os: \[ubuntu-latest, macos-15, windows-latest\]/g) ?? []; assert.equal(osMatrixes.length, 3, "gates, the Extension Host e2e and the isolated install must run on three systems"); for (const job of ["vscode-gates:", "vscode-drift:", "vscode:", "vscode-e2e:", "vscode-isolated-install:", "vscode-graph-browser:"]) { assert.match(workflow, new RegExp(`^ ${job}`, "m"), job); From 0abc6efb16a4388aac47e6faf2246abbc14c34dd Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 18:34:00 +0900 Subject: [PATCH 107/119] openkal-platform 0.19.1 for the ok images: okrun releases the file it ran once the image is loaded (openkal-image 0.1.6) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 4 ++-- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.lock | 40 ++++++++++++++++++++-------------------- mcpp.toml | 8 ++++---- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 170c9f4..af562fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.19.0' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.19.1' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index dbea2f6..e4d8b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,8 +113,8 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 - **mcpp 2026.10.31.2.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`, built over a platform other than mcpp's own - openkal rows'. The backend is on mcpplibs.os 0.2.4 and openkal-platform 0.19.0, ok images included - (openkal 0.18, whose okrun maps the image it runs). + openkal rows'. The backend is on mcpplibs.os 0.2.4 and openkal-platform 0.19.1, ok images included + (openkal 0.18, whose okrun maps the image it runs and releases it once the image is loaded). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. diff --git a/README.md b/README.md index d0155be..13e1181 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.19.0) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.19.1) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index 0b05b11..c465932 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.19.0 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.19.1 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.lock b/mcpp.lock index 9545f48..21e4f50 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -22,11 +22,11 @@ targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."openkal-ld"] namespace = "mcpplibs" -version = "0.1.5" -source = "index+mcpplibs@0.1.5" -hash = "fnv1a:c45ba5726c74f8de" +version = "0.1.6" +source = "index+mcpplibs@0.1.6" +hash = "fnv1a:c45ba4726c74f72b" origin = "platform" -targets = ["aarch64-openkal", "x86_64-openkal"] +targets = ["x86_64-openkal"] [package."openkal-linux"] namespace = "mcpplibs" @@ -36,35 +36,35 @@ hash = "fnv1a:20ab944dd6f2acb2" [package."openkal-llvm-runtime"] namespace = "mcpplibs" -version = "0.16.10" -source = "index+mcpplibs@0.16.10" -hash = "fnv1a:189ed74d4fe89362" +version = "0.16.11" +source = "index+mcpplibs@0.16.11" +hash = "fnv1a:189ed84d4fe89515" origin = "platform" -targets = ["aarch64-openkal", "x86_64-openkal"] +targets = ["x86_64-openkal"] [package."openkal-musl"] namespace = "mcpplibs" -version = "0.23.0" -source = "index+mcpplibs@0.23.0" -hash = "fnv1a:d4b76fdf7f0ffb0a" +version = "0.23.1" +source = "index+mcpplibs@0.23.1" +hash = "fnv1a:d4b770df7f0ffcbd" origin = "platform" -targets = ["aarch64-openkal", "x86_64-openkal"] +targets = ["x86_64-openkal"] [package."openkal-platform"] namespace = "mcpplibs" -version = "0.19.0" -source = "index+mcpplibs@0.19.0" -hash = "fnv1a:e4c15312294f983d" +version = "0.19.1" +source = "index+mcpplibs@0.19.1" +hash = "fnv1a:e4c15212294f968a" origin = "platform" -targets = ["aarch64-openkal", "x86_64-openkal"] +targets = ["x86_64-openkal"] [package."openkal-tools"] namespace = "mcpplibs" -version = "0.1.5" -source = "index+mcpplibs@0.1.5" -hash = "fnv1a:3a0aa273e0da7b49" +version = "0.1.6" +source = "index+mcpplibs@0.1.6" +hash = "fnv1a:3a0a9f73e0da7630" origin = "platform" -targets = ["aarch64-openkal", "x86_64-openkal"] +targets = ["x86_64-openkal"] [package."mcpplibs.os"] namespace = "mcpplibs" diff --git a/mcpp.toml b/mcpp.toml index 6a610cc..5c5e146 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,11 +33,11 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.19.0's version group is openkal 0.18's, -# which mcpplibs.os 0.2.4 is written against and whose okrun maps the image it runs; mcpp's own -# openkal rows pin 0.18.0. +# The platform of the ok images the editor packages carry: 0.19.1's version group is openkal 0.18's, +# which mcpplibs.os 0.2.4 is written against and whose okrun maps the image it runs and releases it +# once the image is loaded; mcpp's own openkal rows pin 0.18.0. [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.19.0" +platform = "openkal-platform@0.19.1" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] From 090b4aeabe260a79147232a9aace2d43309fe8d6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 31 Oct 2026 20:32:21 +0900 Subject: [PATCH 108/119] openkal-platform 0.19.2 for the ok images: on Linux arm64 okrun admits them on ARMv8's SHA-256 instructions (openkal-image 0.1.7) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.lock | 30 +++++++++++++++--------------- mcpp.toml | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af562fd..6abc209 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.19.1' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.19.2' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d8b7b..c288937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,7 +113,7 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 - **mcpp 2026.10.31.2.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`, built over a platform other than mcpp's own - openkal rows'. The backend is on mcpplibs.os 0.2.4 and openkal-platform 0.19.1, ok images included + openkal rows'. The backend is on mcpplibs.os 0.2.4 and openkal-platform 0.19.2, ok images included (openkal 0.18, whose okrun maps the image it runs and releases it once the image is loaded). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the diff --git a/README.md b/README.md index 13e1181..538e48f 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.19.1) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.19.2) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index c465932..10bd0a6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.19.1 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.19.2 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.lock b/mcpp.lock index 21e4f50..6ca273c 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -22,9 +22,9 @@ targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."openkal-ld"] namespace = "mcpplibs" -version = "0.1.6" -source = "index+mcpplibs@0.1.6" -hash = "fnv1a:c45ba4726c74f72b" +version = "0.1.7" +source = "index+mcpplibs@0.1.7" +hash = "fnv1a:c45ba3726c74f578" origin = "platform" targets = ["x86_64-openkal"] @@ -36,33 +36,33 @@ hash = "fnv1a:20ab944dd6f2acb2" [package."openkal-llvm-runtime"] namespace = "mcpplibs" -version = "0.16.11" -source = "index+mcpplibs@0.16.11" -hash = "fnv1a:189ed84d4fe89515" +version = "0.16.12" +source = "index+mcpplibs@0.16.12" +hash = "fnv1a:189ed54d4fe88ffc" origin = "platform" targets = ["x86_64-openkal"] [package."openkal-musl"] namespace = "mcpplibs" -version = "0.23.1" -source = "index+mcpplibs@0.23.1" -hash = "fnv1a:d4b770df7f0ffcbd" +version = "0.23.2" +source = "index+mcpplibs@0.23.2" +hash = "fnv1a:d4b76ddf7f0ff7a4" origin = "platform" targets = ["x86_64-openkal"] [package."openkal-platform"] namespace = "mcpplibs" -version = "0.19.1" -source = "index+mcpplibs@0.19.1" -hash = "fnv1a:e4c15212294f968a" +version = "0.19.2" +source = "index+mcpplibs@0.19.2" +hash = "fnv1a:e4c15112294f94d7" origin = "platform" targets = ["x86_64-openkal"] [package."openkal-tools"] namespace = "mcpplibs" -version = "0.1.6" -source = "index+mcpplibs@0.1.6" -hash = "fnv1a:3a0a9f73e0da7630" +version = "0.1.7" +source = "index+mcpplibs@0.1.7" +hash = "fnv1a:3a0aa073e0da77e3" origin = "platform" targets = ["x86_64-openkal"] diff --git a/mcpp.toml b/mcpp.toml index 5c5e146..42854af 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,11 +33,11 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.19.1's version group is openkal 0.18's, +# The platform of the ok images the editor packages carry: 0.19.2's version group is openkal 0.18's, # which mcpplibs.os 0.2.4 is written against and whose okrun maps the image it runs and releases it # once the image is loaded; mcpp's own openkal rows pin 0.18.0. [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.19.1" +platform = "openkal-platform@0.19.2" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] From 4a3dd8e05072f6c6fe30c2f280aebea1cc5151d4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 1 Nov 2026 06:47:36 +0900 Subject: [PATCH 109/119] mcpplibs.os 0.2.5 and openkal-platform 0.20.0 for the ok images: openkal 0.19, whose okrun verifies an okb's chunk digests on several contexts (openkal-image 0.1.8) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 5 ++-- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.lock | 58 ++++++++++++++++++++-------------------- mcpp.toml | 12 ++++----- 6 files changed, 41 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6abc209..188edbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.19.2' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.20.0' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index c288937..b8095cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,8 +113,9 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 - **mcpp 2026.10.31.2.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`, built over a platform other than mcpp's own - openkal rows'. The backend is on mcpplibs.os 0.2.4 and openkal-platform 0.19.2, ok images included - (openkal 0.18, whose okrun maps the image it runs and releases it once the image is loaded). + openkal rows'. The backend is on mcpplibs.os 0.2.5 and openkal-platform 0.20.0, ok images included + (openkal 0.19, whose okrun releases the image file once loaded and verifies an okb's chunk digests on + several contexts). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. diff --git a/README.md b/README.md index 538e48f..39fc433 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.19.2) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.20.0) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index 10bd0a6..7324880 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.19.2 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.20.0 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.lock b/mcpp.lock index 6ca273c..861d5c5 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -15,62 +15,62 @@ targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal"] namespace = "mcpplibs" -version = "0.18.0" -source = "index+mcpplibs@0.18.0" -hash = "fnv1a:49329d1082d25374" +version = "0.19.0" +source = "index+mcpplibs@0.19.0" +hash = "fnv1a:52a7c010886722fb" targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."openkal-ld"] namespace = "mcpplibs" -version = "0.1.7" -source = "index+mcpplibs@0.1.7" -hash = "fnv1a:c45ba3726c74f578" +version = "0.1.8" +source = "index+mcpplibs@0.1.8" +hash = "fnv1a:c45ba2726c74f3c5" origin = "platform" -targets = ["x86_64-openkal"] +targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-linux"] namespace = "mcpplibs" -version = "0.19.0" -source = "index+mcpplibs@0.19.0" -hash = "fnv1a:20ab944dd6f2acb2" +version = "0.20.0" +source = "index+mcpplibs@0.20.0" +hash = "fnv1a:4f232333297a8e02" [package."openkal-llvm-runtime"] namespace = "mcpplibs" -version = "0.16.12" -source = "index+mcpplibs@0.16.12" -hash = "fnv1a:189ed54d4fe88ffc" +version = "0.16.13" +source = "index+mcpplibs@0.16.13" +hash = "fnv1a:189ed64d4fe891af" origin = "platform" -targets = ["x86_64-openkal"] +targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-musl"] namespace = "mcpplibs" -version = "0.23.2" -source = "index+mcpplibs@0.23.2" -hash = "fnv1a:d4b76ddf7f0ff7a4" +version = "0.24.0" +source = "index+mcpplibs@0.24.0" +hash = "fnv1a:cc2944df7a3f4c07" origin = "platform" -targets = ["x86_64-openkal"] +targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-platform"] namespace = "mcpplibs" -version = "0.19.2" -source = "index+mcpplibs@0.19.2" -hash = "fnv1a:e4c15112294f94d7" +version = "0.20.0" +source = "index+mcpplibs@0.20.0" +hash = "fnv1a:618cc42a62c93aed" origin = "platform" -targets = ["x86_64-openkal"] +targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-tools"] namespace = "mcpplibs" -version = "0.1.7" -source = "index+mcpplibs@0.1.7" -hash = "fnv1a:3a0aa073e0da77e3" +version = "0.1.8" +source = "index+mcpplibs@0.1.8" +hash = "fnv1a:3a0aad73e0da8dfa" origin = "platform" -targets = ["x86_64-openkal"] +targets = ["aarch64-openkal", "x86_64-openkal"] [package."mcpplibs.os"] namespace = "mcpplibs" -version = "0.2.4" -source = "index+mcpplibs@0.2.4" -hash = "fnv1a:d0be9ca500330ffd" +version = "0.2.5" +source = "index+mcpplibs@0.2.5" +hash = "fnv1a:d0be9ba500330e4a" targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."nlohmann.json"] diff --git a/mcpp.toml b/mcpp.toml index 42854af..c0e0568 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,11 +33,11 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.19.2's version group is openkal 0.18's, -# which mcpplibs.os 0.2.4 is written against and whose okrun maps the image it runs and releases it -# once the image is loaded; mcpp's own openkal rows pin 0.18.0. +# The platform of the ok images the editor packages carry: 0.20.0's version group is openkal 0.19's, +# which mcpplibs.os 0.2.5 is written against; its okrun releases the image file once loaded and +# verifies an okb's chunk digests on several contexts (openkal-image 0.1.8). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.19.2" +platform = "openkal-platform@0.20.0" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] @@ -74,9 +74,9 @@ kind = "bin" main = "src/bin/dev.cpp" [dependencies.mcpplibs] -# os 0.2.4: openkal 0.18 (openkal-linux 0.19.0, openkal-macos 0.16.0, openkal-windows 0.14.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, +# os 0.2.5: openkal 0.19 (openkal-linux 0.20.0, openkal-macos 0.17.0, openkal-windows 0.15.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, # so the package declares no openkal-tools of its own beside the platform's. -os = "0.2.4" +os = "0.2.5" [dependencies.nlohmann] json = "3.12.0" From 17921bf80bcec99739906453b139b880a379f4df Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 1 Nov 2026 09:05:44 +0900 Subject: [PATCH 110/119] mcpplibs.os 0.2.6 and openkal-platform 0.20.1 for the ok images: on macOS a context's identity without a system call (openkal-macos 0.17.1) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.lock | 36 ++++++++++++++++++------------------ mcpp.toml | 10 +++++----- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 188edbb..8ac949d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.20.0' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.20.1' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index b8095cb..17e13dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,7 +113,7 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 - **mcpp 2026.10.31.2.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`, built over a platform other than mcpp's own - openkal rows'. The backend is on mcpplibs.os 0.2.5 and openkal-platform 0.20.0, ok images included + openkal rows'. The backend is on mcpplibs.os 0.2.6 and openkal-platform 0.20.1, ok images included (openkal 0.19, whose okrun releases the image file once loaded and verifies an okb's chunk digests on several contexts). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on diff --git a/README.md b/README.md index 39fc433..598934d 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.20.0) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.20.1) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index 7324880..d9aae97 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.20.0 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.20.1 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.lock b/mcpp.lock index 861d5c5..df98d92 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -22,9 +22,9 @@ targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."openkal-ld"] namespace = "mcpplibs" -version = "0.1.8" -source = "index+mcpplibs@0.1.8" -hash = "fnv1a:c45ba2726c74f3c5" +version = "0.1.9" +source = "index+mcpplibs@0.1.9" +hash = "fnv1a:c45ba1726c74f212" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] @@ -36,41 +36,41 @@ hash = "fnv1a:4f232333297a8e02" [package."openkal-llvm-runtime"] namespace = "mcpplibs" -version = "0.16.13" -source = "index+mcpplibs@0.16.13" -hash = "fnv1a:189ed64d4fe891af" +version = "0.16.14" +source = "index+mcpplibs@0.16.14" +hash = "fnv1a:189ed34d4fe88c96" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-musl"] namespace = "mcpplibs" -version = "0.24.0" -source = "index+mcpplibs@0.24.0" -hash = "fnv1a:cc2944df7a3f4c07" +version = "0.24.1" +source = "index+mcpplibs@0.24.1" +hash = "fnv1a:cc2943df7a3f4a54" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-platform"] namespace = "mcpplibs" -version = "0.20.0" -source = "index+mcpplibs@0.20.0" -hash = "fnv1a:618cc42a62c93aed" +version = "0.20.1" +source = "index+mcpplibs@0.20.1" +hash = "fnv1a:618cc32a62c9393a" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-tools"] namespace = "mcpplibs" -version = "0.1.8" -source = "index+mcpplibs@0.1.8" -hash = "fnv1a:3a0aad73e0da8dfa" +version = "0.1.9" +source = "index+mcpplibs@0.1.9" +hash = "fnv1a:3a0aae73e0da8fad" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."mcpplibs.os"] namespace = "mcpplibs" -version = "0.2.5" -source = "index+mcpplibs@0.2.5" -hash = "fnv1a:d0be9ba500330e4a" +version = "0.2.6" +source = "index+mcpplibs@0.2.6" +hash = "fnv1a:d0be9aa500330c97" targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."nlohmann.json"] diff --git a/mcpp.toml b/mcpp.toml index c0e0568..02b5ea7 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,11 +33,11 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.20.0's version group is openkal 0.19's, -# which mcpplibs.os 0.2.5 is written against; its okrun releases the image file once loaded and +# The platform of the ok images the editor packages carry: 0.20.1's version group is openkal 0.19's, +# which mcpplibs.os 0.2.6 is written against; its okrun releases the image file once loaded and # verifies an okb's chunk digests on several contexts (openkal-image 0.1.8). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.20.0" +platform = "openkal-platform@0.20.1" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] @@ -74,9 +74,9 @@ kind = "bin" main = "src/bin/dev.cpp" [dependencies.mcpplibs] -# os 0.2.5: openkal 0.19 (openkal-linux 0.20.0, openkal-macos 0.17.0, openkal-windows 0.15.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, +# os 0.2.6: openkal 0.19 (openkal-linux 0.20.0, openkal-macos 0.17.1, openkal-windows 0.15.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, # so the package declares no openkal-tools of its own beside the platform's. -os = "0.2.5" +os = "0.2.6" [dependencies.nlohmann] json = "3.12.0" From ab65f5d45b1a74e6be6e0cb88b2bee1be1288a65 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 1 Nov 2026 16:32:04 +0900 Subject: [PATCH 111/119] openkal-platform 0.20.2 for the ok images: threads allocate together from their own caches (openkal-musl 0.25.0) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 4 ++-- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.lock | 18 +++++++++--------- mcpp.toml | 7 ++++--- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ac949d..19722fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.20.1' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.20.2' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e13dd..9a985c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,9 +113,9 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 - **mcpp 2026.10.31.2.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`, built over a platform other than mcpp's own - openkal rows'. The backend is on mcpplibs.os 0.2.6 and openkal-platform 0.20.1, ok images included + openkal rows'. The backend is on mcpplibs.os 0.2.6 and openkal-platform 0.20.2, ok images included (openkal 0.19, whose okrun releases the image file once loaded and verifies an okb's chunk digests on - several contexts). + several contexts, and whose C library serves threads that allocate together from their own caches). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. diff --git a/README.md b/README.md index 598934d..436ddc0 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.20.1) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.20.2) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index d9aae97..4708d4e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.20.1 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.20.2 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.lock b/mcpp.lock index df98d92..7cf0339 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -36,25 +36,25 @@ hash = "fnv1a:4f232333297a8e02" [package."openkal-llvm-runtime"] namespace = "mcpplibs" -version = "0.16.14" -source = "index+mcpplibs@0.16.14" -hash = "fnv1a:189ed34d4fe88c96" +version = "0.16.15" +source = "index+mcpplibs@0.16.15" +hash = "fnv1a:189ed44d4fe88e49" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-musl"] namespace = "mcpplibs" -version = "0.24.1" -source = "index+mcpplibs@0.24.1" -hash = "fnv1a:cc2943df7a3f4a54" +version = "0.25.0" +source = "index+mcpplibs@0.25.0" +hash = "fnv1a:c32081df75063e80" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-platform"] namespace = "mcpplibs" -version = "0.20.1" -source = "index+mcpplibs@0.20.1" -hash = "fnv1a:618cc32a62c9393a" +version = "0.20.2" +source = "index+mcpplibs@0.20.2" +hash = "fnv1a:618cc22a62c93787" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] diff --git a/mcpp.toml b/mcpp.toml index 02b5ea7..e030e45 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,11 +33,12 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.20.1's version group is openkal 0.19's, +# The platform of the ok images the editor packages carry: 0.20.2's version group is openkal 0.19's, # which mcpplibs.os 0.2.6 is written against; its okrun releases the image file once loaded and -# verifies an okb's chunk digests on several contexts (openkal-image 0.1.8). +# verifies an okb's chunk digests on several contexts (openkal-image 0.1.8); its C library serves +# threads that allocate together from their own caches (openkal-musl 0.25.0). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.20.1" +platform = "openkal-platform@0.20.2" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] From 1bdb6a04ee3d543b41f5453a83f79694ac9f989e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 2 Nov 2026 19:48:33 +0900 Subject: [PATCH 112/119] openkal-platform 0.21.0 and mcpplibs.os 0.2.7 for the ok images, built with mcpp 2026.11.2.2: an image's instructions mapped from its file (openkal-image 0.3.0), mimalloc in the C library (openkal-musl 0.26.0) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 8 +++--- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 9 +++---- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.lock | 56 +++++++++++++++++++++++----------------- mcpp.toml | 16 ++++++------ 7 files changed, 51 insertions(+), 44 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index 910d306..5336440 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,15 +1,15 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. # The released mcpp CI installs and uses: it states its features (`features`, the project's mcpp of # part 2 §4.3), test-launch among them (2026.10.31.2), and builds the backend's ok images on -# openkal-platform 0.18.0. The mock replays the control contract the fixtures were captured from -# (protocol/fixtures/version.txt). -MCPP_VERSION=2026.10.31.2 +# openkal-platform 0.21.0, whose C library embeds its allocator (2026.11.2.2). The mock replays the +# control contract the fixtures were captured from (protocol/fixtures/version.txt). +MCPP_VERSION=2026.11.2.2 # A ref of mcpp-community/mcpp CI builds mcpp from and uses instead, for a contract not yet released. # Empty: the release above is used. MCPP_SOURCE_REF= # The LLVM the backend builds with on macOS and Windows (the targets mcpp reaches there with it). LLVM_VERSION=23.1.3 -# The xlings mcpp 2026.10.31.2 bundles, which reads the current index. +# The xlings mcpp 2026.11.2.2 bundles, which reads the current index. XLINGS_VERSION=v2026.10.26.1 # The oldest VS Code the extension supports (engines.vscode), which its tests run in. VSCODE_VERSION=1.91.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19722fe..ffc4722 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.20.2' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.21.0' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a985c2..2730e60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,12 +110,11 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 sessions through `XDebuggerManager.newSessionBuilder`, the debugger driver through `createDriverConfiguration(project, CPPEnvironment(toolchain))`; packing is a job over the backend's `pack/formats`. -- **mcpp 2026.10.31.2.** One pack of the target set `openkal` writes the editor packages' tree and +- **mcpp 2026.11.2.2.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads - `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`, built over a platform other than mcpp's own - openkal rows'. The backend is on mcpplibs.os 0.2.6 and openkal-platform 0.20.2, ok images included - (openkal 0.19, whose okrun releases the image file once loaded and verifies an okb's chunk digests on - several contexts, and whose C library serves threads that allocate together from their own caches). + `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`. The backend is on mcpplibs.os 0.2.7 and + openkal-platform 0.21.0, ok images included (openkal 0.21, whose okrun maps an image's instructions + from its file, and whose C library's allocator is mimalloc). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. diff --git a/README.md b/README.md index 436ddc0..bf678cd 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.20.2) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.21.0) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index 4708d4e..c952d80 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.20.2 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.21.0 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.lock b/mcpp.lock index 7cf0339..b3bdde9 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -5,6 +5,14 @@ # file must not change depending on which command ran last. version = 2 +[package."compat.mimalloc"] +namespace = "compat" +version = "3.6.1" +source = "index+compat@3.6.1" +hash = "fnv1a:44a30e7e4403e477" +origin = "platform" +targets = ["aarch64-openkal", "x86_64-openkal"] + [package."mcpp.plugins"] namespace = "mcpp" version = "0.20.0" @@ -15,62 +23,62 @@ targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal"] namespace = "mcpplibs" -version = "0.19.0" -source = "index+mcpplibs@0.19.0" -hash = "fnv1a:52a7c010886722fb" +version = "0.21.0" +source = "index+mcpplibs@0.21.0" +hash = "fnv1a:4707ae0764444448" targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."openkal-ld"] namespace = "mcpplibs" -version = "0.1.9" -source = "index+mcpplibs@0.1.9" -hash = "fnv1a:c45ba1726c74f212" +version = "0.3.0" +source = "index+mcpplibs@0.3.0" +hash = "fnv1a:d518f87275c5bd47" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-linux"] namespace = "mcpplibs" -version = "0.20.0" -source = "index+mcpplibs@0.20.0" -hash = "fnv1a:4f232333297a8e02" +version = "0.22.0" +source = "index+mcpplibs@0.22.0" +hash = "fnv1a:5fe0713332cb49ec" [package."openkal-llvm-runtime"] namespace = "mcpplibs" -version = "0.16.15" -source = "index+mcpplibs@0.16.15" -hash = "fnv1a:189ed44d4fe88e49" +version = "0.17.0" +source = "index+mcpplibs@0.17.0" +hash = "fnv1a:0a754c335bd6d692" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-musl"] namespace = "mcpplibs" -version = "0.25.0" -source = "index+mcpplibs@0.25.0" -hash = "fnv1a:c32081df75063e80" +version = "0.26.0" +source = "index+mcpplibs@0.26.0" +hash = "fnv1a:bb6bf6df70ee901d" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-platform"] namespace = "mcpplibs" -version = "0.20.2" -source = "index+mcpplibs@0.20.2" -hash = "fnv1a:618cc22a62c93787" +version = "0.21.0" +source = "index+mcpplibs@0.21.0" +hash = "fnv1a:59ca812a5ea5c986" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-tools"] namespace = "mcpplibs" -version = "0.1.9" -source = "index+mcpplibs@0.1.9" -hash = "fnv1a:3a0aae73e0da8fad" +version = "0.3.0" +source = "index+mcpplibs@0.3.0" +hash = "fnv1a:4c7af373eb9cd0cc" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."mcpplibs.os"] namespace = "mcpplibs" -version = "0.2.6" -source = "index+mcpplibs@0.2.6" -hash = "fnv1a:d0be9aa500330c97" +version = "0.2.7" +source = "index+mcpplibs@0.2.7" +hash = "fnv1a:d0be99a500330ae4" targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."nlohmann.json"] diff --git a/mcpp.toml b/mcpp.toml index e030e45..0947c33 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -14,7 +14,7 @@ description = "The IDE backend for mcpp: sessions, jobs, records and view models license = "Apache-2.0" authors = ["mcpp-community"] repo = "https://github.com/mcpp-community/mcpp-ide" -mcpp = ">=2026.10.31.2" +mcpp = ">=2026.11.2.2" [language] standard = "c++23" @@ -33,12 +33,12 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.20.2's version group is openkal 0.19's, -# which mcpplibs.os 0.2.6 is written against; its okrun releases the image file once loaded and -# verifies an okb's chunk digests on several contexts (openkal-image 0.1.8); its C library serves -# threads that allocate together from their own caches (openkal-musl 0.25.0). +# The platform of the ok images the editor packages carry: 0.21.0's version group is openkal 0.21's, +# which mcpplibs.os 0.2.7 is written against; its okrun maps an image's instructions from its file +# rather than copying them (openkal-image 0.3.0); its C library's allocator is mimalloc and its +# memory an openkal region (openkal-musl 0.26.0). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.20.2" +platform = "openkal-platform@0.21.0" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] @@ -75,9 +75,9 @@ kind = "bin" main = "src/bin/dev.cpp" [dependencies.mcpplibs] -# os 0.2.6: openkal 0.19 (openkal-linux 0.20.0, openkal-macos 0.17.1, openkal-windows 0.15.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, +# os 0.2.7: openkal 0.21 (openkal-linux 0.22.0, openkal-macos 0.19.0, openkal-windows 0.17.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, # so the package declares no openkal-tools of its own beside the platform's. -os = "0.2.6" +os = "0.2.7" [dependencies.nlohmann] json = "3.12.0" From ae43923cb3c99d223bcf5f266f1e5c568de36d94 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 2 Nov 2026 20:00:32 +0900 Subject: [PATCH 113/119] the project's mcpp is 2026.11.2.2 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .xlings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.xlings.json b/.xlings.json index 5616698..0a869b0 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.10.31.2" + "mcpp": "2026.11.2.2" } } From a7e316cb337b9d5caf37d29ebb34e6bd5d336143 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 2 Nov 2026 21:01:45 +0900 Subject: [PATCH 114/119] openkal-platform 0.21.1 for the ok images: an okb member whose admission was remembered is admitted without reading it (openkal-image 0.3.1) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 2 +- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 4 ++-- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.lock | 30 +++++++++++++++--------------- mcpp.toml | 7 ++++--- 7 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index 5336440..4ee73e4 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,7 +1,7 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. # The released mcpp CI installs and uses: it states its features (`features`, the project's mcpp of # part 2 §4.3), test-launch among them (2026.10.31.2), and builds the backend's ok images on -# openkal-platform 0.21.0, whose C library embeds its allocator (2026.11.2.2). The mock replays the +# openkal-platform 0.21.1, whose C library embeds its allocator (2026.11.2.2). The mock replays the # control contract the fixtures were captured from (protocol/fixtures/version.txt). MCPP_VERSION=2026.11.2.2 # A ref of mcpp-community/mcpp CI builds mcpp from and uses instead, for a contract not yet released. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffc4722..b7a1980 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.21.0' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.21.1' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 2730e60..b9d21f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,8 +113,8 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 - **mcpp 2026.11.2.2.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`. The backend is on mcpplibs.os 0.2.7 and - openkal-platform 0.21.0, ok images included (openkal 0.21, whose okrun maps an image's instructions - from its file, and whose C library's allocator is mimalloc). + openkal-platform 0.21.1, ok images included (openkal 0.21, whose okrun maps an image's instructions + from its file and admits an okb member it has compared before without reading it, and whose C library's allocator is mimalloc). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. diff --git a/README.md b/README.md index bf678cd..a1cfdaf 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.21.0) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.21.1) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index c952d80..24cae93 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.21.0 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.21.1 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。 diff --git a/mcpp.lock b/mcpp.lock index b3bdde9..23551d6 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -30,9 +30,9 @@ targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] [package."openkal-ld"] namespace = "mcpplibs" -version = "0.3.0" -source = "index+mcpplibs@0.3.0" -hash = "fnv1a:d518f87275c5bd47" +version = "0.3.1" +source = "index+mcpplibs@0.3.1" +hash = "fnv1a:d518f77275c5bb94" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] @@ -44,33 +44,33 @@ hash = "fnv1a:5fe0713332cb49ec" [package."openkal-llvm-runtime"] namespace = "mcpplibs" -version = "0.17.0" -source = "index+mcpplibs@0.17.0" -hash = "fnv1a:0a754c335bd6d692" +version = "0.17.1" +source = "index+mcpplibs@0.17.1" +hash = "fnv1a:0a754d335bd6d845" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-musl"] namespace = "mcpplibs" -version = "0.26.0" -source = "index+mcpplibs@0.26.0" -hash = "fnv1a:bb6bf6df70ee901d" +version = "0.26.1" +source = "index+mcpplibs@0.26.1" +hash = "fnv1a:bb6bf5df70ee8e6a" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-platform"] namespace = "mcpplibs" -version = "0.21.0" -source = "index+mcpplibs@0.21.0" -hash = "fnv1a:59ca812a5ea5c986" +version = "0.21.1" +source = "index+mcpplibs@0.21.1" +hash = "fnv1a:59ca822a5ea5cb39" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] [package."openkal-tools"] namespace = "mcpplibs" -version = "0.3.0" -source = "index+mcpplibs@0.3.0" -hash = "fnv1a:4c7af373eb9cd0cc" +version = "0.3.1" +source = "index+mcpplibs@0.3.1" +hash = "fnv1a:4c7af473eb9cd27f" origin = "platform" targets = ["aarch64-openkal", "x86_64-openkal"] diff --git a/mcpp.toml b/mcpp.toml index 0947c33..ffac2c1 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,12 +33,13 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.21.0's version group is openkal 0.21's, +# The platform of the ok images the editor packages carry: 0.21.1's version group is openkal 0.21's, # which mcpplibs.os 0.2.7 is written against; its okrun maps an image's instructions from its file -# rather than copying them (openkal-image 0.3.0); its C library's allocator is mimalloc and its +# rather than copying them, and admits an okb member it has compared before without reading it +# (openkal-image 0.3.1); its C library's allocator is mimalloc and its # memory an openkal region (openkal-musl 0.26.0). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.21.0" +platform = "openkal-platform@0.21.1" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] From 47577fedc6f8046e3950732ec552a9135828a36a Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 11:47:20 +0900 Subject: [PATCH 115/119] openkal-platform 0.23.0 and mcpplibs.os 0.2.8 for the ok images, built with mcpp 2026.11.2.3: an image at a base of the boot, a snapshot of its initialisers, hardening Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 9 +++++---- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 10 ++++++---- mcpp.toml | 18 +++++++++--------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index 4ee73e4..045b27a 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,15 +1,16 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. # The released mcpp CI installs and uses: it states its features (`features`, the project's mcpp of # part 2 §4.3), test-launch among them (2026.10.31.2), and builds the backend's ok images on -# openkal-platform 0.21.1, whose C library embeds its allocator (2026.11.2.2). The mock replays the -# control contract the fixtures were captured from (protocol/fixtures/version.txt). -MCPP_VERSION=2026.11.2.2 +# openkal-platform 0.23.0, which it compiles the std module for with the platform's words +# (2026.11.2.3). The mock replays the control contract the fixtures were captured from +# (protocol/fixtures/version.txt). +MCPP_VERSION=2026.11.2.3 # A ref of mcpp-community/mcpp CI builds mcpp from and uses instead, for a contract not yet released. # Empty: the release above is used. MCPP_SOURCE_REF= # The LLVM the backend builds with on macOS and Windows (the targets mcpp reaches there with it). LLVM_VERSION=23.1.3 -# The xlings mcpp 2026.11.2.2 bundles, which reads the current index. +# The xlings mcpp 2026.11.2.3 bundles, which reads the current index. XLINGS_VERSION=v2026.10.26.1 # The oldest VS Code the extension supports (engines.vscode), which its tests run in. VSCODE_VERSION=1.91.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7a1980..1bbc1f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.21.1' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.23.0' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d21f6..0aaeb6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,11 +110,13 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 sessions through `XDebuggerManager.newSessionBuilder`, the debugger driver through `createDriverConfiguration(project, CPPEnvironment(toolchain))`; packing is a job over the backend's `pack/formats`. -- **mcpp 2026.11.2.2.** One pack of the target set `openkal` writes the editor packages' tree and +- **mcpp 2026.11.2.3.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads - `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`. The backend is on mcpplibs.os 0.2.7 and - openkal-platform 0.21.1, ok images included (openkal 0.21, whose okrun maps an image's instructions - from its file and admits an okb member it has compared before without reading it, and whose C library's allocator is mimalloc). + `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`. The backend is on mcpplibs.os 0.2.8 and + openkal-platform 0.23.0, ok images included (openkal 0.23: okrun places an image at a base of the + boot, keeps its data as a prepared copy and starts it from a snapshot of its initialisers; every + unit, the std module included, is compiled with frame pointers and hardening; mcpp design + 2026-11-03 part 4). - **CI and release.** 0.9.0's gates run in this repository (registry, translations, unit suite on three systems, manifest drift, parity, Extension Host on three systems, isolated install, the graph page in Chromium); the release publishes the VSIX to Open VSX beside the GitHub release. diff --git a/mcpp.toml b/mcpp.toml index ffac2c1..4f74dac 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -14,7 +14,7 @@ description = "The IDE backend for mcpp: sessions, jobs, records and view models license = "Apache-2.0" authors = ["mcpp-community"] repo = "https://github.com/mcpp-community/mcpp-ide" -mcpp = ">=2026.11.2.2" +mcpp = ">=2026.11.2.3" [language] standard = "c++23" @@ -33,13 +33,13 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.21.1's version group is openkal 0.21's, -# which mcpplibs.os 0.2.7 is written against; its okrun maps an image's instructions from its file -# rather than copying them, and admits an okb member it has compared before without reading it -# (openkal-image 0.3.1); its C library's allocator is mimalloc and its -# memory an openkal region (openkal-musl 0.26.0). +# The platform of the ok images the editor packages carry: 0.23.0's version group is openkal 0.23's, +# which mcpplibs.os 0.2.8 is written against; its okrun places an image at a base of the boot, keeps +# its data as a prepared copy and starts it from a snapshot of its initialisers, and spends by the +# program's resource preference (openkal-image 0.4.1); every unit is compiled with frame pointers and +# hardening, the std module included from mcpp 2026.11.2.3 (mcpp design 2026-11-03 part 4). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.21.1" +platform = "openkal-platform@0.23.0" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] @@ -76,9 +76,9 @@ kind = "bin" main = "src/bin/dev.cpp" [dependencies.mcpplibs] -# os 0.2.7: openkal 0.21 (openkal-linux 0.22.0, openkal-macos 0.19.0, openkal-windows 0.17.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, +# os 0.2.8: openkal 0.23 (openkal-linux 0.24.0, openkal-macos 0.21.2, openkal-windows 0.19.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, # so the package declares no openkal-tools of its own beside the platform's. -os = "0.2.7" +os = "0.2.8" [dependencies.nlohmann] json = "3.12.0" From 6da66545c87939a167e3bdfcde48117b5d6eb3ba Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 13:03:11 +0900 Subject: [PATCH 116/119] openkal-platform 0.23.1 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 2 +- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 2 +- mcpp.toml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index 045b27a..a43781b 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,7 +1,7 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. # The released mcpp CI installs and uses: it states its features (`features`, the project's mcpp of # part 2 §4.3), test-launch among them (2026.10.31.2), and builds the backend's ok images on -# openkal-platform 0.23.0, which it compiles the std module for with the platform's words +# openkal-platform 0.23.1, which it compiles the std module for with the platform's words # (2026.11.2.3). The mock replays the control contract the fixtures were captured from # (protocol/fixtures/version.txt). MCPP_VERSION=2026.11.2.3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bbc1f1..0afe1bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.23.0' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.23.1' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aaeb6c..302d6f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,7 +113,7 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 - **mcpp 2026.11.2.3.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`. The backend is on mcpplibs.os 0.2.8 and - openkal-platform 0.23.0, ok images included (openkal 0.23: okrun places an image at a base of the + openkal-platform 0.23.1, ok images included (openkal 0.23: okrun places an image at a base of the boot, keeps its data as a prepared copy and starts it from a snapshot of its initialisers; every unit, the std module included, is compiled with frame pointers and hardening; mcpp design 2026-11-03 part 4). diff --git a/mcpp.toml b/mcpp.toml index 4f74dac..15f9c0e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,13 +33,13 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.23.0's version group is openkal 0.23's, +# The platform of the ok images the editor packages carry: 0.23.1's version group is openkal 0.23's, # which mcpplibs.os 0.2.8 is written against; its okrun places an image at a base of the boot, keeps # its data as a prepared copy and starts it from a snapshot of its initialisers, and spends by the # program's resource preference (openkal-image 0.4.1); every unit is compiled with frame pointers and # hardening, the std module included from mcpp 2026.11.2.3 (mcpp design 2026-11-03 part 4). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.23.0" +platform = "openkal-platform@0.23.1" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] From 485425aa876bcef85f3646a629a0e7bc0c755953 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 17:37:52 +0900 Subject: [PATCH 117/119] ci: select platform 0.23.2 with mcpp 2026.11.3.1 --- .github/versions.env | 8 ++++---- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 4 ++-- mcpp.toml | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index a43781b..9db449d 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,16 +1,16 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. # The released mcpp CI installs and uses: it states its features (`features`, the project's mcpp of # part 2 §4.3), test-launch among them (2026.10.31.2), and builds the backend's ok images on -# openkal-platform 0.23.1, which it compiles the std module for with the platform's words -# (2026.11.2.3). The mock replays the control contract the fixtures were captured from +# openkal-platform 0.23.2, which it compiles the std module for with the platform's words +# (2026.11.3.1). The mock replays the control contract the fixtures were captured from # (protocol/fixtures/version.txt). -MCPP_VERSION=2026.11.2.3 +MCPP_VERSION=2026.11.3.1 # A ref of mcpp-community/mcpp CI builds mcpp from and uses instead, for a contract not yet released. # Empty: the release above is used. MCPP_SOURCE_REF= # The LLVM the backend builds with on macOS and Windows (the targets mcpp reaches there with it). LLVM_VERSION=23.1.3 -# The xlings mcpp 2026.11.2.3 bundles, which reads the current index. +# The xlings mcpp 2026.11.3.1 bundles, which reads the current index. XLINGS_VERSION=v2026.10.26.1 # The oldest VS Code the extension supports (engines.vscode), which its tests run in. VSCODE_VERSION=1.91.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0afe1bf..5252bce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.23.1' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.23.2' } defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 302d6f0..38e1a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,10 +110,10 @@ mcpp-vscode 1.1.0, mcpp-clion 0.2.0 and mcpp-nvim 0.2.0 (mcpp design 2026-10-29 sessions through `XDebuggerManager.newSessionBuilder`, the debugger driver through `createDriverConfiguration(project, CPPEnvironment(toolchain))`; packing is a job over the backend's `pack/formats`. -- **mcpp 2026.11.2.3.** One pack of the target set `openkal` writes the editor packages' tree and +- **mcpp 2026.11.3.1.** One pack of the target set `openkal` writes the editor packages' tree and okb (`--target openkal --mode self-contained --systems all --pkg-format tar,okb`); CI reads `runs_on`; the Windows launcher is `bin/mcpp-ide.exe`. The backend is on mcpplibs.os 0.2.8 and - openkal-platform 0.23.1, ok images included (openkal 0.23: okrun places an image at a base of the + openkal-platform 0.23.2, ok images included (openkal 0.23: okrun places an image at a base of the boot, keeps its data as a prepared copy and starts it from a snapshot of its initialisers; every unit, the std module included, is compiled with frame pointers and hardening; mcpp design 2026-11-03 part 4). diff --git a/mcpp.toml b/mcpp.toml index 15f9c0e..b954d7b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -14,7 +14,7 @@ description = "The IDE backend for mcpp: sessions, jobs, records and view models license = "Apache-2.0" authors = ["mcpp-community"] repo = "https://github.com/mcpp-community/mcpp-ide" -mcpp = ">=2026.11.2.3" +mcpp = ">=2026.11.3.1" [language] standard = "c++23" @@ -33,13 +33,13 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.23.1's version group is openkal 0.23's, +# The platform of the ok images the editor packages carry: 0.23.2's version group is openkal 0.23's, # which mcpplibs.os 0.2.8 is written against; its okrun places an image at a base of the boot, keeps # its data as a prepared copy and starts it from a snapshot of its initialisers, and spends by the # program's resource preference (openkal-image 0.4.1); every unit is compiled with frame pointers and -# hardening, the std module included from mcpp 2026.11.2.3 (mcpp design 2026-11-03 part 4). +# hardening, the std module included from mcpp 2026.11.3.1 (mcpp design 2026-11-03 part 4). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.23.1" +platform = "openkal-platform@0.23.2" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] From 143760a1a0d6175ccf8642b21be8813fdc5de08f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 22:06:38 +0900 Subject: [PATCH 118/119] ci: align IDE with platform 0.23.3 and mcpp 2026.11.3.1 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/versions.env | 2 +- .github/workflows/ci.yml | 2 +- .xlings.json | 2 +- mcpp.toml | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/versions.env b/.github/versions.env index 9db449d..88b0b54 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,7 +1,7 @@ # The versions CI builds and tests with, in one file; .github/actions/setup-mcpp exports them. # The released mcpp CI installs and uses: it states its features (`features`, the project's mcpp of # part 2 §4.3), test-launch among them (2026.10.31.2), and builds the backend's ok images on -# openkal-platform 0.23.2, which it compiles the std module for with the platform's words +# openkal-platform 0.23.3, which it compiles the std module for with the platform's words # (2026.11.3.1). The mock replays the control contract the fixtures were captured from # (protocol/fixtures/version.txt). MCPP_VERSION=2026.11.3.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5252bce..12a4706 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - { os: ubuntu-24.04-arm, platform: linux-arm64, llvm: 'false' } - { os: macos-15, platform: darwin-arm64, llvm: 'true' } - { os: windows-2022, platform: win32-x64, llvm: 'true' } - - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.23.2' } + - { os: ubuntu-24.04, platform: linux-x64, llvm: 'false', host-platform: 'openkal-platform@0.23.3' } defaults: run: shell: bash diff --git a/.xlings.json b/.xlings.json index 0a869b0..116255d 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.11.2.2" + "mcpp": "2026.11.3.1" } } diff --git a/mcpp.toml b/mcpp.toml index b954d7b..98ad6f4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,13 +33,13 @@ toolchain = "llvm@23.1.3" [target.'cfg(windows)'] toolchain = "llvm@23.1.3" -# The platform of the ok images the editor packages carry: 0.23.2's version group is openkal 0.23's, -# which mcpplibs.os 0.2.8 is written against; its okrun places an image at a base of the boot, keeps -# its data as a prepared copy and starts it from a snapshot of its initialisers, and spends by the -# program's resource preference (openkal-image 0.4.1); every unit is compiled with frame pointers and +# The platform of the ok images the editor packages carry: 0.23.3's version group is openkal 0.23's, +# which mcpplibs.os 0.2.9 is written against; its okrun places an image at a base of the boot, keeps +# its data as a prepared copy and can start it from a completed snapshot of eligible initialisers, spending by the +# program's resource preference (openkal-image 0.4.3); every unit is compiled with frame pointers and # hardening, the std module included from mcpp 2026.11.3.1 (mcpp design 2026-11-03 part 4). [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.23.2" +platform = "openkal-platform@0.23.3" # An arm64 Linux machine's own programs: the xlings gcc payload has no aarch64-hosted C++ frontend. [target.aarch64-linux-gnu] @@ -76,9 +76,9 @@ kind = "bin" main = "src/bin/dev.cpp" [dependencies.mcpplibs] -# os 0.2.8: openkal 0.23 (openkal-linux 0.24.0, openkal-macos 0.21.2, openkal-windows 0.19.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, +# os 0.2.9: openkal 0.23 (openkal-linux 0.24.1, openkal-macos 0.21.3, openkal-windows 0.19.0). Since 0.2.1 an ok image's C library, C++ run-time and loader are its platform's, # so the package declares no openkal-tools of its own beside the platform's. -os = "0.2.8" +os = "0.2.9" [dependencies.nlohmann] json = "3.12.0" From 01b0e30cb6aec3b5c7bd5ad1194977438248e60f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 4 Nov 2026 00:01:59 +0900 Subject: [PATCH 119/119] docs: align IDE installation notes with platform 0.23.3 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- README.md | 2 +- README.zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1cfdaf..4fff171 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ the editor I use (VS Code, CLion or Neovim) from its latest release, and check i | Agents, other clients | `mcpp-ide--openkal-bundle-all.tar.gz` (both images; okrun for Linux x64/arm64, macOS arm64, Windows x64), `mcpp-ide-.okb` with okrun, or `xlings install mcpp-ide` | run `bin/mcpp-ide serve` (`bin\mcpp-ide.exe` on Windows), or `okrun mcpp-ide-.okb serve` | Each editor package serves every system: it carries the backend as one ok-image bundle -(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.21.1) and okrun for +(`backend/mcpp-ide.okb`, the x86_64 and aarch64 images on openkal-platform 0.23.3) and okrun for Linux x64/arm64, macOS arm64 and Windows x64. The Neovim plugin carries none: it starts the `mcpp-ide` command xlings installs (okrun on the same bundle). diff --git a/README.zh-CN.md b/README.zh-CN.md index 24cae93..aa67629 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ quickfix 列表。各编辑器插件位于本仓库的 `mide-plugins/`。C++ 文 | Neovim 0.10+ | release 源码树中的 `mide-plugins/nvim` | `xlings install mcpp-ide-nvim` 与 `xlings install mcpp-ide`,再 `require('mcpp').setup()`;或把该目录加入 runtimepath([其 README](mide-plugins/nvim/README.md)) | | Agent、其他客户端 | `mcpp-ide--openkal-bundle-all.tar.gz`(两个架构的映像;Linux x64/arm64、macOS arm64、Windows x64 的 okrun),携带 okrun 的 `mcpp-ide-.okb`,或 `xlings install mcpp-ide` | 运行 `bin/mcpp-ide serve`(Windows 上为 `bin\mcpp-ide.exe`),或 `okrun mcpp-ide-.okb serve` | -每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.21.1 上的 +每个编辑器包都服务全部系统:后端以一个 ok 映像包(`backend/mcpp-ide.okb`,openkal-platform 0.23.3 上的 x86_64 与 aarch64 映像)加上 Linux x64/arm64、macOS arm64、Windows x64 的 okrun 携带。Neovim 插件不携带后端: 它启动 xlings 安装的 `mcpp-ide` 命令(由 okrun 运行同一个映像包)。