TreeView 右键菜单与拖拽
树节点上的右键菜单是核心交互入口:打开、编辑、删除、复制……而拖拽支持让节点可以自由移动重排。本章把树视图打造成完整的「操作台」。
注册右键菜单
contributes.menus 的 view/item/context 位置:
json
{
"contributes": {
"menus": {
"view/item/context": [
{
"command": "myExt.openNode",
"when": "view == myExt.tree",
"group": "inline"
},
{
"command": "myExt.deleteNode",
"when": "view == myExt.tree && viewItem == file",
"group": "1_modification"
},
{
"command": "myExt.copyNode",
"when": "view == myExt.tree",
"group": "2_copypaste"
}
]
}
}
}菜单位置
| 位置 | 说明 |
|---|---|
view/item/context | 树节点右键菜单 |
view/title | 视图标题栏(工具栏按钮) |
view/item/context 的 inline | 行内图标按钮 |
viewItem 条件筛选
viewItem 条件匹配节点的 contextValue:
typescript
// TreeItem 中设置
node.contextValue = 'file';
node.contextValue = 'directory';
node.contextValue = 'file:modified'; // 组合值json
{
"when": "viewItem == file",
"when": "viewItem =~ /^file/"
}| 场景 | when 条件 |
|---|---|
| 所有节点 | view == myExt.tree |
| 仅文件 | viewItem == file |
| 仅目录 | viewItem == directory |
| 文件且修改 | viewItem =~ /^file.*modified/ |
命令接收节点参数
右键菜单命令自动接收当前节点作为参数:
typescript
context.subscriptions.push(
vscode.commands.registerCommand('myExt.deleteNode', (node: FileNode) => {
// node 即右键的节点
vscode.window.showInformationMessage(`删除 ${node.label}`);
// 执行删除逻辑
vscode.workspace.fs.delete(node.uri, { recursive: true });
})
);多选节点
启用多选后,命令接收节点数组:
typescript
vscode.commands.registerCommand('myExt.deleteMany', (nodes: FileNode[]) => {
nodes.forEach((node) => {
console.log(`删除 ${node.label}`);
});
});视图标题栏菜单
view/title 位置注册工具栏按钮:
json
{
"contributes": {
"menus": {
"view/title": [
{
"command": "myExt.refreshTree",
"when": "view == myExt.tree",
"group": "navigation"
}
]
}
}
}typescript
vscode.commands.registerCommand('myExt.refreshTree', () => {
provider.refresh();
vscode.window.showInformationMessage('已刷新');
});拖拽 TreeDragAndDrop
启用拖拽
typescript
const treeView = vscode.window.createTreeView('myExt.tree', {
treeDataProvider: provider,
dragAndDrop: true
});完整拖拽控制器
typescript
import * as vscode from 'vscode';
const MIME_TYPE = 'application/vnd.code.tree.myExt.tree';
class DragDropController implements vscode.TreeDragAndDropController<FileNode> {
readonly dropMimeTypes = [MIME_TYPE];
readonly dragMimeTypes = [MIME_TYPE];
// 拖拽开始:保存源数据
async handleDrag(
source: readonly FileNode[],
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken
): Promise<void> {
// 序列化节点 ID 列表
dataTransfer.set(
MIME_TYPE,
new vscode.DataTransferItem(
source.map((node) => node.uri.fsPath)
)
);
}
// 放下:执行移动
async handleDrop(
target: FileNode | undefined,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken
): Promise<void> {
const item = dataTransfer.get(MIME_TYPE);
if (!item) {
return;
}
const sourcePaths: string[] = item.value;
if (!target) {
vscode.window.showErrorMessage('拖拽目标无效');
return;
}
// 移动文件到目标目录
for (const sourcePath of sourcePaths) {
const sourceUri = vscode.Uri.file(sourcePath);
const targetUri = vscode.Uri.joinPath(
target.uri,
sourceUri.fsPath.split('\\').pop() ?? ''
);
await vscode.workspace.fs.rename(sourceUri, targetUri);
}
// 刷新树
provider.refresh();
vscode.window.showInformationMessage('移动完成');
}
}拖拽事件时机
| 事件 | 触发 |
|---|---|
handleDrag | 开始拖拽时 |
handleDrop | 放到目标上时 |
dropMimeTypes | 声明可接受的拖拽数据类型 |
dragMimeTypes | 声明可拖出的数据类型 |
文件系统拖入树
从系统文件管理器拖文件进树:
typescript
class Controller implements vscode.TreeDragAndDropController<FileNode> {
// 接受系统文件拖拽
readonly dropMimeTypes = [
'application/vnd.code.tree.myExt.tree',
'application/x-files' // 系统文件
];
readonly dragMimeTypes = [
'application/vnd.code.tree.myExt.tree'
];
async handleDrop(
target: FileNode | undefined,
dataTransfer: vscode.DataTransfer
): Promise<void> {
// 处理系统文件
const files = dataTransfer.get('application/x-files');
if (files) {
const uris: vscode.Uri[] = files.value;
console.log('拖入文件', uris);
}
}
}TreeView 与文件系统联动
typescript
import * as vscode from 'vscode';
class LinkedTreeProvider implements vscode.TreeDataProvider<FileNode> {
private _onDidChangeTreeData =
new vscode.EventEmitter<FileNode | undefined | null>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
private watcher: vscode.FileSystemWatcher;
constructor() {
// 监听工作区文件变化
this.watcher = vscode.workspace.createFileSystemWatcher('**/*');
this.watcher.onDidCreate(() => this.refresh());
this.watcher.onDidDelete(() => this.refresh());
this.watcher.onDidChange(() => this.refresh());
}
refresh() {
this._onDidChangeTreeData.fire();
}
}联动场景
| 场景 | 联动 |
|---|---|
| 文件创建/删除 | 自动刷新树 |
| 文件重命名 | 树节点更新 |
| 外部修改 | 提示刷新 |
| 删除树节点 | 同步删除磁盘文件 |
完整示例:文件管理器右键菜单
package.json:
json
{
"contributes": {
"menus": {
"view/item/context": [
{
"command": "myExt.openFile",
"when": "view == myExt.files && viewItem == file",
"group": "inline"
},
{
"command": "myExt.renameFile",
"when": "view == myExt.files && viewItem == file",
"group": "1_modification"
},
{
"command": "myExt.deleteFile",
"when": "view == myExt.files && viewItem == file",
"group": "2_modification"
},
{
"command": "myExt.newFolder",
"when": "view == myExt.files && viewItem == directory",
"group": "navigation"
}
],
"view/title": [
{
"command": "myExt.refreshFiles",
"when": "view == myExt.files",
"group": "navigation"
}
]
}
}
}extension.ts:
typescript
export function activate(context: vscode.ExtensionContext) {
// 打开文件
context.subscriptions.push(
vscode.commands.registerCommand('myExt.openFile', (node: FileNode) => {
vscode.commands.executeCommand('vscode.open', node.uri);
})
);
// 删除文件
context.subscriptions.push(
vscode.commands.registerCommand('myExt.deleteFile', async (node: FileNode) => {
const confirm = await vscode.window.showWarningMessage(
`确认删除 ${node.label}?`,
{ modal: true },
'删除'
);
if (confirm === '删除') {
await vscode.workspace.fs.delete(node.uri, { recursive: true });
provider.refresh();
}
})
);
// 刷新
context.subscriptions.push(
vscode.commands.registerCommand('myExt.refreshFiles', () => {
provider.refresh();
})
);
}常见问题
| 问题 | 处理 |
|---|---|
| 右键菜单不显示 | 检查 when 中 view 与 viewItem 匹配 |
| 命令收不到节点 | 确认命令签名接收 TreeItem 参数 |
| 拖拽无效 | 检查 MIME 类型与视图 ID |
| 菜单分组乱 | 用 group 关键字排序 |
右键菜单 + 拖拽让树视图成为完整的操作界面,配合文件系统联动实现真实的项目管理体验。