♻️ refactor(vscode): 文件注释的存法抽成按路径记的通用表,给问答共用
This commit is contained in:
1 parent
2282646a43
commit
badc722640
8 files changed
+378
-320
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import type { FileDeleteEvent, FileRenameEvent, WorkspaceFolder } from 'vscode'
|
||||
import type { PathMap, PathMapFile } from './pathMap'
|
||||
import { Disposable, EventEmitter, RelativePattern, Uri, window, workspace } from 'vscode'
|
||||
import { changedKeys, dropEntries, entriesUnder, moveEntries, placeEntries, readPathMap, toPathKey, writePathMap } from './pathMap'
|
||||
|
||||
/** 一个文件 / 目录在哪个工作区文件夹里、键是什么。 */
|
||||
export interface PathLocation {
|
||||
folder: WorkspaceFolder
|
||||
key: string
|
||||
}
|
||||
|
||||
/** uri 所在的工作区文件夹和它在那里的键;不在工作区文件夹里、或就是文件夹本身时没有。 */
|
||||
export function locate(uri: Uri): PathLocation | undefined {
|
||||
const folder = workspace.getWorkspaceFolder(uri)
|
||||
const key = folder && toPathKey(folder.uri.path, uri.path)
|
||||
return folder && key !== undefined ? { folder, key } : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个工作区文件夹一份的按路径记的表(见 pathMap):内存里留一份给界面同步取用。
|
||||
* 在 VS Code 里改名、挪位置,条目跟着走;删掉就一起删。外部改了文件(git pull、手改)按文件重读。
|
||||
*/
|
||||
export class FolderStore<T> implements Disposable {
|
||||
private readonly changed = new EventEmitter<Uri[]>()
|
||||
/** 条目变了的那些文件 / 目录 */
|
||||
readonly onDidChange = this.changed.event
|
||||
/** 工作区文件夹 Uri → 它的表 */
|
||||
private readonly maps = new Map<string, PathMap<T>>()
|
||||
/** 工作区文件夹 Uri → 盯它那份文件的监听 */
|
||||
private readonly watchers = new Map<string, Disposable>()
|
||||
private readonly disposables: Disposable[]
|
||||
/** 读写排成一队:自己写到一半时文件监听触发的重读排在写完之后,读不到半截文件;几次改动也不会互相覆盖。 */
|
||||
private queue: Promise<void> = Promise.resolve()
|
||||
|
||||
constructor(private readonly file: PathMapFile<T>) {
|
||||
this.disposables = [
|
||||
this.changed,
|
||||
workspace.onDidChangeWorkspaceFolders((e) => {
|
||||
e.removed.forEach(folder => this.unwatch(folder))
|
||||
e.added.forEach(folder => this.watch(folder))
|
||||
}),
|
||||
workspace.onDidRenameFiles(e => this.moved(e)),
|
||||
workspace.onDidDeleteFiles(e => this.deleted(e)),
|
||||
]
|
||||
for (const folder of workspace.workspaceFolders ?? [])
|
||||
this.watch(folder)
|
||||
}
|
||||
|
||||
get(folder: WorkspaceFolder, key: string): T | undefined {
|
||||
return this.maps.get(folder.uri.toString())?.[key]
|
||||
}
|
||||
|
||||
/** 读盘上最新的一份改好再写回;没变就不写,所以第一条存下之前不会生成文件。 */
|
||||
update(folder: WorkspaceFolder, change: (map: PathMap<T>) => PathMap<T>): Promise<void> {
|
||||
return this.enqueue(async () => {
|
||||
const before = await readPathMap(folder.uri.fsPath, this.file)
|
||||
const after = change(before)
|
||||
if (changedKeys(before, after).length === 0)
|
||||
return
|
||||
await writePathMap(folder.uri.fsPath, this.file, after)
|
||||
this.apply(folder, after)
|
||||
})
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const disposable of [...this.disposables, ...this.watchers.values()])
|
||||
disposable.dispose()
|
||||
this.watchers.clear()
|
||||
}
|
||||
|
||||
private watch(folder: WorkspaceFolder): void {
|
||||
const watcher = workspace.createFileSystemWatcher(new RelativePattern(folder, this.file.path))
|
||||
const reload = (): void => {
|
||||
void this.enqueue(async () => this.apply(folder, await readPathMap(folder.uri.fsPath, this.file)))
|
||||
}
|
||||
this.watchers.set(folder.uri.toString(), Disposable.from(
|
||||
watcher,
|
||||
watcher.onDidChange(reload),
|
||||
watcher.onDidCreate(reload),
|
||||
watcher.onDidDelete(reload),
|
||||
))
|
||||
reload()
|
||||
}
|
||||
|
||||
private unwatch(folder: WorkspaceFolder): void {
|
||||
const id = folder.uri.toString()
|
||||
this.watchers.get(id)?.dispose()
|
||||
this.watchers.delete(id)
|
||||
this.maps.delete(id)
|
||||
}
|
||||
|
||||
private moved(e: FileRenameEvent): void {
|
||||
for (const { oldUri, newUri } of e.files) {
|
||||
const from = locate(oldUri)
|
||||
if (!from)
|
||||
continue
|
||||
const to = locate(newUri)
|
||||
if (to && to.folder.uri.toString() === from.folder.uri.toString()) {
|
||||
void this.update(from.folder, map => moveEntries(map, from.key, to.key))
|
||||
continue
|
||||
}
|
||||
// 挪到了另一个工作区文件夹:两份文件各改一次,队列保证先取出再放进去;挪出工作区就不要了
|
||||
let moving: PathMap<T> = {}
|
||||
void this.update(from.folder, (map) => {
|
||||
moving = entriesUnder(map, from.key)
|
||||
return dropEntries(map, from.key)
|
||||
})
|
||||
if (to)
|
||||
void this.update(to.folder, map => placeEntries(map, to.key, moving))
|
||||
}
|
||||
}
|
||||
|
||||
private deleted(e: FileDeleteEvent): void {
|
||||
for (const uri of e.files) {
|
||||
const at = locate(uri)
|
||||
if (at)
|
||||
void this.update(at.folder, map => dropEntries(map, at.key))
|
||||
}
|
||||
}
|
||||
|
||||
/** 换上新的一份,报出条目变了的那些文件 / 目录。 */
|
||||
private apply(folder: WorkspaceFolder, map: PathMap<T>): void {
|
||||
const id = folder.uri.toString()
|
||||
if (!this.watchers.has(id))
|
||||
return // 排队期间这个文件夹已经移出工作区
|
||||
const keys = changedKeys(this.maps.get(id) ?? {}, map)
|
||||
this.maps.set(id, map)
|
||||
if (keys.length > 0)
|
||||
this.changed.fire(keys.map(key => Uri.joinPath(folder.uri, ...key.split('/'))))
|
||||
}
|
||||
|
||||
private enqueue(task: () => Promise<void>): Promise<void> {
|
||||
const run = this.queue.then(task).catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
void window.showErrorMessage(`${this.file.label}读写 ${this.file.path} 失败:${message}`)
|
||||
})
|
||||
this.queue = run
|
||||
return run
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user