🐛 fix(vscode): 代码阅读预览跳到还没打开的文件时停在文件顶上不再发生:新开的预览注释里有仓库图片时,问图片才把仓库根加进 webview 能读的目录,改了选项 VS Code 重新加载页面,第二次 ready 发的文档不带要滚到的那处;页面能读的目录(页面资源、问答图片、这一页仓库链接的各个根及其真实位置)改在装页面之前一次定好,之后不再改选项,LinkBase 带上它算得出的根

This commit is contained in:
cruldra committed 2026-10-14 23:30:36 +11:00
1 parent 95fc539c3a
commit ad35898716
3 files changed
+66 -35

No files matched your search

+7 -4
View File
@@ -43,8 +43,11 @@ export function featureFolders(): NamedFolder[] {
return (workspace.workspaceFolders ?? []).map(folder => ({ name: folder.name, path: folder.uri.fsPath }))
}
/** 把页面上一条仓库链接的路径换成磁盘上的位置;算不出来的没有。 */
export type LinkBase = (rel: string) => RepoPlace | undefined
/** 一页上的仓库链接从哪儿算:`place` 把一条链接的路径换成磁盘上的位置,算不出来的没有;算出来的位置都在 `roots` 这几个根下面。 */
export interface LinkBase {
place: (rel: string) => RepoPlace | undefined
roots: string[]
}
/**
* 预览这一页上仓库链接的路径从哪儿算:功能对话页从这个功能找代码用的工作区文件夹算(见 [featureFolders]、[folderLinkPlace]),
@@ -53,10 +56,10 @@ export type LinkBase = (rel: string) => RepoPlace | undefined
export async function linkBaseOf(uri: Uri): Promise<LinkBase> {
if (featureIdOf(uri) !== undefined) {
const folders = featureFolders()
return rel => folderLinkPlace(folders, rel)
return { place: rel => folderLinkPlace(folders, rel), roots: folders.map(folder => folder.path) }
}
const root = await repoRootOf(uri.fsPath)
return rel => root === undefined ? undefined : { root, rel }
return root === undefined ? { place: () => undefined, roots: [] } : { place: rel => ({ root, rel }), roots: [root] }
}
export function pathPreviewUri(target: Uri, kind: PathSubject): Uri {
@@ -76,7 +76,7 @@ vi.mock('./repoPath', async importOriginal => ({
repoRootOf: async () => '/workspace',
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']
const files = ['/workspace/web/a.ts', '/workspace/web/b.ts', '/workspace/docs/notes.txt', '/workspace/docs/home.png', '/ws/web/src/app/(main)/page.tsx', '/srv/app/app/routes.py']
return files.includes(fsPath) ? { fsPath, isDirectory: false } : undefined
},
}))
@@ -377,6 +377,27 @@ describe('预览对比什么看这个标签页是从哪儿打开的', () => {
})
})
/** 页面眼下能读的本地目录 */
function readableRoots(page: ReturnType<typeof tab>): unknown[] {
return (page.panel.webview.options.localResourceRoots ?? []).map(root => root.fsPath)
}
describe('页面能读哪些本地目录', () => {
it('新开的预览装页面之前就放行仓库根:注释里的图片问到时不再改页面选项,打开时要滚到的那处还在', async () => {
const { provider, document } = opening()
const page = tab()
await provider.open(document.uri as never, { reveal: { line: 1 } }, async () => provider.resolveCustomTextEditor(document as never, page.panel))
expect(readableRoots(page)).toContain('/workspace')
const options = page.panel.webview.options
await page.send({ type: 'ready' })
await page.send({ type: 'checkRepoImages', id: 1, paths: ['docs/home.png'] })
expect(page.panel.webview.options).toBe(options)
expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'document', reveal: { line: 1 } }))
expect(page.posted()).toContainEqual({ type: 'repoImagesChecked', id: 1, images: [{ path: 'docs/home.png', src: 'file:///workspace/docs/home.png' }] })
})
})
describe('标签页切走时页面留着', () => {
it('注册时就让页面在标签页切走后留着', () => {
const { provider } = opening()
@@ -719,7 +740,7 @@ describe('「了解一块代码」对话页上的仓库链接按这个功能找
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 provider.resolveCustomTextEditor(document as never, page.panel)
await page.send({ type: 'ready' })
page.clear()
return page
@@ -752,6 +773,16 @@ describe('「了解一块代码」对话页上的仓库链接按这个功能找
}
})
it('页面装好之前就放行这个功能找代码用的各个工作区文件夹', async () => {
try {
const page = await featurePage([WEB, SERVER])
expect(readableRoots(page)).toEqual(expect.arrayContaining(['/ws/web', '/srv/app']))
}
finally {
reset()
}
})
it('点链接按同一个算法打开文件;带符号的点了、悬停都在那个文件的文档符号里找', async () => {
try {
const page = await featurePage([WEB, SERVER])
+26 -29
View File
@@ -149,22 +149,41 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
[...this.previews].find(p => p.uri.toString() === key && p.active)?.reopen(request)
}
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
// 问答带的图片(工作区文件夹的 .spx/code-qa/)也要能读,历史里才画得出缩略图
const imageRoot = this.qa.imageRoot(topSubjectOf(document.uri).uri)
panel.webview.options = { enableScripts: true, localResourceRoots: [webviewUiRoot(this.extensionUri), ...(imageRoot ? [imageRoot] : [])] }
panel.webview.html = webviewHtml(panel.webview, this.extensionUri, 'code-reader.html')
/**
* 新开一个预览:先接下给它的打开请求、接上消息,再装页面。页面能读的本地目录(见 [readableRoots])装之前一次定好,之后不再改:
* 改了 webview 的选项 VS Code 会重新加载页面,打开时要滚到的那处就丢了。
*/
async resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): Promise<void> {
const key = document.uri.toString()
const request = this.requests.get(key)
this.requests.delete(key)
const preview = new Preview(document, panel, this, this.registry, this.runtime, this.filter, request)
this.previews.add(preview)
let disposed = false
panel.onDidDispose(() => {
disposed = true
this.previews.delete(preview)
this.navigation.update(preview, false)
this.finding.update(preview, false)
preview.dispose()
})
const roots = await this.readableRoots(document.uri)
// 找目录的工夫标签页已经关了
if (disposed)
return
panel.webview.options = { enableScripts: true, localResourceRoots: roots }
panel.webview.html = webviewHtml(panel.webview, this.extensionUri, 'code-reader.html')
}
/**
* 预览页面能读的本地目录:页面自己的脚本和样式;问答带的图片所在的目录(工作区文件夹的 .spx/code-qa/),历史里才画得出缩略图;
* 注释、回答里的仓库图片所在的各个根(见 [linkBaseOf]),根的真实位置和这个路径不一样时两个都放,读不到真实位置就只放这个路径。
*/
private async readableRoots(uri: Uri): Promise<Uri[]> {
const imageRoot = this.qa.imageRoot(topSubjectOf(uri).uri)
const { roots } = await linkBaseOf(uri)
const repoRoots = await Promise.all(roots.map(async root => [root, await realpath(root).catch(() => root)]))
return [webviewUiRoot(this.extensionUri), ...(imageRoot ? [imageRoot] : []), ...[...new Set(repoRoots.flat())].map(root => Uri.file(root))]
}
/** 「跳到下一处 / 上一处高亮」:发给能跳的那个预览,从命令面板运行时发给活动的预览。 */
@@ -967,35 +986,13 @@ class Preview {
})
}
/** 仓库根放进这个预览能读的本地目录,图片地址才能加载。真实位置和 git 给的路径不一样时两个都放。 */
private addLocalRoot(fsPath: string): void {
const uri = Uri.file(fsPath)
const roots = this.panel.webview.options.localResourceRoots ?? []
if (roots.some(root => root.fsPath === uri.fsPath))
return
this.panel.webview.options = { ...this.panel.webview.options, localResourceRoots: [...roots, uri] }
}
private async allowRepoRoot(root: string): Promise<void> {
this.addLocalRoot(root)
try {
const real = await realpath(root)
if (real !== root)
this.addLocalRoot(real)
}
catch {
// 仓库根这会儿读不到真实位置,就按 git 给的路径放行
}
}
/** 这批仓库图片哪些在。在的带回预览里能直接用的地址;不在、不是图片、跑出仓库的没有地址。 */
private async checkRepoImages(id: number, paths: string[]): Promise<void> {
const base = await linkBaseOf(this.document.uri)
const images = await Promise.all(paths.map(async (rel) => {
const place = isRepoImagePath(rel) ? base(rel) : undefined
const place = isRepoImagePath(rel) ? base.place(rel) : undefined
if (!place)
return { path: rel }
await this.allowRepoRoot(place.root)
const entry = await resolveRepoEntry(place.root, place.rel)
if (!entry || entry.isDirectory)
return { path: rel }
@@ -1164,7 +1161,7 @@ class Preview {
/** 一条仓库链接指到磁盘上的哪个文件或目录,路径按这一页的算法算(见 [linkBaseOf]);算不出来、不在、跑出根的没有。 */
async function linkEntry(base: LinkBase, rel: string): Promise<RepoEntry | undefined> {
const place = base(rel)
const place = base.place(rel)
return place && resolveRepoEntry(place.root, place.rel)
}