✨ feat(vscode): 代码阅读入口、文件树与预览编辑器
This commit is contained in:
1 parent
4d34b8c715
commit
2b823a32ca
24 files changed
+768
-72
No files matched your search
@@ -0,0 +1,93 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user