初衷
开发 React
项目的时候,每次写 useState
都觉得很麻烦,每定义一个 state
就要跟一个 setState
作为共同解构值,定义一个两个还好说,但身为苦逼的搬砖仔,面对头疼的业务已经很痛苦了,定义个 state
就不能让我更轻松点?
一日在用 html
文件写一个小 demo
的时候,一个遗忘已久的小技巧给了我灵感,我为什么不写一个 vscode
插件帮我快速生成 useState
。
想法
有了想法后,我快速搜了一下 vscode
相关的实现,这应该属于代码片段的功能,vscode
内置的snippets 只提供静态生成,但我们需要根据不同的 state
名称去生成代码。
const [name, setName] = useState('John')
const [count, setCount] = useState(0)
所以必须使用 vscode
内置的 api
去动态生成代码。
插件的功能细节为当键入 const
后对后面的字符做解析,通过 /
分割代码,第一段字符为 state
名称,第二个为 ts
类型,第三个则为值,如果没有第三个参数,则第二个参数为值。
// input
const count/0
// output
const [count, setCount] = useState(0)
// input
const name/string/'John'
// output
const [name, setName] = useState<string>('John')
// input
const arr/number[]/[]
// output
const [arr, setArr] = useState<number[]>([])
// input
const fooBar/Foo | Bar/'foo'
// output
const [fooBar, setFooBar] = useState<Foo | Bar>('foo')
registerCompletionItemProvider
vscode
提供了一个 registerCompletionItemProvider api 可以让我们给代码提示功能注入想添加的代码文本
registerCompletionItemProvider
接受三个参数
- 第一个参数为在何种文件格式下触发此函数
- 第二个参数为注入函数的对象合集,传入一个对象,包括两个函数
provideCompletionItems
在键入一个字符时触发,用户可在此阶段向代码提示提供代码文本resolveCompletionItem
当选中此代码提示时的触发动作
- 第三个参数剩余参数,该函数触发的字符,一般来说键入一个英文字符会默认触发,但是数字
.
/
等字符需要手动指定
// 在 html 文件下触发,遇到 . - 字符进行代码提示
const disposable = vscode.languages.registerCompletionItemProvider('html',
{
provideCompletionItems() {},
resolveCompletionItem() {}
},
'.', '-'
);
实现
如何创建一个 vscode
插件项目就不说了,AdminJS有很多示例,推荐这个文章,本插件的代码也参考了这个库。
vscode
插件需要通过 package.json
中的 activationEvents
参数指定该插件在什么情况下触发
// package.json
{
//...
"activationEvents": [
"onLanguage:html",
"onLanguage:vue",
"onLanguage:javascript",
"onLanguage:typescript",
"onLanguage:javascriptreact",
"onLanguage:typescriptreact"
],
// ...
}
在遇到 html vue js ts jsx tsx
文件时触发该插件(多支持几个文件,谁说不能在 vue
文件里写 useState
呢[狗头])。
核心逻辑在 /src/extension.ts
文件中,导出一个 activate
函数,在此函数中添加功能
const TRIGGER_CHARACTERS = ['{', '}', '[', ']', '/', '\'', '"', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
export function activate(context: vscode.ExtensionContext) {
const disposable = vscode.languages.registerCompletionItemProvider(
["javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "html"],
new MyCompletionItemProvider(),
...TRIGGER_CHARACTERS
);
context.subscriptions.push(disposable);
}
TRIGGER_CHARACTERS
为触发字符,我们把核心逻辑包装到 MyCompletionItemProvider
中。
provideCompletionItems
接受 vscode
传入的当前 文档(document)
和 当前光标位置(position)
两个参数,通过这两个参数得到当前行输入的文本,对文本进一步解析,生成我们想要的代码片段,然后通过 new vscode.CompletionItem
生成一个代码提示实例,return
出去
class MyCompletionItemProvider implements vscode.CompletionItemProvider {
private position?: vscode.Position;
private str = "";
constructor() { }
// 提供代码提示的候选项
public provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
) {
this.position = position;
const linePrefix = document
.lineAt(position)
.text.slice(0, position.character);
// 如果文本前缀不为 const ,则不做提示
if (!linePrefix?.startsWith("const ") || !linePrefix.split("const ")[1]) {
this.str = "";
return [];
}
// ... 省略文本解析过程,详细可参考代码仓库
const snippetCompletion = new vscode.CompletionItem(
linePrefix,
vscode.CompletionItemKind.Snippet
);
snippetCompletion.documentation = this.str;
snippetCompletion.detail = 'quickly generate useState';
return [snippetCompletion];
}
public resolveCompletionItem(item: vscode.CompletionItem) {
const label = item.label;
if (this.position && typeof label === "string") {
// 当选中此代码提示时,发起一个 `vscode-extension.quick-useState` 命令
item.command = {
command: "vscode-extension.quick-useState",
title: "refactor",
arguments: [this.position.translate(0, label.length + 1), this.str],
};
}
return item;
}
}
export function activate(context: vscode.ExtensionContext) {
// 在 activate 函数中添加一个 vscode-extension.quick-useState 命令
// 触发此命令执行某些操作
const commandId = "vscode-extension.quick-useState";
const commandHandler = (
editor: vscode.TextEditor,
edit: vscode.TextEditorEdit,
position: vscode.Position,
str: string
) => {
const lineText = editor.document.lineAt(position.line).text;
// 删除当前行
edit.delete(
new vscode.Range(
position.with(undefined, 0),
position.with(undefined, lineText.length)
)
);
// 插入代码提示的片段
edit.insert(position.with(undefined, 0), str);
return Promise.resolve([]);
};
context.subscriptions.push(
vscode.commands.registerTextEditorCommand(commandId, commandHandler)
);
// ...
}
整体的流程为键入字符,触发 provideCompletionItems
函数,为代码提示悬浮框插入一个代码提示,选择此提示,触发 resolveCompletionItem
函数,在此函数中发起一个命令,该命令指向的操作删除文本,插入想要的文本。插件的效果也就完成了。
实现的核心逻辑在此就不赘述了,很简单的一个文本解析,可以点击此处 GitHub 查看。
坑
通过这次写插件也踩了不少坑,不过最大原因还是英语能力不行,插件 api
文档都是英语,翻译出来的中文简直不是人话,只能自己寻摸,或者找类似的库参考实现。
此外就是 registerCompletionItemProvider
函数的触发时机,前文有讲到,一般情况下 vscode
只会在英文字符下出现代码提示,如果想在键入 0
或者 -
等字符也出现代码提示悬浮框,需要作为 registerCompletionItemProvider
的第三第四个参数传入。
代码提示悬浮框由两部分,左侧是代码提示的简略文本,右侧是详细信息,简略文本尽量和键入的文本保持一致,因为 vscode
会优先匹配代码提示的简略文本,如果简略文本中含有此文本,将不会触发 registerCompletionItemProvider
函数。
带三个点就是发布插件时创建发布者如果使用网页方式创建账号,要使用科学上网,不然没办法正确创建,创建的 ID
要和你 package.json
的 publisher
字段保持一致,否则会报错.
// package.json
{
// ...
"publisher": "...",
// ...
}
成品
最终成品 放在这里
GitHub 地址
Visual Studio Code 应用市场
有需要的朋友可以试一下。
本文正在参加「 . 」