94 lines
3.5 KiB
TypeScript
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)
|
|
}
|
|
}
|