手写 useState 真的好麻烦啊!为什么不写一个vscode 插件帮你生成?

lxf2023-03-21 08:34:01

初衷

开发 React 项目的时候,每次写 useState 都觉得很麻烦,每定义一个 state 就要跟一个 setState 作为共同解构值,定义一个两个还好说,但身为苦逼的搬砖仔,面对头疼的业务已经很痛苦了,定义个 state 就不能让我更轻松点?

一日在用 html 文件写一个小 demo 的时候,一个遗忘已久的小技巧给了我灵感,我为什么不写一个 vscode 插件帮我快速生成 useState

手写 useState 真的好麻烦啊!为什么不写一个vscode 插件帮你生成?

想法

有了想法后,我快速搜了一下 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 可以让我们给代码提示功能注入想添加的代码文本

手写 useState 真的好麻烦啊!为什么不写一个vscode 插件帮你生成?

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 函数,在此函数中发起一个命令,该命令指向的操作删除文本,插入想要的文本。插件的效果也就完成了。

手写 useState 真的好麻烦啊!为什么不写一个vscode 插件帮你生成?

实现的核心逻辑在此就不赘述了,很简单的一个文本解析,可以点击此处 GitHub 查看。

通过这次写插件也踩了不少坑,不过最大原因还是英语能力不行,插件 api 文档都是英语,翻译出来的中文简直不是人话,只能自己寻摸,或者找类似的库参考实现。

此外就是 registerCompletionItemProvider 函数的触发时机,前文有讲到,一般情况下 vscode 只会在英文字符下出现代码提示,如果想在键入 0 或者 - 等字符也出现代码提示悬浮框,需要作为 registerCompletionItemProvider 的第三第四个参数传入。

代码提示悬浮框由两部分,左侧是代码提示的简略文本,右侧是详细信息,简略文本尽量和键入的文本保持一致,因为 vscode 会优先匹配代码提示的简略文本,如果简略文本中含有此文本,将不会触发 registerCompletionItemProvider 函数。

带三个点就是发布插件时创建发布者如果使用网页方式创建账号,要使用科学上网,不然没办法正确创建,创建的 ID 要和你 package.jsonpublisher 字段保持一致,否则会报错.

// package.json
{
    // ...
    "publisher": "...",
    // ...
}

成品

最终成品 放在这里

GitHub 地址

Visual Studio Code 应用市场

有需要的朋友可以试一下。

本文正在参加「 . 」