本文是VS Code插件开发系列的第二阶段实战指南,将深入讲解插件开发的核心功能实现。从简单的命令注册到复杂的编辑器操作,从基础的事件监听到高级的UI扩展,通过实际案例帮助你掌握插件开发的核心技能。


🎯 阶段二学习目标

在完成环境搭建和项目创建后,我们需要掌握以下核心技能:

  • 命令系统:注册自定义命令,实现功能入口
  • 编辑器操作:读取、修改、插入文本内容
  • 事件监听:响应编辑器状态变化
  • UI扩展:状态栏、侧边栏、菜单等界面元素
  • 状态管理:插件数据的存储和读取

🚀 核心API概览

VS Code插件开发主要涉及以下几个核心API模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as vscode from 'vscode';

// 命令系统 - 注册和执行命令
vscode.commands.registerCommand()

// 窗口操作 - 显示信息、打开文件等
vscode.window.showInformationMessage()
vscode.window.activeTextEditor

// 工作区操作 - 文件系统、配置等
vscode.workspace.openTextDocument()
vscode.workspace.getConfiguration()

// 编辑器操作 - 文本编辑、选择等
editor.edit()
editor.selection

📝 实战一:命令注册与交互

基础命令注册

让我们从最简单的命令开始:

1
2
3
4
5
6
7
8
9
10
11
// src/extension.ts
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
// 注册一个简单的问候命令
let helloCommand = vscode.commands.registerCommand('myextension.hello', () => {
vscode.window.showInformationMessage('Hello from my extension!');
});

context.subscriptions.push(helloCommand);
}

在package.json中声明命令

1
2
3
4
5
6
7
8
9
{
"contributes": {
"commands": [{
"command": "myextension.hello",
"title": "Say Hello",
"category": "My Extension"
}]
}
}

带参数的命令

1
2
3
4
5
6
// 注册带参数的命令
let greetCommand = vscode.commands.registerCommand('myextension.greet', (name: string) => {
vscode.window.showInformationMessage(`Hello, ${name}!`);
});

// 调用方式:在命令面板中输入 "My Extension: Greet" 并传入参数

实战案例:插入当前时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 插入当前时间的命令
let insertTimeCommand = vscode.commands.registerCommand('myextension.insertTime', () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showWarningMessage('No active editor found!');
return;
}

const currentTime = new Date().toLocaleString();
const position = editor.selection.active;

editor.edit(editBuilder => {
editBuilder.insert(position, currentTime);
});
});

✏️ 实战二:编辑器内容操作

获取编辑器信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取当前活动编辑器
const editor = vscode.window.activeTextEditor;
if (editor) {
// 获取文档信息
const document = editor.document;
console.log('文件路径:', document.fileName);
console.log('语言ID:', document.languageId);
console.log('行数:', document.lineCount);

// 获取选择信息
const selection = editor.selection;
console.log('选择起始位置:', selection.start);
console.log('选择结束位置:', selection.end);
console.log('选择文本:', document.getText(selection));
}

文本编辑操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 插入文本
editor.edit(editBuilder => {
editBuilder.insert(position, 'Hello World');
});

// 替换选中文本
editor.edit(editBuilder => {
editBuilder.replace(selection, 'New Text');
});

// 删除文本
editor.edit(editBuilder => {
editBuilder.delete(selection);
});

实战案例:变量提取工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 将选中文本提取为变量
let extractVariableCommand = vscode.commands.registerCommand('myextension.extractVariable', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;

const selection = editor.selection;
const selectedText = editor.document.getText(selection);

if (!selectedText) {
vscode.window.showWarningMessage('请先选择要提取的文本');
return;
}

// 提示用户输入变量名
const variableName = await vscode.window.showInputBox({
prompt: '请输入变量名',
value: 'extractedValue'
});

if (!variableName) return;

// 生成变量声明
const variableDeclaration = `const ${variableName} = ${selectedText};\n`;

// 在文档开头插入变量声明
const startPosition = new vscode.Position(0, 0);

editor.edit(editBuilder => {
editBuilder.insert(startPosition, variableDeclaration);
});

vscode.window.showInformationMessage(`变量 ${variableName} 提取成功!`);
});

代码片段插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用SnippetString插入代码片段
let insertSnippetCommand = vscode.commands.registerCommand('myextension.insertSnippet', () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;

const snippet = new vscode.SnippetString();
snippet.appendText('function ');
snippet.appendPlaceholder('functionName');
snippet.appendText('(');
snippet.appendPlaceholder('parameters');
snippet.appendText(') {\n\t');
snippet.appendPlaceholder('function body');
snippet.appendText('\n}');

editor.insertSnippet(snippet);
});

