✨ feat(vscode): 代码阅读预览里工作区外的库文件也能问 AI,问答记进第一个工作区文件夹,重写注释不对库文件开放
This commit is contained in:
1 parent
cecae0e244
commit
39dfd9b084
24 files changed
+463
-79
No files matched your search
@@ -29,25 +29,46 @@ vi.mock('vscode', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
/** 工作区只有 /workspace 一个文件夹;/workspace 下面的是项目里的文件,别处的是工作区外的库文件 */
|
||||
const places = vi.hoisted(() => {
|
||||
const folder = { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }
|
||||
/** ⌘ 点进去的标准库源码所在的目录 */
|
||||
const libraryDir = '/home/me/.local/share/uv/python/cpython-3.12/lib/python3.12'
|
||||
const libraryKey = '~/.local/share/uv/python/cpython-3.12/lib/python3.12/pathlib.py'
|
||||
const inWorkspace = (uri: { fsPath: string }): boolean => uri.fsPath.startsWith('/workspace/')
|
||||
/** 同 locateQa:项目里的记在 /workspace、键是相对路径;库文件也记在 /workspace(第一个工作区文件夹),键是 ~/…,发给模型写绝对路径、放行它所在的目录 */
|
||||
const locateQa = (uri: { fsPath: string }) => inWorkspace(uri)
|
||||
? { folder, key: 'a.ts', promptPath: 'a.ts' }
|
||||
: { folder, key: libraryKey, promptPath: uri.fsPath, outsideDir: libraryDir }
|
||||
return { folder, libraryDir, libraryKey, inWorkspace, locateQa }
|
||||
})
|
||||
|
||||
vi.mock('../folderStore', () => ({
|
||||
/** 内存里的一份表:按键取、改了就换 */
|
||||
/** 内存里的表,每个工作区文件夹一份:按键取、改了就换 */
|
||||
FolderStore: class {
|
||||
private map: Record<string, unknown> = {}
|
||||
private readonly maps = new Map<unknown, Record<string, unknown>>()
|
||||
readonly onDidChange = () => ({ dispose: () => {} })
|
||||
get(_folder: unknown, key: string) { return this.map[key] }
|
||||
async update(_folder: unknown, change: (map: Record<string, unknown>) => Record<string, unknown>) {
|
||||
this.map = change(this.map)
|
||||
get(folder: unknown, key: string) { return this.maps.get(folder)?.[key] }
|
||||
async update(folder: unknown, change: (map: Record<string, unknown>) => Record<string, unknown>) {
|
||||
this.maps.set(folder, change(this.maps.get(folder) ?? {}))
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
},
|
||||
locate: () => ({ folder: { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }, key: 'a.ts' }),
|
||||
locate: (uri: { fsPath: string }) => places.inWorkspace(uri) ? { folder: places.folder, key: 'a.ts' } : undefined,
|
||||
locateQa: places.locateQa,
|
||||
}))
|
||||
vi.mock('../editor/pathPreview', () => ({
|
||||
pathQaLocation: () => ({ folder: { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }, key: 'a.ts' }),
|
||||
topQaLocation: () => ({ folder: { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }, key: 'file:///workspace\na.ts' }),
|
||||
pathQaLocation: places.locateQa,
|
||||
topQaLocation: (uri: { fsPath: string }) => {
|
||||
const at = places.locateQa(uri)
|
||||
return { folder: at.folder, key: `file:///workspace\n${at.key}` }
|
||||
},
|
||||
}))
|
||||
/** 材料只用位置给的路径,不读盘 */
|
||||
vi.mock('./pathAskDescribe', () => ({
|
||||
describePath: async ({ at }: { at: { promptPath: string, outsideDir?: string } }) => ({ kind: 'file', path: at.promptPath, ...(at.outsideDir && { outside: true }), head: { unavailable: '测试' } }),
|
||||
}))
|
||||
vi.mock('./pathAskDescribe', () => ({ describePath: async () => ({ kind: 'file', relativePath: 'a.ts', head: { unavailable: '测试' } }) }))
|
||||
vi.mock('../../cc/profiles', () => ({
|
||||
getDefaultProfilePath: () => '/p/offical.json',
|
||||
getProfilesDir: () => '/p',
|
||||
@@ -411,3 +432,49 @@ describe('顶部聊天要结构化结果的一轮(了解一块代码)', () =
|
||||
expect(historyOf(qa)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('工作区外的库文件(⌘ 点进去的标准库源码)', () => {
|
||||
const library = { fsPath: `${places.libraryDir}/pathlib.py`, toString: () => `file://${places.libraryDir}/pathlib.py` } as Uri
|
||||
const viewOf = (qa: CodeQa, of: Uri = library) => qa.view(of, file => file.fsPath, 'file')
|
||||
|
||||
/** 等这一轮问答结束的事件 */
|
||||
function answered(qa: CodeQa, askId: string): Promise<Extract<AskEvent, { kind: 'end' }>> {
|
||||
return new Promise((resolve) => {
|
||||
qa.onDidAnswer((event) => {
|
||||
if (event.kind === 'end' && event.ask.askId === askId)
|
||||
resolve(event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
it('能问 AI,不能重写注释(会改到库文件本身);项目里的文件两样都能', () => {
|
||||
const { qa } = service(context())
|
||||
expect(viewOf(qa)).toMatchObject({ available: true, rewritable: false })
|
||||
expect(viewOf(qa, uri)).toMatchObject({ available: true, rewritable: true })
|
||||
})
|
||||
|
||||
it('问一块:claude 在第一个工作区文件夹里跑、另外放行读库文件所在的目录;答完记进那个文件夹的表,再看能看到这条', async () => {
|
||||
const { qa, runner } = service(context())
|
||||
runner.mockResolvedValueOnce({ text: '按通配符递归找。', sessionId: 's1' })
|
||||
const done = answered(qa, '1')
|
||||
expect(await qa.ask(library, { askId: '1', anchor: 'function rglob', question: '怎么找的', prompt: '完整的', followUp: '短的', images: [] })).toBeUndefined()
|
||||
expect(runner.mock.calls[0][0]).toMatchObject({ cwd: '/workspace', addDirs: [places.libraryDir] })
|
||||
expect((await done).end.status).toBe('done')
|
||||
expect(viewOf(qa).threads).toEqual([{ anchor: 'function rglob', entries: [expect.objectContaining({ question: '怎么找的', answer: '按通配符递归找。' })] }])
|
||||
expect(viewOf(qa, uri).threads).toEqual([])
|
||||
})
|
||||
|
||||
it('顶部问这个文件:一样放行库文件所在的目录,发给模型的是绝对路径', async () => {
|
||||
const { qa, runner } = service(context())
|
||||
expect(await qa.askPath(library, 'file', { askId: '1', question: '这是什么', images: [] })).toBeUndefined()
|
||||
expect(runner.mock.calls[0][0]).toMatchObject({ cwd: '/workspace', addDirs: [places.libraryDir] })
|
||||
expect(runner.mock.calls[0][0].prompt).toContain(`绝对路径:${places.libraryDir}/pathlib.py`)
|
||||
})
|
||||
|
||||
it('重写注释开不了,不启动模型', async () => {
|
||||
const { qa, rewriter } = service(context())
|
||||
const skill = '/ecc:writing-code-comments pathlib.py:1-3'
|
||||
expect(await qa.rewrite(library, { askId: '1', anchor: 'function rglob', question: skill, prompt: skill, images: [], writeBack: async () => undefined })).toMatch(/不能重写注释/)
|
||||
expect(rewriter).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user