Skip to content

Commit c90bf6a

Browse files
committed
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.<sel>.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).
1 parent 1a8ae33 commit c90bf6a

8 files changed

Lines changed: 1951 additions & 2 deletions

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",
@@ -44,7 +45,7 @@
4445
"capabilities": {
4546
"untrustedWorkspaces": {
4647
"supported": "limited",
47-
"description": "未受信任工作区仅启用模块语法高亮,不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。"
48+
"description": "未受信任工作区仅启用模块语法高亮与 mcpp.toml 结构补全(纯文本分析),不执行 CDB、mcpp 或 clangd 指定的任何程序,也不接管 clangd 配置。"
4849
}
4950
},
5051
"main": "./dist/src/extension.js",
@@ -146,6 +147,12 @@
146147
"default": true,
147148
"scope": "resource",
148149
"description": "配置 clangd 时,是否询问关闭 Microsoft C/C++ IntelliSense。"
150+
},
151+
"mcpp.tomlCompletion": {
152+
"type": "boolean",
153+
"default": true,
154+
"scope": "resource",
155+
"description": "为 mcpp.toml 提供结构补全:段头与写法模板(snippet)。所有建议带显式替换范围,并经真实 mcpp 契约测试验证。"
149156
}
150157
}
151158
},

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

4950
const COMMAND_CONFIGURE = "mcpp.configureClangd";
5051
const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase";
@@ -852,6 +853,47 @@ async function autoConfigureModulesWizard(
852853
appendOutputLine(output, `[一键配置] 一键配置完成。clangd:${resolvedClangd.path}`);
853854
}
854855

856+
// mcpp.toml 结构补全:建议由纯函数 computeMcppTomlCompletions 计算,这里只做
857+
// vscode 类型映射。范围:段头 snippet + 开放词汇段的写法模板;不含字段键/枚举
858+
// 与依赖数据(分别等上游版本化 schema 与批量 catalog 接口)。
859+
const mcppTomlCompletionKinds = {
860+
section: vscode.CompletionItemKind.Folder,
861+
template: vscode.CompletionItemKind.Snippet,
862+
} as const;
863+
864+
const mcppTomlCompletionProvider: vscode.CompletionItemProvider = {
865+
provideCompletionItems(document, position) {
866+
// mcpp.toml 结构补全由 mcpp.tomlCompletion 控制,按文档作用域读取。
867+
if (!vscode.workspace.getConfiguration("mcpp", document.uri).get<boolean>("tomlCompletion", true)) {
868+
return undefined;
869+
}
870+
const lines: string[] = [];
871+
for (let line = 0; line <= position.line; line += 1) {
872+
lines.push(document.lineAt(line).text);
873+
}
874+
return computeMcppTomlCompletions(lines, position.line, position.character).map((suggestion) => {
875+
const item = new vscode.CompletionItem(
876+
suggestion.label,
877+
mcppTomlCompletionKinds[suggestion.kind],
878+
);
879+
item.detail = suggestion.detail;
880+
if (suggestion.documentation !== undefined) {
881+
item.documentation = new vscode.MarkdownString(suggestion.documentation);
882+
}
883+
if (suggestion.insertSnippet !== undefined) {
884+
item.insertText = new vscode.SnippetString(suggestion.insertSnippet);
885+
}
886+
item.range = new vscode.Range(
887+
position.line,
888+
suggestion.range.startCharacter,
889+
position.line,
890+
suggestion.range.endCharacter,
891+
);
892+
return item;
893+
});
894+
},
895+
};
896+
855897
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
856898
moduleStatusByProject.clear();
857899
moduleCheckOperations.clear();
@@ -1028,6 +1070,11 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
10281070
output,
10291071
status,
10301072
...cliController.register(),
1073+
vscode.languages.registerCompletionItemProvider(
1074+
{ language: "mcpp-toml" },
1075+
mcppTomlCompletionProvider,
1076+
"[",
1077+
),
10311078
vscode.commands.registerCommand(COMMAND_CONFIGURE, runGuarded(async () => {
10321079
const project = findCurrentProject();
10331080
if (project === undefined) {

src/mcppTomlCompletion.ts

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

0 commit comments

Comments
 (0)