写了一个 vscode 插件:自动添加可选链

以下文章来源于神光的编程秘籍 ,作者神说要有光zxg

使用eslint方法

是否可以通过 eslint 插件实现自动把属性访问变成可选链的方式。
这当然是可以的,我们来实现下:

mkdir auto-optional-chain
cd auto-optional-chain
npm init -y

创建项目,新建 package.json

安装 eslint 的包

npm install --save eslint

然后在 src/index.js 写这样一段代码:

const { ESLint } = require("eslint");

const engine = new ESLint({
    fix: false,
    overrideConfig: {
        parser: "@babel/eslint-parser",
        rules: {
            'semi': ['error', 'never']
        }
    },
    useEslintrc: false
});

async function main() {
    const results = await engine.lintText(`
        function handleRes(data) {
            const res = data.a.b.c + data.e.f.g;

        }
    `)
    
    console.log(results);
}
main();

eslint 一般我们用的是命令行的方式,当然,它也有 api 的方式。

我们 new 了 ESLint 的对象,指定配置,不自动 fix。

然后用 lintText 来检查一段代码,打印结果。

这里我们就用了一个 rule ,也就是检查末尾分号的,设置为不加分号。

这里用到了 @babel/eslint-parser,安装一下:

npm install @babel/eslint-parser

然后创建 babel 配置文件:
在这里插入图片描述

然后跑一下:缺少依赖的话自行安装一下
如果跑成功了
在这里插入图片描述

可以看到确实有一个错误。

展开是这样的:
在这里插入图片描述

在第 3 行第 48 列有一个额外的分号。

但这样看起来太费劲了,我们把它格式化一下:

const formatter = await engine.loadFormatter('stylish');
const resultText = formatter.format(results);
console.log(resultText);

eslint 内置了一些 formatter 用它格式化一下再打印:

在这里插入图片描述

这种错误格式就是我们经常见的那种了。

然后我们再把 fix 改为 true,也就是自动修复:

在这里插入图片描述

打印下 result[0].output,也就是第一个错误自动修复后的结果:

在这里插入图片描述

可以看到,末尾分号被去掉了。

这就是 eslint 的 api 方式的用法。

下面我们来写一个自动添加可选链的插件。

新建 src/auto-optional-chain.js

module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        return {
           BlockStatement(node) {
           }
       }
   }
};

meta 部分是指定这个插件的元信息,比如文档、是否可以自动 fix 等。

create 部分是插件的实现逻辑,指定对什么节点做什么处理。

那我们要处理的是什么 AST 节点呢?

可以用 astexplorer.net (百度搜索)看一下:

在这里插入图片描述

选择 javascript,用 @babel/parser 解析,在后边可以看到 parse 出的 AST。

可以看到,这种 data.name 的语法叫做 MemberExpression 成员表达式。

如果多个 . 的话就是 MemberExpression 嵌套了:

在这里插入图片描述

那 data?.name 呢?

可以看到,叫做 OptionalMemberExpression

在这里插入图片描述

也就是说我们找到 MemberExpression,给它报个错,然后 fix 的时候修复为可选链的方式就好了。

也就是这样:

module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        return {
            MemberExpression(node) {
                context.report({
                    node,
                    loc: {
                        line: 111,
                        column: 222
                    },
                    message: '应该用可选链'
                })
            }
       }
   }
};

指定 rulePaths 也就是去哪里找 rule,然后配置这个 rule 为 error 级别:

在这里插入图片描述

测试下:

在这里插入图片描述

可以看到,确实是报了 6 个错误。

只不过现在的位置不太对。

拿到 . 的位置需要用 token 相关的 api。

也就是这样:

module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        return {
            MemberExpression(node) {
                const tokens = sourceCode.getTokens(node);

                context.report({
                    node,
                    loc: {
                        line: 111,
                        column: 222
                    },
                    message: '应该用可选链'
                })
            }
       }
   }
};

我们断点调试下:

创建 launch.json

在这里插入图片描述

创建 node 类型的调试配置:

在这里插入图片描述

在代码里打个断点:

在这里插入图片描述

点击调试启动:

在这里插入图片描述

代码会在断点处断住:

在这里插入图片描述

可以看到有 7 个 token,分别是 data 和 . 和 a 和 b 和 . 和 c

那我们取哪个 . 的 loc 呢?

可以看到,第一次断住是这样的:

在这里插入图片描述

第二次是这样的:

在这里插入图片描述

第三次是这样的:

在这里插入图片描述

也就是说 data.a.b.c 是从右向左解析的,所以我们要拿到的是最后一个 . 的 token 的位置。

取倒数第二个,可以用数组的 at 方法:

在这里插入图片描述

也就是这样:

module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        return {
            MemberExpression(node) {
                const tokens = sourceCode.getTokens(node);

                context.report({
                    node,
                    loc: tokens.at(-2).loc,
                    message: '应该用可选链'
                })
            }
       }
   }
};

现在的位置就都对了:

在这里插入图片描述

