✨ feat(vscode): 代码阅读预览问 AI 的回答里代码位置写成链接、点了能跳:提示词要求写 [名字](路径#符号) 或 [文件名:起-止](路径#L起-L止) 并给出仓库根,仓库链接认 #L 行号(注释里同一套),旧回答里整段是「路径:行」的行内代码也当链接,带行号的在预览里展开、闪包住那几行的块,不支持预览的用文本编辑器选中

This commit is contained in:
cruldra committed 2026-10-14 06:30:30 +11:00
1 parent ceeaad91a7
commit e401e4aec3
22 files changed
+516 -90

No files matched your search

@@ -30,7 +30,10 @@ vi.mock('vscode', () => ({
},
Range: class {
constructor(readonly startLine: number, readonly startCharacter: number, readonly endLine: number, readonly endCharacter: number) {}
get start() { return { line: this.startLine, character: this.startCharacter } }
get end() { return { line: this.endLine, character: this.endCharacter } }
},
Uri: { file: (fsPath: string) => ({ fsPath, scheme: 'file', toString: () => `file://${fsPath}` }) },
EndOfLine: { LF: 1, CRLF: 2 },
}))
vi.mock('../../logging/logger', () => ({ logger: { add: vi.fn() } }))
@@ -52,6 +55,12 @@ vi.mock('./documentMessage', async importOriginal => ({
}))
/** 文档符号由各个测试给 */
vi.mock('./documentSymbols', () => ({ documentSymbolNodes: vi.fn(async () => []) }))
/** 仓库在 /workspace,里面只有这几个文件 */
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,
}))
const SOURCE = ['// 旧说明', 'export function load() {}']
@@ -189,7 +198,7 @@ function themeColors(colors: CodeColors) {
}
/** 对比基准只认从「未提交的更改」点开的:从那儿点开的对比上一次提交,别处打开的不比 */
function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }, colors = themeColors(COLORS)) {
function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }, colors = themeColors(COLORS), languageFor: (languageId: string) => unknown = () => undefined) {
const subscribe = () => ({ dispose: () => {} })
const diffBase = vi.fn((_uri: unknown, from: OpenedFrom): DiffBase | undefined => from === 'uncommitted' ? HEAD_BASE : undefined)
const qa = {
@@ -202,7 +211,7 @@ function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }, co
}
const provider = new PreviewEditorProvider(...[
{},
{ selectedTranslator: () => undefined },
{ selectedTranslator: () => undefined, languageFor },
{},
{ subscribe, current: { excluded: {}, onlyChanges: false } },
colors,
@@ -565,6 +574,60 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () =
})
})
describe('点了带几行的仓库链接', () => {
/** 文件按名字认语言,第 n 行(0 起)有 n 个字,一共 20 行 */
function textDocument(fsPath: string) {
const languageId = fsPath.endsWith('.ts') ? 'typescript' : 'plaintext'
return { uri: { fsPath, scheme: 'file', toString: () => `file://${fsPath}` }, languageId, lineCount: 20, lineAt: (line: number) => ({ text: 'x'.repeat(line) }) }
}
async function linked() {
vi.mocked(workspace.openTextDocument).mockImplementation((async (uri: { fsPath: string }) => textDocument(uri.fsPath)) as never)
vi.mocked(commands.executeCommand).mockClear()
const { provider, document } = opening(undefined, undefined, languageId => languageId === 'typescript' ? {} : undefined)
const page = tab()
provider.resolveCustomTextEditor(document as never, page.panel)
await page.send({ type: 'ready' })
await vi.waitFor(() => expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'document' })))
page.clear()
return { provider, page }
}
it('指到这份文件:就在本预览里滚到包住这几行的块', async () => {
const { page } = await linked()
await page.send({ type: 'openRepoLink', path: 'web/a.ts', lines: { start: 2, end: 3 } })
expect(page.posted()).toEqual([{ type: 'reveal', line: 1, endLine: 2 }, { type: 'repoLinkOpened', path: 'web/a.ts' }])
})
it('指到别的有代码阅读的文件:用预览打开,滚到包住这几行的块', async () => {
const { provider, page } = await linked()
const open = vi.spyOn(provider, 'open').mockResolvedValue()
await page.send({ type: 'openRepoLink', path: 'web/b.ts', lines: { start: 5, end: 9 } })
expect(open).toHaveBeenCalledWith(expect.objectContaining({ fsPath: '/workspace/web/b.ts' }), { reveal: { line: 4, endLine: 8 } }, expect.any(Function))
expect(page.posted()).toEqual([{ type: 'repoLinkOpened', path: 'web/b.ts' }])
})
it('指到没有代码阅读的文件:文本编辑器打开,选中这几行', async () => {
const { page } = await linked()
await page.send({ type: 'openRepoLink', path: 'docs/notes.txt', lines: { start: 3, end: 4 } })
expect(commands.executeCommand).toHaveBeenCalledWith('vscode.open', expect.objectContaining({ fsPath: '/workspace/docs/notes.txt' }), expect.objectContaining({ selection: expect.objectContaining({ startLine: 2, startCharacter: 0, endLine: 3, endCharacter: 3 }) }))
expect(page.posted()).toEqual([{ type: 'repoLinkOpened', path: 'docs/notes.txt' }])
})
it('文件没有:标坏,不打开', async () => {
const { page } = await linked()
await page.send({ type: 'openRepoLink', path: 'web/gone.ts', lines: { start: 1, end: 1 } })
expect(page.posted()).toEqual([{ type: 'repoLinkBroken', path: 'web/gone.ts' }])
expect(commands.executeCommand).not.toHaveBeenCalledWith('vscode.open', expect.anything(), expect.anything())
})
it('问哪些路径没有时,带几行的链接按文件问', async () => {
const { page } = await linked()
await page.send({ type: 'checkRepoLinks', id: 3, paths: ['web/a.ts', 'web/gone.ts'], references: [] })
expect(page.posted()).toEqual([{ type: 'repoLinksChecked', id: 3, missing: ['web/gone.ts'], unresolved: [] }])
})
})
describe('预览里的查找栏', () => {
const sent = (page: ReturnType<typeof tab>): ExtensionToWebview[] => page.posted().filter(msg => msg.type === 'openFind' || msg.type === 'moveFind')