vscode插件开发入门: 常用功能

492 阅读6分钟

编程语言

通过插件可以为不同的编程语言提供丰富的功能

语法高亮

VSCode 使用 TextMe 语法来定义编程语言的语法高亮。在 Github 上有多种语言的 tmbundle 的 Github 仓库,包含了基于 TextMate 语法的.tmLanguage 语法高亮定义文件。通过以下步骤,可以导入.tmlanguage 文件,为编程语言创建一个语法高亮的插件

  1. 在命令行中运行 yo code 命令
  2. 选择 New Language Support 插件类型,然后输入.tmLanguage 文件的 URL 或文件路径
  3. 接下来根据后续选项继续创建插件项目
  4. 插件项目创建完成后,在视图中会显示语法高亮插件的文件结构

github.com/textmate/ja…

github.com/textmate/py…

github.com/textmate/ma…

github.com/textmate/ht…

{
  "name": "et-javascript",
  "displayName": "et-javascript",
  "description": "",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.93.0"
  },
  "categories": [
    "Programming Languages"
  ],
  "contributes": {
    "languages": [{
      "id": "javascript",
      "aliases": ["javascript,typescript", "javascript"],
      "extensions": [".js",".ts"],
      "configuration": "./language-configuration.json"
    }],
    "grammars": [{
      "language": "javascript",
      "scopeName": "",
            // 设置了描述新语言的语法的文件路径
      "path": "./syntaxes/javascript.tmLanguage.json"
    }]
  }
}
// 语法文件, 这里采用的是TextMate语法,关于TextMate可以参考
// https://macromates.com/manual/en/language_grammars
// https://www.apeth.com/nonblog/stories/textmatebundle.html

{
    "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
    "name": "javascript,typescript",
    "patterns": [
        {
            "include": "#keywords"
        },
        {
            "include": "#strings"
        }
    ],
    "repository": {
        "keywords": {
            "patterns": [{
                // name是被匹配的表达式的scope selector; 关于scope selector可以参考https://macromates.com/manual/en/scope_selectors
                // vscode根据这个scope selector进行上色,颜色由keyword.control.javascript控制,比如可以修改为: comment.line.double-slash.javascript,有哪些常量选择,可以参考https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json
                "name": "keyword.control.javascript",
                // match是一个正则表达式,但是这里使用的是ruby regular expression,进行匹配
                "match": "\\b(if|while|for|return)\\b"
            }]
        },
        "strings": {
            "name": "string.quoted.double.javascript",
            // 开始正则表达式匹配
            "begin": "\"",
            // 结束正则表达式匹配
            "end": "\"",
            "patterns": [
                {
                    "name": "constant.character.escape.javascript",
                    "match": "\\\\."
                }
            ]
        }
    },
    "scopeName": ""
}

悬停提示

  • 悬停提示的是在 extension.js 中注册一个悬停事件,然后根据提供的 docuemnt、position 以及文件名,文件路径等信息作出相应的逻辑
  • 悬停提示注册方法:registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable;
    返回一个 HoverProvider 对象,这一对象需要加入到 context.subscription 中
  • provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
    这一 API 返回一个 PrioviderResult 对象,当我们把光标放在某个位置时显示的内容,就是这个对象封装的。
// extension.js的激活函数中的代码如下
// 运行插件,保证插件被激活的状态下,将光标放在json文件中的main单词上即可提示
export function activate(context: vscode.ExtensionContext) {
    const hover = vscode.languages.registerHoverProvider('json', {
        provideHover(document, position, token) {
            const fileName = document.fileName;
            const word = document.getText(document.getWordRangeAtPosition(position));
            // 光标放在"main"字符时显示的内容
            if (/\bmain\b/.test(word)) {
                return new vscode.Hover('Hover Tip!');
            }
            return undefined;
        },
    });

    context.subscriptions.push(hover);
}

代码片段

通过 contributes.snippets 贡献点,可以定义代码片段插件;

  • 代码片段也叫 snippets,就是输入一个单词前缀,会根据该前缀得到一个或多个提示,然后回车键入对应的代码块
  • 想要在 vscode 插件中实现 snippets 的功能,首先要在 package.json 的 contributes 配置项中配置代码提示文件的文件路径:
