✨ feat(vscode): 代码阅读「了解一块代码」改成按路径列调用树:结果是几条路径(用户的一种操作一条),每条从入口一路调到后端服务、数据表,跨进程的写明怎么连过去(via),结构用扁平的 nodes 加 parent 连成树、不用递归;侧栏路径下面一层层展开节点,回答里用嵌套列表画树;回答和找的过程里的代码位置写成仓库链接,功能对话页上的链接按找代码用的工作区文件夹检查、打开、悬停;旧格式的结果当没有,栏上方提示打开对话重新找;删功能前先弹模态框确认,说清连带删掉调用树和问答记录,点「删除」才删

This commit is contained in:
cruldra committed 2026-10-14 21:28:40 +11:00
1 parent cc445e7123
commit c6de46ad37
17 files changed
+978 -253

No files matched your search

@@ -45,7 +45,11 @@ vi.mock('vscode', () => ({
vi.mock('../../logging/logger', () => ({ logger: { add: vi.fn() } }))
vi.mock('../../webview/html', () => ({ webviewHtml: () => '', webviewUiRoot: () => ({}) }))
vi.mock('../folderStore', () => ({ locate: () => ({ folder: { uri: { fsPath: '/workspace' } }, key: 'web/a.ts' }) }))
vi.mock('./pathPreview', () => ({ PATH_PREVIEW_SCHEME: 'code-reader-path', topSubjectOf: (uri: unknown) => ({ uri, kind: 'file' }) }))
/** 顶部问答按文件算;仓库链接从哪儿算照真的来(功能对话页按工作区文件夹,别的按仓库根) */
vi.mock('./pathPreview', async importOriginal => ({
...await importOriginal<typeof import('./pathPreview')>(),
topSubjectOf: (uri: unknown) => ({ uri, kind: 'file' }),
}))
vi.mock('./occurrences', () => ({ occurrencesOf: vi.fn() }))
/** 排视图数据不真的解析:一份没有块的文档 */
vi.mock('./documentMessage', async importOriginal => ({
@@ -66,11 +70,15 @@ vi.mock('./definitions', async importOriginal => ({
...await importOriginal<typeof import('./definitions')>(),
languageDefinitions: vi.fn(async () => []),
}))
/** 仓库在 /workspace,里面只有这几个文件 */
/** 仓库在 /workspace;磁盘上只有这几个文件(另两个在「了解一块代码」用的工作区文件夹 /ws/web、/srv/app 里) */
vi.mock('./repoPath', async importOriginal => ({
...await importOriginal<typeof import('./repoPath')>(),
repoRootOf: async () => '/workspace',
resolveRepoEntry: async (_root: string, rel: string) => ['web/a.ts', 'web/b.ts', 'docs/notes.txt'].includes(rel) ? { fsPath: `/workspace/${rel}`, isDirectory: false } : undefined,
resolveRepoEntry: async (root: string, rel: string) => {
const fsPath = `${root}/${rel}`
const files = ['/workspace/web/a.ts', '/workspace/web/b.ts', '/workspace/docs/notes.txt', '/ws/web/src/app/(main)/page.tsx', '/srv/app/app/routes.py']
return files.includes(fsPath) ? { fsPath, isDirectory: false } : undefined
},
}))
const SOURCE = ['// 旧说明', 'export function load() {}']
@@ -698,6 +706,77 @@ describe('点了带几行的仓库链接', () => {
})
})
describe('「了解一块代码」对话页上的仓库链接按这个功能找代码用的工作区文件夹算', () => {
/** 功能对话页:路径是问的那句话,不在磁盘上,也不在哪个 git 仓库里 */
const FEATURE_PAGE = { scheme: 'code-reader-path', query: 'feature=f1', path: '/用户如何聊天', fsPath: '/用户如何聊天', toString: () => 'code-reader-path:/用户如何聊天?feature=f1' }
const WEB = { name: 'web', fsPath: '/ws/web' }
const SERVER = { name: 'server', fsPath: '/srv/app' }
async function featurePage(folders: { name: string, fsPath: string }[]) {
Object.assign(workspace, { workspaceFolders: folders.map(({ name, fsPath }) => ({ name, uri: { fsPath } })) })
vi.mocked(commands.executeCommand).mockClear()
vi.mocked(workspace.openTextDocument).mockImplementation((async (uri: { fsPath: string }) => ({ uri, languageId: 'python', lineCount: 20, lineAt: () => ({ text: '' }), positionAt: (offset: number) => ({ line: 0, character: offset }) })) as never)
const { provider, document } = opening()
Object.assign(document, { uri: FEATURE_PAGE, fileName: FEATURE_PAGE.fsPath, languageId: 'plaintext', getText: () => '' })
const page = tab()
provider.resolveCustomTextEditor(document as never, page.panel)
await page.send({ type: 'ready' })
page.clear()
return page
}
function reset(): void {
Object.assign(workspace, { workspaceFolders: undefined })
vi.mocked(workspace.openTextDocument).mockReset()
}
it('几个工作区文件夹:路径第一段是文件夹名,按那个文件夹查哪些不在;不再全判成找不到', async () => {
try {
const page = await featurePage([WEB, SERVER])
await page.send({ type: 'checkRepoLinks', id: 1, paths: ['web/src/app/(main)/page.tsx', 'server/app/routes.py', 'server/app/gone.py', 'other/a.py'], references: [] })
expect(page.posted()).toEqual([{ type: 'repoLinksChecked', id: 1, missing: ['server/app/gone.py', 'other/a.py'], unresolved: [], builtin: [] }])
}
finally {
reset()
}
})
it('只有一个工作区文件夹:路径相对它,带文件夹名的反倒不认', async () => {
try {
const page = await featurePage([WEB])
await page.send({ type: 'checkRepoLinks', id: 1, paths: ['src/app/(main)/page.tsx', 'web/src/app/(main)/page.tsx'], references: [] })
expect(page.posted()).toEqual([{ type: 'repoLinksChecked', id: 1, missing: ['web/src/app/(main)/page.tsx'], unresolved: [], builtin: [] }])
}
finally {
reset()
}
})
it('点链接按同一个算法打开文件;带符号的点了、悬停都在那个文件的文档符号里找', async () => {
try {
const page = await featurePage([WEB, SERVER])
await page.send({ type: 'openRepoLink', path: 'server/app/routes.py' })
expect(commands.executeCommand).toHaveBeenCalledWith('vscode.open', expect.objectContaining({ fsPath: '/srv/app/app/routes.py' }), expect.anything())
expect(page.posted()).toEqual([{ type: 'repoLinkOpened', path: 'server/app/routes.py' }])
vi.mocked(documentSymbolNodes).mockReset().mockResolvedValue([
{ name: 'HomePage', range: { startIndex: 0, endIndex: 40 }, selection: { startIndex: 16, endIndex: 24 }, children: [] },
])
page.clear()
await page.send({ type: 'peekLink', id: 2, path: 'web/src/app/(main)/page.tsx', symbol: 'HomePage' })
expect(documentSymbolNodes).toHaveBeenCalledWith('file:///ws/web/src/app/(main)/page.tsx')
await page.send({ type: 'openRepoLink', path: 'web/src/app/(main)/page.tsx', symbol: 'HomePage' })
expect(page.posted()).toContainEqual({ type: 'repoLinkOpened', path: 'web/src/app/(main)/page.tsx', symbol: 'HomePage' })
await page.send({ type: 'openRepoLink', path: 'web/src/app/(main)/page.tsx', symbol: 'Gone' })
expect(page.posted()).toContainEqual({ type: 'repoLinkBroken', path: 'web/src/app/(main)/page.tsx', symbol: 'Gone' })
}
finally {
vi.mocked(documentSymbolNodes).mockReset().mockResolvedValue([])
reset()
}
})
})
describe('预览里的查找栏', () => {
const sent = (page: ReturnType<typeof tab>): ExtensionToWebview[] => page.posted().filter(msg => msg.type === 'openFind' || msg.type === 'moveFind')