|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
Visual Studio Code(简称VS Code)作为当前最流行的代码编辑器之一,其强大的扩展性是吸引开发者的重要因素。通过开发自定义插件,我们可以根据个人或团队需求定制编辑器功能,极大地提升开发效率。本教程将手把手教你从零开始开发VS Code插件,帮助你掌握核心技术,打造专属的编辑器增强功能,轻松应对复杂项目。
1. VS Code插件开发基础
1.1 什么是VS Code插件
VS Code插件(Extension)是一种可以扩展编辑器功能的软件模块。通过插件,你可以:
• 添加新的语言支持
• 提供代码补全、 linting、格式化功能
• 创建主题和快捷键
• 添加调试器支持
• 自定义工作区界面
• 集成外部工具和服务
1.2 插件的工作原理
VS Code插件基于Node.js环境,主要使用JavaScript或TypeScript开发。插件通过VS Code提供的扩展API与编辑器交互,这些API允许你访问编辑器的各个功能模块,如文件系统、窗口管理、命令系统等。
2. 开发环境搭建
2.1 前置条件
在开始VS Code插件开发之前,确保你的系统已安装以下软件:
• Node.js(建议版本14.x或更高)
• Visual Studio Code最新版
• Git(用于版本控制)
2.2 安装必要的工具
VS Code官方提供了一套用于插件开发的工具链,主要包含:
1. Yeoman:用于生成项目脚手架
2. VS Code Extension Generator:VS Code插件项目生成器
通过以下命令安装这些工具:
- npm install -g yo generator-code
复制代码
2.3 创建第一个插件项目
1. 打开终端,运行以下命令启动插件生成器:
1. 回答一系列问题来配置你的插件:选择插件类型(TypeScript或JavaScript)输入插件名称输入插件标识符(通常是小写字母,无空格)输入插件描述初始化Git仓库包管理器选择(npm或yarn)
2. 选择插件类型(TypeScript或JavaScript)
3. 输入插件名称
4. 输入插件标识符(通常是小写字母,无空格)
5. 输入插件描述
6. 初始化Git仓库
7. 包管理器选择(npm或yarn)
8. 生成项目后,进入项目目录:
回答一系列问题来配置你的插件:
• 选择插件类型(TypeScript或JavaScript)
• 输入插件名称
• 输入插件标识符(通常是小写字母,无空格)
• 输入插件描述
• 初始化Git仓库
• 包管理器选择(npm或yarn)
生成项目后,进入项目目录:
1. 在VS Code中打开项目:
3. 插件项目结构解析
创建完成后,你的插件项目结构通常如下:
- .
- ├── .vscode/
- │ ├── launch.json // 调试配置
- │ ├── tasks.json // 任务配置
- │ └── extensions.json // 推荐插件列表
- ├── src/
- │ └── extension.ts // 插件主入口文件
- ├── package.json // 插件配置文件
- ├── README.md // 插件说明文档
- ├── CHANGELOG.md // 更新日志
- ├── vsc-extension-quickstart.md // 快速开始指南
- └── tsconfig.json // TypeScript配置文件
复制代码
3.1 package.json 文件详解
package.json是插件的核心配置文件,定义了插件的基本信息、依赖项以及VS Code特定的配置:
- {
- "name": "my-first-extension",
- "displayName": "My First Extension",
- "description": "A sample extension to demonstrate VS Code extensibility",
- "version": "0.0.1",
- "engines": {
- "vscode": "^1.60.0"
- },
- "categories": [
- "Other"
- ],
- "activationEvents": [
- "onCommand:my-first-extension.helloWorld"
- ],
- "main": "./out/extension.js",
- "scripts": {
- "compile": "tsc -p ./",
- "watch": "tsc -w",
- "pretest": "npm run compile && npm run lint",
- "lint": "eslint src --ext ts",
- "test": "node ./out/test/runTest.js",
- "vscode:prepublish": "npm run compile"
- },
- "devDependencies": {
- "@types/vscode": "^1.60.0",
- "@types/node": "^14.x",
- "eslint": "^7.32.0",
- "typescript": "^4.4.4"
- },
- "contributes": {
- "commands": [
- {
- "command": "my-first-extension.helloWorld",
- "title": "Hello World"
- }
- ]
- }
- }
复制代码
关键配置项说明:
• engines.vscode:指定插件兼容的VS Code版本
• activationEvents:定义插件激活的时机
• main:指定插件入口文件
• contributes:定义插件贡献给VS Code的功能点
3.2 插件入口文件
src/extension.ts是插件的主入口文件,包含插件的激活和停用逻辑:
- import * as vscode from 'vscode';
- // 插件激活时调用的函数
- export function activate(context: vscode.ExtensionContext) {
- console.log('Congratulations, your extension "my-first-extension" is now active!');
- // 注册一个命令
- let disposable = vscode.commands.registerCommand('my-first-extension.helloWorld', () => {
- vscode.window.showInformationMessage('Hello World from My First Extension!');
- });
- context.subscriptions.push(disposable);
- }
- // 插件停用时调用的函数
- export function deactivate() {}
复制代码
4. 核心API和概念详解
4.1 插件生命周期
VS Code插件的生命周期主要包括两个阶段:激活(activate)和停用(deactivate)。
• 激活:当满足activationEvents中定义的条件时,VS Code会调用插件的activate函数
• 停用:当VS Code关闭或插件被禁用时,会调用deactivate函数
4.2 常见的激活事件
- "activationEvents": [
- "onLanguage:javascript", // 当打开JavaScript文件时激活
- "onCommand:extension.sayHello", // 当执行特定命令时激活
- "workspaceContains:**/.vscode/settings.json", // 当工作区包含特定文件时激活
- "onView:nodeDependencies", // 当打开特定视图时激活
- "*" // 启动时立即激活(不推荐,会影响性能)
- ]
复制代码
4.3 VS Code API概览
VS Code提供了丰富的API,主要模块包括:
• vscode.commands:命令注册和执行
• vscode.window:UI交互(消息框、输入框、快速选择等)
• vscode.workspace:工作区操作(文件、文件夹、配置等)
• vscode.languages:语言相关功能(文档高亮、智能提示、格式化等)
• vscode.debug:调试功能
• vscode.tasks:任务执行
• vscode scm:源代码管理
5. 常见功能实现
5.1 注册和执行命令
命令是VS Code插件中最基础的功能,通过命令可以绑定快捷键、菜单项等。
- // 注册命令
- const disposable = vscode.commands.registerCommand('extension.myCommand', () => {
- vscode.window.showInformationMessage('Command executed!');
- });
- context.subscriptions.push(disposable);
- // 执行命令
- vscode.commands.executeCommand('extension.anotherCommand');
复制代码
5.2 添加菜单项和快捷键
在package.json中的contributes部分添加菜单和快捷键配置:
- "contributes": {
- "commands": [
- {
- "command": "extension.showHelloWorld",
- "title": "Hello World"
- }
- ],
- "menus": {
- "editor/context": [
- {
- "command": "extension.showHelloWorld",
- "when": "editorHasSelection",
- "group": "myGroup@1"
- }
- ],
- "commandPalette": [
- {
- "command": "extension.showHelloWorld",
- "when": "false" // 不在命令面板显示
- }
- ]
- },
- "keybindings": [
- {
- "command": "extension.showHelloWorld",
- "key": "ctrl+h",
- "mac": "cmd+h",
- "when": "editorTextFocus"
- }
- ]
- }
复制代码
5.3 创建自定义视图
自定义视图可以让你在活动栏中添加新的视图容器,并在其中展示内容。
- // 在extension.ts中
- import * as vscode from 'vscode';
- export function activate(context: vscode.ExtensionContext) {
- // 注册树形视图
- const treeDataProvider = new MyTreeDataProvider();
- vscode.window.registerTreeDataProvider('myView', treeDataProvider);
- // 注册视图
- vscode.window.createTreeView('myView', { treeDataProvider });
- }
- 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('Item 1', vscode.TreeItemCollapsibleState.None),
- new TreeItem('Item 2', vscode.TreeItemCollapsibleState.Collapsed)
- ]);
- } else {
- // 返回子节点
- return Promise.resolve([
- new TreeItem('Child Item 1', vscode.TreeItemCollapsibleState.None),
- new TreeItem('Child Item 2', vscode.TreeItemCollapsibleState.None)
- ]);
- }
- }
- }
- class TreeItem extends vscode.TreeItem {
- constructor(
- public readonly label: string,
- public readonly collapsibleState: vscode.TreeItemCollapsibleState
- ) {
- super(label, collapsibleState);
- }
- }
复制代码
在package.json中添加视图配置:
- "contributes": {
- "views": {
- "explorer": [
- {
- "id": "myView",
- "name": "My View",
- "when": "workbenchState == workspace"
- }
- ]
- }
- }
复制代码
5.4 处理文本编辑器
VS Code API提供了丰富的文本编辑器操作功能:
- // 获取当前活动的文本编辑器
- const editor = vscode.window.activeTextEditor;
- if (editor) {
- const document = editor.document;
- const selection = editor.selection;
-
- // 获取选中的文本
- const selectedText = document.getText(selection);
-
- // 替换选中的文本
- editor.edit(editBuilder => {
- editBuilder.replace(selection, 'New text');
- });
-
- // 在当前位置插入文本
- const position = editor.selection.active;
- editor.edit(editBuilder => {
- editBuilder.insert(position, 'Inserted text');
- });
-
- // 获取整个文档内容
- const fullText = document.getText();
-
- // 修改文档内容
- editor.edit(editBuilder => {
- const lastLine = document.lineAt(document.lineCount - 1);
- const text = lastLine.text;
- editBuilder.replace(lastLine.range, text + ' (modified)');
- });
- }
复制代码
5.5 实现代码补全
代码补全是提高开发效率的重要功能,可以通过实现CompletionItemProvider接口来提供自定义的代码补全:
- // 注册代码补全提供者
- const provider: vscode.CompletionItemProvider = {
- provideCompletionItems(
- document: vscode.TextDocument,
- position: vscode.Position,
- token: vscode.CancellationToken,
- context: vscode.CompletionContext
- ): vscode.CompletionItem[] | vscode.ProviderResult<vscode.CompletionItem[]> {
- // 简单的代码补全示例
- const linePrefix = document.lineAt(position).text.substring(0, position.character);
- if (!linePrefix.endsWith('hello.')) {
- return undefined;
- }
-
- return [
- new vscode.CompletionItem('world', vscode.CompletionItemKind.Keyword),
- new vscode.CompletionItem('vscode', vscode.CompletionItemKind.Keyword)
- ];
- }
- };
- context.subscriptions.push(
- vscode.languages.registerCompletionItemProvider(
- { scheme: 'file', language: 'javascript' },
- provider,
- '.' // 触发补全的字符
- )
- );
复制代码
5.6 实现悬停提示
悬停提示可以帮助开发者更好地理解代码:
- // 注册悬停提示提供者
- const hoverProvider: vscode.HoverProvider = {
- provideHover(
- document: vscode.TextDocument,
- position: vscode.Position,
- token: vscode.CancellationToken
- ): vscode.ProviderResult<vscode.Hover> {
- const range = document.getWordRangeAtPosition(position);
- const word = document.getText(range);
-
- if (word === 'myFunction') {
- const contents = new vscode.MarkdownString();
- contents.appendMarkdown('# myFunction\n\n');
- contents.appendMarkdown('This is a custom function.\n\n');
- contents.appendMarkdown('```javascript\n');
- contents.appendMarkdown('function myFunction(param1, param2) {\n');
- contents.appendMarkdown(' // Function implementation\n');
- contents.appendMarkdown('}\n');
- contents.appendMarkdown('```');
-
- return new vscode.Hover(contents);
- }
-
- return null;
- }
- };
- context.subscriptions.push(
- vscode.languages.registerHoverProvider(
- { scheme: 'file', language: 'javascript' },
- hoverProvider
- )
- );
复制代码
5.7 实现代码格式化
代码格式化可以帮助保持代码风格一致:
- // 注册文档格式化提供者
- const formatter: vscode.DocumentFormattingEditProvider = {
- provideDocumentFormattingEdits(
- document: vscode.TextDocument,
- options: vscode.FormattingOptions,
- token: vscode.CancellationToken
- ): vscode.ProviderResult<vscode.TextEdit[]> {
- const firstLine = document.lineAt(0);
- if (firstLine.text !== '// formatted') {
- return [vscode.TextEdit.insert(firstLine.range.start, '// formatted\n')];
- }
- return [];
- }
- };
- context.subscriptions.push(
- vscode.languages.registerDocumentFormattingEditProvider(
- { scheme: 'file', language: 'javascript' },
- formatter
- )
- );
复制代码
5.8 实现代码诊断
代码诊断可以在编辑器中显示错误和警告信息:
- // 创建诊断集合
- const diagnosticCollection = vscode.languages.createDiagnosticCollection('myExtension');
- // 更新诊断信息
- function updateDiagnostics(document: vscode.TextDocument) {
- if (document.languageId !== 'javascript') {
- return;
- }
-
- const diagnostics: vscode.Diagnostic[] = [];
- const text = document.getText();
-
- // 简单的示例:检测console.log的使用
- const regex = /console\.log\(/g;
- let match;
- while ((match = regex.exec(text)) !== null) {
- const startIndex = match.index;
- const endIndex = startIndex + match[0].length;
- const range = new vscode.Range(
- document.positionAt(startIndex),
- document.positionAt(endIndex)
- );
-
- const diagnostic = new vscode.Diagnostic(
- range,
- 'Avoid using console.log in production code',
- vscode.DiagnosticSeverity.Warning
- );
-
- diagnostics.push(diagnostic);
- }
-
- diagnosticCollection.set(document.uri, diagnostics);
- }
- // 监听文档变化
- vscode.workspace.onDidChangeTextDocument(event => {
- updateDiagnostics(event.document);
- });
- // 监听活动编辑器变化
- vscode.window.onDidChangeActiveTextEditor(editor => {
- if (editor) {
- updateDiagnostics(editor.document);
- }
- });
复制代码
6. 调试和发布插件
6.1 调试插件
VS Code提供了便捷的插件调试功能:
1. 打开插件项目
2. 按F5或点击”Run”菜单中的”Start Debugging”
3. 这将启动一个新的VS Code实例(扩展开发主机),你的插件将在这个实例中运行
4. 在新实例中测试你的插件功能
5. 在原始实例中设置断点并调试代码
6.2 打包插件
插件开发完成后,可以使用vsce(Visual Studio Code Extensions)工具将其打包:
1. 安装vsce:
- npm install -g @vscode/vsce
复制代码
1. 打包插件:
这将生成一个.vsix文件,这是VS Code插件的安装包格式。
6.3 发布插件
如果你想在VS Code市场发布插件,需要:
1. 创建一个Visual Studio Team Services账户
2. 获取发布者ID
3. 使用vsce登录:
- vsce login publisher-name
复制代码
1. 发布插件:
7. 实战案例:开发一个代码片段管理插件
让我们通过一个完整的实例来巩固所学知识。我们将开发一个代码片段管理插件,允许用户保存、管理和插入常用的代码片段。
7.1 项目初始化
首先,使用Yeoman生成器创建一个新的TypeScript插件项目:
选择以下选项:
• What type of extension do you want to create?-New Extension (TypeScript)
• What's the name of your extension?-code-snippet-manager
• What's the identifier of your extension?-code-snippet-manager
• What's the description of your extension?-A simple code snippet manager
• Initialize a git repository?-Yes
• Bundle the source code with webpack?-No
• Which package manager to use?-npm
7.2 设计插件功能
我们的代码片段管理插件将包含以下功能:
1. 添加新的代码片段
2. 查看所有代码片段
3. 插入选中的代码片段
4. 删除代码片段
5. 编辑代码片段
7.3 实现数据存储
首先,我们需要一个地方来存储代码片段。VS Code提供了Memento接口,用于存储简单的键值对数据:
- // src/storage.ts
- import * as vscode from 'vscode';
- export interface CodeSnippet {
- id: string;
- name: string;
- code: string;
- language: string;
- description?: string;
- }
- export class SnippetStorage {
- private static readonly SNIPPETS_KEY = 'codeSnippets';
- static getSnippets(context: vscode.ExtensionContext): CodeSnippet[] {
- const snippetsJson = context.globalState.get<string>(this.SNIPPETS_KEY, '[]');
- return JSON.parse(snippetsJson);
- }
- static saveSnippets(context: vscode.ExtensionContext, snippets: CodeSnippet[]): Thenable<void> {
- return context.globalState.update(this.SNIPPETS_KEY, JSON.stringify(snippets));
- }
- static addSnippet(context: vscode.ExtensionContext, snippet: CodeSnippet): Thenable<void> {
- const snippets = this.getSnippets(context);
- snippets.push(snippet);
- return this.saveSnippets(context, snippets);
- }
- static updateSnippet(context: vscode.ExtensionContext, snippet: CodeSnippet): Thenable<void> {
- const snippets = this.getSnippets(context);
- const index = snippets.findIndex(s => s.id === snippet.id);
- if (index !== -1) {
- snippets[index] = snippet;
- return this.saveSnippets(context, snippets);
- }
- return Promise.resolve();
- }
- static deleteSnippet(context: vscode.ExtensionContext, id: string): Thenable<void> {
- const snippets = this.getSnippets(context);
- const filteredSnippets = snippets.filter(s => s.id !== id);
- return this.saveSnippets(context, filteredSnippets);
- }
- }
复制代码
7.4 实现树形视图
我们需要一个树形视图来显示所有代码片段:
- // src/snippetTreeProvider.ts
- import * as vscode from 'vscode';
- import { CodeSnippet, SnippetStorage } from './storage';
- export class SnippetTreeProvider implements vscode.TreeDataProvider<CodeSnippetItem> {
- private _onDidChangeTreeData: vscode.EventEmitter<CodeSnippetItem | undefined | null | void> = new vscode.EventEmitter<CodeSnippetItem | undefined | null | void>();
- readonly onDidChangeTreeData: vscode.Event<CodeSnippetItem | undefined | null | void> = this._onDidChangeTreeData.event;
- constructor(private context: vscode.ExtensionContext) {}
- refresh(): void {
- this._onDidChangeTreeData.fire();
- }
- getTreeItem(element: CodeSnippetItem): vscode.TreeItem {
- return element;
- }
- getChildren(element?: CodeSnippetItem): Thenable<CodeSnippetItem[]> {
- if (!element) {
- // 返回所有代码片段
- const snippets = SnippetStorage.getSnippets(this.context);
- return Promise.resolve(snippets.map(snippet => new CodeSnippetItem(
- snippet.name,
- snippet,
- vscode.TreeItemCollapsibleState.None
- )));
- }
- return Promise.resolve([]);
- }
- }
- export class CodeSnippetItem extends vscode.TreeItem {
- constructor(
- public readonly label: string,
- public readonly snippet: CodeSnippet,
- public readonly collapsibleState: vscode.TreeItemCollapsibleState
- ) {
- super(label, collapsibleState);
- this.tooltip = `${snippet.name} (${snippet.language})`;
- this.description = snippet.description;
- this.contextValue = 'snippet';
- this.command = {
- command: 'code-snippet-manager.insertSnippet',
- title: 'Insert Snippet',
- arguments: [snippet]
- };
- }
- }
复制代码
7.5 实现命令
接下来,实现各种命令:
- // src/commands.ts
- import * as vscode from 'vscode';
- import { CodeSnippet, SnippetStorage } from './storage';
- import { SnippetTreeProvider } from './snippetTreeProvider';
- export function registerCommands(context: vscode.ExtensionContext, treeDataProvider: SnippetTreeProvider) {
- // 添加新代码片段
- const addSnippetDisposable = vscode.commands.registerCommand('code-snippet-manager.addSnippet', async () => {
- const name = await vscode.window.showInputBox({ prompt: 'Enter snippet name' });
- if (!name) return;
- const code = await vscode.window.showInputBox({ prompt: 'Enter snippet code' });
- if (!code) return;
- const language = await vscode.window.showQuickPick([
- 'javascript', 'typescript', 'html', 'css', 'json', 'python', 'java', 'csharp', 'cpp'
- ], { placeHolder: 'Select language' });
- if (!language) return;
- const description = await vscode.window.showInputBox({ prompt: 'Enter snippet description (optional)' });
- const snippet: CodeSnippet = {
- id: Date.now().toString(),
- name,
- code,
- language,
- description
- };
- await SnippetStorage.addSnippet(context, snippet);
- treeDataProvider.refresh();
- vscode.window.showInformationMessage('Snippet added successfully!');
- });
- // 插入代码片段
- const insertSnippetDisposable = vscode.commands.registerCommand('code-snippet-manager.insertSnippet', async (snippet: CodeSnippet) => {
- const editor = vscode.window.activeTextEditor;
- if (!editor) {
- vscode.window.showErrorMessage('No active editor');
- return;
- }
- editor.edit(editBuilder => {
- editBuilder.insert(editor.selection.active, snippet.code);
- });
- });
- // 删除代码片段
- const deleteSnippetDisposable = vscode.commands.registerCommand('code-snippet-manager.deleteSnippet', async (item: any) => {
- const snippet = item.snippet as CodeSnippet;
- const confirm = await vscode.window.showWarningMessage(
- `Are you sure you want to delete "${snippet.name}"?`,
- { modal: true },
- 'Delete'
- );
- if (confirm === 'Delete') {
- await SnippetStorage.deleteSnippet(context, snippet.id);
- treeDataProvider.refresh();
- vscode.window.showInformationMessage('Snippet deleted successfully!');
- }
- });
- // 编辑代码片段
- const editSnippetDisposable = vscode.commands.registerCommand('code-snippet-manager.editSnippet', async (item: any) => {
- const snippet = item.snippet as CodeSnippet;
-
- const name = await vscode.window.showInputBox({
- prompt: 'Enter snippet name',
- value: snippet.name
- });
- if (!name) return;
- const code = await vscode.window.showInputBox({
- prompt: 'Enter snippet code',
- value: snippet.code
- });
- if (!code) return;
- const language = await vscode.window.showQuickPick([
- 'javascript', 'typescript', 'html', 'css', 'json', 'python', 'java', 'csharp', 'cpp'
- ], { placeHolder: 'Select language', placeHolder: snippet.language });
- if (!language) return;
- const description = await vscode.window.showInputBox({
- prompt: 'Enter snippet description (optional)',
- value: snippet.description || ''
- });
- const updatedSnippet: CodeSnippet = {
- ...snippet,
- name,
- code,
- language,
- description
- };
- await SnippetStorage.updateSnippet(context, updatedSnippet);
- treeDataProvider.refresh();
- vscode.window.showInformationMessage('Snippet updated successfully!');
- });
- context.subscriptions.push(
- addSnippetDisposable,
- insertSnippetDisposable,
- deleteSnippetDisposable,
- editSnippetDisposable
- );
- }
复制代码
7.6 整合所有功能
现在,让我们在主入口文件中整合所有功能:
- // src/extension.ts
- import * as vscode from 'vscode';
- import { SnippetTreeProvider, CodeSnippetItem } from './snippetTreeProvider';
- import { registerCommands } from './commands';
- export function activate(context: vscode.ExtensionContext) {
- console.log('Code Snippet Manager is now active!');
- // 创建树形视图数据提供者
- const treeDataProvider = new SnippetTreeProvider(context);
-
- // 注册树形视图
- vscode.window.registerTreeDataProvider('codeSnippets', treeDataProvider);
-
- // 创建视图
- const treeView = vscode.window.createTreeView('codeSnippets', {
- treeDataProvider,
- showCollapseAll: true
- });
-
- // 注册命令
- registerCommands(context, treeDataProvider);
- }
- export function deactivate() {}
复制代码
7.7 更新package.json
最后,更新package.json以添加必要的配置:
- {
- "name": "code-snippet-manager",
- "displayName": "Code Snippet Manager",
- "description": "A simple code snippet manager",
- "version": "0.0.1",
- "engines": {
- "vscode": "^1.60.0"
- },
- "categories": [
- "Other"
- ],
- "activationEvents": [
- "onView:codeSnippets"
- ],
- "main": "./out/extension.js",
- "scripts": {
- "compile": "tsc -p ./",
- "watch": "tsc -w",
- "pretest": "npm run compile && npm run lint",
- "lint": "eslint src --ext ts",
- "test": "node ./out/test/runTest.js",
- "vscode:prepublish": "npm run compile"
- },
- "devDependencies": {
- "@types/vscode": "^1.60.0",
- "@types/node": "^14.x",
- "eslint": "^7.32.0",
- "typescript": "^4.4.4"
- },
- "contributes": {
- "commands": [
- {
- "command": "code-snippet-manager.addSnippet",
- "title": "Add New Snippet",
- "icon": "$(add)"
- },
- {
- "command": "code-snippet-manager.insertSnippet",
- "title": "Insert Snippet"
- },
- {
- "command": "code-snippet-manager.deleteSnippet",
- "title": "Delete Snippet",
- "icon": "$(trash)"
- },
- {
- "command": "code-snippet-manager.editSnippet",
- "title": "Edit Snippet",
- "icon": "$(edit)"
- }
- ],
- "menus": {
- "view/title": [
- {
- "command": "code-snippet-manager.addSnippet",
- "when": "view == codeSnippets",
- "group": "navigation"
- }
- ],
- "view/item/context": [
- {
- "command": "code-snippet-manager.insertSnippet",
- "when": "view == codeSnippets && viewItem == snippet",
- "group": "inline"
- },
- {
- "command": "code-snippet-manager.editSnippet",
- "when": "view == codeSnippets && viewItem == snippet",
- "group": "inline"
- },
- {
- "command": "code-snippet-manager.deleteSnippet",
- "when": "view == codeSnippets && viewItem == snippet",
- "group": "inline"
- }
- ]
- },
- "views": {
- "explorer": [
- {
- "id": "codeSnippets",
- "name": "Code Snippets",
- "when": "workbenchState == workspace"
- }
- ]
- },
- "viewsWelcome": [
- {
- "view": "codeSnippets",
- "contents": "No snippets found yet. [Add a new snippet](command:code-snippet-manager.addSnippet) to get started."
- }
- ]
- }
- }
复制代码
7.8 测试插件
现在,我们可以测试我们的插件了:
1. 按F5启动调试
2. 在新打开的VS Code窗口中,你应该能在资源管理器中看到”Code Snippets”视图
3. 点击视图标题栏上的”+“按钮添加新的代码片段
4. 右键点击代码片段可以编辑、删除或插入代码片段
8. 最佳实践和性能优化
8.1 插件性能优化
1. 延迟加载:只在需要时加载插件功能,使用activationEvents控制激活时机
- "activationEvents": [
- "onLanguage:javascript",
- "onCommand:extension.myCommand"
- ]
复制代码
1. 避免阻塞UI线程:将耗时操作放在异步函数中执行
- async function processLargeData() {
- // 使用setTimeout分批处理大量数据
- const data = getLargeDataSet();
- const batchSize = 100;
-
- for (let i = 0; i < data.length; i += batchSize) {
- const batch = data.slice(i, i + batchSize);
- processBatch(batch);
-
- // 让UI有机会更新
- await new Promise(resolve => setTimeout(resolve, 0));
- }
- }
复制代码
1. 缓存计算结果:避免重复计算
- class CacheManager {
- private static cache = new Map<string, any>();
-
- static get(key: string): any {
- return this.cache.get(key);
- }
-
- static set(key: string, value: any): void {
- this.cache.set(key, value);
- }
-
- static clear(): void {
- this.cache.clear();
- }
- }
复制代码
8.2 代码组织最佳实践
1. 模块化设计:将功能拆分为多个模块
- src/
- ├── extension.ts // 主入口文件
- ├── commands/ // 命令模块
- │ ├── index.ts
- │ ├── snippetCommands.ts
- │ └── editorCommands.ts
- ├── providers/ // 提供者模块
- │ ├── index.ts
- │ ├── completionProvider.ts
- │ └── hoverProvider.ts
- ├── models/ // 数据模型
- │ ├── index.ts
- │ └── snippet.ts
- └── utils/ // 工具函数
- ├── index.ts
- └── logger.ts
复制代码
1. 使用TypeScript类型:充分利用TypeScript的类型系统
- interface CodeSnippet {
- id: string;
- name: string;
- code: string;
- language: string;
- description?: string;
- }
- type Language = 'javascript' | 'typescript' | 'html' | 'css' | 'json' | 'python' | 'java' | 'csharp' | 'cpp';
复制代码
1. 错误处理:妥善处理可能出现的错误
- async function safeExecute(operation: () => Promise<any>, errorMessage: string): Promise<void> {
- try {
- await operation();
- } catch (error) {
- vscode.window.showErrorMessage(`${errorMessage}: ${error.message}`);
- console.error(error);
- }
- }
复制代码
8.3 用户体验优化
1. 提供进度反馈:对于耗时操作,显示进度通知
- async function longRunningOperation() {
- vscode.window.withProgress({
- location: vscode.ProgressLocation.Notification,
- title: "Processing data...",
- cancellable: true
- }, async (progress, token) => {
- token.onCancellationRequested(() => {
- console.log("User canceled the long running operation");
- });
-
- const totalSteps = 100;
- for (let i = 0; i <= totalSteps; i++) {
- progress.report({ increment: 1, message: `${i} of ${totalSteps} steps completed` });
- await new Promise(resolve => setTimeout(resolve, 100));
-
- if (token.isCancellationRequested) {
- return;
- }
- }
- });
- }
复制代码
1. 使用快速选择:提供友好的选择界面
- async function selectFile() {
- const files = await getFiles();
- const selected = await vscode.window.showQuickPick(
- files.map(file => ({
- label: file.name,
- description: file.path,
- detail: `${file.size} bytes`,
- file: file
- })),
- {
- placeHolder: 'Select a file',
- matchOnDetail: true,
- matchOnDescription: true
- }
- );
-
- if (selected) {
- // 处理选中的文件
- processFile(selected.file);
- }
- }
复制代码
1. 配置选项:允许用户自定义插件行为
- "contributes": {
- "configuration": {
- "title": "Code Snippet Manager",
- "properties": {
- "codeSnippetManager.storageLocation": {
- "type": "string",
- "default": "global",
- "enum": ["global", "workspace"],
- "enumDescriptions": [
- "Store snippets globally (across all workspaces)",
- "Store snippets in the current workspace"
- ],
- "description": "Where to store code snippets"
- },
- "codeSnippetManager.autoInsert": {
- "type": "boolean",
- "default": false,
- "description": "Automatically insert snippets when selected"
- }
- }
- }
- }
复制代码
在代码中访问配置:
- const config = vscode.workspace.getConfiguration('codeSnippetManager');
- const storageLocation = config.get<string>('storageLocation', 'global');
- const autoInsert = config.get<boolean>('autoInsert', false);
复制代码
9. 进阶主题
9.1 Webview开发
Webview允许你在VS Code中创建自定义的UI界面,使用HTML、CSS和JavaScript构建复杂的交互界面。
- // 创建Webview面板
- const panel = vscode.window.createWebviewPanel(
- 'myWebview', // 标识符
- 'My Webview', // 面板标题
- vscode.ViewColumn.One, // 显示在编辑器的哪个位置
- {
- enableScripts: true, // 启用JavaScript
- localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')]
- }
- );
- // 设置HTML内容
- panel.webview.html = getWebviewContent(context.extensionUri);
- // 处理来自Webview的消息
- panel.webview.onDidReceiveMessage(
- async message => {
- switch (message.command) {
- case 'alert':
- vscode.window.showInformationMessage(message.text);
- return;
- case 'getData':
- const data = await fetchData();
- panel.webview.postMessage({ command: 'dataResponse', data });
- return;
- }
- },
- undefined,
- context.subscriptions
- );
- // 获取Webview HTML内容
- function getWebviewContent(extensionUri: vscode.Uri): string {
- const scriptUri = vscode.Uri.joinPath(extensionUri, 'media', 'main.js');
- const styleUri = vscode.Uri.joinPath(extensionUri, 'media', 'styles.css');
-
- return `<!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>My Webview</title>
- <link href="${styleUri}" rel="stylesheet">
- </head>
- <body>
- <h1>My Webview</h1>
- <button id="alertButton">Show Alert</button>
- <button id="getDataButton">Get Data</button>
- <div id="dataContainer"></div>
- <script src="${scriptUri}"></script>
- </body>
- </html>`;
- }
复制代码
9.2 语言服务器协议(LSP)
语言服务器协议(Language Server Protocol, LSP)是一种协议,用于在编辑器和语言服务器之间通信,提供语言智能功能如代码补全、错误检查、定义跳转等。
创建语言服务器的步骤:
1. 创建语言服务器项目:
- npm init -y
- npm install vscode-languageserver vscode-languageserver-textdocument
复制代码
1. 实现服务器:
- // server.ts
- import {
- createConnection,
- TextDocuments,
- ProposedFeatures,
- InitializeParams,
- DidChangeConfigurationNotification,
- TextDocumentSyncKind,
- InitializeResult,
- CompletionItem,
- CompletionItemKind,
- TextDocumentPositionParams
- } from 'vscode-languageserver/node';
- import {
- TextDocument
- } from 'vscode-languageserver-textdocument';
- // 创建连接和文档管理器
- const connection = createConnection(ProposedFeatures.all);
- const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
- let hasConfigurationCapability = false;
- let hasWorkspaceFolderCapability = false;
- let hasDiagnosticRelatedInformationCapability = false;
- connection.onInitialize((params: InitializeParams) => {
- const capabilities = params.capabilities;
- hasConfigurationCapability = !!(
- capabilities.workspace && !!capabilities.workspace.configuration
- );
- hasWorkspaceFolderCapability = !!(
- capabilities.workspace && !!capabilities.workspace.workspaceFolders
- );
- hasDiagnosticRelatedInformationCapability = !!(
- capabilities.textDocument &&
- capabilities.textDocument.publishDiagnostics &&
- capabilities.textDocument.publishDiagnostics.relatedInformation
- );
- const result: InitializeResult = {
- capabilities: {
- textDocumentSync: TextDocumentSyncKind.Incremental,
- completionProvider: {
- resolveProvider: true
- }
- }
- };
- if (hasWorkspaceFolderCapability) {
- result.capabilities.workspace = {
- workspaceFolders: {
- supported: true
- }
- };
- }
- return result;
- });
- // 注册文档内容变化监听器
- documents.onDidChangeContent(change => {
- validateTextDocument(change.document);
- });
- // 验证文档
- async function validateTextDocument(textDocument: TextDocument): Promise<void> {
- // 实现文档验证逻辑
- }
- // 提供代码补全
- connection.onCompletion(
- (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
- // 返回补全项
- return [
- {
- label: 'TypeScript',
- kind: CompletionItemKind.Text,
- data: 1
- },
- {
- label: 'JavaScript',
- kind: CompletionItemKind.Text,
- data: 2
- }
- ];
- }
- );
- // 监听文档打开、关闭、变化事件
- documents.listen(connection);
- connection.listen();
复制代码
1. 在插件中连接语言服务器:
- // client.ts
- import * as path from 'path';
- import * as vscode from 'vscode';
- import {
- LanguageClient,
- LanguageClientOptions,
- ServerOptions,
- TransportKind
- } from 'vscode-languageclient/node';
- let client: LanguageClient;
- export function activate(context: vscode.ExtensionContext) {
- // 服务器选项
- const serverModule = context.asAbsolutePath(
- path.join('server', 'out', 'server.js')
- );
-
- const serverOptions: ServerOptions = {
- run: { module: serverModule, transport: TransportKind.ipc },
- debug: {
- module: serverModule,
- transport: TransportKind.ipc,
- options: { execArgv: ['--nolazy', '--inspect=6009'] }
- }
- };
- // 客户端选项
- const clientOptions: LanguageClientOptions = {
- documentSelector: [{ scheme: 'file', language: 'mylang' }],
- synchronize: {
- configurationSection: 'languageServerExample',
- fileEvents: vscode.workspace.createFileSystemWatcher('**/.clientrc')
- }
- };
- // 创建语言客户端
- client = new LanguageClient(
- 'languageServerExample',
- 'Language Server Example',
- serverOptions,
- clientOptions
- );
- // 启动客户端
- client.start();
- }
- export function deactivate(): Thenable<void> | undefined {
- if (!client) {
- return undefined;
- }
- return client.stop();
- }
复制代码
9.3 调试适配器协议(DAP)
调试适配器协议(Debug Adapter Protocol, DAP)是一种用于调试器和编辑器之间通信的协议,允许你为新的编程语言或运行时实现调试支持。
创建调试适配器的步骤:
1. 创建调试适配器项目:
- npm init -y
- npm install vscode-debugadapter
复制代码
1. 实现调试适配器:
- // debugAdapter.ts
- import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, ContinuedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter';
- import { DebugProtocol } from 'vscode-debugprotocol';
- interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
- program: string;
- }
- class MyDebugSession extends DebugSession {
- private static THREAD_ID = 1;
- private _configurationDone = false;
- private _variableHandles = new Handles<string>();
- public constructor() {
- super();
- this.setDebuggerLinesStartAt1(false);
- this.setDebuggerColumnsStartAt1(false);
- }
- protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
- response.body = response.body || {};
- response.body.supportsConfigurationDoneRequest = true;
- response.body.supportsEvaluateForHovers = true;
- response.body.supportsStepBack = false;
- this.sendResponse(response);
- }
- protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void {
- // 启动调试目标
- this.sendResponse(response);
- }
- protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void {
- this._configurationDone = true;
- this.sendResponse(response);
- }
- protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void {
- // 设置断点
- response.body = {
- breakpoints: []
- };
- this.sendResponse(response);
- }
- protected threadsRequest(response: DebugProtocol.ThreadsResponse): void {
- response.body = {
- threads: [
- new Thread(MyDebugSession.THREAD_ID, "thread 1")
- ]
- };
- this.sendResponse(response);
- }
- protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
- // 返回堆栈跟踪
- const startFrame = typeof args.startFrame === 'number' ? args.startFrame : 0;
- const maxLevels = typeof args.levels === 'number' ? args.levels : 1000;
- const endFrame = startFrame + maxLevels;
- const stackFrames: StackFrame[] = [];
-
- response.body = {
- stackFrames: stackFrames,
- totalFrames: 0
- };
- this.sendResponse(response);
- }
- protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
- // 返回变量作用域
- response.body = {
- scopes: [
- new Scope("Local", this._variableHandles.create("local"), false),
- new Scope("Global", this._variableHandles.create("global"), true)
- ]
- };
- this.sendResponse(response);
- }
- protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void {
- // 返回变量
- const variables: DebugProtocol.Variable[] = [];
- response.body = {
- variables: variables
- };
- this.sendResponse(response);
- }
- protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
- // 继续执行
- this.sendResponse(response);
- }
- protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
- // 单步执行
- this.sendResponse(response);
- }
- protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void {
- // 步入
- this.sendResponse(response);
- }
- protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void {
- // 步出
- this.sendResponse(response);
- }
- protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void {
- // 计算表达式
- response.body = {
- result: 'evaluate result',
- variablesReference: 0
- };
- this.sendResponse(response);
- }
- }
- DebugSession.run(MyDebugSession);
复制代码
1. 在插件中注册调试适配器:
- // extension.ts
- import * as path from 'path';
- import * as vscode from 'vscode';
- import { workspace, ExtensionContext } from 'vscode';
- export function activate(context: ExtensionContext) {
- context.subscriptions.push(
- vscode.debug.registerDebugConfigurationProvider('mydebug', new MyDebugConfigurationProvider())
- );
- const provider = new MyDebugAdapterDescriptorFactory();
- context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('mydebug', provider));
- if ('dispose' in provider) {
- context.subscriptions.push(provider);
- }
- }
- class MyDebugConfigurationProvider implements vscode.DebugConfigurationProvider {
- resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult<vscode.DebugConfiguration> {
- if (!config.type && !config.request && !config.name) {
- const editor = vscode.window.activeTextEditor;
- if (editor && editor.document.languageId === 'mylang') {
- config.type = 'mydebug';
- config.name = 'Launch';
- config.request = 'launch';
- config.program = '${file}';
- config.stopOnEntry = true;
- }
- }
-
- if (!config.program) {
- return vscode.window.showInformationMessage("Cannot find a program to debug").then(_ => {
- return undefined;
- });
- }
-
- return config;
- }
- }
- class MyDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory {
- createDebugAdapterDescriptor(session: vscode.DebugSession): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
- return new vscode.DebugAdapterExecutable(path.join(__dirname, 'debugAdapter.js'));
- }
- dispose() {}
- }
复制代码
10. 总结
通过本教程,我们全面介绍了VS Code插件开发的各个方面,从基础概念到高级主题。我们学习了如何:
1. 搭建VS Code插件开发环境
2. 理解插件项目结构和核心API
3. 实现常见功能如命令、菜单、快捷键、代码补全等
4. 开发一个完整的代码片段管理插件
5. 优化插件性能和用户体验
6. 探索进阶主题如Webview、语言服务器和调试适配器
VS Code插件开发是一个强大而灵活的领域,通过掌握这些技术,你可以根据自己的需求定制编辑器,提高开发效率,解决复杂项目中的挑战。
希望本教程能帮助你开启VS Code插件开发之旅,不断探索和创新,打造出更多有价值的插件,为开发者社区做出贡献。 |
|