开发共享vscode文件书签插件

介绍

目前工作上开发的项目很大,找文件很辛苦,所以想找一个文件书签的vscode插件,但看了一圈,发现目前市面上的vscode文件书签插件功能缺少分组(文件夹)、共享书签功能,因此有了开发File-Keeper插件的想法。大致上是基于Explorer-Bookmark做了分组、拖拽、导入导出功能,方便在团队中复用该插件。

功能演示

在项目文件点击右键进行加入书签

通过新增目录与拖拽文件,整理书签

导出书签数据,可以提供给他人复用

功能分划

加入书签功能

在项目目录中需增加右键加入书签功能

书签展示

新建一个视图展示书签列表

点击书签

点击书签打开指定文件

书签备注功能

书签新增备注,方便用户识别文件

新建目录与拖拽节点功能

提供书签分组与移动分组功能

导入导出功能

方便多人使用书签,可共享部分或全部书签

删除书签/目录功能

提供删除功能,并添加确认提示

书签本地存储

防止在vscode关闭时书签丢失

技术方案确认

vscode插件提供了 Tree View API。方便展示树状结构的数据,还可以设置右键菜单与视图右上角菜单。

在Tree View中,每个节点是按照vscode.TreeItem的格式展示的,其中包括了id、label、command(点击节点后执行的命令)、tooltip等功能。所以在需要展示时,生成vscode.TreeItem格式的列表即可。

而我们使用时,还需要用一个自定义列表bookmarkedDirectories存储节点信息,这个列表将存储在用户本地,导出导入功能借助这个列表即可丝滑同步。

每次用户操作后更新bookmarkedDirectories列表,并在每次操作更新后刷新视图,读取bookmarkedDirectories列表生成vscode.TreeItem的数据结构返回到视图中。

技术实现

树状视图

可以使用内置方法vscode.window.createTreeView创建树状视图,其中可入参配置树状视图数据管理、拖拽管理等方法。

js 复制代码
    const filerKeeperWorker = new FileKeeperWorker(
      context,
      vscode.workspace.workspaceFolders
    );
    const fileKeeperDataProvider = new FileKeeperDataProvider(
      filerKeeperWorker
    );

    this.ftpViewer = vscode.window.createTreeView("file-keeper", {
      treeDataProvider: fileKeeperDataProvider,
      dragAndDropController: fileKeeperDataProvider,
    });

树状视图数据管理可以基于TreeDataProvider实现,必须包含getChildren方法,这个方案就是用于展示数据,返回vscode.TreeItem格式的列表,如果是打开目录,就会入参vscode.TreeItem格式的数据,需要返回该目录下一级的数据。

js 复制代码
  // 展示子节点
  public async getChildren(
    element?: FileSystemObject
  ): Promise<FileSystemObject[]> {
    let children: TypedDirectory[] = [];
    if (element) {
      const childrenIds =
        this.bookmarkedDirectoriesMap.get(element.id)?.children || [];
      children = childrenIds
        .map((child) => {
          return this.bookmarkedDirectoriesMap.get(child);
        })
        .filter(Boolean) as TypedDirectory[];
      // return this.directorySearch(element.resourceUri);
    } else {
      children = this.bookmarkedDirectories.filter((dir) => !dir.parent);
    }
    return children.length > 0
      ? this.createEntries(children)
      : Promise.resolve([]);
  }
  
  // 生成文件列表
  private async createEntries(bookmarkedDirectories: TypedDirectory[]) {
    let fileSystem: FileSystemObject[] = [];

    for (const dir of bookmarkedDirectories) {
      const { path: filePath, type: type, id, parent, label } = dir;
      const file = vscode.Uri.file(filePath);

      let fileLabel: vscode.TreeItemLabel | string = "";
      // 存在备注的文件节点名称:备注(文件名)
      if (label && type === vscode.FileType.File) {
        const filename = `${path.basename(dir.path)}`;
        const longLabel = `${label} (${filename})`;
        fileLabel = {
          label: longLabel,
          highlights: [[label.length + 2, longLabel.length - 1]],
        };
      } else {
        fileLabel = label || `${path.basename(dir.path)}`;
      }

      fileSystem.push(
        new FileSystemObject({
          id,
          parent,
          label: fileLabel,
          collapsibleState:
            type === vscode.FileType.File
              ? vscode.TreeItemCollapsibleState.None
              : vscode.TreeItemCollapsibleState.Expanded,
          uri: file,
        }).setContextValue(this.bookmarkedDirectoryContextValue)
      );
    }

    return fileSystem;
  }

