Vscode插件开发-代码段查看器

前言

最近在研究低代码,发现很多的低代码都是表单生成器。自己也照猫画虎地造了一下轮子,因为一开始的方向就是生成代码,并不是通过json再次渲染,结果在网页端生成代码,最后只能生成一个文件,发现了局限性很大。之前也写过通过eletron和nodejs就能通过模板生成多个文件,然后就想到了代码段,其实自己写过的代码也是一个知识库, 很多代码其实都可以复用,例如通用表单,图片上传等。但在vscode里是看不到自己配置了那些代码段,所以我明明装了很多的代码段扩展,却因为不知道里面的代码段导致每次都是看官方文档再复制,于是产生了写一个插件能够看到vscode里面所配置的代码段并且可以点击使用。

功能需求

  • 能显示扩展,vscode用户自定义,外部自定义的代码段列表
  • 鼠标悬停能查看代码段内容
  • 点击列表项能插入代码段

项目搭建

从命令行安装Yeoman和VSCode扩展生成器

npm install -g yo generator-code

在命令行中输入如下命令来启动生成器

yo code

配置选择

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WgjO3G6Q-1661322420979)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9726f61977ad4f10ac9933551427cd37~tplv-k3u1fbpfcp-zoom-1.image)]

视图开发

因为要展示的是类似目录的视图,可以使用树视图,也可以使用webview来实现。最后选择树视图,因为想着就放在侧边栏使用就好,比较方便。
Tree View文档:https://code.visualstudio.com/api/extension-guides/tree-view

package.json配置

"contributes": {
   // 图标栏
    "viewsContainers": {
      "activitybar": [
        {
          "id": "snippet-viwer", // id对应视图id
          "title": "代码段查看器",
          "icon": "img/left_icon.svg"
        }
      ]
    },
   // 视图栏
    "views": {
      "snippet-viwer": [
        {
          "id": "plugin-view",
          "name": "扩展目录"
        }
      ]
    }
  },

第二步是向你注册的视图提供数据,以便 VSCode 可以在视图中显示数据。

树节点类

主要设置节点的图标,label名称等

export class TreeItemNode extends TreeItem {
  constructor(
    // readonly 只可读
    public readonly label: string,
    public readonly icon: string,
    public readonly body: string,
    public readonly children: TreeChild[] | string,
    public collapsibleState: TreeItemCollapsibleState
  ) {
    super(label, collapsibleState);
    this.iconPath = Uri.file(join(__filename, "..", "..", icon));
    // command: 为每项添加点击事件的命令
    if (this.collapsibleState === TreeItemCollapsibleState.None) {
      this.command = {
        title: this.label, // 标题
        command: "itemClick", // 命令 ID
        tooltip: this.body, // 鼠标覆盖时的小小提示框
        arguments: [
          // 向 registerCommand 传递的参数。
          this.body,
        ],
      };
    }
  }

  tooltip = this.body;
}

TreeDataProvider实现类

主要通过实现getChildren方法来返回每一级的列表数据

export class TreeViewProvider implements TreeDataProvider<TreeItemNode> {
  private _onDidChangeTreeData: EventEmitter<
    TreeItemNode | undefined | null | void
  > = new EventEmitter<TreeItemNode | undefined | null | void>();
  private treeList: Tree[] = [];
  private context: ExtensionContext;
  private customDisposableList: Disposable[] = [];

  constructor(context: ExtensionContext) {
    this.context = context;
    this.initList();
  }

  /**
   * 初始化列表
   */
  initList() {
    this.treeList = [];
  }

  onDidChangeTreeData?:
    | import("vscode").Event<TreeItemNode | undefined | null | void> =
    this._onDidChangeTreeData.event;

  getTreeItem(element: TreeItemNode): TreeItem | Thenable<TreeItem> {
    console.log("获取节点", element.label);
    return element;
  }

  getChildren(
    element?: TreeItemNode | undefined
  ): ProviderResult<TreeItemNode[]> {
    // 返回树子节点类列表
  }

  public static initTreeViewItem(context: ExtensionContext) {
    // 实例化 TreeViewProvider
    const treeViewProvider = new TreeViewProvider(context);
    // registerTreeDataProvider:注册树视图
    window.registerTreeDataProvider("plugin-view", treeViewProvider);
    return treeViewProvider;
  }
}

代码段数据获取

获取扩展的代码段数据

extensions文档:https://code.visualstudio.com/api/references/vscode-api#extensions

通过extensions.all便可以得到所有的扩展数据,数据结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UK6ndA1v-1661322420981)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f93e70ca8009442cacf56c3c420d0a55~tplv-k3u1fbpfcp-zoom-1.image)]
通过其数据结构分析,extensionPath为扩展的目录,packageJSON->contributes->snippets为代码段的数据,包含代码段生效的编程语言和代码段文件的路径,这样就可以获取所有的代码段文件并将其载入就行

    // 加载扩展代码段
    let extensionsList = extensions.all;
    extensionsList = extensionsList.filter(
      (item) => !!item?.packageJSON?.contributes?.snippets
    );
    extensionsList.forEach((item) => {
      this.treeList.push({
        name: item?.packageJSON?.name,
        icon: "img/folder_type_plugin.svg",
        children: item.packageJSON.contributes.snippets.map(
          (snippetItem: SnippetManifest) => ({
            name: snippetItem.language,
            icon: "img/folder_type_src.svg",
            children: path.join(item?.extensionPath || "", snippetItem.path),
          })
        ),
      });
    });

