利用 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(函数地图),程序员提效神器

相关推荐
移民找老国1 天前
加拿大移民新风向
java-ee·maven·phpstorm·visual studio code·nio
闪亮Girl3 天前
vs2015安装插件QtPackage.vsix等vsix文件类型
visual studio code
SuperYing4 天前
💯What?维护新老项目频繁切换node版本太恼火?开发一个vscode插件自动切换版本,从此告别烦恼
前端·visual studio code
羊小猪~~5 天前
数据结构C语言描述1(图文结合)--顺序表讲解,实现,表达式求值应用,考研可看
java·c语言·数据结构·c++·链表·visual studio code·visual studio
羊小猪~~5 天前
C/C++语言基础--C++模板与元编程系列三(变量模板、constexpr、萃取等…………)
java·c语言·开发语言·c++·visual studio code·visual studio
羊小猪~~8 天前
C/C++语言基础--C++模板与元编程系列二类模板、全特化、偏特化、编译模型简介、实现简单Vetctor等…………)
java·c语言·开发语言·c++·visual studio code·visual studio
编程老船长14 天前
用PlantUML让状态图“动”起来 —— 快速绘制老师申报课程流程
uml·visual studio code
风雪中的兔子17 天前
vscode插件开发入门:小试牛刀
前端·visual studio code
musiclvme17 天前
ubuntu22.04下GStreamer源码编译单步调试
ubuntu·音视频·visual studio code
Luncert19 天前
Vscode 插件开发 - TreeView
visual studio code