创建新的代码片段

通过以下步骤可以把自定义的代码片段发布成插件。

  • 通过 Ctrl + Shift + P 快捷键打开命令面板,然后输入并自行 preferences: Configure User Snippets 命令,可以创建一个代码片段文件,如: snippets.json
  • 把 snippets.json 文件复制到插件的文件夹中
  • 在 package.json 文件中添加 contributes.snippets 贡献点
{
  "contributes": {
            "snippets": [
            {
                "language": "itest",
                "path": "./snippets.json"
            }
        ]
  }
}
导入已有的代码片段

VSCode 支持直接导入 TextMate(.tmSnippets 文件)和 Sublime(.sublime-snippets 文件)这两种格式的代码片段。可以通过以下步骤导入 TextMate 或 Sublime 代码片段

  1. 在命令行中运行 yo code 命令
  2. 选择 New Code Snippets 插件类型, 然后输入包含.tmSnippets 或.sublime-snippets 代码片段文件的文件夹
  3. 根据后续选项创建插件项目
  4. 插件项目创建完成后目录结构如下:
    ├── snippets  
    │ └── snippets.json // 代码片段的 JSON 文件
    │
    └── package.json // 插件清单文件
测试代码片段

测试代码片段前,需要把代码片段的插件项目复制到 VSCode 插件安装目录中。对于不同系统,安装目录有所不同:

  • Windows: %USERPROFILE%.vscode\extensions
  • maxOs: ~/.vscode/extensions
  • Linux: ~/.vscode/extensions

复制完成后,重启 VSCode,代码片段及其插件就生效了,此时便可以对代码片段进行测试了

// package.json文件中的contributes属性-snippets
"snippets": [
   {
      // 表示语言何种语言(如python\javascript),这里的etest表示一个自定义的etest语言,即.etest后缀的文件;
      "language": "etest",
      // snippets的文件的路径
      "path": "./snippets/snippets.json"
   }
]
// snippets.json文件中的片段定义
{
   // snippet的名称
   "View组件": {
      // 前缀,即输入什么字符可以出现snippets的提示
      "prefix": "View",
      // 回车键自动键入的代码片段(内容), 是一个数组,数组里面是字符串,每个字符串代表一行代码,${1}表示第一个光标的位置,同样,${2}表示第二个光标的位置
      "body": [
         "<View>",
         "${1}",
         "</VIew>"
      ],
      // snippet的描述,当我们选中这个snipets提示时,描述会出现在后面
      "description": "View组件"
   }
}

自动补全

代码提示是我们使用 vscode 开发的时候不可或缺的重要功能,当我们输入代码的一部分的时候,vscode 会显示一个提示列表,我们可以选择其中一个提示项,按下回车后代码的剩余部分会被自动补全

代码提示相关的主要的 API:

  • registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable;
  • 第一个参数是实现代码提示的文件的类型。
  • 第二个参数是一个 CompletionItemProvider 类型的对象,在创建这个对象内部,我们需要根据 document、position 等信息进行逻辑处理,返回一个 CompletionItem 的数组,每一个 CompletionItem 就代表一个提示项。
  • 第三个参数是可选的触发提示的字符列表

下面列出一些与代码提示相关的其他的一些 API,这些 API 大多与文本、单词的处理相关,因为我们进行代码提示时需要知道当前光标所在单词的上下文,这样才能很好的给出智能提示,而要得到当前光标的上下文,就需要对光标附近乃至整个文件进行文本分析。

  • 与 TextDocument 相关
    TextDocument 的对象实际是当前文件对象,所以我们可以根据该对象得到当前文件与文本相关的所有信息。
  • lineAt(line: number): TextLine; 根据行数返回一个行的对象
  • lineAt(position: Position): TextLine; 根据一个位置返回这一行的行对象
  • getText(range?: Range): string; 根据范围,返回这个范围的文本
  • getWordRangeAtPosition(position: Position, regex?: RegExp): Range | undefined; 根据 position 返回这个位置所在的单词。
  • text.charAt() 返回字符串在某个位置的字符