拖拽管理可以基于TreeDragAndDropController实现,必须包含dragMimeTypes、dropMimeTypes参数与handleDrag、handleDrop方法,dragMimeTypes声明了可拖拽的类型(具体节点类型可以通过Debug模式查看),dropMimeTypes是用于拖拽过程中获取拖拽过程的数据。handleDrag开始拖拽节点,handleDrop放置节点。

js 复制代码
  // Drag and drop controller
  // 放置节点,移动整个节点树
  public async handleDrop(
    target: FileSystemObject | undefined,
    sources: vscode.DataTransfer,
    _token: vscode.CancellationToken
  ): Promise<void> {
    const transferItem = sources.get(
      "application/vnd.code.tree.fileKeeperViewDragAndDrop"
    );
    if (!transferItem) {
      return;
    }
    const treeItems: FileSystemObject[] = transferItem.value;

    let newParent: TypedDirectory | undefined;
    if (target?.collapsibleState === vscode.TreeItemCollapsibleState.None) {
      newParent = target.parent
        ? this.bookmarkedDirectoriesMap.get(target.parent)
        : undefined;
    } else if (target?.collapsibleState) {
      newParent = this.bookmarkedDirectoriesMap.get(target.id);
    }

    treeItems.forEach((item) => {
      const typedDirectory = this.bookmarkedDirectoriesMap.get(item.id);
      if (typedDirectory && item.id !== newParent?.id) {
        // Remove from previous parent
        if (item.parent) {
          const typedDirectoryParent = this.bookmarkedDirectoriesMap.get(
            item.parent
          );
          if (typedDirectoryParent?.children?.length) {
            typedDirectoryParent.children.splice(
              typedDirectoryParent.children.indexOf(typedDirectory.id),
              1
            );
          }
        }

        typedDirectory.parent = newParent?.id;
        newParent?.children.push(typedDirectory.id);
      }
    });

    this.saveBookmarks();
  }

  // 拖拽节点,将节点信息存储在 DataTransfer 中
  public async handleDrag(
    source: FileSystemObject[],
    treeDataTransfer: vscode.DataTransfer,
    _token: vscode.CancellationToken
  ): Promise<void> {
    treeDataTransfer.set(
      "application/vnd.code.tree.fileKeeperViewDragAndDrop",
      new vscode.DataTransferItem(source)
    );
  }

导出功能需要遍历节点树后,利用vscode.workspace.openTextDocumentvscode.window.showTextDocument展示输出数据。而导入时需注意在部分修改的节点应该如何处理。对于文件的地址也需要对项目根地址vscode.workspace.workspaceFolders做替换操作,以免在不同电脑上找不到对应位置。

