♻️ refactor(vscode): 文件注释的存法抽成按路径记的通用表,给问答共用

This commit is contained in:
cruldra committed 2026-09-26 21:12:34 +08:00
1 parent 2282646a43
commit badc722640
8 files changed
+378 -320

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
/**
* 按文件 / 目录记东西的一份表(文件注释、代码问答):每个工作区文件夹一份,落盘到 `.spx` 里的一个 JSON 文件,
* 形状是 `{ "相对路径": 值 }`。键相对这个工作区文件夹、`/` 分隔;改名、挪位置、删除时整棵子树跟着走。
*/
import { promises as fsp } from 'node:fs'
import * as path from 'node:path'
export type PathMap<T> = Record<string, T>
/** 表落在哪、叫什么、每条的值怎么认。 */
export interface PathMapFile<T> {
/** 相对工作区文件夹的位置(`/` 分隔),文件监听也用它 */
path: string
/** 读写出错时提示里怎么称呼它 */
label: string
/** 认一条的值:不合法的返回 undefined,这条就跳过;合法的可以顺手规范成统一写法 */
parse: (value: unknown) => T | undefined
}
/** 规范成表的键:去掉 `./`、重复和结尾的 `/`;工作区文件夹本身、文件夹外面的路径没有键。 */
export function normalizeKey(relativePath: string): string | undefined {
const key = path.posix.normalize(relativePath).replace(/\/+$/, '')
return key === '' || key === '.' || key === '..' || key.startsWith('../') || key.startsWith('/') ? undefined : key
}
/** @param rootPath 工作区文件夹的 `Uri.path`;@param targetPath 目录 / 文件的 `Uri.path` */
export function toPathKey(rootPath: string, targetPath: string): string | undefined {
return normalizeKey(path.posix.relative(rootPath, targetPath))
}
function isUnder(candidate: string, key: string): boolean {
return candidate === key || candidate.startsWith(`${key}/`)
}
/** key 自己和它下面各层的条目,键换成相对 key 的路径(key 自己是 `''`)。 */
export function entriesUnder<T>(map: PathMap<T>, key: string): PathMap<T> {
return Object.fromEntries(Object.entries(map)
.filter(([candidate]) => isUnder(candidate, key))
.map(([candidate, value]) => [candidate.slice(key.length + 1), value]))
}
/** 删掉 key 自己和它下面各层的条目(目录被删时连带里面的)。 */
export function dropEntries<T>(map: PathMap<T>, key: string): PathMap<T> {
return Object.fromEntries(Object.entries(map).filter(([candidate]) => !isUnder(candidate, key)))
}
/** 把 entriesUnder 取出的条目放到 key 下面。 */
export function placeEntries<T>(map: PathMap<T>, key: string, under: PathMap<T>): PathMap<T> {
const placed = Object.entries(under).map(([rest, value]) => [rest === '' ? key : `${key}/${rest}`, value])
return { ...map, ...Object.fromEntries(placed) }
}
/** 改名 / 挪位置:条目跟着走,目录连带里面各层的条目一起走。 */
export function moveEntries<T>(map: PathMap<T>, from: string, to: string): PathMap<T> {
return placeEntries(dropEntries(map, from), to, entriesUnder(map, from))
}
/** 两份表里有增、删、改的键;值按 JSON 写法比。 */
export function changedKeys<T>(before: PathMap<T>, after: PathMap<T>): string[] {
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
return [...keys].filter(key => JSON.stringify(before[key]) !== JSON.stringify(after[key]))
}
function fileIn(workspaceRoot: string, file: PathMapFile<unknown>): string {
return path.join(workspaceRoot, ...file.path.split('/'))
}
/** 读工作区文件夹里的这份表。文件缺失 / 解析失败 / 不是对象都返回 `{}`;键不合法、值认不出的条目跳过。 */
export async function readPathMap<T>(workspaceRoot: string, file: PathMapFile<T>): Promise<PathMap<T>> {
let raw: string
try {
raw = await fsp.readFile(fileIn(workspaceRoot, file), 'utf8')
}
catch {
return {}
}
try {
const parsed = JSON.parse(raw) as unknown
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
return {}
const map: PathMap<T> = {}
for (const [rawKey, rawValue] of Object.entries(parsed as Record<string, unknown>)) {
const key = normalizeKey(rawKey)
const value = file.parse(rawValue)
if (key !== undefined && value !== undefined)
map[key] = value
}
return map
}
catch {
return {}
}
}
/** 写工作区文件夹里的这份表(空表也写,文件留着):父目录按需创建,键排好序、两格缩进、末尾换行,git diff 稳定。 */
export async function writePathMap<T>(workspaceRoot: string, file: PathMapFile<T>, map: PathMap<T>): Promise<void> {
const target = fileIn(workspaceRoot, file)
await fsp.mkdir(path.dirname(target), { recursive: true })
const sorted = Object.fromEntries(Object.entries(map).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
await fsp.writeFile(target, `${JSON.stringify(sorted, null, 2)}\n`, 'utf8')
}