光报错意义不大,我们再实现自动 fix。

eslint 的 fix 是基于字符串替换实现的,它提供了一个 fixer api。

打断点看看:

在这里插入图片描述

很明显,这里比较适合用 insertTextBefore 来做。

也就是这样:

context.report({
    node,
    loc: dotToken.loc,
    message: '应该用可选链',
    fix: fixer => {
        return fixer.insertTextBefore(dotToken, '?')
    }
})

但要注意,fix 之后会再次 lint,这时候拿到的 token 就这样了:

在这里插入图片描述

这种应该不再 fix,直接跳过:

module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        return {
            MemberExpression(node) {
                const tokens = sourceCode.getTokens(node);

                const dotToken = tokens.at(-2);

                if(dotToken.value === '?.'){
                    return;
                }

                context.report({
                    node,
                    loc: dotToken.loc,
                    message: '应该用可选链',
                    fix: fixer => {
                        return fixer.insertTextBefore(dotToken, '?')
                    }
                })
            }
       }
   }
};

测试下:

在这里插入图片描述

修复后的代码是对的。

只要项目里用到了这个 rule,开启自动 fix 就可以自动加上可选链。

但这样其实有个问题:不是所有的 data.xxx 都需要变成可选链的方式,而现在这个 eslint rule 是把所有的 data.xxx 都自动 fix 了。

使用babel方法

也可以写个 babel 插件来做这件事情,不修改源代码,只是在编译的时候做:

const { transformSync } = require('@babel/core');

function autoOptionalPlugin() {
    return {
        visitor: {
            MemberExpression(path, state) {
                const text = path.toString();

                path.replaceWithSourceString(text.replace(/\./g, '?.'));
            }
        }
    }
}

const res = transformSync(`
    function handleRes(data) {
        const res = data.a.b.c + data.e.f.g;

    }
`, {
    plugins: [autoOptionalPlugin]
});

console.log(res.code);

用 transformSync 来编译源代码为目标代码,过程中调用 autoOptionalPlugin。

插件里处理 MemberExpression,拿到代码对应的字符串,然后把 . 改成 ?. 再替换回去。

效果和 eslint 插件是一样的:

在这里插入图片描述

babel 插件的好处是不修改源码,可以在编译过程中无感的做这件事情。

那我如果就是想把代码改了,但是还不能全部改,而是我选中哪部分就自动修复哪部分代码呢?

使用vscode方法+babel

这种就要用 vscode 插件来做了。

安装 vscode 插件的脚手架:

npm install -g yo generator-code

生成 vscode 插件项目:

yo code 

在这里插入图片描述

生成的项目是这样的:

在这里插入图片描述

它已经配置好了调试配置,点击就可以调试:

在这里插入图片描述

它会启动一个新的 vscode 窗口,然后输入 hello world 命令,右下角会有提示框:

在这里插入图片描述

这就代表 vscode 插件运行成功了。

然后想一下我们的插件要做成什么样子:

选中一段代码,右键菜单里会有转换为可选链的选项,点击就可以转换。

或者选中之后,按快捷键也可以转换。

在 src/extention.ts 里实现下命令的注册:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    const transformCommand = vscode.commands.registerCommand('transformToOptionalChain', () => {
            vscode.window.showInformationMessage('转换成功!');
    });

    context.subscriptions.push(transformCommand);
}

然后在 package.json 里也要声明:

在这里插入图片描述

"contributes": {
    "commands": [
      {
        "command": "transformToOptionalChain",
        "title": "xxx"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "transformToOptionalChain"
        }
      ]
    }
},

commands 里声明这个 command,指定 title。

menus 声明 editor/context 也就是编辑器的上下文菜单,添加一个。

测试下:

在这里插入图片描述

确实多了一个菜单项,点击之后会执行 command 的逻辑:

在这里插入图片描述

回过头来看下这段配置:

在这里插入图片描述

这里的 editor/context 是注册编辑器的右键菜单,当然,还有很多别的地方的菜单可以注册:

在这里插入图片描述

这个菜单项还可以指定出现的时机,显示的分组:

在这里插入图片描述

比如 1_modification 就是这里:

在这里插入图片描述

而 navigation 就是这里:

在这里插入图片描述
在这里插入图片描述

这个分组在文档里也有写:

在这里插入图片描述

然后指定菜单项出现的时机:

在这里插入图片描述

当语言类型 为 js 或者 ts,并且选中文本的时候才出现:

这样在非 js、ts 文件里是没这个菜单的:

在这里插入图片描述

在 js、ts 里不选中也是没有的:

在这里插入图片描述

只有在 js、ts 文件,并且选中文本,才会出现这个菜单项:

在这里插入图片描述

然后我们就可以写具体的逻辑了:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {

 const transformCommand = vscode.commands.registerCommand('transformToOptionalChain', () => {
  const editor = vscode.window.activeTextEditor;

  if(editor) {
   const selectedText = editor.document.getText(editor.selection);
  
   editor.edit(builder => {
    builder.replace(editor.selection, selectedText.toUpperCase());
   });
   vscode.window.showInformationMessage('转换成功!');
  }
 });

 context.subscriptions.push(transformCommand);
}