获取外部自定义的代码段数据

反复查文档也找不到能够获取设置的用户自定义代码段的获取方法,但由于用户自定义的代码段都会被存放在一个文件夹下,可以通过配置路径的方法来实现。后来出现了把自定义的代码段作为一个项目的想法,这样就可以实时的同步代码段等,感觉这样子管理也很好。就只提供配置自定义代码段文件夹的方式来实现自定义代码段的导入。

this.treeList = [];
    for (const disposable of this.customDisposableList) {
      disposable.dispose();
    }
    this.customDisposableList = [];
    // 加载自定义代码段
    const customConfig = workspace.getConfiguration("SnippetViewer");
    if (customConfig.customUrl) {
      try {
        delete require.cache[join(customConfig.customUrl, "config.js")];
        const customConfigList: Tree[] = JSON5.parse(
          fs.readFileSync(
            path.join(customConfig.customUrl, "config.json"),
            "utf8"
          )
        );
      } catch (error) {
        console.error("自定义配置错误");
        window.showErrorMessage("自定义代码段配置错误");
      }
    }

代码段的目录结构

snippets                          
├─ custom    
│  └─ vue.json
│  └─ javacript.json                 
└─ config.json               

config.json 数据结构

[
  {
    "name": "custom",
    "children": [
      {
        "name": "vue",
        "children": "custom/vue.json"
      }
    ]
  }
]

一级name表示名称,二级name表示语言

获取用户自定义的代码段数据

因为在开发过程中也在不断查看学习其他插件的代码,然后在Settings Sync发现新大陆,在他的environmentPath.ts文件里面有个USER_FOLDER的常量,他就是用户的数据存放的目录路径,而用户自定义代码段就是存放在其下面的snippets文件夹下,这样,用户自定义代码段数据也能解决了。

constructor(context: vscode.ExtensionContext) {
  this.isPortable = !!process.env.VSCODE_PORTABLE;
  if (!this.isPortable) {
    this.path = resolve(context.globalStorageUri.fsPath, "../../..").concat(
      normalize("/")
    );
    this.userFolder = resolve(this.path, "User").concat(normalize("/"));
  } else {
    this.userFolder = resolve(this.path, "user-data/User").concat(
      normalize("/")
    );
  }
  this.snippetsFolder = this.userFolder.concat("/snippets/");
}

// 加载用户自定义代码段
    try {
      this.treeList.push({
        name: "user-snippets",
        icon: "vscode",
        body: "user-snippets",
        isOutCustomRoot: false,
        expression: `${this.treeList.length}`,
        disabled: false,
        children: fs
          .readdirSync(this.environment.snippetsFolder)
          .map((fileName, fileIndex) => {
            return {
              name: fileName.substring(0, fileName.lastIndexOf(".")),
              icon: "src",
              isOutCustomRoot: false,
              disabled: false,
              expression: `${this.treeList.length}.${fileIndex}`,
              children: join(this.environment.snippetsFolder, fileName),
            };
          }),
      });
    } catch (error) {}

数据显示

更新TreeViewProvidergetChildren 逻辑

getChildren(
    element?: TreeItemNode | undefined
  ): ProviderResult<TreeItemNode[]> {
    if (element) {
      if (Array.isArray(element.children)) {
        return element.children.map((item) => {
          return new TreeItemNode(
            item.name,
            item.icon,
            item.name,
            item.children,
            TreeItemCollapsibleState.Collapsed as TreeItemCollapsibleState
          );
        });
      } else {
        let resultArr: string[] = [];
        let json: { [key: string]: SnippetJSON } = {};
        try {
          json = JSON5.parse(
            fs.readFileSync(path.join(element.children), "utf8")
          );
          resultArr = Object.keys(json);
        } catch (error) {
          console.log(error);
          window.showErrorMessage("代码段文件错误");
        }
        return resultArr.map(
          (key) =>
            new TreeItemNode(
              key,
              "img/code.svg",
              Array.isArray(json[key].body)
                ? (json[key].body as string[]).join("\n")
                : (json[key].body as string),
              "",
              TreeItemCollapsibleState.None as TreeItemCollapsibleState
            )
        );
      }
    } else {
      // 不包含elment, 根节点
      return this.treeList.map((item) => {
        return new TreeItemNode(
          item.name,
          item.icon,
          item.name,
          item.children,
          TreeItemCollapsibleState.Collapsed as TreeItemCollapsibleState
        );
      });
    }
  }

踩坑:

格式化json数据的时候发现有些代码段的json文件格式有错误,一开始发现的是最后有逗号,我用Prettier格式化一下就好了,所以一开始使用Prettier来进行格式化,发现还是报错,我又打开一个代码段json查看,里面竟然有注释,那时我想,json还能这样写的吗,但是vscode读取的时候也是可以读取的,那他肯定是可以读取了,然后查了一会才发现有json5这个东西,是json的一个超集,可以使用逗号结尾和注释等,json5传送门,最后通过json5来格式化json,就没问题了。

