利用 chatgpt 开发一个 vscode 插件

前言

有了 chatgpt 之后,写代码真的变得很方便,很多自己不熟悉的代码,直接问 chatgpt 就能得到相对准备的答案,虽然有时候可能不会 100%准确,但只需要少量需改便可以实现想要的效果。chatgpt 可以有效的帮助我们去开发一个不太熟悉的应用。

前几天逛掘金的时候看到一个FnMap(函数地图),程序员提效神器,主要功能就是展示 vue 文件的 script,template,style 结构树,如下图,感觉还挺好用的。

于是想自己模仿一个自用。有实力可以支持一下作者,原作者做了更好的兼容。因为每个人的代码格式可能会有些不同,我这里只是简单的实现了一下基础功能,并且没有去兼容不同的情况,只适用于我自己的代码结构,功能没有原作者优秀。

我之前是没有写过 vscode 插件的,所以直接去问 chatgpt,真的很方便,即使你完全不知道 vscode 插件提供的 api 也没有关系。

好的,开干!

github源码地址vsce-simple-tools 插件下载地址simple tools - Visual Studio Marketplace

过程

怎么制作一个 vscode 插件

首先,直接问 chatgpt:"怎么制作一个 vscode 插件",跟着回答我们先创建一个 vscode 插件项目。

怎么创建一个侧边工具栏

然后,问 chatgpt:"vscode 插件怎么创建一个侧边工具栏",下面是 chatgpt 给我们的代码,

js 复制代码
const vscode = require("vscode");

// 定义一个树视图提供者
class MyTreeDataProvider {
  getTreeItem(element) {
    return element;
  }

  getChildren() {
    // 返回一个包含树视图项目的数组
    return [
      new vscode.TreeItem("Item 1"),
      new vscode.TreeItem("Item 2"),
      new vscode.TreeItem("Item 3"),
    ];
  }
}

// 激活插件
function activate(context) {
  // 创建树视图
  const myTreeView = vscode.window.createTreeView("myTree", {
    treeDataProvider: new MyTreeDataProvider(),
  });

  context.subscriptions.push(myTreeView);
}

exports.activate = activate;

这里因为我们会得到一个错误 "No view is registered with id: myTree",因为没有在 package.json 中配置 contributes.views,没关系,直接问。

于是我们在资源管理器下得到了这样一个树结构,很好,很顺利!

获取函数名

没有技巧,直接大白话说出我们的需求:"我想要获取当前文件的方法名设置为 TreeItem"

贴出 chatgpt 给的代码,这里已经实现了将 函数名 显示到 treeItems 中,需要注意并没有实现 findMethodNamesInDocument ,也就是解析 document 获取方法名这部分代码没有给。没有关系,我们接着问,坚决不写一行代码。

js 复制代码
const vscode = require("vscode");

class MyTreeDataProvider {
  getTreeItem(element) {
    return element;
  }

  getChildren() {
    const activeTextEditor = vscode.window.activeTextEditor;

    if (activeTextEditor) {
      const document = activeTextEditor.document;
      const methodNames = findMethodNamesInDocument(document);

      const treeItems = methodNames.map((methodName) => {
        const treeItem = new vscode.TreeItem(methodName);
        treeItem.iconPath = new vscode.ThemeIcon("symbol-method"); // 可选的图标
        return treeItem;
      });

      return treeItems;
    }

    return [];
  }
}

function findMethodNamesInDocument(document) {
  // 在文档中查找方法名的逻辑,例如使用正则表达式
  // 返回一个包含方法名的数组
  const methodNames = []; // 替换为实际的查找逻辑
  return methodNames;
}

export const myTreeView = vscode.window.createTreeView("myExtension", {
  treeDataProvider: new MyTreeDataProvider(),
});

直接问:处理 document.getText()获取方法名。chatgpt 给我们代码直接用正则表达式去获取了所有方法名。

js 复制代码
function findMethodNames(document) {
  const text = document.getText();
  const methodRegex = /function\s+(\w+)/g; // 此正则表达式匹配函数名,您可以根据需要调整

  const methodNames = [];
  let match;
  while ((match = methodRegex.exec(text))) {
    methodNames.push(match[1]);
  }

  return methodNames;
}

很好,已经可以获取到方法名,当然,chatgpt 这里的解析十分的简单,面对稍微复杂一点的情况就完全不行了。但是没关系,解析 document 这件事可以我们后面自己在进行修改。咱先把功能问完,再改逻辑。

跳转到函数对应位置

简单直接:我想要点击方法名跳转到对应的行数。

这里提一嘴,之所以变成 chatgpt 提供的代码变成了 ts 是因为我把我有问题的 ts 代码直接丢给 chatgpt 修改,所以 chatgpt 后面给我的都是 ts 代码。

创建 MethodTreeItem 类继承 vscode.ThemeIcon,使用 vscode.commands.registerCommand 注册 extension.goToMethod 实现跳转函数名

ts 复制代码
// 返回vscode.TreeItem改成返回MethodTreeItem实例
methodNames.forEach((methodName, index) => {
  const treeItem = new MethodTreeItem(methodName, index);
  methodItems.push(treeItem);
});

