介绍
目前工作上开发的项目很大,找文件很辛苦,所以想找一个文件书签的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.openTextDocument
与vscode.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 自定义的右键菜单
技术要点
- 将bookmarkedDirectories列表数据存储在
的Map中,方便后续查询元素 - 每次删除、移动操作时,需修改父级节点的children参数
- 在view/title定义的指令也会入参当前选中的节点参数,所以如需要不受影响,应新增一个无入参的命令
- vscode.window.showWarningMessage 打开确认弹窗
- vscode.window.showInputBox 打开输入框
- vscode.window.showErrorMessage 提示
- vscode.env.clipboard.writeText/readText 剪切板写/读
- vscode.workspace.openTextDocument 创建一个新的未保存的文件
总结
由于是第一次开发vscode插件,对于vscode的语法不熟悉,但是在github copilot协助下,开发、调研时间大大缩短,在前期调研时通过功能寻找相似插件,在开发中对遇到的错误,api都可以获得有效解答。
此外通过stable-diffusion,也可以快速地生成一个图标。
参考链接
code.visualstudio.com/api/extensi…
评论记录:
回复评论: