实战:项目管理器插件
把 TreeView 的全部能力综合运用,实现一个项目管理器插件:在侧边栏管理常用项目列表,支持快速打开、编辑、删除、拖拽排序,数据持久化到配置文件。
目标
- TreeView 展示项目列表(名称 + 路径)
- 右键菜单:打开、编辑、删除项目
- 视图工具栏:添加项目、刷新
- 拖拽排序项目顺序
- 数据持久化(GlobalState 或文件)
第一步:注册视图与菜单
package.json:
json
{
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "myExtProjects",
"title": "项目管理器",
"icon": "media/icon.svg"
}
]
},
"views": {
"myExtProjects": [
{
"id": "projectsView",
"name": "项目列表"
}
]
},
"commands": [
{
"command": "myExt.addProject",
"title": "添加项目",
"category": "项目管理器"
},
{
"command": "myExt.openProject",
"title": "打开项目",
"category": "项目管理器"
},
{
"command": "myExt.editProject",
"title": "编辑项目",
"category": "项目管理器"
},
{
"command": "myExt.deleteProject",
"title": "删除项目",
"category": "项目管理器"
}
],
"menus": {
"view/title": [
{
"command": "myExt.addProject",
"when": "view == projectsView",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "myExt.openProject",
"when": "view == projectsView",
"group": "inline"
},
{
"command": "myExt.editProject",
"when": "view == projectsView",
"group": "1_modification"
},
{
"command": "myExt.deleteProject",
"when": "view == projectsView",
"group": "2_modification"
}
]
}
}
}第二步:数据模型与持久化
创建 src/models.ts:
typescript
import * as vscode from 'vscode';
export interface Project {
id: string;
name: string;
path: string;
}
// 数据存取:用 workspaceState 持久化
export class ProjectStore {
private static STORAGE_KEY = 'myExt.projects';
constructor(private readonly state: vscode.Memento) {}
load(): Project[] {
return this.state.get<Project[]>(ProjectStore.STORAGE_KEY, []);
}
save(projects: Project[]): void {
this.state.update(ProjectStore.STORAGE_KEY, projects);
}
}第三步:树节点与 Provider
创建 src/projectProvider.ts:
typescript
import * as vscode from 'vscode';
import { Project } from './models';
export class ProjectNode extends vscode.TreeItem {
constructor(public readonly project: Project) {
super(project.name, vscode.TreeItemCollapsibleState.None);
// 图标与路径描述
this.iconPath = new vscode.ThemeIcon('project');
this.description = project.path;
this.tooltip = new vscode.MarkdownString(
`**项目**: ${project.name}\n\n**路径**: \`${project.path}\``
);
this.contextValue = 'project';
// 双击打开项目
this.command = {
command: 'myExt.openProject',
title: '打开项目',
arguments: [this]
};
}
}
export class ProjectProvider implements vscode.TreeDataProvider<ProjectNode> {
private _onDidChangeTreeData =
new vscode.EventEmitter<ProjectNode | undefined | null>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
private projects: Project[];
constructor(private readonly store: ProjectStore) {
this.projects = store.load();
}
getTreeItem(element: ProjectNode): vscode.TreeItem {
return element;
}
getChildren(element?: ProjectNode): ProjectNode[] {
return element ? [] : this.projects.map((p) => new ProjectNode(p));
}
// 项目操作
add(project: Project) {
this.projects.push(project);
this.persist();
}
update(id: string, updates: Partial<Project>) {
const index = this.projects.findIndex((p) => p.id === id);
if (index > -1) {
this.projects[index] = { ...this.projects[index], ...updates };
this.persist();
}
}
remove(id: string) {
this.projects = this.projects.filter((p) => p.id !== id);
this.persist();
}
move(fromIndex: number, toIndex: number) {
const [moved] = this.projects.splice(fromIndex, 1);
this.projects.splice(toIndex, 0, moved);
this.persist();
}
private persist() {
this.store.save(this.projects);
this._onDidChangeTreeData.fire();
}
}第四步:拖拽排序
创建 src/dragDrop.ts:
typescript
import * as vscode from 'vscode';
import { ProjectNode } from './projectProvider';
import { ProjectProvider } from './projectProvider';
const MIME = 'application/vnd.code.tree.projectsView';
export class ProjectDragDrop
implements vscode.TreeDragAndDropController<ProjectNode> {
readonly dropMimeTypes = [MIME];
readonly dragMimeTypes = [MIME];
constructor(private readonly provider: ProjectProvider) {}
async handleDrag(
source: readonly ProjectNode[],
dataTransfer: vscode.DataTransfer
): Promise<void> {
// 记录被拖拽的项目 ID
dataTransfer.set(
MIME,
new vscode.DataTransferItem(
source.map((node) => node.project.id)
)
);
}
async handleDrop(
target: ProjectNode | undefined,
dataTransfer: vscode.DataTransfer
): Promise<void> {
const item = dataTransfer.get(MIME);
if (!item) {
return;
}
const ids: string[] = item.value;
const targetIndex = target
? this.provider.getChildren().findIndex(
(n) => n.project.id === target.project.id
)
: -1;
// 移动到目标之前或末尾
ids.forEach((id) => {
const fromIndex = this.provider
.getChildren()
.findIndex((n) => n.project.id === id);
if (fromIndex > -1) {
this.provider.move(fromIndex, targetIndex === -1 ? -1 : targetIndex);
}
});
}
}第五步:命令实现
创建 src/extension.ts:
typescript
import * as vscode from 'vscode';
import { ProjectStore } from './models';
import { ProjectProvider } from './projectProvider';
import { ProjectDragDrop } from './dragDrop';
export function activate(context: vscode.ExtensionContext) {
// 数据存储
const store = new ProjectStore(context.workspaceState);
// 数据提供器
const provider = new ProjectProvider(store);
// 注册视图 + 拖拽
vscode.window.registerTreeDataProvider('projectsView', provider);
const treeView = vscode.window.createTreeView('projectsView', {
treeDataProvider: provider,
showCollapseAll: false,
dragAndDrop: true,
canSelectMany: true
});
context.subscriptions.push(treeView);
// 添加项目
context.subscriptions.push(
vscode.commands.registerCommand('myExt.addProject', async () => {
// 选择文件夹
const uris = await vscode.window.showOpenDialog({
title: '选择项目文件夹',
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false
});
if (!uris || uris.length === 0) {
return;
}
const uri = uris[0];
const name = await vscode.window.showInputBox({
title: '项目名称',
prompt: '为项目起个名称',
placeHolder: uri.fsPath.split('\\').pop()
});
if (!name) {
return;
}
provider.add({
id: Date.now().toString(),
name,
path: uri.fsPath
});
vscode.window.showInformationMessage(`已添加项目 ${name}`);
})
);
// 打开项目
context.subscriptions.push(
vscode.commands.registerCommand(
'myExt.openProject',
(node: ProjectNode) => {
const folder = vscode.Uri.file(node.project.path);
vscode.commands.executeCommand('vscode.openFolder', folder);
}
)
);
// 编辑项目
context.subscriptions.push(
vscode.commands.registerCommand(
'myExt.editProject',
async (node: ProjectNode) => {
const newName = await vscode.window.showInputBox({
title: '修改项目名称',
value: node.project.name
});
if (newName && newName !== node.project.name) {
provider.update(node.project.id, { name: newName });
}
}
)
);
// 删除项目
context.subscriptions.push(
vscode.commands.registerCommand(
'myExt.deleteProject',
async (node: ProjectNode) => {
const confirm = await vscode.window.showWarningMessage(
`确认从列表删除「${node.project.name}」?`,
{ modal: true },
'删除'
);
if (confirm === '删除') {
provider.remove(node.project.id);
}
}
)
);
// 拖拽排序
treeView.onDidChangeSelection((event) => {
// 可在此处联动其他 UI
});
}运行与验证
按 F5 启动调试:
| 步骤 | 操作 | 预期 |
|---|---|---|
| 1 | 活动栏点击项目管理器图标 | 显示空项目列表 |
| 2 | 点击「添加项目」 | 选择文件夹并命名 |
| 3 | 项目显示在列表中 | 图标 + 名称 + 路径 |
| 4 | 右键项目 | 打开/编辑/删除菜单 |
| 5 | 拖拽项目排序 | 顺序持久化 |
| 6 | 重启 Extension Host | 项目列表保留 |
功能扩展
| 扩展方向 | 实现 |
|---|---|
| 最近打开 | 自动记录打开历史 |
| 搜索过滤 | 添加过滤输入框 |
| 分组管理 | 按标签/类型分组 |
| 导入导出 | 导出项目清单 JSON |
| 快捷命令 | 状态栏快速切换项目 |
常见问题
| 问题 | 处理 |
|---|---|
| 项目不保存 | 检查 workspaceState 读写 |
| 拖拽后顺序不变 | 确认 move 逻辑与刷新 |
| 右键菜单缺失 | 检查 when 中 view ID |
| 重复添加 | 添加前检查路径唯一 |
本实战完整覆盖了 TreeView 的项目:数据模型、持久化、树展示、右键菜单、拖拽排序、命令绑定,是「工具型插件」的经典模板。