Files
superwork/vscode/src/codeReader/explorer/fileTree.ts
T

94 lines
3.5 KiB
TypeScript

import type { Event, TreeDataProvider, WorkspaceFolder } from 'vscode'
import type { ExcludeSetting } from './fileEntries'
import * as path from 'node:path'
import { EventEmitter, FileType, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, workspace } from 'vscode'
import { createExcludeMatcher, sortEntries } from './fileEntries'
export interface FileNode {
uri: Uri
folder: WorkspaceFolder
isDirectory: boolean
isRoot: boolean
}
/** 批量改文件(git checkout、构建输出)时合并成一次刷新。 */
const REFRESH_DELAY_MS = 300
/**
* 侧栏的工作区文件树:每个工作区文件夹一个根节点,只看和打开。
* 节点用 resourceUri,图标、git 改动颜色都由 VS Code 按文件图标主题和 SCM 装饰自动加上。
*/
export class FileTreeProvider implements TreeDataProvider<FileNode> {
private readonly changed = new EventEmitter<void>()
readonly onDidChangeTreeData: Event<void> = this.changed.event
private readonly disposables: { dispose: () => unknown }[]
private timer: NodeJS.Timeout | undefined
/** @param openCommand 点文件时执行的命令,参数是文件 Uri */
constructor(private readonly openCommand: string) {
// 只关心增删:文件内容变化不影响树的结构
const watcher = workspace.createFileSystemWatcher('**/*', false, true, false)
this.disposables = [
this.changed,
watcher,
watcher.onDidCreate(() => this.scheduleRefresh()),
watcher.onDidDelete(() => this.scheduleRefresh()),
workspace.onDidChangeWorkspaceFolders(() => this.refresh()),
workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('files.exclude'))
this.refresh()
}),
]
}
refresh(): void {
clearTimeout(this.timer)
this.timer = undefined
this.changed.fire()
}
getTreeItem(node: FileNode): TreeItem {
if (!node.isDirectory) {
const item = new TreeItem(node.uri, TreeItemCollapsibleState.None)
item.iconPath = ThemeIcon.File
item.command = { command: this.openCommand, title: '打开', arguments: [node.uri] }
return item
}
const item = new TreeItem(node.uri, node.isRoot ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed)
item.iconPath = ThemeIcon.Folder
if (node.isRoot)
item.label = node.folder.name
return item
}
async getChildren(node?: FileNode): Promise<FileNode[]> {
if (!node)
return (workspace.workspaceFolders ?? []).map(folder => ({ uri: folder.uri, folder, isDirectory: true, isRoot: true }))
let entries: [string, FileType][]
try {
entries = await workspace.fs.readDirectory(node.uri)
}
catch {
return [] // 目录刚被删掉或没有权限
}
const excluded = createExcludeMatcher(workspace.getConfiguration('files', node.folder).get<ExcludeSetting>('exclude'))
const names = new Set(entries.map(([name]) => name))
const dir = path.posix.relative(node.folder.uri.path, node.uri.path)
return sortEntries(entries.map(([name, type]) => ({ name, isDirectory: (type & FileType.Directory) !== 0 })))
.filter(entry => !excluded(dir ? `${dir}/${entry.name}` : entry.name, names))
.map(entry => ({ uri: Uri.joinPath(node.uri, entry.name), folder: node.folder, isDirectory: entry.isDirectory, isRoot: false }))
}
dispose(): void {
clearTimeout(this.timer)
for (const disposable of this.disposables)
disposable.dispose()
}
private scheduleRefresh(): void {
clearTimeout(this.timer)
this.timer = setTimeout(() => this.refresh(), REFRESH_DELAY_MS)
}
}