👂 实战三:事件监听与状态管理

编辑器事件监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 监听活动编辑器变化
let editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor) {
console.log('切换到文件:', editor.document.fileName);
updateStatusBar(editor.document);
}
});

// 监听文档内容变化
let documentChangeDisposable = vscode.workspace.onDidChangeTextDocument(event => {
console.log('文档内容变化:', event.document.fileName);
console.log('变化内容:', event.contentChanges);
});

// 监听文件保存
let saveDisposable = vscode.workspace.onDidSaveTextDocument(document => {
console.log('文件已保存:', document.fileName);
});

状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用ExtensionContext存储数据
export function activate(context: vscode.ExtensionContext) {
// 存储数据
context.globalState.update('lastUsedTime', new Date().toISOString());

// 读取数据
const lastUsed = context.globalState.get('lastUsedTime');
console.log('上次使用时间:', lastUsed);

// 监听数据变化
context.globalState.onDidChange(e => {
console.log('状态变化:', e.key, e.value);
});
}

实战案例:文件行数统计器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 状态栏项目
let statusBarItem: vscode.StatusBarItem;

export function activate(context: vscode.ExtensionContext) {
// 创建状态栏项目
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
statusBarItem.command = 'myextension.showLineCount';
context.subscriptions.push(statusBarItem);

// 监听编辑器变化
let editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor(editor => {
updateLineCount(editor);
});

context.subscriptions.push(editorChangeDisposable);

// 初始化显示
updateLineCount(vscode.window.activeTextEditor);
}

function updateLineCount(editor: vscode.TextEditor | undefined) {
if (editor) {
const lineCount = editor.document.lineCount;
const charCount = editor.document.getText().length;

statusBarItem.text = `$(file) ${lineCount} 行, ${charCount} 字符`;
statusBarItem.show();
} else {
statusBarItem.hide();
}
}

🎨 实战四:用户界面扩展

状态栏扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建状态栏项目
let statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);

// 设置状态栏内容
statusBarItem.text = "$(clock) 当前时间";
statusBarItem.tooltip = "点击更新时间";
statusBarItem.command = 'myextension.updateTime';

// 显示状态栏
statusBarItem.show();

// 更新状态栏内容
function updateStatusBar() {
const now = new Date();
statusBarItem.text = `$(clock) ${now.toLocaleTimeString()}`;
}

右键菜单扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"contributes": {
"menus": {
"editor/context": [{
"command": "myextension.extractVariable",
"when": "editorHasSelection",
"group": "1_modification"
}],
"explorer/context": [{
"command": "myextension.openFile",
"group": "navigation"
}]
}
}
}

侧边栏视图扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 注册树视图提供者
class MyTreeDataProvider implements vscode.TreeDataProvider<TreeItem> {
getTreeItem(element: TreeItem): vscode.TreeItem {
return element;
}

getChildren(element?: TreeItem): Thenable<TreeItem[]> {
if (!element) {
// 根节点
return Promise.resolve([
new TreeItem('项目文件', vscode.TreeItemCollapsibleState.Collapsed),
new TreeItem('配置文件', vscode.TreeItemCollapsibleState.Collapsed)
]);
}

// 子节点
return Promise.resolve([]);
}
}

// 注册视图
vscode.window.registerTreeDataProvider('myExtensionView', new MyTreeDataProvider());
1
2
3
4
5
6
7
8
9
10
{
"contributes": {
"views": {
"explorer": [{
"id": "myExtensionView",
"name": "我的扩展视图"
}]
}
}
}

🔧 实战五:综合案例 - 代码注释工具

让我们创建一个综合性的代码注释工具,展示多个核心功能的结合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// 代码注释工具
export class CommentTool {
private statusBarItem: vscode.StatusBarItem;

constructor(private context: vscode.ExtensionContext) {
this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90);
this.statusBarItem.command = 'myextension.toggleComments';
this.context.subscriptions.push(this.statusBarItem);

this.updateStatusBar();
}

// 切换注释状态
public toggleComments() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;

const document = editor.document;
const selections = editor.selections;

editor.edit(editBuilder => {
selections.forEach(selection => {
const lines = this.getLinesInSelection(document, selection);
const shouldComment = this.shouldCommentLines(document, lines);

lines.forEach(line => {
if (shouldComment) {
this.commentLine(editBuilder, line);
} else {
this.uncommentLine(editBuilder, line);
}
});
});
});

this.updateStatusBar();
}

// 获取选择范围内的行
private getLinesInSelection(document: vscode.TextDocument, selection: vscode.Selection): number[] {
const lines: number[] = [];
for (let i = selection.start.line; i <= selection.end.line; i++) {
lines.push(i);
}
return lines;
}

