vscode 代码提示插件开发:动态的光标位置 & lsp server-client 通信

        这次带来更多的代码插件功能
        在开始前,还请下载官方的插件 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 的作用,可以参考下方的解决办法

  1. 无需进行代码文件分析的简单的代码提示片段,移动至 client
  2. 需要进行复杂分析的代码片段依旧由 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(···)
}

        最后是效果演示
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
根据你提供的引用内容,你在写Vue项目时遇到了一个错误,错误信息是"Module not found: Error: Can't resolve 'less-loader' in 'E:\NodeDemo\vue_assistant_lsp'"。解决这个问题的方法是添加依赖并进行相应的配置。首先,你需要通过运行命令"npm install --save-dev less-loader less"来添加依赖。然后,你需要打开webpack.base.conf.js文件,在其中添加以下配置: ``` { test: /\.less$/, loader: "style-loader!css-loader!less-loader", } ``` 如果你遇到了"npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree"这种报错,可能是因为你的npm版本太高。你可以参考这篇文章进行具体操作:https://blog.csdn.net/weixin_38345306/article/details/118700876。希望这些信息能够帮助你解决问题。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [Module not found: Error: Can't resolve 'less-loader' in 'E:\NodeDemo\vue_assistant_lsp'](https://blog.csdn.net/qq_43290288/article/details/105405583)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [vue项目报错Module not found: Error: Can‘t resolve ‘less-loader‘ in。。。。](https://blog.csdn.net/weixin_38345306/article/details/118699650)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值