export function activate(context: vscode.ExtensionContext) {
    const provider = vscode.languages.registerCompletionItemProvider('plaintext', {
        provideCompletionItems(document, position) {
            const completionItem1 = new vscode.CompletionItem('Hello World!');
            const completionItem2 = new vscode.CompletionItem('World Peace!');
            return [completionItem1, completionItem2];
        },
    });

    // 注意:plaintext这里指的是文本文件,不是javascript, 在.txt下生效
    const provider2 = vscode.languages.registerCompletionItemProvider(
        'plaintext',
        {
            provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
                // get all text until the `position` and check if it reads `console.`
                // and if so then complete if `log`, `warn`, and `error`
                const linePrefix = document.lineAt(position).text.slice(0, position.character);
                if (!linePrefix.endsWith('console.')) {
                    return undefined;
                }

                return [
                    new vscode.CompletionItem('log', vscode.CompletionItemKind.Method),
                    new vscode.CompletionItem('warn', vscode.CompletionItemKind.Method),
                    new vscode.CompletionItem('error', vscode.CompletionItemKind.Method),
                ];
            },
        },
        '.' // triggered whenever a '.' is being typed
    );

    context.subscriptions.push(provider, provider2);
}

跳转到定义

跳转到定义其实很简单,通过 vscode.languages.registerDefinitionProvider 注册一个 provider,这个 provider 如果返回了 new vscode.Location()就表示当前光标所在单词支持跳转,并且跳转到对应 location

// 方法定义文件
const vscode = require('vscode');
const path = require('path');
const fs = require('fs');

/**
 * 支持 package.json 中 dependencies、devDependencies 跳转到对应依赖包, 直接从 node_modules 文件夹下面去找
 * 查找文件定义的provider,匹配到了就return一个location,否则不做处理
 * 最终效果是,当按住Ctrl键时,如果return了一个location,字符串就会变成一个可以点击的链接,否则无任何效果
 * @param {*} document
 * @param {*} position
 * @param {*} token
 */
function provideDefinition(document: any, position: any, token: any) {
    const fileName = document.fileName;
    const workDir = path.dirname(fileName);
    const word = document.getText(document.getWordRangeAtPosition(position));
    const line = document.lineAt(position);
    const projectPath = __dirname; // util.getProjectPath(document);

    // 只处理package.json文件
    if (/\package\.json$/.test(fileName)) {
        const json = document.getText();
        if (
            new RegExp(
                `"(dependencies|devDependencies)":\\s*?\\{[\\s\\S]*?${word.replace(
                    /\//g,
                    '\\/'
                )}[\\s\\S]*?\\}`,
                'gm'
            ).test(json)
        ) {
            let destPath = `${workDir}/node_modules/${word.replace(/"/g, '')}/package.json`;

            if (fs.existsSync(destPath)) {
                // new vscode.Position(0, 0) 表示跳转到某个文件的第一行第一列
                // new vscode.Location接收2个参数,第一个是要跳转到文件的路径,第二个是跳转之后光标所在位置,接收Range或者Position对象,Position对象的初始化接收2个参数,行row和列col
                return new vscode.Location(vscode.Uri.file(destPath), new vscode.Position(0, 0));
            }
        }
    }
}

export { provideDefinition };
// extenstion.js
export function activate(context: vscode.ExtensionContext) {
    // 注册如何实现跳转到定义,第一个参数表示仅对json文件生效
    context.subscriptions.push(
        vscode.languages.registerDefinitionProvider(['json'], {
            provideDefinition,
        })
    );
}

语言配置

通过 contributes.languages 贡献点可以对编程语言进行配置

// language-configuration.json文件完整代码
// https://github.com/microsoft/vscode-extension-samples/blob/main/language-configuration-sample/language-configuration.json
{
    "comments": {
        "lineComment": "//",
        "blockComment": [ "/*", "*/" ]
    },
    "brackets": [
        ["{", "}"],
        ["[", "]"],
        ["(", ")"]
    ],
    "autoClosingPairs": [
        { "open": "{", "close": "}" },
        { "open": "[", "close": "]" },
        { "open": "(", "close": ")" },
        { "open": "'", "close": "'", "notIn": ["string", "comment"] },
        { "open": "\"", "close": "\"", "notIn": ["string"] },
        { "open": "`", "close": "`", "notIn": ["string", "comment"] },
        { "open": "/**", "close": " */", "notIn": ["string"] }
    ],
    "autoCloseBefore": ";:.,=}])>` \n\t",
    "surroundingPairs": [
        ["{", "}"],
        ["[", "]"],
        ["(", ")"],
        ["'", "'"],
        ["\"", "\""],
        ["`", "`"]
    ],
    "folding": {
        "markers": {
            "start": "^\\s*//\\s*#?region\\b",
            "end": "^\\s*//\\s*#?endregion\\b"
        }
    },
    "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
    "indentationRules": {
        "increaseIndentPattern": "^((?!.*?\\/\\*).*\\*\/)?\\s*[\\}\\]].*$",
        "decreaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$"
    }
}
  • 注释

