♻️ 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,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')
|
||||
}
|
||||
Reference in new issue
Block a user