class MethodTreeItem extends vscode.TreeItem {
  constructor(label: string, lineNumber: number) {
    super(label, vscode.TreeItemCollapsibleState.None);
    this.command = {
      title: "Go to method",
      command: "extension.goToMethod",
      arguments: [lineNumber],
    };
  }
}

// 注册跳转
vscode.commands.registerCommand(
  "extension.goToMethod",
  (lineNumber: number) => {
    if (vscode.window.activeTextEditor) {
      const position = new vscode.Position(lineNumber, 0);
      vscode.window.activeTextEditor.selection = new vscode.Selection(
        position,
        position
      );
      vscode.window.activeTextEditor.revealRange(
        new vscode.Range(position, position),
        vscode.TextEditorRevealType.InCenter
      );
    }
  }
);

光是注册跳转事件是无法实现跳转的,原因是我们还没有与 TreeView 关联,直接询问 chatgpt:无法跳转到函数名。

package.json 文件中注册 extension.goToMethod 命令

json 复制代码
"contributes": {
  "commands": [
    {
      "command": "extension.goToMethod",
      "title": "Go to Method"
    }
  ]
}

继续问 chatgpt:除了跳转到函数名的行数之外,还想要将光标定位到对应的方法名或者变量名上。

这里和上面注册跳转是一样的,只是将 charIndex 传给Position

ts 复制代码
vscode.commands.registerCommand(
  "extension.goToMethod",
  (lineNumber: number, charIndex: number) => {
    if (vscode.window.activeTextEditor) {
      const position = new vscode.Position(lineNumber, charIndex);
      vscode.window.activeTextEditor.selection = new vscode.Selection(
        position,
        position
      );
      vscode.window.activeTextEditor.revealRange(
        new vscode.Range(position, position),
        vscode.TextEditorRevealType.InCenter
      );
    }
  }
);

只需在创建 MethodTreeItem 实例的时候把计算好的 charIndex 传给 this.command ,细心的朋友应该能发现,上面 chatgpt 给的代码传入的 lineNumber 是错的,它直接将 methodNames 数组中元素的下标直接给传过去了,这样跳转的行数是错误的。不过没关系,我们自己处理一下就好了,也可以直接给 chatgpt 说你传错了,让它修改。

document 获取具体行数的方法

ts 复制代码
const line = document.positionAt(match.index).line;

解析document

我想一行行去解析,就也用不到了,直接告诉 chatgpt 我想要一行行解析 document ,并且判断出 functionclassvar/let/const等变量名

ts 复制代码
function parseDocument(document: vscode.TextDocument): MethodTreeItem[] {
  const outline: MethodTreeItem[] = [];
  let currentFunction: MethodTreeItem | null = null;
  let isInsideClass = false;

  for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
    const line = document.lineAt(lineIndex);
    const lineText = line.text.trim();

    if (lineText.startsWith("class ")) {
      // 类定义
      const className = lineText.substring(6, lineText.length - 1).trim();
      const classItem = new MethodTreeItem(
        className,
        lineIndex,
        vscode.TreeItemCollapsibleState.Collapsed
      );
      outline.push(classItem);
      currentFunction = classItem;
      isInsideClass = true;
    } else if (lineText.startsWith("function ") && lineText.endsWith("{")) {
      // 函数定义
      const functionName = lineText.substring(9, lineText.length - 1).trim();
      const functionItem = new MethodTreeItem(functionName, lineIndex);
      if (isInsideClass) {
        currentFunction?.children.push(functionItem);
      } else {
        outline.push(functionItem);
      }
      currentFunction = functionItem;
    } else if (
      lineText.startsWith("var ") ||
      lineText.startsWith("let ") ||
      lineText.startsWith("const ")
    ) {
      // 变量定义
      const variableName = lineText.split(" ")[1].replace(";", "").trim();
      const variableItem = new MethodTreeItem(variableName, lineIndex);
      if (isInsideClass) {
        currentFunction?.children.push(variableItem);
      } else {
        outline.push(variableItem);
      }
    }
  }
  return outline;
}

其实到这里已经差不多可以不用问 chatgpt 了,已经可以自己进行一些修改就可以实现大概的功能了。

告诉 chatgpt:用 startsWith 是不严谨的,声明之前可能还有关键字。

得到一些正则表达式。

ts 复制代码
// 匹配类定义
const classRegex = /class\s+([\w\d_]+)(?:\s+extends\s+([\w\d_]+))?/;