通过 comments.lineComment 和 comments.blockComment 可以定义单行注释和块注释

"comments": {
        "lineComment": "//",
        "blockComment": [ "/*", "*/" ]
    },
  • 括号匹配
    当光标移动到某一括号上时,相匹配的括号也会被高亮显示,设置如下:
"brackets": [        ["{", "}"],
        ["[", "]"],
        ["(", ")"]
    ],
  • 自动闭合
    当输入 open 属性中的符号时,VSCode 会自动添加 close 属性中的符号,设置如下:
"autoClosingPairs": [
        { "open": "{", "close": "}" },
        { "open": "[", "close": "]" },
        { "open": "(", "close": ")" },
        { "open": "'", "close": "'", "notIn": ["string", "comment"] },
        { "open": "\"", "close": "\"", "notIn": ["string"] },
        { "open": "`", "close": "`", "notIn": ["string", "comment"] },
        { "open": "/**", "close": " */", "notIn": ["string"] }
    ],
  • 自动包围
    当选中代码片段并键入左括号时,VSCode 会自动添加右括号,包围选中的代码片段。如下:
// 通过surroundingPairs属性,可以定义用于包围的字符对
    "surroundingPairs": [        ["{", "}"],
        ["[", "]"],
        ["(", ")"],
        ["'", "'"],
        ["\"", "\""],
        ["`", "`"]
    ],
  • 代码折叠
    通过 folding.markers 属性,可以定义代码折叠的正则表达式。如下:
// 设置项中将//#region和//#endregion定义为代码折叠的标记
    "folding": {
        "markers": {
            "start": "^\\s*//\\s*#?region\\b",
            "end": "^\\s*//\\s*#?endregion\\b"
        }
    },
  • 单词的模式
// 通过wordPattern属性可以定义单词的匹配模式
{
    "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
}
  • 缩进规则
// 通过indentationRules属性可以定义增加缩进和减少缩进的规则
    "indentationRules": {
        "increaseIndentPattern": "^((?!.*?\\/\\*).*\\*\/)?\\s*[\\}\\]].*$",
        "decreaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$"
    }

编程语言 API

通过 vscode.languages.*API ,插件可以为不同的编程语言提供非常丰富的功能。以下是一个关于悬停信息的例子

// 通过vscode.languages.registerHoverProvider API 为JavaScript文件提供了显示悬停信息的功能
vscode.languages.registerHoverProvider('javascript', {
    provideHover(document, position, token) {
        return {
            contents: ['Hover Content'],
        };
    },
});

此外,我们也可以基于 Language Server Protocol(语言服务协议)实现一个 Language Server(语言服务),来为编程语言提供相应的功能。相比于直接使用 VSCode 的 vscode.languages.*API ,使用 Language Server 有以下两大好处

  • Language Server 可以使用任何语言编写。比如,Java 的 Language Server 使用 Java 编写可以有更好的实现,且可以复用现有的 Java 库
  • Language Server 可以被其他支持 Language Server protocol 的开发工具复用,如 VSCode、Sublime Text、Eclipse IDE 等

下表是 VSCode 的 vscode.languages.* 编程语言 API 和 Language Server Protocol 函数的对照表,开发者可根据实际情况选择相应的实现方式

VSCode 的 vscode.language.*编程语言 APILanguage Server Protocol 函数描述
registerCompletionItemProviderCompletion & Completion Resolve提供代码补齐提示
registerHoverProviderHover光标停留在 token 上时触发
registerSignatureHelpProviderSignatureHelp提供函数签名提示
待补充