vscode插件开发指南(四)-实战篇-自动补全实现

前言

我们的目标是实现一个基础库的插件,其功能包含以下内容:

  • 语法校验,校验API命令调用的正确性,参数类型的正确性,参数个数的正确性,关键参数和系统打通校验有效性等
  • 语法自动补全,辅助用户编写代码
  • 语法悬停提示,辅助说明语法
  • 其他功能
    • webview查看远程信息

本篇即为自动补全实现篇,我们先看一下最后的效果,是不是很神奇,快来和我一起探讨到底是怎么实现的吧~ 效果

完整代码可在我的github中查看。

一、方案一:代码片段

1.1 特点

比起自动补全,把它理解成代码片段其实更准确一些,通过一些触发字符,快速输入一段代码。支持tab切换焦点,其他方案都不支持。

实际上,如果你在日常工作中尝试使用这个功能,你会发现它会很大程度提高你的效率,比如我会使用vue触发我的单文件组件模版的初始化

1.2 语法

// in file package.json
{
  "contributes": {
    "snippets": [
      {
        "language": "javascript", // 支持的语言
        "path": "./snippets.json" // 配置地址
      }
    ]
  }
}
复制代码
// in file 'Code/User/snippets/javascript.json'
{
  "For Loop": { // 名称,"For Loop" is the snippet name. It is displayed via IntelliSense if no description is provided.
    "prefix": ["for", "for-const"], // 触发字符,可模糊匹配,如fr也可以触发
    "body": ["for (const ${2:element} of ${1:array}) {", "\t$0", "}"], // 实际插入的代码片段
    "description": "A for loop." // 描述说明
  }
}
复制代码

这里面也可以使用变量,比如:

  • 使用$1, $2标识tab定位,使用tab可快速切换
  • 使用${1:array}标识tab定位,且有默认值
  • 一些内置变量,比如CURRENT_YEAR,可用于自定义注释头
  • 更多内容详见snippet-syntax

1.3 实现

这里我们设计一个下述代码的快捷输入,使用tycA或testA触发

function test(val: number, val2: string) {
	return tyc_test.a(val, val2);
}
复制代码
  1. 首先是配置文件
// snippets.json
{
	"tyc_test": {
		"prefix": [
			"tycA",
			"testA"
		],
		"body": [
            "function test(${1:val}: number, ${2:val2}: string) {",
            "    return tyc_test.a(val, val2);",
            "}"
		],
		"description":[
			"快速写一个test函数",
			"",
			"调用tyc_test.a方法",
			"@param val — 参数1",
			"@param val2 — 参数2."
		]
	}
}
复制代码
  1. 其次是配置贡献点
// package.json
"contributes": {
		"snippets": [
			{
				"language": "javascript",
				"path": "./snippets.json"
			}
		]
}
复制代码

1.4 效果说明

效果

二、 方案二:在client层实现

2.1 特点

触发某个输入操作时触发,可进一步定制语法匹配规则,具有一定的定制性,但触发字符只支持非word类型,通常都是.触发

A completion item provider can be associated with a set of triggerCharacters. When trigger characters are being typed, completions are requested but only from providers that registered the typed character. Because of that trigger characters should be different than word characters, a common trigger character is . to trigger member completions.

2.2 语法

registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable
复制代码

2.3 实现

tyc_test下有一个testA方法

// client/src/provider/autoCompletion.ts
const doc = [
    {
        kind: 'Function',
        body: [
            "testA"
        ],
        detail: "(method) tyc_test.testA( key: string, value: any)",
        documentation: `测试testA自动补全
@param key — 要设置的字段名.
@param value — 要设置的值.`
    }
];