js 复制代码
  // 根据节点遍历出所有子节点
  getTypedDirectoryTree(element: TypedDirectory) {
    let children: TypedDirectory[] = [];
    if (element) {
      const childrenIds = element.children || [];
      childrenIds.forEach((child) => {
        const el = this.bookmarkedDirectoriesMap.get(child);
        if (el) {
          children = [...children, ...this.getTypedDirectoryTree(el)];
        }
      });
    }
    return [element, ...children];
  }

  // 导出文件书签
  public async exportBookmarks(element?: FileSystemObject) {
    let list: TypedDirectory[] = this.bookmarkedDirectories;
    if (element) {
      const typedDirectory = this.bookmarkedDirectoriesMap.get(element.id);
      if (typedDirectory) {
        list = this.getTypedDirectoryTree(typedDirectory);
      }
    }
    // 获取当前工作区的文件夹
    const workspaceFolders = vscode.workspace.workspaceFolders;

    if (workspaceFolders && workspaceFolders.length > 0) {
      // 获取第一个工作区文件夹的路径
      const projectPath = workspaceFolders[0].uri.fsPath;
      list = list.map((dir) => {
        const path = dir.path?.replace(projectPath, PROJECT_PATH);
        return { ...dir, path };
      });
    }

    const contentInfo = JSON.stringify(list, undefined, 2);
    // 创建一个新的未保存的文件
    const document = await vscode.workspace.openTextDocument({
      content: contentInfo,
    });
    vscode.env.clipboard.writeText(contentInfo);
    // 显示该文件
    await vscode.window.showTextDocument(document);
    vscode.window.showInformationMessage("信息已经复制至剪切版");
  }

  // 导入文件书签
  public async importBookmarks() {
    // 获取当前工作区的文件夹
    const workspaceFolders = vscode.workspace.workspaceFolders;
    if (workspaceFolders && workspaceFolders.length > 0) {
      try {
        const clipboardContent = await vscode.env.clipboard.readText();
        const newList = JSON.parse(clipboardContent) as TypedDirectory[];
        if (Array.isArray(newList)) {
          newList.forEach((dir) => {
            if (!this.bookmarkedDirectoriesMap.has(dir.id)) {
              if (dir.path && dir.path.startsWith(PROJECT_PATH)) {
                const projectPath = workspaceFolders[0].uri.fsPath;
                dir.path = dir.path.replace(PROJECT_PATH, projectPath);
              }
              // 子节点的位置被变换,不做移动
              if (dir?.children?.length) {
                dir.children = dir.children.filter((child) => {
                  const childTypeDir = this.bookmarkedDirectoriesMap.get(child);
                  return childTypeDir?.parent === dir.id;
                });
              }
              // 如果有父节点,将子节点添加到父节点的 children 中
              if (dir.parent) {
                const parentTypeDir = this.bookmarkedDirectoriesMap.get(dir.parent);
                if (parentTypeDir) {
                  if (!parentTypeDir.children?.length) {
                    parentTypeDir.children = [dir.id];
                  } else if (!parentTypeDir.children.includes(dir.id)) {
                    parentTypeDir.children.push(dir.id);
                  }
                }
              }
              const typedDirectory = new TypedDirectory(dir);
              this.bookmarkedDirectories.push(typedDirectory);
              this.bookmarkedDirectoriesMap.set(
                typedDirectory.id,
                typedDirectory
              );
            }
          });
          this.saveBookmarks();
        }
      } catch (e) {
        vscode.window.showErrorMessage("Invalid JSON content");
      }
    } else {
      vscode.window.showErrorMessage("项目不存在");
    }
  }

package.json规则:

contributes.views.explorer 注册窗口

contributes.commands 命令列表,在此注册的命令后续才能被其他规则使用

contributes.menus.explorer/context 资源管理器的右键菜单

contributes.menus.view/title 自定义视口的右上角菜单

contributes.menus.view/item/context 自定义的右键菜单

技术要点

  1. 将bookmarkedDirectories列表数据存储在<id, obj>的Map中,方便后续查询元素
  2. 每次删除、移动操作时,需修改父级节点的children参数
  3. 在view/title定义的指令也会入参当前选中的节点参数,所以如需要不受影响,应新增一个无入参的命令
  4. vscode.window.showWarningMessage 打开确认弹窗
  5. vscode.window.showInputBox 打开输入框
  6. vscode.window.showErrorMessage 提示
  7. vscode.env.clipboard.writeText/readText 剪切板写/读
  8. vscode.workspace.openTextDocument 创建一个新的未保存的文件

总结

由于是第一次开发vscode插件,对于vscode的语法不熟悉,但是在github copilot协助下,开发、调研时间大大缩短,在前期调研时通过功能寻找相似插件,在开发中对遇到的错误,api都可以获得有效解答。

此外通过stable-diffusion,也可以快速地生成一个图标。

参考链接

code.visualstudio.com/api/extensi...

code.visualstudio.com/api/referen...

github.com/microsoft/v...

github.com/UrosVuj/Exp...

相关推荐
outstanding木槿11 分钟前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
小吕学编程1 小时前
Jackson使用详解
java·javascript·数据库·json
霸王蟹1 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹1 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
听吉米讲故事2 小时前
Slidev集成Chart.js:专业数据可视化演示文稿优化指南
javascript·信息可视化·数据分析
菥菥爱嘻嘻2 小时前
JS手写代码篇---手写 new 操作符
开发语言·javascript·原型模式
胡桃夹夹子3 小时前
【前端优化】vue2 webpack4项目升级webpack5,大大提升运行速度
前端·javascript·vue.js·webpack·性能优化
Stringzhua3 小时前
JavaScript【7】BOM模型
开发语言·前端·javascript
DT——3 小时前
ECMAScript 2018(ES2018):异步编程与正则表达式的深度进化
开发语言·javascript·ecmascript
双叶8363 小时前
(C语言)超市管理系统 (正式版)(指针)(数据结构)(清屏操作)(文件读写)(网页版预告)(html)(js)(json)
c语言·javascript·数据结构·html·json