// 匹配函数定义
const functionRegex = /function\s+([\w\d_]+)\s*\(/;

// 匹配变量定义
const variableRegex = /(const|var|let)\s+([\w\d_]+)(?=\s*=?[^=])/;

// 匹配 interface 或 type 定义
const interfaceTypeRegex = /(interface|type)\s+([\w\d_]+)/;

修改

现在我们稍微修改一下代码。

NodeTreeItem

首先,继承 vscode.TreeItem 的类需要添加一些属性

  • name 变量名
  • lineNumber 在代码中的行数
  • children 子变量(function 和 class 的内部变量)
  • type 记录变量的类型
ts 复制代码
class NodeTreeItem extends vscode.TreeItem {
  name: string = "";
  lineNumber: number = 0;
  children: NodeTreeItem[] = [];
  type: NodeType = NodeType.Empty;

  constructor(config: NodeItem) {
    super(config.name);
    this.name = config.name;
    this.lineNumber = config.lineNumber;
    this.type = config.type;
    this.iconPath = new vscode.ThemeIcon(config.type);

    this.command = {
      title: "Go to method",
      command: "extension.goToMethod",
      arguments: [config.lineNumber, config.charIndex],
    };
  }

  addChild(item: NodeTreeItem) {
    if (this.children.findIndex((child) => child.name === item.name) === -1) {
      this.children.push(item);
    }
  }
}

NodeType

用于显示 vscode.ThemeIcon 图标

  • Empty 空
  • Variable 变量、内部属性
  • Method 函数
  • Enum 枚举
  • Type 类型
  • Module 类
  • InMethod 内部函数
ts 复制代码
enum NodeType {
  Empty = "empty",
  Variable = "symbol-variable",
  Method = "symbol-method",
  Enum = "symbol-enum",
  Type = "symbol-value",
  Module = "symbol-module",
  InMethod = "symbol-method",
}

正则规则

正则表达式匹配属性

ts 复制代码
const MethodReg = /function\s+([\w\d_]+)\s*\(/;
const EnumReg = /enum\s+([\w\d_]+)\s*\{/;
const TypeReg = /(interface|type)\s+([\w\d_]+)/;
const ModuleReg = /class\s+([\w\d_]+)(?:\s+extends\s+([\w\d_]+))?/;
const VariableReg = /^\s*(?:(const|var|let|private|public|static|readonly)\s+)*\s*([\w]+)\?*\s*[:=]\s*[^;]/;
const InMethodReg = /\s*(private|public|static)?\s*(\w+)\(([^)]*)\):*\s*([^;]*)\s{/;

parseDocument

其次,最重要的文档解析

逐行解析 documenttabCount 用于记录缩进,判断函数体是否结束;methodStack 用于记录一个模块(function/class/enum/type)的局部空间,生成二级树;

创建 NodeTreeItem 实例,如果存在函数体,则添加到子节点中,否则直接添加;

当实例的类型不为 Variable 属性或者变量时,记录当前的缩进。

当遇到 } 并且缩进相同时,pop 出 methodStack 记录的模块。

ts 复制代码
class MyTreeDataProvider {
  private parseDocument(document: vscode.TextDocument): NodeTreeItem[] {
    const itemList: NodeTreeItem[] = [];
    const tabCount: number[] = [];
    const methodStack: NodeTreeItem[] = [];

    for (let index = 0; index < document.lineCount; index++) {
      const text = document.lineAt(index).text;
      const item = createNodeTreeItem(text, index);
      if (item) {
        if (methodStack.length > 0) {
          methodStack[methodStack.length - 1]?.addChild(item);
        } else {
          itemList.push(item);
        }

        if (item.type !== NodeType.Variable) {
          const count = text.match(/\S/)?.index || 0;
          tabCount.push(count);
          methodStack.push(item);
        }
      }

      if (text.includes("}")) {
        const count = text.match(/\S/)?.index || 0;
        if (count === tabCount[tabCount.length - 1]) {
          methodStack.pop();
          tabCount.pop();
        }
      }
    }

    return itemList.map((item) => {
      if (item.children.length > 0) {
        item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
      }
      return item;
    });
  }
}

最终达到效果,就是本项目文件的属性树。当然,这样的判断并不严谨,兼容性并不强,只能用于学习或者自己修改使用。对于一些特殊的格式就会出问题。

结尾

如果感兴趣的话可以关注我的项目vsce-simple-tools,可以到simple tools - Visual Studio Marketplace下载使用。

有实力的朋友可以支持一下原作者FnMap(函数地图),程序员提效神器

相关推荐
萌萌哒草头将军9 小时前
🎉🎉🎉Cursor 宣布完成 C 轮 9 亿美元融资,市场估值 99 亿美元!
visual studio code·cursor·trae
LinXunFeng10 小时前
Flutter - GetX Helper 助你规范应用 tag
flutter·github·visual studio code
Jooolin13 小时前
【编程史】IDE 是谁发明的?从 punch cards 到 VS Code
ai编程·visual studio code·编译器
vueTmp3 天前
VSCode 插件离线下载攻略
visual studio code
穷人小水滴7 天前
在 Android 设备上写代码 (Termux, code-server)
android·linux·visual studio code
哎呦你好10 天前
VS Code 安装后设置中文界面并添加常用插件的详细指南
编辑器·visual studio code
魔术师ID20 天前
vue2.0 组件生命周期
前端·javascript·vue.js·学习·visual studio code
MingT 明天你好!20 天前
在vs code 中无法运行npm并报无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查
前端·npm·node.js·visual studio code
Ynov24 天前
详细解释api
javascript·visual studio code
程序员Bears1 个月前
现代前端工具链深度解析:从包管理到构建工具的完整指南
前端·python·visual studio code