class CompletionItemProvider {
    provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext) {
        // 支持换行 代码从起始位置到输入位置
        const text = document.getText(new vscode.Range(
            new vscode.Position(0, 0),
            position
        ));

        // 只有tyc_test调用会触发联想内容
        if(/tyc_test\.$/.test(text)){
            return doc.map(item => {
                return item.body.map(iitem => {
                    let completionItem = new vscode.CompletionItem(iitem, vscode.CompletionItemKind[item.kind]);
                    completionItem.detail=item.detail;
                    completionItem.documentation = item.documentation;
                    // 代码替换位置,查找位置会同步应用
                    completionItem.range = new vscode.Range(new vscode.Position(position.line, position.character), new vscode.Position(position.line, position.character));
                    return completionItem;
                });
            }).flat();
        }
    }

    // resolveCompletionItem(){}
}

export default function autoCompletion(context: vscode.ExtensionContext) {
    context.subscriptions.push(vscode.languages.registerCompletionItemProvider(file, new CompletionItemProvider(), '.'));
}
复制代码

在插件入口中引入

// client/src/extension.ts
import autoCompletion from './provider/autoCompletion';

export function activate(context: vscode.ExtensionContext) {
	// 自动补全
	autoCompletion(context);
}
复制代码

2.4 效果说明

效果

三、在server层实现

3.1 特点

支持模糊匹配的代码补全项列表,效果与client实现有类似之处,但主要由输入触发,默认是输入word触发,如果想要由符号触发,可在connect初始化的时候声明triggerCharacters

与client最大的差别是,client如果不是由触发字符触发,后面输入字母等就不在有模糊匹配了,但server端的实现,可以实现只有有输入,随时匹配并提供补全项,举例

// 这种情况下,输入a,client的补全项无法触发,但server端可以
tyc_test.t
复制代码

因为是在服务端异步支持,相对来说,性能更好。

3.2 实现

我们把tyc_test改成tyc_test2,用于对比

// server/src/autoCompletion.ts
import {
    TextDocuments,
	CompletionItem,
	CompletionItemKind,
	TextDocumentPositionParams,
    Connection,
    _,
} from 'vscode-languageserver';
import {
	TextDocument
} from 'vscode-languageserver-textdocument';
import { CallHierarchy } from 'vscode-languageserver/lib/callHierarchy.proposed';
import { SemanticTokens } from 'vscode-languageserver/lib/sematicTokens.proposed';

const doc = [
    {
        kind: 'Function',
        body: [
            "testA"
        ],
        detail: "(method) tyc_test2.testA( key: string, value: any)",
        documentation: `测试testA自动补全
@param key — 要设置的字段名.
@param value — 要设置的值.`
    }
];


export default function autoCompletion(connection: Connection<_, _, _, _, _, _, CallHierarchy & SemanticTokens>, documents: TextDocuments<TextDocument>) {
    connection.onCompletion(
        (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] | null => {
            const document = documents.get(_textDocumentPosition.textDocument.uri);
            if (!document) {
                return null;
            }
            const text = document.getText({
                start: document.positionAt(0),
                end: _textDocumentPosition.position
            });

            const offsetAt = document.offsetAt(_textDocumentPosition.position);

            // tyc_test关键字联想
            let res: CompletionItem[] = [{
                label: 'tyc_test2',
                kind: CompletionItemKind.Variable,
                detail: 'tyc_test2',
                documentation: '我的测试方法库'
            }];

            // 服务端根据输入字母,识别到是tyc_test的调用后,提供联想项
            if (/tyc_test2\.[a-zA-Z]*$/.test(text)){
                res = res.concat(
                    doc.map((item) => ({
                            label: item.body[0],
                            kind: CompletionItemKind[item.kind as keyof typeof CompletionItemKind],
                            detail: item.detail,
                            documentation: item.documentation
                        })
                    ) as CompletionItem[]
                );
            }
            return res;
        }
    );

    connection.onCompletionResolve(
        (item: CompletionItem): CompletionItem => {
            return {
                ...item,
            };
        }
    );
}
复制代码

在server入口文件引入

import autoCompletion from './autoCompletion';
// ...
// 自动补全
autoCompletion(connection, documents);
复制代码

3.3 效果说明

效果

四、系列文章

分类:
前端