✨ feat(vscode): 代码阅读侧栏跟着当前文件走,目录链接在侧栏树上定位

This commit is contained in:
cruldra committed 2026-10-08 07:17:26 +08:00
1 parent ff713db6e7
commit c841ae678a
10 files changed
+464 -7

No files matched your search

+1 -1
View File
@@ -145,7 +145,7 @@ export type ExtensionToWebview
* selectAskProfile:「问 AI」换 profile(id 是 profile 名,「默认」是 ''),扩展端记下后发 askProfiles 给所有预览
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发;
* 扩展端按它设 when 子句用的上下文键
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在资源管理器里定位目录
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录
* checkRepoLinks:预览里这批仓库路径,问哪些文件或目录没有。id 标出是哪一次问
* checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问
*/
@@ -79,6 +79,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
readonly diffBases: DiffBases,
/** 哪些文件是项目写明的入口、预览里哪些名字的块标成入口 */
readonly entries: Pick<WorkspaceEntries, 'namesOf' | 'onDidChange'>,
/** 在代码阅读侧栏的树上定位一个目录(仓库链接指到目录时);树上没有它时返回 false */
readonly revealInTree: (uri: Uri) => Promise<boolean>,
) {}
/** 侧栏「问 AI」打开预览,并把光标放进顶部输入框。已经开着的就揭到前面再聚焦。 */
@@ -704,7 +706,7 @@ class Preview {
}
/**
* 打开注释里的仓库链接:目录在资源管理器里定位,文件用编辑器打开。
* 打开注释里的仓库链接:目录在代码阅读侧栏的树上定位,文件用编辑器打开。
* 带了符号就按文档符号的名字跳过去;文件或符号没有就不打开。
*/
private async openRepoLink(rel: string, symbol?: string): Promise<void> {
@@ -721,7 +723,8 @@ class Preview {
this.markRepoLinkBroken(rel, symbol)
return
}
await commands.executeCommand('revealInExplorer', uri)
if (!await this.owner.revealInTree(uri))
void window.showInformationMessage(`代码阅读:侧栏的树上没有 ${rel}`)
this.markRepoLinkOpened(rel)
return
}
@@ -0,0 +1,204 @@
import type { WorkspaceFolder } from 'vscode'
import type { ChangedSection, FileNode, FileTreeDecorator, TreeNode } from './fileTree'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Uri } from 'vscode'
import { buildChangedTree } from './changedTree'
import { FileTreeProvider, isGroupNode } from './fileTree'
/** 假的工作区:目录路径 → 下面一层的 [名字, 类型];files.exclude 的值 */
const disk = vi.hoisted(() => ({
dirs: new Map<string, [string, number][]>(),
folders: [] as unknown[],
exclude: {} as Record<string, boolean>,
}))
vi.mock('vscode', async () => {
const { posix } = await import('node:path')
class FakeUri {
constructor(readonly scheme: string, readonly authority: string, readonly path: string) {}
static file(path: string): FakeUri {
return new FakeUri('file', '', path)
}
static joinPath(base: FakeUri, ...segments: string[]): FakeUri {
return base.with({ path: posix.join(base.path, ...segments) })
}
with(change: { path?: string }): FakeUri {
return new FakeUri(this.scheme, this.authority, change.path ?? this.path)
}
toString(): string {
return `${this.scheme}://${this.authority}${this.path}`
}
}
const listen = (): { dispose: () => void } => ({ dispose: () => {} })
return {
EventEmitter: class {
event = listen
fire(): void {}
dispose(): void {}
},
FileType: { File: 1, Directory: 2 },
ThemeIcon: { File: {}, Folder: {} },
TreeItem: class {},
TreeItemCollapsibleState: { None: 0, Collapsed: 1, Expanded: 2 },
Uri: FakeUri,
workspace: {
createFileSystemWatcher: () => ({ onDidCreate: listen, onDidDelete: listen, dispose: () => {} }),
onDidChangeWorkspaceFolders: listen,
onDidChangeConfiguration: listen,
get workspaceFolders() {
return disk.folders
},
fs: {
readDirectory: async (uri: FakeUri) => {
const entries = disk.dirs.get(uri.path)
if (!entries)
throw new Error(`没有 ${uri.path}`)
return entries
},
},
getConfiguration: () => ({ get: () => disk.exclude }),
},
}
})
const FILE = 1
const DIR = 2
const decorator = { onDidChange: () => ({ dispose: () => {} }), decorate: () => {} } as unknown as FileTreeDecorator
let folder: WorkspaceFolder
beforeEach(() => {
folder = { uri: Uri.file('/ws'), name: 'ws', index: 0 }
disk.folders = [folder]
disk.exclude = {}
disk.dirs = new Map([
['/ws', [['backend', DIR], ['dist', DIR], ['README.md', FILE]]],
['/ws/backend', [['src', DIR]]],
['/ws/backend/src', [['luffy', DIR], ['gate.py', FILE]]],
['/ws/backend/src/luffy', [['visibility.py', FILE]]],
['/ws/dist', [['bundle.js', FILE]]],
])
})
async function childNamed(files: FileTreeProvider, parent: TreeNode, name: string): Promise<FileNode> {
const child = (await files.getChildren(parent)).find(node => !isGroupNode(node) && node.uri.path.endsWith(`/${name}`))
if (!child || isGroupNode(child))
throw new Error(`树上没有 ${name}`)
return child
}
/** 从 node 顺着 getParent 走到最上面,每一层写成路径(分组行写它的名字) */
function chainUp(files: FileTreeProvider, node: TreeNode | undefined): string[] {
const chain: string[] = []
for (let current = node; current; current = files.getParent(current))
chain.push(isGroupNode(current) ? `分组:${current.group.label}` : current.uri.path)
return chain
}
/** 从 node 顺着 getParent 走到的最上面那一行 */
function topOf(files: FileTreeProvider, node: TreeNode): TreeNode {
let top = node
for (let parent = files.getParent(top); parent; parent = files.getParent(top))
top = parent
return top
}
describe('工作区文件树:按 uri 找到那一行、一层层找上一层', () => {
it('多层目录:getParent 交回 getChildren 给过的同一个对象,根的上一层没有', async () => {
const files = new FileTreeProvider('open', decorator)
const [root] = await files.getChildren()
const backend = await childNamed(files, root, 'backend')
const src = await childNamed(files, backend, 'src')
const luffy = await childNamed(files, src, 'luffy')
const visibility = await childNamed(files, luffy, 'visibility.py')
expect(files.getParent(visibility)).toBe(luffy)
expect(files.getParent(luffy)).toBe(src)
expect(files.getParent(src)).toBe(backend)
expect(files.getParent(backend)).toBe(root)
expect(files.getParent(root)).toBeUndefined()
})
it('nodeOf 一层层读目录找到文件,就是树上摆的那一行', async () => {
const files = new FileTreeProvider('open', decorator)
const found = await files.nodeOf(Uri.file('/ws/backend/src/luffy/visibility.py'))
expect(found?.isDirectory).toBe(false)
expect(chainUp(files, found)).toEqual(['/ws/backend/src/luffy/visibility.py', '/ws/backend/src/luffy', '/ws/backend/src', '/ws/backend', '/ws'])
expect(await files.getChildren(files.getParent(found!))).toContain(found)
expect(topOf(files, found!)).toBe((await files.getChildren())[0])
})
it('目录也能找到', async () => {
const files = new FileTreeProvider('open', decorator)
const found = await files.nodeOf(Uri.file('/ws/backend/src'))
expect(found?.isDirectory).toBe(true)
expect(found?.isRoot).toBe(false)
expect(chainUp(files, found)).toEqual(['/ws/backend/src', '/ws/backend', '/ws'])
})
it('树上没有的找不到:工作区外、被 files.exclude 藏了的目录里、盘上没有的', async () => {
disk.exclude = { '**/dist': true }
const files = new FileTreeProvider('open', decorator)
expect(await files.nodeOf(Uri.file('/elsewhere/a.py'))).toBeUndefined()
expect(await files.nodeOf(Uri.file('/ws/dist/bundle.js'))).toBeUndefined()
expect(await files.nodeOf(Uri.file('/ws/backend/missing.py'))).toBeUndefined()
})
})
describe('只摆改动的文件时:在树上就找到,不在就没有', () => {
function sections(): { staged: ChangedSection, unstaged: ChangedSection } {
return {
staged: { label: '暂存的更改', roots: [{ folder, tree: buildChangedTree([{ path: 'backend/src/gate.py', change: 'M' }]) }] },
unstaged: { label: '更改', roots: [{ folder, tree: buildChangedTree([
{ path: 'backend/src/luffy/visibility.py', change: 'M' },
{ path: 'backend/src/gate.py', change: 'M' },
]) }] },
}
}
it('找到那个分组里的那一行,上一层一直走到分组行', async () => {
const files = new FileTreeProvider('open', decorator)
const { staged, unstaged } = sections()
files.showChanges([staged, unstaged])
const found = await files.nodeOf(Uri.file('/ws/backend/src/luffy/visibility.py'))
expect(found?.section).toBe(unstaged)
expect(found?.change).toBe('M')
expect(chainUp(files, found)).toEqual(['/ws/backend/src/luffy/visibility.py', '/ws/backend/src/luffy', '/ws/backend/src', '/ws/backend', '/ws', '分组:更改'])
expect(topOf(files, found!)).toBe((await files.getChildren())[1])
expect(await files.getChildren(files.getParent(found!))).toContain(found)
})
it('没摆出来的文件、目录找不到', async () => {
const files = new FileTreeProvider('open', decorator)
const { staged, unstaged } = sections()
files.showChanges([staged, unstaged])
expect(await files.nodeOf(Uri.file('/ws/README.md'))).toBeUndefined()
expect(await files.nodeOf(Uri.file('/ws/dist'))).toBeUndefined()
expect(await files.nodeOf(Uri.file('/ws/backend/src/other.py'))).toBeUndefined()
})
it('同一个文件摆在两个分组里时取最先摆的', async () => {
const files = new FileTreeProvider('open', decorator)
const { staged, unstaged } = sections()
files.showChanges([staged, unstaged])
expect((await files.nodeOf(Uri.file('/ws/backend/src/gate.py')))?.section).toBe(staged)
})
it('分组没有名字时根就在最上面一层,根的上一层没有', async () => {
const files = new FileTreeProvider('open', decorator)
files.showChanges([{ roots: [{ folder, tree: buildChangedTree([{ path: 'backend/src/gate.py', change: 'A' }]) }] }])
const found = await files.nodeOf(Uri.file('/ws/backend/src/gate.py'))
const [root] = await files.getChildren()
expect(chainUp(files, found)).toEqual(['/ws/backend/src/gate.py', '/ws/backend/src', '/ws/backend', '/ws'])
expect(topOf(files, found!)).toBe(root)
})
})
@@ -4,6 +4,7 @@ import type { ExcludeSetting } from './fileEntries'
import * as path from 'node:path'
import { EventEmitter, FileType, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, workspace } from 'vscode'
import { createExcludeMatcher, entryContext, GROUP_CONTEXT, sortEntries } from './fileEntries'
import { pathRelation } from './treeReveal'
export interface FileNode {
uri: Uri
@@ -64,6 +65,7 @@ const REFRESH_DELAY_MS = 300
* 节点用 resourceUri,图标、git 改动颜色都由 VS Code 按文件图标主题和 SCM 装饰自动加上。
* 筛选时换成只摆改动的文件(showChanges:审查工单时 PR 改过的,未提交的更改时各仓库里没提交的,分「暂存的更改」「更改」两组):
* 目录全部展开,删掉的文件列出来但打不开。
* 能按 Uri 找到树上那一行和它的上一层(nodeOf、getParent),侧栏定位到某个文件 / 目录(TreeLocator)靠它。
*/
export class FileTreeProvider implements TreeDataProvider<TreeNode> {
/** undefined 是整棵树重画,数组是只重画这几行 */
@@ -145,6 +147,28 @@ export class FileTreeProvider implements TreeDataProvider<TreeNode> {
.map(entry => this.nodeAt(Uri.joinPath(node.uri, entry.name), node.folder, entry.isDirectory, false))
}
/**
* 一行的上一层,交回 getChildren 给过的同一个对象(`TreeView.reveal` 按对象认行):
* 文件、目录的上一层是所在目录,根节点的上一层是它所在的分组行(只摆改动的文件、分组有名字时),别的没有。
*/
getParent(node: TreeNode): TreeNode | undefined {
if (isGroupNode(node))
return undefined
if (node.isRoot)
return this.groups.find(({ group }) => group === node.section && group.label !== undefined)
const parentPath = path.posix.dirname(node.uri.path)
const isRoot = path.posix.relative(node.folder.uri.path, parentPath) === ''
return this.nodeAt(isRoot ? node.folder.uri : node.uri.with({ path: parentPath }), node.folder, true, isRoot, node.section)
}
/**
* 树上 uri 这一行(文件或目录):从最上面一层层往下找,只进通往它的目录,路上的行按 getChildren 摆出来的记下。
* 不在树上(工作区外、被 files.exclude 藏了、只摆改动的文件时没摆它)时没有;同一个文件摆在几个分组里时取最先摆的那行。
*/
async nodeOf(uri: Uri): Promise<FileNode | undefined> {
return this.findBelow(undefined, uri)
}
dispose(): void {
clearTimeout(this.timer)
for (const disposable of this.disposables)
@@ -209,6 +233,22 @@ export class FileTreeProvider implements TreeDataProvider<TreeNode> {
return section.roots.map(({ folder }) => this.nodeAt(folder.uri, folder, true, true, section))
}
private async findBelow(parent: TreeNode | undefined, uri: Uri): Promise<FileNode | undefined> {
for (const child of await this.getChildren(parent)) {
if (!isGroupNode(child)) {
const relation = pathRelation(child.uri, uri)
if (relation === 'self')
return child
if (relation !== 'ancestor' || !child.isDirectory)
continue
}
const found = await this.findBelow(child, uri)
if (found)
return found
}
return undefined
}
/** 还没展开过的行不用管,展开时自然按最新的画。 */
private redraw(uris: readonly Uri[]): void {
const nodes = uris.flatMap(uri => [...this.nodes.values()].flatMap(known => known.get(uri.toString()) ?? []))
@@ -0,0 +1,107 @@
import type { Disposable, TreeView, Uri } from 'vscode'
import type { ExcludeSetting } from './fileEntries'
import type { FileNode, FileTreeProvider, TreeNode } from './fileTree'
import * as path from 'node:path'
import { TabInputCustom, TabInputNotebook, TabInputNotebookDiff, TabInputText, TabInputTextDiff, window, workspace } from 'vscode'
import { logger } from '../../logging/logger'
import { pathPreviewOf } from '../editor/pathPreview'
import { createExcludeMatcher } from './fileEntries'
import { followsActiveFile, revealExcluded } from './treeReveal'
/**
* 激活的标签页开着哪个文件:文本编辑器、代码阅读预览这种自定义编辑器、笔记本是它开的文件,对比编辑器取改后的那边(同 VS Code 资源管理器),
* 路径页(问文件夹的预览)还原成那个文件夹;终端、设置页这些没有。
*/
function activeTabUri(): Uri | undefined {
const input = window.tabGroups.activeTabGroup.activeTab?.input
const uri = input instanceof TabInputText || input instanceof TabInputCustom || input instanceof TabInputNotebook
? input.uri
: input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff ? input.modified : undefined
return uri && (pathPreviewOf(uri)?.uri ?? uri)
}
/**
* 侧栏的树定位到某个文件 / 目录:展开路上的目录、选中那一行(目录顺带展开),焦点留在编辑器。
* 跟着当前文件走:激活的标签页换成一个文件就定位过去,点链接、跳到定义、在标签栏切换、侧栏点开都走这一条;
* 规则同 VS Code 资源管理器:`explorer.autoReveal` 关掉不跟,`explorer.autoRevealExclude` 里的不跟;
* 树上没有的(工作区外、被 files.exclude 藏了、筛选时没摆它)不动,也不退出筛选。
* 侧栏这棵树没露出来时不跟(reveal 会把它亮出来,抢了用户的侧栏),露出来时补上。
*/
export class TreeLocator {
private readonly disposables: Disposable[]
/** 上一次跟过去的文件:同一个标签页别的变化(改了没保存、标题变了)不再定位,用户在树上折起来的不给展开回去 */
private followed: string | undefined
/** 第几次定位:找那一行要读目录,连着切几个标签页、中间点了目录链接时,跟的只认最后一次 */
private round = 0
constructor(private readonly files: FileTreeProvider, private readonly view: TreeView<TreeNode>) {
this.disposables = [
window.tabGroups.onDidChangeTabs(() => this.follow()),
window.tabGroups.onDidChangeTabGroups(() => this.follow()),
view.onDidChangeVisibility(({ visible }) => {
if (visible)
this.follow()
else
this.followed = undefined
}),
]
this.follow()
}
/**
* 在树上定位 uri(仓库链接指到的目录);侧栏这棵树没露出来时 VS Code 把它亮出来,点了链接就是要看。
* @returns 树上没有它(工作区外、被藏了、筛选时没摆它)时 false
*/
async reveal(uri: Uri): Promise<boolean> {
// 要看的是这一行:正在跟的作废,树因此亮出来时也不再跟回当前文件
this.followed = activeTabUri()?.toString()
this.round++
const node = await this.files.nodeOf(uri)
if (!node)
return false
await this.show(node)
return true
}
dispose(): void {
for (const disposable of this.disposables)
disposable.dispose()
}
private follow(): void {
if (!this.view.visible || !followsActiveFile(workspace.getConfiguration('explorer').get('autoReveal')))
return
const uri = activeTabUri()
const key = uri?.toString()
if (key === this.followed)
return
this.followed = key
const round = ++this.round
if (uri)
void this.followTo(uri, round)
}
private async followTo(uri: Uri, round: number): Promise<void> {
const node = await this.files.nodeOf(uri)
if (!node || round !== this.round || !this.view.visible || this.excluded(node))
return
try {
await this.show(node)
}
catch (err) {
// 找到行之后树刚好刷新了(文件增删、换了筛选),这一行已经不是树上的那个对象
const message = err instanceof Error ? err.message : String(err)
logger.add({ level: 'warn', source: 'codeReader', message: `侧栏没能定位到 ${uri.fsPath}:${message}` })
}
}
/** `explorer.autoRevealExclude` 按这一行所在的根算相对路径 */
private excluded(node: FileNode): boolean {
const setting = workspace.getConfiguration('explorer', node.folder.uri).get<ExcludeSetting>('autoRevealExclude')
return revealExcluded(createExcludeMatcher(setting), path.posix.relative(node.folder.uri.path, node.uri.path))
}
private async show(node: FileNode): Promise<void> {
await this.view.reveal(node, { select: true, focus: false, expand: node.isDirectory })
}
}
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest'
import { createExcludeMatcher } from './fileEntries'
import { followsActiveFile, pathRelation, revealExcluded } from './treeReveal'
const file = (path: string): { scheme: string, authority: string, path: string } => ({ scheme: 'file', authority: '', path })
describe('树上一行和要找的路径的关系', () => {
it('同一个路径是它自己', () => {
expect(pathRelation(file('/ws/backend/a.py'), file('/ws/backend/a.py'))).toBe('self')
})
it('上面任何一层目录都是往里找的那层', () => {
expect(pathRelation(file('/ws'), file('/ws/backend/src/a.py'))).toBe('ancestor')
expect(pathRelation(file('/ws/backend'), file('/ws/backend/src/a.py'))).toBe('ancestor')
expect(pathRelation(file('/'), file('/ws/a.py'))).toBe('ancestor')
})
it('名字开头相同的兄弟目录、下面的路径、别的协议都不相干', () => {
expect(pathRelation(file('/ws/back'), file('/ws/backend/a.py'))).toBeUndefined()
expect(pathRelation(file('/ws/backend/a.py'), file('/ws/backend'))).toBeUndefined()
expect(pathRelation(file('/ws/other'), file('/ws/backend/a.py'))).toBeUndefined()
expect(pathRelation({ scheme: 'git', authority: '', path: '/ws' }, file('/ws/a.py'))).toBeUndefined()
expect(pathRelation({ scheme: 'file', authority: 'host', path: '/ws' }, file('/ws/a.py'))).toBeUndefined()
})
})
describe('explorer.autoReveal 的几种值', () => {
it('true 跟,false 不跟', () => {
expect(followsActiveFile(true)).toBe(true)
expect(followsActiveFile(false)).toBe(false)
})
it('focusNoScroll 当作开着(插件的 reveal 选中就会滚过去)', () => {
expect(followsActiveFile('focusNoScroll')).toBe(true)
})
it('没读到时照 VS Code 的默认,开着', () => {
expect(followsActiveFile(undefined)).toBe(true)
})
})
describe('explorer.autoRevealExclude', () => {
const excluded = createExcludeMatcher({ '**/node_modules': true, '**/bower_components': true, 'build': false })
it('它自己或路上任何一层目录命中都不跟', () => {
expect(revealExcluded(excluded, 'node_modules/react/index.js')).toBe(true)
expect(revealExcluded(excluded, 'packages/web/node_modules/vue/dist/vue.js')).toBe(true)
expect(revealExcluded(excluded, 'node_modules')).toBe(true)
})
it('没命中、或那条关掉了的照跟', () => {
expect(revealExcluded(excluded, 'backend/src/visibility.py')).toBe(false)
expect(revealExcluded(excluded, 'build/out.js')).toBe(false)
expect(revealExcluded(excluded, 'src/node_modules_helper.ts')).toBe(false)
})
})
@@ -0,0 +1,37 @@
import type { ExcludeMatcher } from './fileEntries'
/** Uri 里比路径用到的几样:vscode 的 Uri 就有这几样,测试里不用起 VS Code。 */
export interface UriPath {
scheme: string
authority: string
path: string
}
/** 树上一行和要找的 target 的关系:self 就是它,ancestor 是 target 上面的某层目录(往它里面找),undefined 不相干。 */
export type PathRelation = 'self' | 'ancestor' | undefined
export function pathRelation(row: UriPath, target: UriPath): PathRelation {
if (row.scheme !== target.scheme || row.authority !== target.authority)
return undefined
if (row.path === target.path)
return 'self'
const dir = row.path.endsWith('/') ? row.path : `${row.path}/`
return target.path.startsWith(dir) ? 'ancestor' : undefined
}
/**
* `explorer.autoReveal` 的值 → 侧栏要不要跟着当前文件走,同 VS Code 资源管理器:只有 false 不跟。
* `focusNoScroll`(选中但不滚动)当作开着:插件能用的 `TreeView.reveal` 没有不滚动的选项,选中就会滚过去。
*/
export function followsActiveFile(autoReveal: unknown): boolean {
return autoReveal !== false
}
/**
* 相对根的路径命中 `explorer.autoRevealExclude` 时不跟过去,同 VS Code 资源管理器:它自己或路上任何一层目录命中都算,
* 所以 `node_modules` 这一条管到它里面所有的文件。规则里看同级有没有某个文件的 `when` 不认(这里没有同级的名字)。
*/
export function revealExcluded(excluded: ExcludeMatcher, relativePath: string): boolean {
const segments = relativePath.split('/')
return segments.some((_, index) => excluded(segments.slice(0, index + 1).join('/'), new Set()))
}
+4 -1
View File
@@ -18,6 +18,7 @@ import { FileTreeProvider } from './explorer/fileTree'
import { registerPathAsk } from './explorer/pathAskPanel'
import { RowDecorator } from './explorer/rowDecorator'
import { TreeFilters } from './explorer/treeFilters'
import { TreeLocator } from './explorer/treeLocator'
import { createBashAdapter } from './languages/bash'
import { createDockerfileAdapter } from './languages/dockerfile'
import { createPythonAdapter } from './languages/python'
@@ -102,11 +103,12 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
const entries = new WorkspaceEntries()
const files = new FileTreeProvider(OPEN_COMMAND, new RowDecorator(notes, entries))
const view = window.createTreeView(FILES_VIEW, { treeDataProvider: files, showCollapseAll: true })
const locator = new TreeLocator(files, view)
const filters = new TreeFilters(files, view, context.workspaceState)
const review = new ReviewMode(context, FILES_VIEW, view, files, filters)
const uncommitted = new UncommittedMode(context, FILES_VIEW, view, files, filters)
// 侧栏开着的筛选定预览对比什么:未提交的更改对比上一次提交,审查工单对比 PR 的目标分支
const previews = new PreviewEditorProvider(context.extensionUri, registry, runtime, new BlockFilter(context.workspaceState), resolve, qa, namesIn, (index, key) => indexes.find(index, key), filters, entries)
const previews = new PreviewEditorProvider(context.extensionUri, registry, runtime, new BlockFilter(context.workspaceState), resolve, qa, namesIn, (index, key) => indexes.find(index, key), filters, entries, uri => locator.reveal(uri))
context.subscriptions.push(
qa,
@@ -132,6 +134,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
window.registerCustomEditorProvider(PREVIEW_VIEW_TYPE, previews),
files,
view,
locator,
filters,
review,
uncommitted,