通过 vscode.window.activeTextEditor 拿到当前的 editor,然后拿到选中区域的文本,执行替换。

这里只是替换为了大写。

打个断点试试:

在这里插入图片描述

代码执行到这里会断住:

在这里插入图片描述

可以看到,拿到的文本就是选中的。

那我们把之前用 babel 插件做代码转换的逻辑拿过来就好了。

安装用到的 @babel/core 包和它的 ts 类型包:

npm install --save @babel/core

npm i --save-dev @types/babel__core

然后用它来做下选中代码的转换:

import * as vscode from 'vscode';
import * as babel from '@babel/core';
import type { NodePath, types} from '@babel/core';

function transform(code: string): string{
 function autoOptionalPlugin() {
  return {
   visitor: {
    MemberExpression(path: NodePath<types.MemberExpression>) {
     const text = path.toString();

     path.replaceWithSourceString(text.replace(/\./g, '?.'));
    }
   }
  }
 }

 const res = babel.transformSync(code, {
  plugins: [autoOptionalPlugin]
 });

 return res?.code || '';
}

export function activate(context: vscode.ExtensionContext) {

 const transformCommand = vscode.commands.registerCommand('transformToOptionalChain', () => {
  const editor = vscode.window.activeTextEditor;

  if(editor) {
   const selectedText = editor.document.getText(editor.selection);
  
   if(!selectedText) {
    return;
   }

   editor.edit(builder => {
    builder.replace(editor.selection, transform(selectedText));
   });
   vscode.window.showInformationMessage('转换成功!');
  }
 });

 context.subscriptions.push(transformCommand);
}

这里的 transform 方法就是前面讲过的用 babel 做代码转换的实现。

测试下:

在这里插入图片描述

只有选中的代码才会做转换,没选中的不会:

在这里插入图片描述

还可以把这个功能注册成快捷键:

在这里插入图片描述

"keybindings": [
  {
    "command": "transformToOptionalChain",
    "key": "ctrl+y",
    "mac": "cmd+y",
    "when": "(resourceLangId == javascript || resourceLangId == typescript) && editorHasSelection"
  }
]

在 windows 下是 ctrl + y,在 mac 下是 command + y

在这里插入图片描述

是不是用起来超级方便?

总结

我们想自动把代码里的 data.xxx 转成可选链的形式 data?.xxx。

于是写了 eslint 插件、babel 插件来做这件事。

eslint 插件的 fix 是通过字符串替换的方式修改源码。

babel 插件是通过 ast 的方式修改代码,而且只是改了编译后的代码。

但这俩都是全局替换的,还是自己选择替换哪部分更好,所以我们又写了一个 vscode 插件。

vscode 插件可以在 package.json 的 contributes 里配置 commands、menus、keybindings。

我们注册了一个命令,配置了编辑器右键菜单,并且绑定了快捷键。

当执行这个命令的时候,拿到选中的文本内容,通过 babel 插件来做转换,之后替换回去。

写完这个 vscode 插件以后,再遇到这种情况,只要选中文本,按个快捷键就可以搞定。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以回答这个问题。编一个VSCode插件需要使用TypeScript或JavaScript编,并使用VSCode API来与编辑器交互。以下是一个简单的示例,它创建了一个命令,当用户运行该命令时,它会在编辑器中插入一些文本。 首先,创建一个新的文件夹,并在其中创建一个名为“package.json”的文件,其中包含插件的元数据和依赖项。例如: ``` { "name": "my-extension", "displayName": "My Extension", "description": "A sample VS Code extension", "version": "0.0.1", "publisher": "my-publisher", "engines": { "vscode": "^1.0.0" }, "dependencies": { "vscode": "^1.0.0" } } ``` 然后,创建一个名为“extension.ts”的文件,并添加以下代码: ``` import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { console.log('Congratulations, your extension "my-extension" is now active!'); let disposable = vscode.commands.registerCommand('my-extension.insertText', () => { let editor = vscode.window.activeTextEditor; if (editor) { editor.edit(builder => { builder.insert(editor.selection.active, 'Hello, World!'); }); } }); context.subscriptions.push(disposable); } export function deactivate() {} ``` 这个代码创建了一个名为“my-extension.insertText”的命令,当用户运行该命令时,它会在当前光标位置插入文本“Hello, World!”。 最后,在“package.json”文件中添加以下内容,以告诉VSCode如何加载插件: ``` "activationEvents": [ "onCommand:my-extension.insertText" ], "contributes": { "commands": [ { "command": "my-extension.insertText", "title": "Insert Text" } ] } ``` 现在,将整个文件夹打包成一个“.vsix”文件,并在VSCode中安装它。当您运行插件时,您应该能够看到“Insert Text”命令,并且当您运行该命令时,它会在编辑器中插入文本。 希望这个示例对您有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值