主栏标题栏的放大镜问要了解哪一块,占一个栏位建一栏(和收藏共用 30 个栏位,视图改名 slot1..30,按 slotKindN 分收藏 / 功能挂标题栏按钮),同时开对话页让 Claude 只读地在全部工作区文件夹里找,按 --json-schema 交回环节 → 文件 + 符号 + 一句话。 对话复用预览顶部的聊天(CodeQa.askTop:会话续接、进行中的一轮、记录存 workspaceState);结构不合格算这一轮没答成、留着能再画出来。 条目每次画、点时按眼下的代码找:有语言插件的解析成代码块按「类名.方法名」找,没有的问 VS Code 文档符号;找不到画成灰的。
112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
/**
|
|
* 预览顶部的问答:文件、文件夹按「工作区文件夹 + 相对路径」,「了解一块代码」的功能对话页按功能的 id,记在这个窗口的 workspaceState 里。
|
|
* 不写进按块存的 `.spx/code-qa.json`。
|
|
*/
|
|
|
|
import type { QaEntry } from './qaMap'
|
|
import * as path from 'node:path'
|
|
import { normalizeKey } from '../pathMap'
|
|
|
|
/** workspaceState 里这份表的键 */
|
|
export const PATH_QA_STATE_KEY = 'codeReader.pathQa'
|
|
|
|
/**
|
|
* 顶部这一轮的记号。块的记号是「种类 名字」,不会是这个,
|
|
* 所以页面上按记号取问答时,顶部的不会挂到某一块下面。
|
|
*/
|
|
export const PATH_ASK_ANCHOR = '\0path'
|
|
|
|
/** 工作区文件夹 Uri → 那条路径上的问答(按问的先后) */
|
|
export type PathQaMap = Record<string, QaEntry[]>
|
|
|
|
/** 工作区根的相对路径是空字符串,跟任何一个文件或子目录都不同。 */
|
|
export function relativeQaKey(rootPath: string, targetPath: string): string | undefined {
|
|
const relative = path.posix.relative(rootPath, targetPath)
|
|
if (relative === '')
|
|
return ''
|
|
return normalizeKey(relative)
|
|
}
|
|
|
|
/** 同一工作区文件夹、同一相对路径才是同一条记录。 */
|
|
export function pathQaKey(folderUri: string, relativePath: string): string {
|
|
return `${folderUri}\n${relativePath}`
|
|
}
|
|
|
|
/** 「了解一块代码」一个功能的对话记录的键。工作区文件夹的 Uri 带协议名(`file://`),和它撞不上。 */
|
|
export function featureQaKey(id: string): string {
|
|
return `feature\n${id}`
|
|
}
|
|
|
|
/** 给人看的相对路径;工作区根写成 `.`。 */
|
|
export function displayRelativePath(relativePath: string): string {
|
|
return relativePath === '' ? '.' : relativePath
|
|
}
|
|
|
|
export function pathQaEntries(map: PathQaMap, key: string): QaEntry[] {
|
|
return map[key] ?? []
|
|
}
|
|
|
|
/** 新的一次问答接在这条路径已有的后面,别的路径不动。 */
|
|
export function withPathQa(map: PathQaMap, key: string, entry: QaEntry): PathQaMap {
|
|
return { ...map, [key]: [...pathQaEntries(map, key), entry] }
|
|
}
|
|
|
|
/** 删掉这一次;这条路径问光了就把键拿掉。没有这一次时原样返回。 */
|
|
export function withoutPathQa(map: PathQaMap, key: string, entryId: string): PathQaMap {
|
|
const entries = pathQaEntries(map, key)
|
|
const kept = entries.filter(entry => entry.id !== entryId)
|
|
if (kept.length === entries.length)
|
|
return map
|
|
if (kept.length === 0) {
|
|
const next = { ...map }
|
|
delete next[key]
|
|
return next
|
|
}
|
|
return { ...map, [key]: kept }
|
|
}
|
|
|
|
/** 去掉这个键下的全部问答;没有这个键时原样返回。 */
|
|
export function withoutPathQaKey(map: PathQaMap, key: string): PathQaMap {
|
|
if (!(key in map))
|
|
return map
|
|
const next = { ...map }
|
|
delete next[key]
|
|
return next
|
|
}
|
|
|
|
/** 读 workspaceState 里存的表;认不出的丢掉,不让一条坏数据把整份弄丢。 */
|
|
export function parsePathQa(value: unknown): PathQaMap {
|
|
if (!isRecord(value))
|
|
return {}
|
|
const map: PathQaMap = {}
|
|
for (const [key, entries] of Object.entries(value)) {
|
|
if (!Array.isArray(entries))
|
|
continue
|
|
const parsed = entries.flatMap((entry) => {
|
|
const one = parseEntry(entry)
|
|
return one ? [one] : []
|
|
})
|
|
if (parsed.length > 0)
|
|
map[key] = parsed
|
|
}
|
|
return map
|
|
}
|
|
|
|
function parseEntry(value: unknown): QaEntry | undefined {
|
|
if (!isRecord(value))
|
|
return undefined
|
|
const { id, question, answer, createdAt, sessionId, profile, images } = value
|
|
if (typeof id !== 'string' || typeof question !== 'string' || typeof answer !== 'string' || typeof createdAt !== 'string' || typeof sessionId !== 'string')
|
|
return undefined
|
|
const entry: QaEntry = { id, question, answer, createdAt, sessionId }
|
|
if (typeof profile === 'string' && profile !== '')
|
|
entry.profile = profile
|
|
if (Array.isArray(images) && images.every(name => typeof name === 'string'))
|
|
entry.images = images
|
|
return entry
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
}
|