这次带来更多的代码插件功能
在开始前,还请下载官方的插件 sample ,我们可以直接在其代码基础上进行开发
https://github.com/microsoft/vscode-extension-samples/tree/main/lsp-sample
文章中的代码会同步更新至 gitee,如果看文章觉得思维有些跳跃跟不上可以参考完整的源代码
https://gitee.com/mminogo/better-react-ts-tips
修改代码插入后光标位置
有这种情形和需求,我们在插入代码后想要调整光标的位置
const [word, setWord] = useState({$1})
如上,我们想要移动光标至 {$1} 的位置,如何实现
如果您此前的代码插件例子没有用到 lsp,即您的插件代码目录下只有一个 src,而不是 client 和 server 的话,那么这个功能很简单
直接创建一个代码插入提示片段
import {
CompletionItem,
SnippetString,
} from 'vscode';
const useState = new CompletionItem('useState');
useState.detail = 'React - useState';
useState.filterText = '@use';
useState.insertText = new SnippetString('const [word, setWord] = useState({$1})');
然后在合适的地方注册这个代码片段
import {
CompletionItem,
languages,
MarkdownString,
CompletionItemKind,
} from 'vscode';
const tsConfigProvider = languages.registerCompletionItemProvider(['typescript', 'typescriptreact'], {
provideCompletionItems() {
return [
useState // 此处即是刚刚声明的代码片段
];
},
});
最后在主文件使用这个注册的代码片段(一般在官方例子中,这个文件是extension.ts)
export function activate(context: ExtensionContext) {
···
context.subscriptions.push(tsConfigProvider);
···
}
这样就能够实现我们需要的功能,使代码片段插入后光标在我们期望的位置,如果有多个依次的位置,直接{$2}, {$3}等等,依次增加即可
上面的创建代码片段的方法仅有非 lsp,或者 lsp 的 client 中可以使用,lsp 的 server 中是不支持这么做的
为什么呢?
因为 server 中的 completionItem 和 client 的 completionItem 有一定区别,在我们上面的例子中,我们可以看到我们可以给 useState.insertText 传一个特殊类型的对象:SnippetString
正是这个对象,能够帮助 vscode 确认我们是否需要改变光标的位置,而在 server 中,我们无法传递一个对象给 insertText,它仅支持字符串类型
两者类型对比:
这样我们就知道了,server 中提供的代码片段是没有办法改变光标位置的
真的没办法了吗?
当然有解决办法,考虑到 lsp 的作用,可以参考下方的解决办法
- 无需进行代码文件分析的简单的代码提示片段,移动至 client
- 需要进行复杂分析的代码片段依旧由 server 计算提供,但是不直接通过 connection.onCompletion 提供,而是通过 server - client 通信传给 client ,再让 client 修改 insertText 类型,重新生成代码插入片段
修改 insertText 类型,重新生成代码片段,各位肯定都知道怎么做,本质和上面的流程是一致的,主要是要拿到 server 分析后的代码片段
那么如何获取 server 提供的代码片段呢?
server - client 通信
毫无疑问,lsp 的 server 和 client 本身就一直保持着通信,现在的问题是我们需要一个方法,让 server 向 client 传递我们定制的代码片段
而事实上,官方其实有提供多种方法让 server 与 client 通信,而我们这次要介绍的是
connection.sendNotification
sendNotification 一般接受两个参数,第一个参数为一个 method,是一个字符串。它可以是任意的字符串,我们只要保证通信两边保持一致就行了
// server 端发送消息
connection.sendNotification('testMethod');
// client 端接收消息
client.onNotification('testMethod', () => {})
如上,server 端发送的是标记为 “testMethod” 的消息,那么在 client 要接收该消息,就需要在 onNotification 方法的一个参数也使用该字符串
我们看到 onNotification 还有一个参数,这个参数当然是个回调函数,即受到消息后要做的动作
sendNotification 的第二个参数就是我们需要传递的内容,我们可以直接将需要的代码片段通过第二个参数传递,而 client 端可以在回调中拿到这个参数
下面写一个简单的例子
// server.ts
connection.onCompletion(
(_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
const itemList: CompletionItem[] = [];
const useEffectTxt = `useEffect(() => {
console.log('这是测试 lsp 通讯的代码片段')
}, [{$1}]);`
const useEffect = {
label: '@ABC',
detail: '测试通讯',
documentation: useEffectTxt,
filterText: '@@ABC',
insertText: useEffectTxt,
}
connection.sendNotification('testMethod', [useEffect]);
return itemList;
}
);
// extension.ts (client/src)
import {
workspace,
ExtensionContext,
commands,
window,
languages,
CompletionItem,
SnippetString,
Position,
} from 'vscode';
···
const extraCompletionList = [];
const needInfo = {
uri: null,
position: new Position(0, 0),
text: '',
tempText: '',
};
const testProvider = languages.registerCompletionItemProvider(['typescript', 'typescriptreact'], {
provideCompletionItems() {
return [
...extraCompletionList,
];
},
});
client.onReady().then(() => {
client.onNotification('testMethod', (list) => {
extraCompletionList.length = 0;
if (Array.isArray(list)) {
for (let item of list) {
const newCompletionItem = new CompletionItem(item.label);
newCompletionItem.detail = item.detail;
newCompletionItem.filterText = item.filterText;
newCompletionItem.insertText = new SnippetString(item.insertText);
extraCompletionList.push(newCompletionItem);
}
if (needInfo.tempText !== needInfo.text) {
const params = [
needInfo.uri,
needInfo.position,
needInfo.text,
];
needInfo.tempText = needInfo.text;
commands.executeCommand('vscode.executeCompletionItemProvider', ...params);
}
}
})
});
context.subscriptions.push(testProvider);
workspace.onDidChangeTextDocument((e) => {
needInfo.uri = e.document.uri;
const change = e.contentChanges[0];
needInfo.position = new Position(change.range.start.line, change.range.start.character + 1)
needInfo.text = change.text;
})
虽然是个简单的例子,但是代码比较多,我们一点点看
首先 server 端的比较简单,我们创建并提供了一个代码提示片段 useEffect,但是我们没有直接让这个代码片段生效,因为我们返回的 itemList 是一个空数组,这个代码片段我们用 sendNotification 传给了 client
接下来是 client 部分的代码,这里的操作会有点多,我们先看我们注册的 client.onNotification ,我们将其放在了 client.onReady().then 的回调函数里面,这是避免 client 还没有准备完 server 端就发送消息,这时 onNotification 会报错。而 onNotification 的代码前半部分就比较简单了,我们拿到 server 传过来的 代码片段数组,遍历并重新生成新的代码片段,新生成的代码片段的插入内容我们用 SnippetString 重新赋值,这样就能使得光标插入位置 {$1} 生效。好的,到此,我们在上文中讲到的内容就结束了,那么 client 多出来的其他代码是干什么的呢?
首先我们必须知道,我们到 extraCompletionList.push(newCompletionItem); 这一步为止,我们只是拿到了新的代码片段,并没有做其他动作,没有使其注册生效让 vscode 识别。好的,现在我们知道了,我们需要让从 server 端拿到的代码片段生效,因此 client 需要补充下面的内容
const testProvider = languages.registerCompletionItemProvider(['typescript', 'typescriptreact'], {
provideCompletionItems() {
return [
...extraCompletionList,
];
},
});
context.subscriptions.push(testProvider);
但是这样还不够,当我们在 编辑器中键入代码并唤起代码提示时,testProvider 的 provideCompletionItems 就已经执行了,所以这时即便我们更新了 extraCompletionList,但是弹出的代码提示并不会有新的内容,因为 vscode 并没有去重新执行 provideCompletionItems,所以需要一个方法告知 vscode 重新执行这个函数以提供新的代码提示片段,因此有了下面的内容
if (needInfo.tempText !== needInfo.text) {
const params = [
needInfo.uri,
needInfo.position,
needInfo.text,
];
needInfo.tempText = needInfo.text;
commands.executeCommand('vscode.executeCompletionItemProvider', ...params);
}
而其中最关键的一行便是
commands.executeCommand('vscode.executeCompletionItemProvider', ...params);
这一行能够让 vscode 重新执行提供代码片段的函数
到这里我们的主要流程就都打通了,如下
好的,整个流程通了之后我们再解释剩下的代码,这些其实都是细节问题的处理。首先,我们执行 vscode.executeCompletionItemProvider 需要传递三个参数,分别是 文档路径(uri),代码提示唤起的位置(position),触发代码提示的文本(text),也就是这个对象
const needInfo = {
uri: null,
position: new Position(0, 0),
text: '',
tempText: '',
};
tempText 是干嘛的我们后面解释
而下面的代码就是为了获取这些内容的
workspace.onDidChangeTextDocument((e) => {
needInfo.uri = e.document.uri;
const change = e.contentChanges[0];
needInfo.position = new Position(change.range.start.line, change.range.start.character + 1)
needInfo.text = change.text;
})
这个函数会在每次文档更新的时候执行传给它的回调函数,我们可以依此获取我们需要的内容
好的,最后我们解释一下 tempText 的作用,其实我相信大家应该都已经猜到了
commands.executeCommand(‘vscode.executeCompletionItemProvider’, …params) 能够帮我们重新执行对应的代码提供函数,这个重新执行当然不止是 client 端的,也包括 server 端的,因此在 server 端的
connection.onCompletion
便会再次执行,问题就来了,如果再次执行这个函数,我们写在回调里的 sendNotification 会再次执行,再次发送代码片段,到了客户端通过我们的函数,又会再次 commands.executeCommand,这样就会出现死循环
为了避免这样的死循环,我们用 tempText 来记录触发代码提示的文本,如果两次比较一致,就说明此时用户并没有输入新的内容,而是我们写的函数触发的,这时我们就不做处理,也就解决了死循环,因此会有下面的代码
if (needInfo.tempText !== needInfo.text) {
···
needInfo.tempText = needInfo.text;
···
commands.executeCommand(···)
}
最后是效果演示