🐛 fix(vscode): 定义卡片里注释的 [Name] 渲染成链接,按卡片那份文件解析
This commit is contained in:
1 parent
af91b03857
commit
bea1f2e679
19 files changed
+319
-151
No files matched your search
@@ -97,4 +97,16 @@ describe('悬停看别的文件里的定义时读成的文档(DocumentReads)
|
||||
await reads.read('file:///b.py', file(1))
|
||||
expect(parses()).toBe(once * 4)
|
||||
})
|
||||
|
||||
it('带着卡片里注释中的引用按它找的样子:块树就是读成的块,导入引进来的名字、代码里用到又没定义的名字;同一份读成的文档给同一个对象', async () => {
|
||||
const reads = new DocumentReads(registryWithPython(), runtime)
|
||||
const source = 'from app.bridge import tauri_bridge\n\nclass UserService:\n def update_user(self):\n raise ValueError(tauri_bridge)\n'
|
||||
const snapshot = { ...file(1), getText: () => source }
|
||||
const { message, references } = await reads.read('file:///app/service.py', snapshot)
|
||||
expect(references.version).toBe(1)
|
||||
expect(references.sent?.blocks).toBe(message.blocks)
|
||||
expect(references.imports.map(item => item.name)).toEqual(['tauri_bridge'])
|
||||
expect([...references.uses.keys()]).toEqual(expect.arrayContaining(['tauri_bridge', 'ValueError']))
|
||||
expect((await reads.read('file:///app/service.py', snapshot)).references).toBe(references)
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DocumentRead, DocumentSnapshot } from './documentMessage'
|
||||
import type { ReferenceInputs } from './referenceTargets'
|
||||
import { RecentPromises } from '../recentPromises'
|
||||
import { outsideUses } from '../symbolLinks'
|
||||
import { documentMessage } from './documentMessage'
|
||||
|
||||
/** 默认记几份文件 */
|
||||
const LIMIT = 8
|
||||
|
||||
/** 悬停看定义用到的那部分:读成的文档,和里面起了中文名的定义。 */
|
||||
export type PeekRead = Pick<DocumentRead, 'message' | 'displayNames'>
|
||||
/**
|
||||
* 悬停看定义用到的那部分:读成的文档,里面起了中文名的定义,和卡片里注释中不带路径的引用按它找的样子(`references`,见 [ReferenceInputs];
|
||||
* 同一份读成的文档给的是同一个对象,解析过的引用接着用)。
|
||||
*/
|
||||
export type PeekRead = Pick<DocumentRead, 'message' | 'displayNames'> & { references: ReferenceInputs }
|
||||
|
||||
/**
|
||||
* 悬停看别的文件里的定义时,那份文件读成的文档(见 [documentMessage]),各预览共用,免得每次悬停都把整份文件重新解析一遍。
|
||||
@@ -37,7 +42,8 @@ export class DocumentReads {
|
||||
|
||||
private async parse(document: DocumentSnapshot): Promise<PeekRead> {
|
||||
// 那个文件里规则出的错由它自己的预览记
|
||||
const { message, displayNames } = await documentMessage(document, this.registry, this.runtime, () => {})
|
||||
return { message, displayNames }
|
||||
const { message, displayNames, imports, bindings } = await documentMessage(document, this.registry, this.runtime, () => {})
|
||||
const blocks = message.error === undefined ? { blocks: message.blocks } : undefined
|
||||
return { message, displayNames, references: { version: message.version, sent: blocks, imports, uses: outsideUses(bindings, message.source) } }
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,13 @@ export interface TranslatorOption { id: string, label: string }
|
||||
/**
|
||||
* 注释里一个不带路径的引用(`[Name]`、`[Class.member]`):symbol 是写的名字;block 是这段注释属于哪一块(块的 anchor),
|
||||
* 从那块代码的声明起一层层往外找(见 symbolLinks.ts)。不在哪块的注释里的没有 block,只从文件顶层找。
|
||||
* 同一个名字写在不同块的注释里,指到的可以不一样。
|
||||
* file 是这段注释写在哪份文件里(uri,见 [DefinitionPeek] 的 `uri`):定义卡片里的注释带着卡片那份文件,按那份文件的块、导入找;
|
||||
* 页面上这份文档自己的注释没有 file。同一个名字写在不同块、不同文件的注释里,指到的可以不一样。
|
||||
*/
|
||||
export interface SymbolReference {
|
||||
symbol: string
|
||||
block?: string
|
||||
file?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,12 +101,14 @@ export type PageBlock = PageCard & {
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(从工作区文件夹算起的相对路径,同挑选框)、定义处那一行(0 起),
|
||||
* 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(path 是给人看的、从工作区文件夹算起的相对路径,同挑选框;
|
||||
* uri 是这份文件本身,卡片里注释中不带路径的引用带着它,按这份文件找,见 [SymbolReference])、定义处那一行(0 起),
|
||||
* 盖住定义处的那块(连同子块,和那个文件的预览画的是同一张卡片;它自己当那个文件的顶层第一块,span 从文件开头算),
|
||||
* 显示成中文名的名字(下标是那个文件里的)。
|
||||
*/
|
||||
export interface DefinitionPeek {
|
||||
path: string
|
||||
uri: string
|
||||
line: number
|
||||
block: PageBlock
|
||||
names: ShownName[]
|
||||
@@ -214,9 +218,9 @@ export interface StickyScrollSettings {
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用,
|
||||
* builtin 是语言自带、又问不到定义的不带路径的引用(Python 的 `ValueError`、`str`,页面当普通文字);两样都原样带回问的那几条
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到,
|
||||
* block 同 openRepoLink
|
||||
* repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,页面当普通文字;symbol、block 同 openRepoLink
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block 同 openRepoLink
|
||||
* block、file 同 openRepoLink
|
||||
* repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,页面当普通文字;symbol、block、file 同 openRepoLink
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block、file 同 openRepoLink
|
||||
* repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在
|
||||
* callTreeCounts:version 这份文档里,每个能看调用树的块(BlockView.callTree)落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
|
||||
* callTree:anchor 那块的调用树。trees 空着、又没有 contractMiss 是没有调用它的地方;接口卡片在契约文件里没对上时带 contractMiss。
|
||||
@@ -245,9 +249,9 @@ export type ExtensionToWebview
|
||||
| { type: 'diff', diff: DocumentDiff | null }
|
||||
| { type: 'openedFrom', from: OpenedFrom }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[], builtin: SymbolReference[] }
|
||||
| { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string, file?: string }
|
||||
| ({ type: 'repoLinkBuiltin' } & SymbolReference)
|
||||
| { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string, file?: string }
|
||||
| { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] }
|
||||
| { type: 'focusPathAsk' }
|
||||
| { type: 'callTreeCounts', version: number, counts: Record<string, number> }
|
||||
@@ -261,7 +265,7 @@ export type ExtensionToWebview
|
||||
* version 是这份下标所属的文档版本
|
||||
* goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义
|
||||
* peekDefinition:鼠标停在带下标的名字上,要 offset 处名字的定义卡片(几处定义取第一处);id、version 同 probeDefinition
|
||||
* peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block 同 openRepoLink
|
||||
* peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block、file 同 openRepoLink
|
||||
* findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition
|
||||
* setFilter:改块筛选(整份替换),扩展端记下后发 filter 给所有预览
|
||||
* setOnlyChanges:打开 / 关掉「只看改动」,扩展端记下后发 filter 给所有预览
|
||||
@@ -280,8 +284,8 @@ export type ExtensionToWebview
|
||||
* findNavigation:眼下查找栏的快捷键归不归这个页面(查找栏开着、页面有焦点),变了才发;扩展端按它设 when 子句用的上下文键
|
||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,lines 是文件里的几行(和 symbol 二选一),
|
||||
* 都没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按这份文件找 symbol:block 是它写在哪块的注释里(见 SymbolReference),
|
||||
* 从那块的声明起一层层往外找本文件定义的,再是导入的(见 symbolLinks.ts)
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按它写在的那份文件找 symbol:file 是那份文件(定义卡片里的注释才有,没有是这份文件),
|
||||
* block 是它写在哪块的注释里(见 SymbolReference),从那块的声明起一层层往外找那份文件定义的,再是导入的(见 symbolLinks.ts)
|
||||
* checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(references)问哪些找不到。id 标出是哪一次问
|
||||
* checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问
|
||||
* openCallTree:打开 anchor 那块的调用树。version 是这块所在的文档版本,对不上就不查
|
||||
@@ -301,7 +305,7 @@ export type WebviewToExtension
|
||||
| { type: 'probeDefinition', id: number, version: number, offset: number }
|
||||
| { type: 'goToDefinition', version: number, offset: number }
|
||||
| { type: 'peekDefinition', id: number, version: number, offset: number }
|
||||
| { type: 'peekLink', id: number, path?: string, symbol: string, block?: string }
|
||||
| { type: 'peekLink', id: number, path?: string, symbol: string, block?: string, file?: string }
|
||||
| { type: 'findOccurrences', id: number, version: number, offset: number }
|
||||
| { type: 'ask', askId: string, anchor: string, question: string, images: ClaudeImage[] }
|
||||
| { type: 'stopAsk', askId: string }
|
||||
@@ -309,7 +313,7 @@ export type WebviewToExtension
|
||||
| { type: 'selectProfile', kind: ChatKind, id: string }
|
||||
| { type: 'occurrenceNavigation', available: boolean }
|
||||
| { type: 'findNavigation', available: boolean }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string, block?: string, lines?: LineRange }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string, block?: string, file?: string, lines?: LineRange }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], references: SymbolReference[] }
|
||||
| { type: 'checkRepoImages', id: number, paths: string[] }
|
||||
| { type: 'openCallTree', version: number, anchor: string }
|
||||
|
||||
@@ -673,6 +673,70 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () =
|
||||
vi.mocked(workspace.openTextDocument).mockReset()
|
||||
}
|
||||
})
|
||||
|
||||
it('定义卡片里别的文件的注释:卡片带着那份文件,引用按那份文件的导入、定义找,标坏链接、点了打开都按它;写着本文件的照本文件找', async () => {
|
||||
const OTHER = '/workspace/web/b.ts'
|
||||
const BRIDGE = '/workspace/web/tauri-bridge.ts'
|
||||
const EVENTS = 'function useShellEvents'
|
||||
const textDocument = (fsPath: string) => ({ uri: Uri.file(fsPath), version: 1, languageId: 'typescript', fileName: fsPath, getText: () => '', positionAt: (offset: number) => ({ line: 0, character: offset }), offsetAt: (position: { character: number }) => position.character })
|
||||
const other = textDocument(OTHER)
|
||||
const bridge = textDocument(BRIDGE)
|
||||
const read = (blocks: BlockView[], imports: unknown[]) => ({
|
||||
message: { type: 'document', version: 1, languageId: 'typescript', blocks, source: '', colors: { roles: [], runs: [] }, attributes: [], translators: [], translatorId: '', names: [] },
|
||||
displayNames: [],
|
||||
symbols: [],
|
||||
imports,
|
||||
kinds: [],
|
||||
bindings: [],
|
||||
})
|
||||
const original = vi.mocked(documentMessage).getMockImplementation()!
|
||||
vi.mocked(documentMessage).mockImplementation(async input => (input.fileName === OTHER
|
||||
? read([declared(EVENTS, 300)], [{ name: 'tauriBridge', offset: 20, range: { startIndex: 0, endIndex: 40 } }])
|
||||
: read([declared(HELPER, 950)], [])) as never)
|
||||
vi.mocked(documentSymbolNodes).mockReset().mockImplementation(async uri => uri === other.uri.toString()
|
||||
? [symbol('useShellEvents', 300, 600)]
|
||||
: [symbol('helper', 950, 990)])
|
||||
vi.mocked(languageDefinitions).mockReset().mockImplementation((async (uri: { toString: () => string }, position: { character: number }) =>
|
||||
uri.toString() === other.uri.toString() && position.character === 20 ? [{ uri: bridge.uri, range: new Range(0, 13, 0, 24) }] : []) as never)
|
||||
const { provider, document } = opening()
|
||||
Object.assign(workspace, { textDocuments: [document, other, bridge] })
|
||||
vi.mocked(workspace.openTextDocument).mockImplementation((async (uri: { toString: () => string }) =>
|
||||
[other, bridge].find(doc => doc.uri.toString() === uri.toString()) ?? document) as never)
|
||||
try {
|
||||
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' })))
|
||||
vi.mocked(documentMessage).mockClear()
|
||||
vi.mocked(commands.executeCommand).mockClear()
|
||||
|
||||
await page.send({ type: 'peekLink', id: 1, path: 'web/b.ts', symbol: 'useShellEvents' })
|
||||
expect(page.posted()).toContainEqual({ type: 'definitionPeeked', id: 1, peek: expect.objectContaining({ uri: other.uri.toString() }) })
|
||||
|
||||
const imported = { symbol: 'tauriBridge', block: EVENTS, file: other.uri.toString() }
|
||||
const defined = { symbol: 'useShellEvents', file: other.uri.toString() }
|
||||
const ownName = { symbol: 'helper', file: other.uri.toString() }
|
||||
const writtenHere = { symbol: 'helper', file: document.uri.toString() }
|
||||
const onPage = { symbol: 'tauriBridge', block: HELPER }
|
||||
await page.send({ type: 'checkRepoLinks', id: 2, paths: [], references: [imported, defined, ownName, writtenHere, onPage] })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 2, missing: [], unresolved: [ownName, onPage], builtin: [] })
|
||||
|
||||
await page.send({ type: 'openRepoLink', ...imported })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinkOpened', ...imported })
|
||||
expect(commands.executeCommand).toHaveBeenCalledWith('vscode.open', expect.objectContaining({ fsPath: BRIDGE }), expect.anything())
|
||||
expect(languageDefinitions).toHaveBeenCalledWith(expect.objectContaining({ fsPath: OTHER }), expect.objectContaining({ character: 20 }))
|
||||
await page.send({ type: 'openRepoLink', ...ownName })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinkBroken', ...ownName })
|
||||
expect(documentMessage).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
finally {
|
||||
vi.mocked(documentMessage).mockImplementation(original)
|
||||
vi.mocked(documentSymbolNodes).mockReset().mockResolvedValue([])
|
||||
vi.mocked(languageDefinitions).mockReset().mockResolvedValue([])
|
||||
Object.assign(workspace, { textDocuments: [] })
|
||||
vi.mocked(workspace.openTextDocument).mockReset()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('点了带几行的仓库链接', () => {
|
||||
|
||||
@@ -15,7 +15,8 @@ import type { ThemeColors } from '../theme/codeColors'
|
||||
import type { IndexFind } from '../workspaceIndex'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
import type { FoundLocation } from './definitions'
|
||||
import type { ChatKind, DefinitionPeek, ExtensionToWebview, LineRange, Occurrence, Reveal, SymbolReference, WebviewToExtension } from './messages'
|
||||
import type { PeekRead } from './documentReads'
|
||||
import type { ChatKind, DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, SymbolReference, WebviewToExtension } from './messages'
|
||||
import type { TargetSpot } from './openTarget'
|
||||
import type { LinkBase, TopSubject } from './pathPreview'
|
||||
import type { RepoEntry } from './repoPath'
|
||||
@@ -89,7 +90,9 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
/** 调用的名字的定义是哪种块:各预览共用,定义所在的文件同一版只解析一次 */
|
||||
readonly definitionKinds: DefinitionKinds
|
||||
/** 悬停看别的文件里的定义时那份文件读成的文档:各预览共用,同一版不再重新解析 */
|
||||
readonly documentReads: DocumentReads
|
||||
private readonly documentReads: DocumentReads
|
||||
/** 别的文件的注释里不带路径的引用解析过的指到哪,一份文件一套(见 [referenceTargets]):各预览共用,文件关掉、没人再用时跟着丢 */
|
||||
private readonly elsewhereReferences = new WeakMap<TextDocument, ReferenceTargets>()
|
||||
|
||||
constructor(
|
||||
private readonly extensionUri: Uri,
|
||||
@@ -127,6 +130,23 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
return window.registerCustomEditorProvider(PREVIEW_VIEW_TYPE, this, { webviewOptions: { retainContextWhenHidden: true } })
|
||||
}
|
||||
|
||||
/** 别的文件 `document` 按它的语言读成的文档(见 [DocumentReads]):悬停看定义时卡片画它的块,卡片里注释中不带路径的引用按它找。 */
|
||||
readElsewhere(document: TextDocument): Promise<PeekRead> {
|
||||
const { uri, languageId, version, fileName } = document
|
||||
return this.documentReads.read(uri.toString(), { languageId, version, getText: () => document.getText(), fileName, entryNames: this.entries.namesOf(uri) })
|
||||
}
|
||||
|
||||
/** 别的文件 `document` 的注释里不带路径的引用 `reference` 指到哪:按那份文件读成的文档找,和它自己的预览找的一样(见 [referenceTargets])。 */
|
||||
async resolveElsewhere(document: TextDocument, reference: SymbolReference): Promise<ReferenceTarget> {
|
||||
const { references } = await this.readElsewhere(document)
|
||||
let targets = this.elsewhereReferences.get(document)
|
||||
if (!targets) {
|
||||
targets = referenceTargets(document, this.registry)
|
||||
this.elsewhereReferences.set(document, targets)
|
||||
}
|
||||
return targets.resolve(references, reference)
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 `show` 打开 `uri` 的预览,把 `request` 交给它:新建的预览解析时带走;`show` 只是切到了已经开着的预览(没有新建)时,交给切到的那个。
|
||||
*
|
||||
@@ -266,10 +286,7 @@ class Preview {
|
||||
offset => this.requests.distance(this.document.positionAt(offset).line),
|
||||
)
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用解析过的指到哪(见 [ReferenceTargets])。文档符号问这种语言的扩展(同仓库链接找符号),导入的、代码里用到的名字问语言扩展它的定义
|
||||
* (同跳到定义,几处取第一处),哪些名字是语言自带的问语言插件([LanguageAdapter.builtins](vscode/src/codeReader/contract.ts#LanguageAdapter.builtins))
|
||||
*/
|
||||
/** 这份文档的注释里不带路径的引用解析过的指到哪(见 [referenceTargets]) */
|
||||
private readonly references: ReferenceTargets
|
||||
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
|
||||
private symbols: { version: number, list: LocalSymbol[] } | undefined
|
||||
@@ -313,19 +330,7 @@ class Preview {
|
||||
return found.length > 0 ? found : this.localDefinitions(offset)
|
||||
},
|
||||
})
|
||||
this.references = new ReferenceTargets({
|
||||
uri: document.uri.toString(),
|
||||
symbols: documentSymbolNodes,
|
||||
definition: async (offset) => {
|
||||
const [found] = await languageDefinitions(document.uri, document.positionAt(offset))
|
||||
if (!found)
|
||||
return undefined
|
||||
const doc = await workspace.openTextDocument(found.uri)
|
||||
return { uri: found.uri.toString(), selection: { startIndex: doc.offsetAt(found.range.start), endIndex: doc.offsetAt(found.range.end) } }
|
||||
},
|
||||
builtin: name => registry.languageFor(document.languageId)?.builtins?.has(name) ?? false,
|
||||
versionOf: uri => workspace.textDocuments.find(doc => doc.uri.toString() === uri)?.version,
|
||||
})
|
||||
this.references = referenceTargets(document, registry)
|
||||
this.names = new DocumentNames((version, names) => {
|
||||
if (version === document.version)
|
||||
this.post({ type: 'displayNames', version, names })
|
||||
@@ -795,13 +800,13 @@ class Preview {
|
||||
await this.peekDefinition(msg.id, msg.version, msg.offset)
|
||||
return
|
||||
case 'peekLink':
|
||||
await this.peekLink(msg.id, msg.path, msg.symbol, msg.block)
|
||||
await this.peekLink(msg.id, msg.path, referenceOf(msg))
|
||||
return
|
||||
case 'findOccurrences':
|
||||
await this.findOccurrences(msg.id, msg.version, msg.offset)
|
||||
return
|
||||
case 'openRepoLink':
|
||||
await this.openRepoLink(msg.path, msg.symbol, msg.block, msg.lines)
|
||||
await this.openRepoLink(msg)
|
||||
return
|
||||
case 'checkRepoLinks':
|
||||
await this.checkRepoLinks(msg.id, msg.paths, msg.references)
|
||||
@@ -899,14 +904,14 @@ class Preview {
|
||||
}
|
||||
|
||||
/** 回答 webview 第 id 次要注释里的链接指的符号的卡片:符号同点这个链接时找的那个(symbolTarget),找不到、是语言自带的名字时没有。 */
|
||||
private async peekLink(id: number, rel: string | undefined, symbol: string, block: string | undefined): Promise<void> {
|
||||
private async peekLink(id: number, rel: string | undefined, reference: SymbolReference): Promise<void> {
|
||||
let peek: DefinitionPeek | undefined
|
||||
try {
|
||||
const target = await this.symbolTarget(rel, symbol, block)
|
||||
const target = await this.symbolTarget(rel, reference)
|
||||
peek = target && target !== 'builtin' ? await this.peekAt(Uri.parse(target.uri), target.selection.startIndex) : undefined
|
||||
}
|
||||
catch (err) {
|
||||
const link = rel === undefined ? `[${symbol}]` : `${rel}#${symbol}`
|
||||
const link = rel === undefined ? `[${reference.symbol}]` : `${rel}#${reference.symbol}`
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `没能找 ${link} 指的符号:${err instanceof Error ? err.message : String(err)}` })
|
||||
}
|
||||
this.post({ type: 'definitionPeeked', id, ...(peek && { peek }) })
|
||||
@@ -922,19 +927,17 @@ class Preview {
|
||||
const target = await workspace.openTextDocument(uri)
|
||||
const line = target.positionAt(offset).line
|
||||
const path = workspace.asRelativePath(uri)
|
||||
if (uri.toString() === this.document.uri.toString()) {
|
||||
const file = uri.toString()
|
||||
if (file === this.document.uri.toString()) {
|
||||
const { sent } = this
|
||||
if (sent?.version !== target.version)
|
||||
return undefined
|
||||
const block = peekBlock(sent.blocks, offset, line)
|
||||
return block && { path, line, block: pagePeekBlock(block, sent.source), names: namesOnCard(block, this.names.known(target.version)) }
|
||||
return block && { path, uri: file, line, block: pagePeekBlock(block, sent.source), names: namesOnCard(block, this.names.known(target.version)) }
|
||||
}
|
||||
const { message, displayNames } = await this.owner.documentReads.read(
|
||||
uri.toString(),
|
||||
{ languageId: target.languageId, version: target.version, getText: () => target.getText(), fileName: target.fileName, entryNames: this.owner.entries.namesOf(uri) },
|
||||
)
|
||||
const { message, displayNames } = await this.owner.readElsewhere(target)
|
||||
const block = message.error === undefined ? peekBlock(message.blocks, offset, line) : undefined
|
||||
return block && { path, line, block: pagePeekBlock(block, message.source), names: namesOnCard(block, definitionSiteNames(displayNames)) }
|
||||
return block && { path, uri: file, line, block: pagePeekBlock(block, message.source), names: namesOnCard(block, definitionSiteNames(displayNames)) }
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `没能读出 ${uri.toString()} 里定义的那块:${err instanceof Error ? err.message : String(err)}` })
|
||||
@@ -970,7 +973,10 @@ class Preview {
|
||||
}
|
||||
}
|
||||
|
||||
/** 预览里这批仓库路径(从哪儿算见 [linkBaseOf]),哪些文件或目录不在;这批不带路径的引用,各按写在哪块找,哪些找不到、哪些是语言自带的名字(页面当普通文字)。 */
|
||||
/**
|
||||
* 预览里这批仓库路径(从哪儿算见 [linkBaseOf]),哪些文件或目录不在;这批不带路径的引用,各按写在哪份文件、哪块找(见 [resolveSymbolLink]),
|
||||
* 哪些找不到、哪些是语言自带的名字(页面当普通文字)。
|
||||
*/
|
||||
private async checkRepoLinks(id: number, paths: string[], references: SymbolReference[]): Promise<void> {
|
||||
const base = await linkBaseOf(this.document.uri)
|
||||
const [missing, resolved] = await Promise.all([
|
||||
@@ -1005,23 +1011,24 @@ class Preview {
|
||||
/**
|
||||
* 打开注释、问 AI 的回答里的仓库链接:目录在代码阅读侧栏的树上定位,文件用编辑器打开。
|
||||
* 带了符号就按文档符号的名字跳过去,带了几行(`lines`)就跳到这几行(见 [linesSpot]);文件或符号没有就不打开。
|
||||
* 没有路径的是不带路径的引用,按这份文件、从它写在的那块(`block`)找(见 [resolveSymbolLink]);它是语言自带的名字时不打开,告诉页面当普通文字。
|
||||
* 没有路径的是不带路径的引用,按它写在的那份文件(`file`)、那块(`block`)找(见 [resolveSymbolLink]);它是语言自带的名字时不打开,告诉页面当普通文字。
|
||||
*/
|
||||
private async openRepoLink(rel: string | undefined, symbol?: string, block?: string, lines?: LineRange): Promise<void> {
|
||||
private async openRepoLink({ path: rel, symbol, block, file, lines }: Extract<WebviewToExtension, { type: 'openRepoLink' }>): Promise<void> {
|
||||
const link = rel ?? `[${symbol}]`
|
||||
try {
|
||||
if (symbol !== undefined) {
|
||||
const target = await this.symbolTarget(rel, symbol, block)
|
||||
const reference = referenceOf({ symbol, block, file })
|
||||
const target = await this.symbolTarget(rel, reference)
|
||||
if (target === 'builtin') {
|
||||
this.post({ type: 'repoLinkBuiltin', symbol, ...(block !== undefined && { block }) })
|
||||
this.post({ type: 'repoLinkBuiltin', ...reference })
|
||||
return
|
||||
}
|
||||
if (!target) {
|
||||
this.markRepoLinkBroken(rel, symbol, block)
|
||||
this.markRepoLinkBroken(rel, reference)
|
||||
return
|
||||
}
|
||||
await this.showLinkTarget(target)
|
||||
this.markRepoLinkOpened(rel, symbol, block)
|
||||
this.markRepoLinkOpened(rel, reference)
|
||||
return
|
||||
}
|
||||
if (rel === undefined)
|
||||
@@ -1050,15 +1057,15 @@ class Preview {
|
||||
}
|
||||
|
||||
/**
|
||||
* 链接指的符号在哪:没有 path 的是不带路径的引用,按这份文件、从它写在的那块(block)找(resolveSymbolLink);
|
||||
* 链接指的符号在哪:没有 path 的是不带路径的引用,按它写在的那份文件、那块找([resolveSymbolLink]);
|
||||
* 有 path 的在那个文件的文档符号里找(locateRepoSymbol)。文件没有、是目录、符号没有时没有;不带路径的引用是语言自带的名字时是 `'builtin'`。
|
||||
* 点链接、悬停链接看定义卡片都按它找。
|
||||
*/
|
||||
private async symbolTarget(rel: string | undefined, symbol: string, block: string | undefined): Promise<ReferenceTarget> {
|
||||
private async symbolTarget(rel: string | undefined, reference: SymbolReference): Promise<ReferenceTarget> {
|
||||
if (rel === undefined)
|
||||
return this.resolveSymbolLink({ symbol, block })
|
||||
return this.resolveSymbolLink(reference)
|
||||
const entry = await linkEntry(await linkBaseOf(this.document.uri), rel)
|
||||
return entry && !entry.isDirectory ? this.locateRepoSymbol(Uri.file(entry.fsPath), symbol) : undefined
|
||||
return entry && !entry.isDirectory ? this.locateRepoSymbol(Uri.file(entry.fsPath), reference.symbol) : undefined
|
||||
}
|
||||
|
||||
/** 用文档符号按名字找(`Agent`、`Agent.owner_id`);没有这个名字就没有,不打开到文件顶上。 */
|
||||
@@ -1070,13 +1077,17 @@ class Preview {
|
||||
/**
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪,检查坏链接、点击、悬停看定义卡片都走这里;解析过的记在 [ReferenceTargets] 里,不再问语言扩展。
|
||||
*
|
||||
* 从它写在的那块(在最近发出去的块树里找)的声明起一层层往外找本文件定义的,再找导入的、代码里用到的语言自带的名字,
|
||||
* 按它写在的那份文件找([SymbolReference] 的 `file`):没写、写的就是这份文档的,按最近发出去的块树和这份文档的导入、用到的名字找;
|
||||
* 别的文件的(定义卡片里的注释)按那份文件读成的文档找(见 [PreviewEditorProvider.resolveElsewhere])。
|
||||
* 都是从它写在的那块的声明起一层层往外找那份文件定义的,再找导入的、代码里用到的语言自带的名字,
|
||||
* 语言自带又问不到定义的是 `'builtin'`(见 [解析引用](vscode/src/codeReader/symbolLinks.ts#resolveReference))。出错时记一笔,当找不到。
|
||||
*/
|
||||
private async resolveSymbolLink(reference: SymbolReference): Promise<ReferenceTarget> {
|
||||
const { document, sent, imports, uses } = this
|
||||
try {
|
||||
return await this.references.resolve({ version: document.version, sent, imports, uses }, reference)
|
||||
if (reference.file === undefined || reference.file === document.uri.toString())
|
||||
return await this.references.resolve({ version: document.version, sent, imports, uses }, reference)
|
||||
return await this.owner.resolveElsewhere(await workspace.openTextDocument(Uri.parse(reference.file)), reference)
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `没能找注释里的引用 [${reference.symbol}]:${err instanceof Error ? err.message : String(err)}` })
|
||||
@@ -1099,12 +1110,12 @@ class Preview {
|
||||
await this.openElsewhere(uri, spot)
|
||||
}
|
||||
|
||||
private markRepoLinkBroken(path: string | undefined, symbol?: string, block?: string): void {
|
||||
this.post({ type: 'repoLinkBroken', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }), ...(block !== undefined && { block }) })
|
||||
private markRepoLinkBroken(path: string | undefined, reference?: SymbolReference): void {
|
||||
this.post({ type: 'repoLinkBroken', ...(path !== undefined && { path }), ...reference })
|
||||
}
|
||||
|
||||
private markRepoLinkOpened(path: string | undefined, symbol?: string, block?: string): void {
|
||||
this.post({ type: 'repoLinkOpened', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }), ...(block !== undefined && { block }) })
|
||||
private markRepoLinkOpened(path: string | undefined, reference?: SymbolReference): void {
|
||||
this.post({ type: 'repoLinkOpened', ...(path !== undefined && { path }), ...reference })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1165,6 +1176,32 @@ async function linkEntry(base: LinkBase, rel: string): Promise<RepoEntry | undef
|
||||
return place && resolveRepoEntry(place.root, place.rel)
|
||||
}
|
||||
|
||||
/**
|
||||
* `document` 这份文件的注释里不带路径的引用指到哪,解析过的记着(见 [ReferenceTargets])。文档符号问这种语言的扩展(同仓库链接找符号),
|
||||
* 导入的、代码里用到的名字问语言扩展它在这份文件里的定义(同跳到定义,几处取第一处),
|
||||
* 哪些名字是语言自带的问这份文件的语言插件([LanguageAdapter.builtins](vscode/src/codeReader/contract.ts#LanguageAdapter.builtins))。
|
||||
*/
|
||||
function referenceTargets(document: TextDocument, registry: Pick<CodeReaderRegistry, 'languageFor'>): ReferenceTargets {
|
||||
return new ReferenceTargets({
|
||||
uri: document.uri.toString(),
|
||||
symbols: documentSymbolNodes,
|
||||
definition: async (offset) => {
|
||||
const [found] = await languageDefinitions(document.uri, document.positionAt(offset))
|
||||
if (!found)
|
||||
return undefined
|
||||
const doc = await workspace.openTextDocument(found.uri)
|
||||
return { uri: found.uri.toString(), selection: { startIndex: doc.offsetAt(found.range.start), endIndex: doc.offsetAt(found.range.end) } }
|
||||
},
|
||||
builtin: name => registry.languageFor(document.languageId)?.builtins?.has(name) ?? false,
|
||||
versionOf: uri => workspace.textDocuments.find(doc => doc.uri.toString() === uri)?.version,
|
||||
})
|
||||
}
|
||||
|
||||
/** 消息里带的那条引用:只取 `symbol`、`block`、`file` 里写了的,报回页面时和页面问的那条一样。 */
|
||||
function referenceOf({ symbol, block, file }: SymbolReference): SymbolReference {
|
||||
return { symbol, ...(block !== undefined && { block }), ...(file !== undefined && { file }) }
|
||||
}
|
||||
|
||||
/** 写回重写好的注释:换掉原来那几行(同块编辑),或在块上面插进新写的几行。 */
|
||||
async function writeCommentEdit(document: TextDocument, edit: CommentEdit): Promise<WriteResult> {
|
||||
if (edit.kind === 'replace')
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { SymbolReference } from './messages'
|
||||
import { referenceScope, resolveReference } from '../symbolLinks'
|
||||
|
||||
/**
|
||||
* 解析引用时本文件的样子:文档版本、最近一次发出去的块树(没有时只从文件顶层找)、各条导入引进来的名字、代码里用到又没定义的名字各在哪几处。
|
||||
* 哪样换了,引用从哪个声明起找、导入和用到的名字在哪都可能跟着变。
|
||||
* 解析引用时这份文件的样子:文档版本、块树(本预览最近一次发出去的,或别的文件读成的;没有时只从文件顶层找)、各条导入引进来的名字、
|
||||
* 代码里用到又没定义的名字各在哪几处。哪样换了,引用从哪个声明起找、导入和用到的名字在哪都可能跟着变。
|
||||
*/
|
||||
export interface ReferenceInputs {
|
||||
version: number
|
||||
@@ -33,7 +33,10 @@ interface Round {
|
||||
symbols: Map<string, Kept<readonly SymbolNode[]>>
|
||||
}
|
||||
|
||||
/** 引用的键:同一个名字写在不同块的注释里分开记,指到的可以不一样(拼法同[页面那边的键](vscode/webview-ui/src/codeReader/repoLink.ts#referenceKey))。 */
|
||||
/**
|
||||
* 引用的键:同一个名字写在不同块的注释里分开记,指到的可以不一样。记着的都是同一份文件里的引用,不分 `file`
|
||||
* ([页面那边的键](vscode/webview-ui/src/codeReader/repoLink.ts#referenceKey)还要分)。
|
||||
*/
|
||||
function referenceKey({ symbol, block }: SymbolReference): string {
|
||||
return `${symbol}\u0000${block ?? ''}`
|
||||
}
|
||||
@@ -43,7 +46,7 @@ function sameInputs(a: ReferenceInputs, b: ReferenceInputs): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个预览里注释中不带路径的引用指到哪(见 [resolveReference]),解析过的记着:标链接、悬停、点击同一个引用只问一次语言扩展,同时问的等同一个 Promise。
|
||||
* 一份文件的注释里不带路径的引用指到哪(见 [resolveReference]),解析过的记着:标链接、悬停、点击同一个引用只问一次语言扩展,同时问的等同一个 Promise。
|
||||
*
|
||||
* 本文件的样子([ReferenceInputs])换了,记着的全部作废。指到的文件改过、关掉了的再问一遍,免得范围指错地方。
|
||||
* 找不到的、语言自带的、出错的不留,因为语言扩展还没准备好时会暂时答空,下次要再问。各文件的文档符号也这样记着,一批引用共用一份。
|
||||
|
||||
Reference in new issue
Block a user