外部自定义代码段增加代码补全

扩展的代码段vscode是会自动加载的,我们自己通过配置的代码段目前只能通过侧边栏来点击使用,希望能够像扩展一样同时使用代码补全功能,主要通过languages.registerCompletionItemProvider来实现。

/**
   * 增加自定义代码段
   * @param {string} language
   * @param {object} json
   */
  addCustomSnippets(
    language: string,
    json: { [key: string]: SnippetJSON }
  ): void {
    const disposable = languages.registerCompletionItemProvider(
      { scheme: "file", language },
      {
        provideCompletionItems() {
          return Object.keys(json).map((key) => {
            const snippetCompletion = new CompletionItem(
              {
                label: json[key].prefix,
                description: key,
                detail: "(custom)",
              },
              CompletionItemKind.Snippet
            );
            snippetCompletion.insertText = new SnippetString(
              Array.isArray(json[key].body)
                ? (json[key].body as string[]).join("\n")
                : (json[key].body as string)
            );
            snippetCompletion.detail = key;
            snippetCompletion.documentation =
              new MarkdownString().appendCodeblock(
                snippetCompletion.insertText.value
              );
            return snippetCompletion;
          });
        },
      }
    );
    this.customDisposableList.push(disposable);
    this.context.subscriptions.push(disposable);
  }

最后通过customDisposableList来存储所有的注册,刷新列表时注销之前的注册再重新注册。

增加刷新按钮

更新扩展或者修改自定义代码段时,能够通过刷新按钮刷新可视化列表。

package.json

 "contributes": {
    "commands": [
      {
        "command": "snippets-viewer.refresh",
        "title": "Refresh List",
        "icon": "$(refresh)"
      }
    ],
    "menus": {
      "view/title": [
        {
          "command": "snippets-viewer.refresh",
          "when": "view == plugin-view",
          "group": "navigation"
        }
      ]
    }
}

TreeViewProvider

主要通过_onDidChangeTreeData来实现刷新

/**
   * 刷新列表
   */
  refresh(): void {
    this.initList();
    this._onDidChangeTreeData.fire();
  }

踩坑

一开始自定义的config.json是使用config.js的,通过require的方式引入,require引入之后会有缓存,需要每次重新加载都要清除之前的缓存,嫌麻烦就直接全改成json了。

最后注册代码

export function activate(context: vscode.ExtensionContext) {
  // Use the console to output diagnostic information (console.log) and errors (console.error)
  // This line of code will only be executed once when your extension is activated
  console.log(
    'Congratulations, your extension "snippets viewer" is now active!'
  );

  // 实现树视图的初始化
  const treeViewProvider = TreeViewProvider.initTreeViewItem(context);

  // The command has been defined in the package.json file
  // Now provide the implementation of the command with registerCommand
  // The commandId parameter must match the command field in package.json
  let itemClickDisposable = vscode.commands.registerCommand(
    "itemClick",
    (body) => {
      const editor = vscode.window.activeTextEditor;
      if (editor) {
        editor.insertSnippet(new vscode.SnippetString(body));
      }
    }
  );

  let refreshDisposable = vscode.commands.registerCommand(
    "snippets-viewer.refresh",
    () => treeViewProvider.refresh()
  );

  context.subscriptions.push(itemClickDisposable, refreshDisposable);
}

// this method is called when your extension is deactivated
export function deactivate() {}

图标能够实现展开折叠样式

一开始实现图标是在vscode里面扣了几个出来,但是发现他是一成不变的,然后也想着能够通过修改element的状态来实现,但是他并不会调用getTreeItemgetChildren。也看了leetcode的代码,主要里面有文件夹展开的,但是还是搞不懂他是怎么做到的。最后终于被我搞明白了T_T,在TreeItem里面有个resourceUri属性

/**
 * The {@link Uri} of the resource representing this item.
 *
 * Will be used to derive the {@link TreeItem.label label}, when it is not provided.
 * Will be used to derive the icon from current file icon theme, when {@link TreeItem.iconPath iconPath} has {@link ThemeIcon} value.
 */
resourceUri?: Uri;

就是在不设置icon的情况下,可以通过设置resourceUri来展示系统的图标,如果有主题就会使用主题。resourceUri的类型是Uri,其中能够影响的是path,例如path = ‘/src’,他就会显示src的图标,并且在节点被展开折叠时会自动显示展开折叠样式。

最后

上面的代码仅仅是开发的过程思路,最终的代码也发生了改变。后面也根据issue实现了一下禁用外部自定义代码段和根据语言来分组的功能,感兴趣的话可以自行阅读源码。

代码和插件已发布,搜索vscode-snippets-viewer就能搜到,喜欢的话给个star吧
github:https://github.com/shilim-developer/snippets-viewer
vscode market:https://marketplace.visualstudio.com/items?itemName=shilim.vscode-snippets-viewer

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340