/** * 文件夹、以及代码阅读不认识的文件,用这个协议打开代码阅读预览: * 文档是空的,页面上没有代码块,只在顶部聊天。源码文件直接开它自己的预览。 * 「了解一块代码」找的功能的对话页也用这个协议,只是对的不是路径,是功能。 */ import type { WorkspaceFolder } from 'vscode' import type { QaLocation } from '../folderStore' import type { NamedFolder, RepoPlace } from './repoPath' import * as path from 'node:path' import { Uri, workspace } from 'vscode' import { locateQa } from '../folderStore' import { featureQaKey, pathQaKey, relativeQaKey } from '../qa/pathQa' import { folderLinkPlace, repoRootOf } from './repoPath' export const PATH_PREVIEW_SCHEME = 'code-reader-path' export type PathSubject = 'file' | 'folder' /** 预览顶部的聊天问的是什么:文件、文件夹,或「了解一块代码」找的一个功能 */ export type TopSubject = PathSubject | 'feature' /** 功能对话页的查询串:后面跟着功能的 id */ const FEATURE_QUERY = 'feature=' /** 「了解一块代码」一个功能的对话页:路径是问的那句话(标签页上显示它,`/` 换成全角的免得被当成目录),查询串带着功能的 id。 */ export function featurePageUri(id: string, question: string): Uri { return Uri.from({ scheme: PATH_PREVIEW_SCHEME, path: `/${question.replaceAll('/', '/')}`, query: `${FEATURE_QUERY}${id}` }) } /** 一个功能的问答按这个 Uri 记、按它对事件:对话页标签上叫什么都是它。 */ export function featureSubjectUri(id: string): Uri { return Uri.from({ scheme: PATH_PREVIEW_SCHEME, path: '/', query: `${FEATURE_QUERY}${id}` }) } /** 功能对话页(或 featureSubjectUri)对的功能 id;别的 Uri 没有。 */ export function featureIdOf(uri: Uri): string | undefined { return uri.scheme === PATH_PREVIEW_SCHEME && uri.query.startsWith(FEATURE_QUERY) ? uri.query.slice(FEATURE_QUERY.length) : undefined } /** 「了解一块代码」找代码用的工作区文件夹:眼下打开的全部,claude 在第一个里面跑,别的加进来读。 */ export function featureFolders(): NamedFolder[] { return (workspace.workspaceFolders ?? []).map(folder => ({ name: folder.name, path: folder.uri.fsPath })) } /** 一页上的仓库链接从哪儿算:`place` 把一条链接的路径换成磁盘上的位置,算不出来的没有;算出来的位置都在 `roots` 这几个根下面。 */ export interface LinkBase { place: (rel: string) => RepoPlace | undefined roots: string[] } /** * 预览这一页上仓库链接的路径从哪儿算:功能对话页从这个功能找代码用的工作区文件夹算(见 [featureFolders]、[folderLinkPlace]), * 别的页从文档所在的 git 仓库根算,不在 git 仓库里时一条也算不出来。 */ export async function linkBaseOf(uri: Uri): Promise { if (featureIdOf(uri) !== undefined) { const folders = featureFolders() return { place: rel => folderLinkPlace(folders, rel), roots: folders.map(folder => folder.path) } } const root = await repoRootOf(uri.fsPath) return root === undefined ? { place: () => undefined, roots: [] } : { place: rel => ({ root, rel }), roots: [root] } } export function pathPreviewUri(target: Uri, kind: PathSubject): Uri { return target.with({ scheme: PATH_PREVIEW_SCHEME, query: kind, fragment: '' }) } /** 预览文档是路径页时,还原成真实文件或文件夹;普通源码预览不是。 */ export function pathPreviewOf(uri: Uri): { uri: Uri, kind: PathSubject } | undefined { if (uri.scheme !== PATH_PREVIEW_SCHEME) return undefined if (uri.query !== 'file' && uri.query !== 'folder') return undefined return { uri: uri.with({ scheme: 'file', query: '', fragment: '' }), kind: uri.query } } /** 预览这一页顶部问的是什么:功能对话页是那个功能(featureSubjectUri),路径页还原成真实文件或文件夹,源码预览就是文档自己。 */ export function topSubjectOf(uri: Uri): { uri: Uri, kind: TopSubject } { const feature = featureIdOf(uri) if (feature !== undefined) return { uri: featureSubjectUri(feature), kind: 'feature' } return pathPreviewOf(uri) ?? { uri, kind: 'file' } } /** * 这条路径的顶部问答记在哪、发给模型时怎么写它:同 [locateQa](工作区外的库文件也能问), * 另外认工作区根本身,键和发给模型的路径都是空字符串(locate 不认根,顶部问答要认)。 */ export function pathQaLocation(uri: Uri): QaLocation | undefined { const folder = workspace.getWorkspaceFolder(uri) if (folder && relativeQaKey(path.posix.normalize(folder.uri.path), path.posix.normalize(uri.path)) === '') return { folder, key: '', promptPath: '' } return locateQa(uri) } /** * 顶部问答记在哪:在哪个工作区文件夹(claude 在它里面跑,带的图片存在它的 `.spx/code-qa/`)、记录表里的键(见 pathQa.ts)。 * 文件、文件夹按「工作区文件夹 + 键」(见 [pathQaLocation],工作区外的库文件记在第一个工作区文件夹);功能对话页按功能的 id,记在第一个工作区文件夹。 * 没打开文件夹时没有。 */ export function topQaLocation(uri: Uri): { folder: WorkspaceFolder, key: string } | undefined { const feature = featureIdOf(uri) if (feature !== undefined) { const folder = workspace.workspaceFolders?.[0] return folder && { folder, key: featureQaKey(feature) } } const at = pathQaLocation(uri) return at && { folder: at.folder, key: pathQaKey(at.folder.uri.toString(), at.key) } }