Skip to content

Commit e8586b5

Browse files
authored
Merge pull request #9 from Ximiaw/feat/mcpp-toml-structure-completion
feat: add structural completion for mcpp.toml
2 parents 18d828e + d5b953a commit e8586b5

9 files changed

Lines changed: 1988 additions & 3 deletions

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的
1818
| --- | --- | --- | --- |
1919
| 识别 mcpp 工程 | 支持 | 支持 | 支持 |
2020
| `mcpp.toml` TOML 语法高亮 | 支持 | 支持 | 支持 |
21+
| `mcpp.toml` 结构补全(段头 + 写法模板) | 支持 | 支持 | 支持 |
2122
| `build.mcpp``.cppm``.ixx``.mpp``.ccm` 文件关联 | 支持 | 支持 | 支持 |
2223
| `module``export module``import` 语法高亮 | 支持 | 支持 | 支持 |
2324
| 构建、运行、测试、清理命令 | 支持 | 支持 | 支持 |
@@ -97,6 +98,19 @@ TextMate 语法规则提供。
9798
`import mcpp;` 不会再被误报为缺少 C++ 模块。真正的 mcpp API 补全需要 mcpp 核心
9899
未来生成宿主 helper 的 CDB 和 PCM 映射。
99100

101+
### mcpp.toml 结构补全
102+
103+
- 段头补全:26 个段(`[package]``[targets.<name>]``[dependencies]``[build-dependencies]`
104+
`[features]``[indices]``[pack]` 等),参数化段带可跳转占位符。
105+
- 写法模板:依赖段的依赖写法(版本 / 路径 / git / features / tools)、`[features]` 表形式、
106+
capabilities / xlings / tools.overrides / generated_files 等开放段的条目形态。
107+
- 每条建议携带显式替换范围,部分输入(`[dep``na`)不会残留无效文本;所有语义规则有真实
108+
mcpp 契约测试(44 例,无 mcpp 环境自动跳过)。
109+
- 范围边界:不提供静态字段键/枚举(等上游版本化 manifest schema),不提供依赖包名/版本候选
110+
(等上游批量 catalog 接口);未知自定义段与 `[[...]]` 数组表不提供建议。
111+
-`mcpp.tomlCompletion` 设置控制(默认开启);未受信任工作区仅做纯文本分析,不执行任何
112+
外部程序。
113+
100114
### LLVM 与 clangd 集成
101115

