实战:代码片段管理插件
把语言服务特性综合运用,实现一个代码片段管理插件:自定义代码片段补全、语法错误诊断、悬停文档显示,一条龙增强代码编辑体验。
目标
- 自定义代码片段:输入关键字弹出模板补全
- 语法错误提示:不符合规则的代码标红
- 悬停文档:悬停片段关键字显示说明
第一步:注册语言服务
src/extension.ts:
typescript
import * as vscode from 'vscode';
import { SnippetProvider } from './snippetProvider';
import { SnippetHoverProvider } from './hoverProvider';
import { SnippetDiagnosticProvider } from './diagnosticProvider';
export function activate(context: vscode.ExtensionContext) {
// 片段数据源
const snippets = loadSnippets();
// 补全 Provider
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
'plaintext',
new SnippetProvider(snippets),
'.' // 输入 . 触发
)
);
// 悬停 Provider
context.subscriptions.push(
vscode.languages.registerHoverProvider(
'plaintext',
new SnippetHoverProvider(snippets)
)
);
// 诊断集合
const diagnostics = vscode.languages.createDiagnosticCollection(
'snippets'
);
context.subscriptions.push(diagnostics);
// 文档校验
const diagnosticProvider = new SnippetDiagnosticProvider(diagnostics);
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((doc) =>
diagnosticProvider.validate(doc)
),
vscode.workspace.onDidChangeTextDocument((event) =>
diagnosticProvider.validate(event.document)
)
);
}第二步:片段数据模型
src/snippets.ts:
typescript
export interface SnippetDef {
name: string; // 触发关键字
description: string; // 描述
body: string; // 片段模板(SnippetString)
doc: string; // 悬停文档
}
export function loadSnippets(): SnippetDef[] {
return [
{
name: 'fn',
description: '函数定义',
body: 'function ${1:name}(${2:args}) {\n\t${3}\n}',
doc: '定义一个新函数。\n\n```javascript\nfunction add(a, b) { return a + b; }\n```'
},
{
name: 'if',
description: '条件判断',
body: 'if (${1:condition}) {\n\t${2}\n}',
doc: '条件判断语句。\n\n```javascript\nif (score > 60) { pass(); }\n```'
},
{
name: 'for',
description: '循环',
body: 'for (let ${1:i} = 0; ${1:i} < ${2:length}; ${1:i}++) {\n\t${3}\n}',
doc: 'for 循环遍历。'
},
{
name: 'try',
description: '异常捕获',
body: 'try {\n\t${1}\n} catch (${2:error}) {\n\t${3}\n}',
doc: '异常捕获语句。'
}
];
}第三步:补全 Provider
src/snippetProvider.ts:
typescript
import * as vscode from 'vscode';
import { SnippetDef } from './snippets';
export class SnippetProvider
implements vscode.CompletionItemProvider {
constructor(private readonly snippets: SnippetDef[]) {}
provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
): vscode.CompletionItem[] {
// 获取当前输入前缀
const line = document.lineAt(position.line).text;
const before = line.slice(0, position.character);
const wordMatch = before.match(/(\w+)\.?$/);
const prefix = wordMatch ? wordMatch[1] : '';
// 过滤匹配的片段
return this.snippets
.filter((s) => s.name.startsWith(prefix))
.map((snippet) => this.buildItem(snippet));
}
private buildItem(snippet: SnippetDef): vscode.CompletionItem {
const item = new vscode.CompletionItem(snippet.name);
item.kind = vscode.CompletionItemKind.Snippet;
item.detail = snippet.description;
item.insertText = new vscode.SnippetString(snippet.body);
item.documentation = new vscode.MarkdownString(
`**${snippet.name}** — ${snippet.description}\n\n` +
snippet.doc
);
item.sortText = '0'; // 模板排最前
// 补全后触发参数补全
item.command = {
command: 'editor.action.triggerSuggest',
title: '继续补全'
};
return item;
}
}第四步:悬停 Provider
src/hoverProvider.ts:
typescript
import * as vscode from 'vscode';
import { SnippetDef } from './snippets';
export class SnippetHoverProvider
implements vscode.HoverProvider {
constructor(private readonly snippets: SnippetDef[]) {}
provideHover(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Hover | undefined {
// 获取悬停单词
const range = document.getWordRangeAtPosition(position);
if (!range) {
return undefined;
}
const word = document.getText(range);
// 查找匹配片段
const snippet = this.snippets.find((s) => s.name === word);
if (!snippet) {
return undefined;
}
// 构建悬停内容
const contents = new vscode.MarkdownString();
contents.appendMarkdown(`### \`${snippet.name}\`\n\n`);
contents.appendMarkdown(snippet.doc);
contents.appendMarkdown('\n\n$(zap) 代码片段');
return new vscode.Hover(contents, range);
}
}第五步:诊断 Provider
src/diagnosticProvider.ts:
typescript
import * as vscode from 'vscode';
export class SnippetDiagnosticProvider {
constructor(
private readonly collection: vscode.DiagnosticCollection
) {}
validate(document: vscode.TextDocument): void {
if (document.languageId !== 'plaintext') {
return;
}
const diagnostics: vscode.Diagnostic[] = [];
for (let line = 0; line < document.lineCount; line++) {
const text = document.lineAt(line).text;
// 规则 1:片段名后缺少括号
const noParenMatch = text.match(/\b(fn|if|for|try)\s+[^(]/);
if (noParenMatch) {
const index = text.indexOf(noParenMatch[1]);
diagnostics.push(
new vscode.Diagnostic(
new vscode.Range(line, index, line, index + noParenMatch[1].length),
'片段名后需要括号,如 fn(名称)',
vscode.DiagnosticSeverity.Warning
)
);
}
// 规则 2:不完整片段
const dangling = text.match(/\b(if|for|try)\s*\([^)]*\)\s*$/);
if (dangling) {
diagnostics.push(
new vscode.Diagnostic(
new vscode.Range(
line, 0, line, text.length
),
'缺少代码块 { }',
vscode.DiagnosticSeverity.Error
)
);
}
// 规则 3:行过长
if (text.length > 100) {
diagnostics.push(
new vscode.Diagnostic(
new vscode.Range(line, 100, line, text.length),
'行过长(超过 100 字符)',
vscode.DiagnosticSeverity.Hint
)
);
}
}
this.collection.set(document.uri, diagnostics);
}
}第六步:快捷键与命令
package.json:
json
{
"contributes": {
"commands": [
{
"command": "snippets.insert",
"title": "插入代码片段",
"category": "代码片段"
}
],
"keybindings": [
{
"command": "snippets.insert",
"key": "ctrl+shift+i",
"mac": "cmd+shift+i"
}
]
}
}extension.ts 增加命令:
typescript
// 命令:QuickPick 选择片段插入
context.subscriptions.push(
vscode.commands.registerCommand('snippets.insert', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
// QuickPick 选择片段
const picked = await vscode.window.showQuickPick(
snippets.map((s) => ({
label: s.name,
description: s.description,
snippet: s
})),
{ title: '选择代码片段' }
);
if (picked) {
await editor.insertSnippet(
new vscode.SnippetString(picked.snippet.body)
);
}
})
);运行与验证
按 F5 启动调试:
| 步骤 | 操作 | 预期 |
|---|---|---|
| 1 | 打开空白文件,输入 fn | 弹出片段补全 |
| 2 | 选择 fn | 插入函数模板,光标在名称处 |
| 3 | Tab 依次填写 | 名称 → 参数 → 函数体 |
| 4 | 悬停 fn 字样 | 显示文档说明 |
| 5 | 输入 if 条件(缺括号) | 黄色警告波浪线 |
| 6 | 按 Ctrl+Shift+I | QuickPick 选择片段插入 |
功能扩展
| 扩展方向 | 实现 |
|---|---|
| 自定义片段库 | 配置文件 + 用户自定义 |
| 语法高亮 | 添加 TextMate 语法 |
| 自动修复 | 诊断关联 CodeAction |
| 片段分类 | 按语言/场景分组 |
| 片段搜索 | 全局搜索片段 |
常见问题
| 问题 | 处理 |
|---|---|
| 补全不弹出 | 检查语言 ID 与触发字符 |
| 模板不联动 | 确认 SnippetString 占位符语法 |
| 诊断不更新 | 检查事件监听 |
| 悬停无内容 | 检查片段名匹配 |
本实战完整串联了 Completion、Hover、Diagnostic 三大语言服务能力,是构建「编辑增强类」插件的模板。