// 判断是否应该注释(检查是否已有注释)
private shouldCommentLines(document: vscode.TextDocument, lines: number[]): boolean {
return lines.some(line => {
const text = document.lineAt(line).text.trim();
return text.length > 0 && !text.startsWith('//');
});
}

// 注释单行
private commentLine(editBuilder: vscode.TextEditorEdit, lineNumber: number) {
const line = this.getLine(lineNumber);
const text = line.text;
const trimmedText = text.trim();

if (trimmedText.length > 0 && !trimmedText.startsWith('//')) {
const insertPosition = new vscode.Position(lineNumber, line.firstNonWhitespaceCharacterIndex);
editBuilder.insert(insertPosition, '// ');
}
}

// 取消注释单行
private uncommentLine(editBuilder: vscode.TextEditorEdit, lineNumber: number) {
const line = this.getLine(lineNumber);
const text = line.text;
const trimmedText = text.trim();

if (trimmedText.startsWith('// ')) {
const startPosition = new vscode.Position(lineNumber, line.firstNonWhitespaceCharacterIndex);
const endPosition = new vscode.Position(lineNumber, line.firstNonWhitespaceCharacterIndex + 3);
editBuilder.delete(new vscode.Range(startPosition, endPosition));
}
}

// 获取行信息
private getLine(lineNumber: number): vscode.TextLine {
const editor = vscode.window.activeTextEditor;
if (!editor) throw new Error('No active editor');
return editor.document.lineAt(lineNumber);
}

// 更新状态栏
private updateStatusBar() {
const editor = vscode.window.activeTextEditor;
if (editor) {
const selection = editor.selection;
const lineCount = selection.end.line - selection.start.line + 1;

this.statusBarItem.text = `$(comment) ${lineCount} 行`;
this.statusBarItem.tooltip = '点击切换注释状态';
this.statusBarItem.show();
} else {
this.statusBarItem.hide();
}
}
}

注册命令

1
2
3
4
5
6
7
8
9
10
export function activate(context: vscode.ExtensionContext) {
const commentTool = new CommentTool(context);

// 注册切换注释命令
let toggleCommand = vscode.commands.registerCommand('myextension.toggleComments', () => {
commentTool.toggleComments();
});

context.subscriptions.push(toggleCommand);
}

🧪 调试技巧

日志输出

1
2
3
4
5
6
7
8
// 输出到调试控制台
console.log('调试信息');
console.error('错误信息');

// 输出到输出面板
const outputChannel = vscode.window.createOutputChannel('My Extension');
outputChannel.appendLine('插件运行日志');
outputChannel.show();

断点调试

  1. 在代码中设置断点
  2. F5 启动调试
  3. 在新窗口中触发命令
  4. 观察变量值和调用栈

错误处理

1
2
3
4
5
6
7
8
// 使用try-catch捕获错误
try {
// 可能出错的代码
const result = await someAsyncOperation();
} catch (error) {
vscode.window.showErrorMessage(`操作失败: ${error.message}`);
console.error('详细错误:', error);
}

📋 阶段二练习任务

完成以下任务来巩固所学知识:

任务1:文本统计工具

  • 创建命令统计选中文本的字数、行数
  • 在状态栏显示统计结果
  • 支持多种统计模式(字符数、单词数等)

任务2:代码格式化工具

  • 实现简单的代码缩进调整
  • 支持批量处理多行代码
  • 添加格式化选项配置

任务3:文件操作助手

  • 创建右键菜单项
  • 实现文件重命名、复制路径等功能
  • 集成到资源管理器上下文菜单

任务4:项目信息面板

  • 创建侧边栏视图
  • 显示项目文件结构
  • 支持文件快速打开

🔗 相关资源


📚 下期预告

在下一篇文章中,我们将进入阶段三:调试、测试与发布,包括:

  • 插件调试技巧和工具使用
  • 单元测试的编写和配置
  • 插件打包和发布流程
  • 性能优化和错误处理

术语白话解释

  • 命令(Command):可以在命令面板中调用的功能,就像是插件的”入口点”
  • 编辑器(Editor):VS Code中显示和编辑文件的界面,插件可以操作其中的内容
  • 状态栏(Status Bar):VS Code底部的信息显示区域,可以显示插件状态
  • 事件监听(Event Listening):监听VS Code中发生的各种事件,比如文件变化、编辑器切换等
  • 上下文菜单(Context Menu):右键点击时显示的菜单,可以添加自定义选项

通过本阶段的学习,你已经掌握了VS Code插件开发的核心技能。接下来就可以开始创建实用的插件功能了!