102116
扩展读取 mcpp 生成的 `compile_commands.json`,然后:
@@ -172,7 +186,8 @@ xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB
172186
"mcpp.path": "/path/to/mcpp",
173187
"mcpp.clangd.path": "/path/to/matching/clangd",
174188
"mcpp.modulesSupport": "auto",
175-
"mcpp.configureCppTools": true
189+
"mcpp.configureCppTools": true,
190+
"mcpp.tomlCompletion": true
176191
}
177192
```
178193

@@ -182,6 +197,7 @@ xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB
182197
| `mcpp.clangd.path` || 与 CDB 中 LLVM 编译器匹配的 clangd;空值表示自动发现 |
183198
| `mcpp.modulesSupport` | `auto` | `auto``on``off`,控制 clangd 实验模块参数 |
184199
| `mcpp.configureCppTools` | `true` | 手动配置 clangd 时,是否询问关闭当前工作区的 cpptools IntelliSense |
200+
| `mcpp.tomlCompletion` | `true` |`mcpp.toml` 提供结构补全:段头与写法模板(snippet);所有建议带显式替换范围,并经真实 mcpp 契约测试验证 |
185201

186202
`mcpp.path` 只影响插件执行 mcpp CLI 命令。工程实际编译器来自
187203
`compile_commands.json``mcpp.clangd.path` 只指定语言服务器,三者相互独立。

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"workspaceContains:mcpp.toml",
2525
"onLanguage:cpp",
2626
"onLanguage:mcpp-build",
27+
"onLanguage:mcpp-toml",
2728
"onCommand:mcpp.configureClangd",
2829
"onCommand:mcpp.refreshCompilationDatabase",
2930
"onCommand:mcpp.checkModuleSupport",
@@ -45,7 +46,7 @@
4546
"capabilities": {
4647
"untrustedWorkspaces": {
4748
"supported": "limited",
48-
"description": "未受信任工作区仅启用模块语法高亮,不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。"
49+
"description": "未受信任工作区仅启用模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。"
4950
}
5051
},
5152
"main": "./dist/src/extension.js",
@@ -151,6 +152,12 @@
151152
"default": true,
152153
"scope": "resource",
153154
"description": "配置 clangd 时,是否询问关闭 Microsoft C/C++ IntelliSense。"
155+
},
156+
"mcpp.tomlCompletion": {
157+
"type": "boolean",
158+
"default": true,
159+
"scope": "resource",
160+
"description": "为 mcpp.toml 提供结构补全:段头与写法模板(snippet)。所有建议带显式替换范围,并经真实 mcpp 契约测试验证。"
154161
}
155162
}
156163
},

src/extension.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
} from "./workflow";
4646
import { classifyTaskExit, type TaskCompletion } from "./tasks";
4747
import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./inProject";
48+
import { computeMcppTomlCompletions } from "./mcppTomlCompletion";
4849
import {
4950
buildModuleSetupPlan,
5051
executeModuleSetup,
@@ -825,6 +826,47 @@ async function autoConfigureModulesWizard(
825826
}
826827
}
827828

829+
// mcpp.toml 结构补全:建议由纯函数 computeMcppTomlCompletions 计算,这里只做
830+
// vscode 类型映射。范围:段头 snippet + 开放词汇段的写法模板;不含字段键/枚举
831+
// 与依赖数据(分别等上游版本化 schema 与批量 catalog 接口)。
832+
const mcppTomlCompletionKinds = {
833+
section: vscode.CompletionItemKind.Folder,
834+
template: vscode.CompletionItemKind.Snippet,
835+
} as const;
836+
837+
const mcppTomlCompletionProvider: vscode.CompletionItemProvider = {
838+
provideCompletionItems(document, position) {
839+
// mcpp.toml 结构补全由 mcpp.tomlCompletion 控制,按文档作用域读取。
840+
if (!vscode.workspace.getConfiguration("mcpp", document.uri).get<boolean>("tomlCompletion", true)) {
841+
return undefined;
842+
}
843+
const lines: string[] = [];
844+
for (let line = 0; line <= position.line; line += 1) {
845+
lines.push(document.lineAt(line).text);
846+
}
847+
return computeMcppTomlCompletions(lines, position.line, position.character).map((suggestion) => {
848+
const item = new vscode.CompletionItem(
849+
suggestion.label,
850+
mcppTomlCompletionKinds[suggestion.kind],
851+
);
852+
item.detail = suggestion.detail;
853+
if (suggestion.documentation !== undefined) {
854+
item.documentation = new vscode.MarkdownString(suggestion.documentation);
855+
}
856+
if (suggestion.insertSnippet !== undefined) {
857+
item.insertText = new vscode.SnippetString(suggestion.insertSnippet);
858+
}
859+
item.range = new vscode.Range(
860+
position.line,
861+
suggestion.range.startCharacter,
862+
position.line,
863+
suggestion.range.endCharacter,
864+
);
865+
return item;
866+
});
867+
},
868+
};
869+
828870
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
829871
moduleStatusByProject.clear();
830872
moduleCheckOperations.clear();
@@ -1001,6 +1043,11 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
10011043
output,
10021044
status,
10031045
...cliController.register(),
1046+
vscode.languages.registerCompletionItemProvider(
1047+
{ language: "mcpp-toml" },
1048+
mcppTomlCompletionProvider,
1049+
"[",
1050+
),
10041051
vscode.commands.registerCommand(COMMAND_CONFIGURE, runGuarded(async () => {
10051052
const project = findCurrentProject();
10061053
if (project === undefined) {

src/mcppTomlCompletion.ts

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// mcpp.toml 的代码补全查询层(结构补全版)。
2+
//
3+
// 范围:段头结构建议 + 开放词汇段的写法模板。每条建议携带显式替换范围。
4+
// 依赖包名/版本等动态数据补全与静态字段键/枚举补全均不在本版——前者等上游
5+
// 批量 catalog 接口,后者等版本化 manifest schema(见设计 issue #8 与
6+
// mcpp RFC #379)。
7+
//
8+
// 本模块不依赖 vscode API;上下文来自 mcppTomlParser 的 contextAt(容错解析)。
9+
10+
import {
11+
contextAt,
12+
type ReplaceRange,
13+
type SectionResolution,
14+
} from "./mcppTomlParser";
15+
16+
export type McppTomlSuggestionKind = "section" | "template";
17+
18+
export interface McppTomlSuggestion {
19+
label: string;
20+
kind: McppTomlSuggestionKind;
21+
detail: string;
22+
documentation?: string;
23+
/** 插入文本;含 $1 等 snippet 占位符。缺省时插入 label。 */
24+
insertSnippet?: string;
25+
/** 替换范围(光标所在行的起止列)。 */
26+
range: ReplaceRange;
27+
}
28+
29+
export interface SectionHeaderSpec {
30+
group: string;
31+
label: string;
32+
/** snippet 形式的段头(含 ${1:...} 占位)。 */
33+
header: string;
34+
detail: string;
35+
}
36+
37+
// 段头结构清单:TOML 结构语法,非字段语义。出处:mcpp 文档 02/03/05/06
38+
// 与 src/manifest/toml.cppm 的段清单(契约测试用真实 mcpp 逐段验证)。
39+
export const SECTION_HEADERS: readonly SectionHeaderSpec[] = [
40+
{ group: "package", label: "[package]", header: "[package]", detail: "包元数据" },
41+
{ group: "lib", label: "[lib]", header: "[lib]", detail: "库根模块约定" },
42+
{ group: "build", label: "[build]", header: "[build]", detail: "构建配置" },
43+
{ group: "generated_files", label: "[generated_files]", header: "[generated_files]", detail: "生成文件(路径 → 内容)" },
44+
{ group: "dependencies", label: "[dependencies]", header: "[dependencies]", detail: "运行时依赖" },
45+
{ group: "dev-dependencies", label: "[dev-dependencies]", header: "[dev-dependencies]", detail: "开发/测试依赖" },
46+
{ group: "build-dependencies", label: "[build-dependencies]", header: "[build-dependencies]", detail: "构建期依赖(仅构建期拉取,运行时不可见)" },
47+
{ group: "workspace", label: "[workspace]", header: "[workspace]", detail: "工作空间成员声明" },
48+
{ group: "workspace.dependencies", label: "[workspace.dependencies]", header: "[workspace.dependencies]", detail: "集中声明依赖版本,成员用 workspace = true 继承" },
49+
{ group: "features", label: "[features]", header: "[features]", detail: "feature 定义" },
50+
{ group: "feature-deps", label: "[feature-deps.<name>]", header: "[feature-deps.${1:name}]", detail: "由 feature 拉取的可选依赖" },
51+
{ group: "capabilities", label: "[capabilities]", header: "[capabilities]", detail: "capability 绑定(provider 选择)" },
52+
{ group: "targets", label: "[targets.<name>]", header: "[targets.${1:name}]", detail: "构建目标" },
53+
{ group: "profile", label: "[profile.<name>]", header: "[profile.${1:name}]", detail: "构建档案" },
54+
{ group: "runtime", label: "[runtime]", header: "[runtime]", detail: "主机运行时能力" },
55+
{ group: "resources", label: "[resources]", header: "[resources]", detail: "编译进产物的元数据与资产(仅 PE 目标)" },
56+
{ group: "toolchain", label: "[toolchain]", header: "[toolchain]", detail: "编译器工具链简写" },
57+
{ group: "xlings", label: "[xlings]", header: "[xlings]", detail: "构建环境(xlings 供给)" },
58+
{ group: "xlings.workspace", label: "[xlings.workspace]", header: "[xlings.workspace]", detail: "固定工具版本" },
59+
{ group: "xlings.envs", label: "[xlings.envs]", header: "[xlings.envs]", detail: "工具环境的环境变量" },
60+
{ group: "target", label: "[target.<triple>]", header: "[target.${1:x86_64-linux-gnu}]", detail: "按目标三元组的配置" },
61+
{ group: "pack", label: "[pack]", header: "[pack]", detail: "mcpp pack 打包配置" },
62+
{ group: "pack.bundle-project", label: "[pack.bundle-project]", header: "[pack.bundle-project]", detail: "vendored 过滤策略微调" },
63+
{ group: "indices", label: "[indices]", header: "[indices]", detail: "项目级索引重定向" },
64+
{ group: "tools.overrides", label: "[tools.overrides]", header: "[tools.overrides]", detail: "host 工具二进制覆盖" },
65+
{ group: "language", label: "[language]", header: "[language]", detail: "旧版兼容字段;新项目请用 [package].standard" },
66+
];
67+
68+
/** 依赖类段(键位置给依赖写法模板)。 */
69+
const DEPENDENCY_GROUPS: ReadonlySet<string> = new Set([
70+
"dependencies",
71+
"dev-dependencies",
72+
"build-dependencies",
73+
"workspace.dependencies",
74+
"feature-deps",
75+
]);
76+
77+
interface TemplateSpec {
78+
label: string;
79+
detail: string;
80+
documentation?: string;
81+
insertSnippet: string;
82+
}
83+
84+
const DEPENDENCY_TEMPLATES: readonly TemplateSpec[] = [
85+
{
86+
label: 'name = "version"',
87+
detail: "SemVer 版本依赖",
88+
documentation: "默认 caret 约束(^);也支持 ~、= 与 \">=1.0, <2.0\" 范围组合。",
89+
insertSnippet: '${1:name} = "${2:1.0.0}"',
90+
},
91+
{
92+
label: "name = { path = ... }",
93+
detail: "路径依赖(本地开发)",
94+
insertSnippet: '${1:name} = { path = "${2:../mylib}" }',
95+
},
96+
{
97+
label: "name = { git = ..., tag = ... }",
98+
detail: "Git 依赖(tag / branch / rev 三选一)",
99+
insertSnippet: '${1:name} = { git = "${2:https://github.com/user/repo.git}", tag = "${3:v1.0.0}" }',
100+
},
101+
{
102+
label: "name = { version = ..., features = [...] }",
103+
detail: "长式 dep spec:请求该依赖的 feature",
104+
insertSnippet: '${1:name} = { version = "${2:1.0}", features = ["${3:feature}"] }',
105+
},
106+
{
107+
label: "name = { version = ..., tools = [...] }",
108+
detail: "依赖产出的 host 工具(须为该包的 bin target)",
109+
insertSnippet: '${1:name} = { version = "${2:1.0}", tools = ["${3:protoc}"] }',
110+
},
111+
];
112+
113+
const FEATURE_TEMPLATES: readonly TemplateSpec[] = [
114+
{ label: "name = [...]", detail: "数组简写:仅隐含 feature", insertSnippet: "${1:name} = [${2}]" },
115+
{ label: "name = { defines = [...] }", detail: "表形式:激活时贡献包自有宏", insertSnippet: '${1:name} = { defines = ["${2:MACRO}"] }' },
116+
{ label: "name = { requires = [...] }", detail: "表形式:需要 capability", insertSnippet: '${1:name} = { requires = ["${2:blas}"] }' },
117+
{ label: "name = { sources = [...] }", detail: "表形式:feature 门控的源 glob", insertSnippet: '${1:name} = { sources = ["${2:src/simd/**}"] }' },
118+
];
119+
120+
const GENERATED_FILE_TEMPLATES: readonly TemplateSpec[] = [
121+
{
122+
label: '"path" = "content"',
123+
detail: "生成文件(相对路径 → 内容,进指纹)",
124+
insertSnippet: '"${1:src/gen/wrap.cppm}" = """\n${2:}\n"""',
125+
},
126+
];
127+
128+
const CAPABILITY_TEMPLATES: readonly TemplateSpec[] = [
129+
{
130+
label: 'capability = "provider"',
131+
detail: "capability 绑定(等价于 --cap)",
132+
insertSnippet: '${1:blas} = "${2:compat.openblas}"',
133+
},
134+
];
135+
136+
const XLINGS_WORKSPACE_TEMPLATES: readonly TemplateSpec[] = [
137+
{ label: 'tool = "version"', detail: "固定工具版本", insertSnippet: '${1:clang} = "${2:20.1.7}"' },
138+
];
139+
140+
const XLINGS_ENVS_TEMPLATES: readonly TemplateSpec[] = [
141+
{ label: 'NAME = "value"', detail: "应用到工具环境的环境变量", insertSnippet: '${1:NAME} = "${2:value}"' },
142+
];
143+
144+
const TOOLS_OVERRIDES_TEMPLATES: readonly TemplateSpec[] = [
145+
{
146+
label: '"pkg:tool" = "path"',
147+
detail: "用已有二进制覆盖 host 工具(跳过构建)",
148+
insertSnippet: '"${1:compat.protobuf:protoc}" = "${2:/usr/bin/protoc}"',
149+
},
150+
];
151+
152+
const TEMPLATES_BY_GROUP: Record<string, readonly TemplateSpec[]> = {
153+
"features": FEATURE_TEMPLATES,
154+
"generated_files": GENERATED_FILE_TEMPLATES,
155+
"capabilities": CAPABILITY_TEMPLATES,
156+
"xlings.workspace": XLINGS_WORKSPACE_TEMPLATES,
157+
"xlings.envs": XLINGS_ENVS_TEMPLATES,
158+
"tools.overrides": TOOLS_OVERRIDES_TEMPLATES,
159+
};
160+
161+
function sectionHeaderSuggestions(range: ReplaceRange): McppTomlSuggestion[] {
162+
return SECTION_HEADERS.map((section) => ({
163+
label: section.label,
164+
kind: "section",
165+
detail: section.detail,
166+
insertSnippet: section.header,
167+
range,
168+
}));
169+
}
170+
171+
function templateSuggestions(templates: readonly TemplateSpec[], range: ReplaceRange): McppTomlSuggestion[] {
172+
return templates.map((template) => ({
173+
label: template.label,
174+
kind: "template",
175+
detail: template.detail,
176+
documentation: template.documentation,
177+
insertSnippet: template.insertSnippet,
178+
range,
179+
}));
180+
}
181+
182+
/**
183+
* 计算 mcpp.toml 在指定位置的补全建议(结构补全:段头 + 写法模板)。
184+
*/
185+
export function computeMcppTomlCompletions(
186+
lines: readonly string[],
187+
line: number,
188+
character: number,
189+
): McppTomlSuggestion[] {
190+
const context = contextAt(lines, line, character);
191+
192+
if (context.kind === "section-header") {
193+
// mcpp manifest 不使用 TOML 数组表([[...]]);[[ 内不提供建议,
194+
// 避免把用户意图的数组表悄悄替换成普通段 [x](未知段会被 mcpp 静默忽略)。
195+
if (context.isArray) {
196+
return [];
197+
}
198+
// parser 的替换范围从段名 token 开始;段头建议插入的是完整 "[xxx]",
199+
// 需要把范围扩展到本行的 "[",避免留下 "[["。仅当 "[" 是行内首个
200+
// 非空白字符时才扩展(section-header 上下文正常都满足,防御奇怪输入)。
201+
const lineText = (lines[line] ?? "").replace(/\r$/, "");
202+
const bracket = lineText.indexOf("[");
203+
const firstNonWs = lineText.search(/\S/);
204+
const range = bracket >= 0 && bracket === firstNonWs
205+
? { startCharacter: bracket, endCharacter: context.replaceRange.endCharacter }
206+
: context.replaceRange;
207+
return sectionHeaderSuggestions(range);
208+
}
209+
210+
if (context.kind === "key") {
211+
const { section, containerPath, replaceRange } = context;
212+
// 文档顶部(尚无段头):提示段头。未知段:不提供建议
213+
// (附录 A:不支持包自定义 toml 键)。
214+
if (section.kind === "top") {
215+
return sectionHeaderSuggestions(replaceRange);
216+
}
217+
if (section.kind !== "known" || containerPath.length > 0) {
218+
return [];
219+
}
220+
if (DEPENDENCY_GROUPS.has(section.group)) {
221+
return templateSuggestions(DEPENDENCY_TEMPLATES, replaceRange);
222+
}
223+
const templates = TEMPLATES_BY_GROUP[section.group];
224+
return templates === undefined ? [] : templateSuggestions(templates, replaceRange);
225+
}
226+
227+
// 值位置:自由格式值不瞎猜(版本候选等动态数据层落地后再说)。
228+
return [];
229+
}

0 commit comments

Comments
 (0)