🐛 fix(vscode): 注释里不带路径的引用从被注释的声明起一层层往外找,同类的方法、类的成员都能链到
This commit is contained in:
1 parent
15972c0b35
commit
5120e50228
15 files changed
+450
-167
No files matched your search
@@ -177,6 +177,14 @@ describe('挂在卡片子行上的子块', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('卡片记着块记号', () => {
|
||||
it('每张卡片带着它那块的记号(data-anchor):说明里不带路径的引用靠它认自己写在哪块', () => {
|
||||
const method = view('0.1', 'function', '', { anchor: 'class AgentService > function get_default_agent', translation: { markdown: '先看 [get_agent_admitted]' } })
|
||||
const markup = renderToStaticMarkup(<BlockLevel blocks={[method]} tree={state(new Set())} />)
|
||||
expect(markup).toMatch(/<div class="cr-block [^"]*"[^>]*data-anchor="class AgentService > function get_default_agent"[\s\S]*href="symbol:get_agent_admitted"/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('「注释」一行和「重写注释」按钮', () => {
|
||||
const comment = { label: '注释', value: { markdown: '按名字找' } }
|
||||
const html = (block: BlockView): string => renderToStaticMarkup(<BlockLevel blocks={[block]} tree={state(new Set())} />)
|
||||
|
||||
@@ -312,7 +312,10 @@ function cardSlots(block: BlockView, tree: BlockTreeState): GridSlots | undefine
|
||||
}
|
||||
}
|
||||
|
||||
/** 一块:折叠箭头、种类图标、预览内容或源码、右上角的一排图标按钮(BlockTools,只读的块树没有)。 */
|
||||
/**
|
||||
* 一块:折叠箭头、种类图标、预览内容或源码、右上角的一排图标按钮(BlockTools,只读的块树没有)。
|
||||
* 卡片上记着块记号(data-anchor),说明里不带路径的引用靠它认自己写在哪块(见 commentLinkOf)。
|
||||
*/
|
||||
function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||||
const { edit } = tree
|
||||
const editing = edit?.editing.has(block.id) ?? false
|
||||
@@ -327,6 +330,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
|
||||
className="cr-block relative flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2"
|
||||
{...kindProps(block)}
|
||||
data-id={block.id}
|
||||
data-anchor={block.anchor}
|
||||
data-line={block.startLine}
|
||||
data-editing={editing ? '' : undefined}
|
||||
data-change={change?.change}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DefinitionPeeks, linkPeekSource } from './definitionPeek'
|
||||
import { DiffText } from './DiffText'
|
||||
import { commentLinkOf } from './repoLink'
|
||||
import { MarkdownKindContext, TextPreview } from './TextPreview'
|
||||
|
||||
let container: HTMLDivElement
|
||||
@@ -121,7 +122,7 @@ describe.each(['TextPreview', 'DiffText'] as const)('%s 的 Markdown 页面元
|
||||
controller.setDelays(300, 500)
|
||||
const point = (): void => {
|
||||
const link = anchor()
|
||||
const source = linkPeekSource(link.getAttribute('href')!)
|
||||
const source = linkPeekSource(commentLinkOf(link))
|
||||
if (!source)
|
||||
throw new Error('符号链接不能预览')
|
||||
controller.point({ anchor: link, source })
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PeekTarget, ShownPeek } from './definitionPeek'
|
||||
import type { DefinitionPeek, WebviewToExtension } from './types'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DefinitionPeeks, linkPeekSource, placePeek } from './definitionPeek'
|
||||
import { parseCommentLink } from './repoLink'
|
||||
|
||||
const DELAY = 300
|
||||
const HIDING = 500
|
||||
@@ -99,6 +100,17 @@ describe('定义卡片什么时候弹', () => {
|
||||
expect(shown()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('不带路径的引用:问 peekLink 和点标题时都带着它写在哪块', () => {
|
||||
const { peeks, sent } = setup()
|
||||
const anchor = { name: 'reference' }
|
||||
peeks.point({ anchor, source: { symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent' } })
|
||||
vi.advanceTimersByTime(DELAY)
|
||||
expect(sent).toEqual([{ type: 'peekLink', id: 1, symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent' }])
|
||||
peeks.answer(1, PEEK)
|
||||
peeks.jump()
|
||||
expect(sent.at(-1)).toEqual({ type: 'openRepoLink', symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent' })
|
||||
})
|
||||
|
||||
it('还没收到 VS Code 的悬停延迟:不弹', () => {
|
||||
const sent: WebviewToExtension[] = []
|
||||
const peeks = new DefinitionPeeks<Anchor>(msg => sent.push(msg), () => {})
|
||||
@@ -208,15 +220,16 @@ describe('定义卡片什么时候收', () => {
|
||||
})
|
||||
|
||||
describe('链接能不能弹定义卡片', () => {
|
||||
it('指到符号的才能:带 #符号 的仓库链接、不带路径的引用', () => {
|
||||
expect(linkPeekSource('app/devices.py#Repo.find')).toEqual({ path: 'app/devices.py', symbol: 'Repo.find' })
|
||||
expect(linkPeekSource('symbol:DesktopOnly')).toEqual({ symbol: 'DesktopOnly' })
|
||||
it('指到符号的才能:带 #符号 的仓库链接、不带路径的引用(带着写在哪块)', () => {
|
||||
expect(linkPeekSource(parseCommentLink('app/devices.py#Repo.find'))).toEqual({ path: 'app/devices.py', symbol: 'Repo.find' })
|
||||
expect(linkPeekSource(parseCommentLink('symbol:DesktopOnly'))).toEqual({ symbol: 'DesktopOnly' })
|
||||
expect(linkPeekSource({ symbol: 'DesktopOnly', block: 'function gate' })).toEqual({ symbol: 'DesktopOnly', block: 'function gate' })
|
||||
})
|
||||
|
||||
it('只指到文件、目录的,网址:不能', () => {
|
||||
expect(linkPeekSource('app/devices.py')).toBeUndefined()
|
||||
expect(linkPeekSource('app/')).toBeUndefined()
|
||||
expect(linkPeekSource('https://example.com#x')).toBeUndefined()
|
||||
expect(linkPeekSource(parseCommentLink('app/devices.py'))).toBeUndefined()
|
||||
expect(linkPeekSource(parseCommentLink('app/'))).toBeUndefined()
|
||||
expect(linkPeekSource(parseCommentLink('https://example.com#x'))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CommentLink } from './repoLink'
|
||||
import type { DefinitionPeek, WebviewToExtension } from './types'
|
||||
import { parseCommentLink } from './repoLink'
|
||||
|
||||
/** 定义卡片浮窗的根上的属性:浮窗里的名字、链接是别的文件的,页面上管名字的那几处(跳到定义、高亮各处出现、再弹卡片)都不认。 */
|
||||
export const PEEK_ATTRIBUTE = 'data-peek'
|
||||
|
||||
/** 能弹定义卡片的东西:带源码下标的名字,或注释里指到符号的链接(带 #符号 的仓库链接、不带路径的引用)。 */
|
||||
export type PeekSource = { offset: number } | { path?: string, symbol: string }
|
||||
/** 能弹定义卡片的东西:带源码下标的名字,或注释里指到符号的链接(带 #符号 的仓库链接、不带路径的引用,后者带着写在哪块)。 */
|
||||
export type PeekSource = { offset: number } | (CommentLink & { symbol: string })
|
||||
|
||||
/** 指针下能弹定义卡片的一处:页面上的哪个元素(浮窗挂在它旁边),它是什么。 */
|
||||
export interface PeekTarget<A> {
|
||||
@@ -31,12 +31,9 @@ export function documentNameAt(target: EventTarget | null): HTMLElement | null {
|
||||
return name && !inPeek(name) ? name : null
|
||||
}
|
||||
|
||||
/** 注释里一个链接地址能不能弹定义卡片:指到符号的才能(带 #符号 的仓库链接、不带路径的引用),只指到文件、目录的不能。 */
|
||||
export function linkPeekSource(href: string): PeekSource | undefined {
|
||||
const link = parseCommentLink(href)
|
||||
if (link?.symbol === undefined)
|
||||
return undefined
|
||||
return link.path === undefined ? { symbol: link.symbol } : { path: link.path, symbol: link.symbol }
|
||||
/** 注释里一个链接(见 commentLinkOf)能不能弹定义卡片:指到符号的才能(带 #符号 的仓库链接、不带路径的引用),只指到文件、目录的不能。 */
|
||||
export function linkPeekSource(link: CommentLink | undefined): PeekSource | undefined {
|
||||
return link?.symbol === undefined ? undefined : { ...link, symbol: link.symbol }
|
||||
}
|
||||
|
||||
/** 浮窗与名字之间的间距。 */
|
||||
@@ -186,9 +183,8 @@ export class DefinitionPeeks<A> {
|
||||
this.post({ type: 'peekDefinition', id, version, offset: source.offset })
|
||||
return
|
||||
}
|
||||
const link = source.path === undefined ? { symbol: source.symbol } : { path: source.path, symbol: source.symbol }
|
||||
this.asked = { id, anchor, jump: { type: 'openRepoLink', ...link } }
|
||||
this.post({ type: 'peekLink', id, ...link })
|
||||
this.asked = { id, anchor, jump: { type: 'openRepoLink', ...source } }
|
||||
this.post({ type: 'peekLink', id, ...source })
|
||||
}
|
||||
|
||||
private cancelAsk(): void {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { CommentLink } from './repoLink'
|
||||
import type { WebviewToExtension } from './types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseCommentLink, parseRepoLink, RepoLinks, symbolHref } from './repoLink'
|
||||
import { commentLinkOf, parseCommentLink, parseRepoLink, RepoLinks, symbolHref } from './repoLink'
|
||||
|
||||
/** 页面上一个链接地址是哪种能点的链接;测试里写的都得是 */
|
||||
function link(href: string): CommentLink {
|
||||
const parsed = parseCommentLink(href)
|
||||
if (!parsed)
|
||||
throw new Error(`不是能点的链接:${href}`)
|
||||
return parsed
|
||||
}
|
||||
|
||||
describe('parseRepoLink', () => {
|
||||
it('合法路径', () => {
|
||||
@@ -55,36 +66,36 @@ function setup(): { links: RepoLinks, sent: WebviewToExtension[] } {
|
||||
describe('repoLinks', () => {
|
||||
it('点仓库链接发给扩展端,http 不管', () => {
|
||||
const { links, sent } = setup()
|
||||
expect(links.click('backend/src/luffy_agent/agent/models.py#Agent.owner_id')).toBe(true)
|
||||
expect(links.click(parseCommentLink('backend/src/luffy_agent/agent/models.py#Agent.owner_id'))).toBe(true)
|
||||
expect(sent).toEqual([{
|
||||
type: 'openRepoLink',
|
||||
path: 'backend/src/luffy_agent/agent/models.py',
|
||||
symbol: 'Agent.owner_id',
|
||||
}])
|
||||
expect(links.click('https://example.com')).toBe(false)
|
||||
expect(links.click('file.py#123')).toBe(false)
|
||||
expect(links.click(parseCommentLink('https://example.com'))).toBe(false)
|
||||
expect(links.click(parseCommentLink('file.py#123'))).toBe(false)
|
||||
})
|
||||
|
||||
it('路径变了才问哪些文件没有,迟到的回答丢掉', () => {
|
||||
const { links, sent } = setup()
|
||||
links.scan(['vendor/agents', 'https://example.com', 'backend/a.py#Agent'])
|
||||
links.scan(['backend/a.py#Agent', 'vendor/agents'])
|
||||
expect(sent).toEqual([{ type: 'checkRepoLinks', id: 1, paths: ['backend/a.py', 'vendor/agents'], symbols: [] }])
|
||||
links.scan([link('vendor/agents'), link('backend/a.py#Agent')])
|
||||
links.scan([link('backend/a.py#Agent'), link('vendor/agents')])
|
||||
expect(sent).toEqual([{ type: 'checkRepoLinks', id: 1, paths: ['backend/a.py', 'vendor/agents'], references: [] }])
|
||||
|
||||
expect(links.checked(0, ['vendor/agents'], [])).toBe(false)
|
||||
expect(links.isBroken('vendor/agents')).toBe(false)
|
||||
expect(links.isBroken(link('vendor/agents'))).toBe(false)
|
||||
expect(links.checked(1, ['vendor/agents'], [])).toBe(true)
|
||||
expect(links.isBroken('vendor/agents')).toBe(true)
|
||||
expect(links.isBroken('backend/a.py#Agent')).toBe(false)
|
||||
expect(links.isBroken(link('vendor/agents'))).toBe(true)
|
||||
expect(links.isBroken(link('backend/a.py#Agent'))).toBe(false)
|
||||
})
|
||||
|
||||
it('符号没有才标坏;打开成功就不再标', () => {
|
||||
const { links } = setup()
|
||||
links.broken('backend/a.py', 'Agent')
|
||||
expect(links.isBroken('backend/a.py#Agent')).toBe(true)
|
||||
expect(links.isBroken('backend/a.py')).toBe(false)
|
||||
links.opened('backend/a.py', 'Agent')
|
||||
expect(links.isBroken('backend/a.py#Agent')).toBe(false)
|
||||
links.broken({ path: 'backend/a.py', symbol: 'Agent' })
|
||||
expect(links.isBroken(link('backend/a.py#Agent'))).toBe(true)
|
||||
expect(links.isBroken(link('backend/a.py'))).toBe(false)
|
||||
links.opened({ path: 'backend/a.py', symbol: 'Agent' })
|
||||
expect(links.isBroken(link('backend/a.py#Agent'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,32 +109,83 @@ describe('不带路径的引用', () => {
|
||||
expect(parseCommentLink('backend/a.py#Agent')).toEqual({ path: 'backend/a.py', symbol: 'Agent' })
|
||||
})
|
||||
|
||||
it('点了发 openRepoLink(没有 path);和仓库路径一起问,找不到的标坏,打开成功就不再标', () => {
|
||||
it('点了发 openRepoLink(没有 path,带着写在哪块);和仓库路径一起问,找不到的标坏,打开成功就不再标', () => {
|
||||
const { links, sent } = setup()
|
||||
expect(links.click('symbol:LocalRuntimeDisabled')).toBe(true)
|
||||
expect(sent).toEqual([{ type: 'openRepoLink', symbol: 'LocalRuntimeDisabled' }])
|
||||
expect(links.click({ symbol: 'LocalRuntimeDisabled', block: 'function gate' })).toBe(true)
|
||||
expect(sent).toEqual([{ type: 'openRepoLink', symbol: 'LocalRuntimeDisabled', block: 'function gate' }])
|
||||
|
||||
links.scan(['symbol:USER', 'backend/a.py', 'symbol:Missing', 'symbol:USER'])
|
||||
expect(sent.at(-1)).toEqual({ type: 'checkRepoLinks', id: 1, paths: ['backend/a.py'], symbols: ['Missing', 'USER'] })
|
||||
expect(links.checked(1, [], ['Missing'])).toBe(true)
|
||||
expect(links.isBroken('symbol:Missing')).toBe(true)
|
||||
expect(links.isBroken('symbol:USER')).toBe(false)
|
||||
links.opened(undefined, 'Missing')
|
||||
expect(links.isBroken('symbol:Missing')).toBe(false)
|
||||
links.broken(undefined, 'USER')
|
||||
expect(links.isBroken('symbol:USER')).toBe(true)
|
||||
links.scan([{ symbol: 'USER', block: 'a' }, link('backend/a.py'), { symbol: 'Missing', block: 'a' }, { symbol: 'USER', block: 'a' }])
|
||||
expect(sent.at(-1)).toEqual({ type: 'checkRepoLinks', id: 1, paths: ['backend/a.py'], references: [{ symbol: 'Missing', block: 'a' }, { symbol: 'USER', block: 'a' }] })
|
||||
expect(links.checked(1, [], [{ symbol: 'Missing', block: 'a' }])).toBe(true)
|
||||
expect(links.isBroken({ symbol: 'Missing', block: 'a' })).toBe(true)
|
||||
expect(links.isBroken({ symbol: 'USER', block: 'a' })).toBe(false)
|
||||
links.opened({ symbol: 'Missing', block: 'a' })
|
||||
expect(links.isBroken({ symbol: 'Missing', block: 'a' })).toBe(false)
|
||||
links.broken({ symbol: 'USER', block: 'a' })
|
||||
expect(links.isBroken({ symbol: 'USER', block: 'a' })).toBe(true)
|
||||
})
|
||||
|
||||
it('同一个名字写在两块的注释里:分开问,一块找得到一块找不到时各标各的', () => {
|
||||
const { links, sent } = setup()
|
||||
const inMethod = { symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent' }
|
||||
const inFunction = { symbol: 'get_agent_admitted', block: 'function helper' }
|
||||
const outside = { symbol: 'get_agent_admitted' }
|
||||
links.scan([inMethod, inFunction, outside, inMethod])
|
||||
expect(sent).toEqual([{ type: 'checkRepoLinks', id: 1, paths: [], references: [outside, inMethod, inFunction] }])
|
||||
|
||||
links.checked(1, [], [inFunction, outside])
|
||||
expect(links.isBroken(inMethod)).toBe(false)
|
||||
expect(links.isBroken(inFunction)).toBe(true)
|
||||
expect(links.isBroken(outside)).toBe(true)
|
||||
|
||||
links.opened(inFunction)
|
||||
links.broken(inMethod)
|
||||
expect(links.isBroken(inFunction)).toBe(false)
|
||||
expect(links.isBroken(inMethod)).toBe(true)
|
||||
expect(links.isBroken(outside)).toBe(true)
|
||||
})
|
||||
|
||||
it('同样的链接不再问;文档换了版本(stale)再问一次', () => {
|
||||
const { links, sent } = setup()
|
||||
links.scan(['symbol:USER'])
|
||||
links.scan(['symbol:USER'])
|
||||
links.scan([{ symbol: 'USER', block: 'a' }])
|
||||
links.scan([{ symbol: 'USER', block: 'a' }])
|
||||
expect(sent).toHaveLength(1)
|
||||
links.stale()
|
||||
links.scan(['symbol:USER'])
|
||||
links.scan([{ symbol: 'USER', block: 'a' }])
|
||||
expect(sent).toEqual([
|
||||
{ type: 'checkRepoLinks', id: 1, paths: [], symbols: ['USER'] },
|
||||
{ type: 'checkRepoLinks', id: 2, paths: [], symbols: ['USER'] },
|
||||
{ type: 'checkRepoLinks', id: 1, paths: [], references: [{ symbol: 'USER', block: 'a' }] },
|
||||
{ type: 'checkRepoLinks', id: 2, paths: [], references: [{ symbol: 'USER', block: 'a' }] },
|
||||
])
|
||||
})
|
||||
|
||||
it('名字一样、写在另一块里的也算变了,要再问', () => {
|
||||
const { links, sent } = setup()
|
||||
links.scan([{ symbol: 'USER', block: 'a' }])
|
||||
links.scan([{ symbol: 'USER', block: 'b' }])
|
||||
expect(sent.map(msg => msg.type === 'checkRepoLinks' && msg.references)).toEqual([[{ symbol: 'USER', block: 'a' }], [{ symbol: 'USER', block: 'b' }]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('页面上的链接写在哪块的注释里', () => {
|
||||
it('不带路径的引用带上离它最近的块卡片的块记号;仓库路径链接、不在块卡片里的、删掉的块(没有块记号)里的不带', () => {
|
||||
document.body.innerHTML = `
|
||||
<div class="cr-block" data-anchor="class AgentService > function get_default_agent">
|
||||
<div class="cr-markdown"><a id="method" href="symbol:get_agent_admitted">get_agent_admitted</a><a id="path" href="backend/a.py#Agent">Agent</a></div>
|
||||
<div class="cr-block" data-anchor="class AgentService > function get_default_agent > if @3fa1c2d0">
|
||||
<div class="cr-markdown"><a id="slotted" href="symbol:get_agent_admitted">get_agent_admitted</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cr-block" data-anchor="function helper"><div class="cr-markdown"><a id="function" href="symbol:get_agent_admitted">x</a></div></div>
|
||||
<div class="cr-block cr-removed"><div class="cr-markdown"><a id="removed" href="symbol:get_agent_admitted">x</a></div></div>
|
||||
<div class="cr-markdown"><a id="outside" href="symbol:get_agent_admitted">x</a><a id="web" href="https://example.com">x</a></div>
|
||||
`
|
||||
const at = (id: string): CommentLink | undefined => commentLinkOf(document.getElementById(id)!)
|
||||
expect(at('method')).toEqual({ symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent' })
|
||||
expect(at('slotted')).toEqual({ symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent > if @3fa1c2d0' })
|
||||
expect(at('function')).toEqual({ symbol: 'get_agent_admitted', block: 'function helper' })
|
||||
expect(at('path')).toEqual({ path: 'backend/a.py', symbol: 'Agent' })
|
||||
expect(at('removed')).toEqual({ symbol: 'get_agent_admitted' })
|
||||
expect(at('outside')).toEqual({ symbol: 'get_agent_admitted' })
|
||||
expect(at('web')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WebviewToExtension } from './types'
|
||||
import type { SymbolReference, WebviewToExtension } from './types'
|
||||
|
||||
/** 注释里指向仓库代码的链接:从仓库根起的路径,可选一个符号。 */
|
||||
export interface RepoLink {
|
||||
@@ -9,12 +9,11 @@ export interface RepoLink {
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`):没有路径,扩展端按当前文件找这个符号(本文件定义的,再是导入的)。
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`):没有路径,扩展端按当前文件、从它写在的那块(block,见 commentLinkOf)找这个符号。
|
||||
* 页面上它是地址为 `symbol:Name` 的链接(见 symbolHref)。
|
||||
*/
|
||||
export interface SymbolLink {
|
||||
export interface SymbolLink extends SymbolReference {
|
||||
path?: undefined
|
||||
symbol: string
|
||||
}
|
||||
|
||||
/** 注释里能点的链接:仓库路径链接,或不带路径的引用。 */
|
||||
@@ -85,13 +84,37 @@ function isSymbol(symbol: string): boolean {
|
||||
return (parts.length === 1 || parts.length === 2) && parts.every(part => IDENT.test(part))
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面上一个链接元素是哪种能点的链接(同 parseCommentLink)。不带路径的引用再带上它写在哪块的注释里:离它最近的那张块卡片(.cr-block)
|
||||
* 上记着的块记号(data-anchor);不在块卡片里的、那张卡片没记块记号的(删掉的块)不带,扩展端只从文件顶层找。
|
||||
*/
|
||||
export function commentLinkOf(element: Element): CommentLink | undefined {
|
||||
const link = parseCommentLink(element.getAttribute('href') ?? '')
|
||||
if (!link || link.path !== undefined)
|
||||
return link
|
||||
const block = element.closest<HTMLElement>('.cr-block')?.dataset.anchor
|
||||
return block === undefined ? link : { ...link, block }
|
||||
}
|
||||
|
||||
function symbolKey(path: string, symbol: string): string {
|
||||
return `${path}#${symbol}`
|
||||
}
|
||||
|
||||
/** 扩展端报回来打没打开的一个链接(repoLinkBroken / repoLinkOpened),path、symbol、block 同 openRepoLink。 */
|
||||
interface ReportedLink {
|
||||
path?: string
|
||||
symbol?: string
|
||||
block?: string
|
||||
}
|
||||
|
||||
/** 不带路径的引用的键:同一个名字写在不同块的注释里分开记(指到的可以不一样)。 */
|
||||
function referenceKey({ symbol, block }: SymbolReference): string {
|
||||
return `${symbol}\u0000${block ?? ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面上的仓库链接和不带路径的引用:点了发给扩展端打开;预览出来后把路径、引用发去问哪些文件没有、哪些引用找不到;
|
||||
* 扩展端说没有的,标成坏的。仓库链接的符号在不在要点了才知道。
|
||||
* 扩展端说没有的,标成坏的。仓库链接的符号在不在要点了才知道。不带路径的引用按(写在哪块,名字)分开问、分开标。
|
||||
*/
|
||||
export class RepoLinks {
|
||||
private request = 0
|
||||
@@ -99,36 +122,36 @@ export class RepoLinks {
|
||||
private sent = ''
|
||||
private missingPaths = new Set<string>()
|
||||
private missingSymbols = new Set<string>()
|
||||
/** 找不到的不带路径的引用 */
|
||||
/** 找不到的不带路径的引用(referenceKey) */
|
||||
private unresolved = new Set<string>()
|
||||
|
||||
constructor(private readonly post: (msg: WebviewToExtension) => void) {}
|
||||
|
||||
/** 点在仓库链接或不带路径的引用上:发给扩展端打开,返回 true。别的链接不管。 */
|
||||
click(href: string | null): boolean {
|
||||
const link = href === null ? undefined : parseCommentLink(href)
|
||||
/** 点在仓库链接或不带路径的引用上(见 commentLinkOf):发给扩展端打开,返回 true。别的链接(没认出来的)不管。 */
|
||||
click(link: CommentLink | undefined): boolean {
|
||||
if (!link)
|
||||
return false
|
||||
this.post({ type: 'openRepoLink', ...(link.path !== undefined && { path: link.path }), ...(link.symbol !== undefined && { symbol: link.symbol }) })
|
||||
this.post({ type: 'openRepoLink', ...link })
|
||||
return true
|
||||
}
|
||||
|
||||
/** 页面上现有的链接。路径、引用变了(或文档换了版本,见 stale)才问扩展端哪些路径没有、哪些引用找不到。 */
|
||||
scan(hrefs: readonly string[]): void {
|
||||
const links = hrefs.flatMap((href) => {
|
||||
const link = parseCommentLink(href)
|
||||
return link ? [link] : []
|
||||
})
|
||||
/** 页面上现有的链接。路径、引用(名字或写在哪块)变了(或文档换了版本,见 stale)才问扩展端哪些路径没有、哪些引用找不到。 */
|
||||
scan(links: readonly CommentLink[]): void {
|
||||
const paths = [...new Set(links.flatMap(link => link.path === undefined ? [] : [link.path]))].sort()
|
||||
const symbols = [...new Set(links.flatMap(link => link.path === undefined ? [link.symbol] : []))].sort()
|
||||
const key = `${paths.join('\n')}\u0000${symbols.join('\n')}`
|
||||
const references = new Map<string, SymbolReference>()
|
||||
for (const link of links) {
|
||||
if (link.path === undefined)
|
||||
references.set(referenceKey(link), link)
|
||||
}
|
||||
const keys = [...references.keys()].sort()
|
||||
const key = JSON.stringify([paths, keys])
|
||||
if (key === this.sent)
|
||||
return
|
||||
this.sent = key
|
||||
if (paths.length === 0 && symbols.length === 0)
|
||||
if (paths.length === 0 && keys.length === 0)
|
||||
return
|
||||
this.request += 1
|
||||
this.post({ type: 'checkRepoLinks', id: this.request, paths, symbols })
|
||||
this.post({ type: 'checkRepoLinks', id: this.request, paths, references: keys.map(item => references.get(item)!) })
|
||||
}
|
||||
|
||||
/** 文档换了版本:引用指到哪跟着源码变(加了定义、改了导入),下次 scan 照样再问。 */
|
||||
@@ -137,19 +160,19 @@ export class RepoLinks {
|
||||
}
|
||||
|
||||
/** id 那次询问的回答。不是最近一次的丢掉,返回这次有没有用上。 */
|
||||
checked(id: number, missing: readonly string[], unresolved: readonly string[]): boolean {
|
||||
checked(id: number, missing: readonly string[], unresolved: readonly SymbolReference[]): boolean {
|
||||
if (id !== this.request)
|
||||
return false
|
||||
this.missingPaths = new Set(missing)
|
||||
this.unresolved = new Set(unresolved)
|
||||
this.unresolved = new Set(unresolved.map(referenceKey))
|
||||
return true
|
||||
}
|
||||
|
||||
/** 扩展端打开时发现文件或符号没有;没有路径的是不带路径的引用找不到。 */
|
||||
broken(path: string | undefined, symbol?: string): void {
|
||||
/** 扩展端打开时发现文件或符号没有;没有路径的是写在 block 那块里的不带路径的引用找不到。 */
|
||||
broken({ path, symbol, block }: ReportedLink): void {
|
||||
if (path === undefined) {
|
||||
if (symbol !== undefined)
|
||||
this.unresolved.add(symbol)
|
||||
this.unresolved.add(referenceKey({ symbol, block }))
|
||||
return
|
||||
}
|
||||
if (symbol !== undefined)
|
||||
@@ -158,11 +181,11 @@ export class RepoLinks {
|
||||
this.missingPaths.add(path)
|
||||
}
|
||||
|
||||
/** 打开成功:这个路径在,这个符号也在;没有路径的是不带路径的引用找到了。 */
|
||||
opened(path: string | undefined, symbol?: string): void {
|
||||
/** 打开成功:这个路径在,这个符号也在;没有路径的是写在 block 那块里的不带路径的引用找到了。 */
|
||||
opened({ path, symbol, block }: ReportedLink): void {
|
||||
if (path === undefined) {
|
||||
if (symbol !== undefined)
|
||||
this.unresolved.delete(symbol)
|
||||
this.unresolved.delete(referenceKey({ symbol, block }))
|
||||
return
|
||||
}
|
||||
this.missingPaths.delete(path)
|
||||
@@ -170,13 +193,12 @@ export class RepoLinks {
|
||||
this.missingSymbols.delete(symbolKey(path, symbol))
|
||||
}
|
||||
|
||||
/** 这个地址现在是不是坏的(文件没有,这个符号没有,或引用找不到)。还没问到的不算。 */
|
||||
isBroken(href: string): boolean {
|
||||
const link = parseCommentLink(href)
|
||||
/** 这个链接现在是不是坏的(文件没有,这个符号没有,或写在这块里的引用找不到)。还没问到的不算。 */
|
||||
isBroken(link: CommentLink | undefined): boolean {
|
||||
if (!link)
|
||||
return false
|
||||
if (link.path === undefined)
|
||||
return this.unresolved.has(link.symbol)
|
||||
return this.unresolved.has(referenceKey(link))
|
||||
if (this.missingPaths.has(link.path))
|
||||
return true
|
||||
return link.symbol !== undefined && this.missingSymbols.has(symbolKey(link.path, link.symbol))
|
||||
|
||||
@@ -184,6 +184,16 @@ export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: Pre
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
/**
|
||||
* 注释里一个不带路径的引用(`[Name]`、`[Class.member]`):symbol 是写的名字;block 是这段注释属于哪一块(块的 anchor),
|
||||
* 扩展端从那块代码的声明起一层层往外找。不在哪块的注释里的没有 block,只从文件顶层找。
|
||||
* 同一个名字写在不同块的注释里,指到的可以不一样。
|
||||
*/
|
||||
export interface SymbolReference {
|
||||
symbol: string
|
||||
block?: string
|
||||
}
|
||||
|
||||
/** 这份文件里出现过的一个属性值:显示名和有几块是这个值 */
|
||||
export interface AttributeValueCount {
|
||||
id: string
|
||||
@@ -472,9 +482,10 @@ export type OpenedFrom = 'uncommitted' | 'other'
|
||||
* displayNames:version 这份文档里显示成中文名的名字,查到一步来一步;不是页面上这个版本的不理
|
||||
* diff:这份文件相比对比基准改了什么(审查工作副本里的、从「未提交的更改」点开的有改动的文件才有);每来一份文档之后补来,基准变了、换了从哪儿打开再来;null 是不比了
|
||||
* openedFrom:这个标签页是从哪儿打开的,记进页面状态;ready 后来一次,开着时又从侧栏点开、换了从哪儿打开的再来
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用(原样带回问的那几条)
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到,
|
||||
* block 同 openRepoLink
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block 同 openRepoLink
|
||||
* repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在
|
||||
* callTreeCounts:version 这份文档里,每个能看调用树的块落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
|
||||
* callTree:anchor 那块的调用树。trees 空着、又没有 contractMiss 是没有调用它的地方;接口卡片在契约文件里没对上时带 contractMiss。
|
||||
@@ -538,9 +549,9 @@ export type ExtensionToWebview
|
||||
| RewriteEnd
|
||||
| { type: 'diff', diff: DocumentDiff | null }
|
||||
| { type: 'openedFrom', from: OpenedFrom }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: string[] }
|
||||
| { type: 'repoLinkBroken', path?: string, symbol?: string }
|
||||
| { type: 'repoLinkOpened', path?: string, symbol?: string }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[] }
|
||||
| { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] }
|
||||
| { type: 'focusPathAsk' }
|
||||
| { type: 'callTreeCounts', version: number, counts: Record<string, number> }
|
||||
@@ -554,7 +565,7 @@ export type ExtensionToWebview
|
||||
* version 是这份下标所属的文档版本
|
||||
* goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义
|
||||
* peekDefinition:鼠标停在带下标的名字上,要 offset 处名字的定义卡片(几处定义取第一处);id、version 同 probeDefinition
|
||||
* peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol 同 openRepoLink
|
||||
* peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block 同 openRepoLink
|
||||
* findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition
|
||||
* setFilter:改块筛选(整份替换);不在本地先改,等扩展端发回 filter 再按它显示
|
||||
* setOnlyChanges:打开 / 关掉「只看改动」;同样等扩展端发回 filter 再按它显示
|
||||
@@ -570,8 +581,9 @@ export type ExtensionToWebview
|
||||
* selectProfile:kind 那种对话换 profile;不在本地先改,等扩展端发回这种的 profiles 再按它显示
|
||||
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发
|
||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按这份文件找 symbol
|
||||
* checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(symbols)问哪些找不到。id 标出是哪一次问
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按这份文件找 symbol:block 是它写在哪块的注释里(见 SymbolReference),
|
||||
* 从那块的声明起一层层往外找
|
||||
* checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(references)问哪些找不到。id 标出是哪一次问
|
||||
* checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问
|
||||
* openCallTree:打开 anchor 那块的调用树。version 是这块所在的文档版本
|
||||
* goToCallNode:点了调用树上的一个节点,打开 uri 那个文件,跳到 line 行 character 列(见 CallTreeNode.jump)
|
||||
@@ -590,15 +602,15 @@ 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 }
|
||||
| { type: 'peekLink', id: number, path?: string, symbol: string, block?: string }
|
||||
| { type: 'findOccurrences', id: number, version: number, offset: number }
|
||||
| { type: 'ask', askId: string, anchor: string, question: string, images: AskImage[] }
|
||||
| { type: 'stopAsk', askId: string }
|
||||
| { type: 'deleteQa', anchor: string, entryId: string }
|
||||
| { type: 'selectProfile', kind: ChatKind, id: string }
|
||||
| { type: 'occurrenceNavigation', available: boolean }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], references: SymbolReference[] }
|
||||
| { type: 'checkRepoImages', id: number, paths: string[] }
|
||||
| { type: 'openCallTree', version: number, anchor: string }
|
||||
| { type: 'goToCallNode', uri: string, line: number, character: number }
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PeekTarget, ShownPeek } from './definitionPeek'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { DefinitionPeeks, documentNameAt, inPeek, linkPeekSource } from './definitionPeek'
|
||||
import { commentLinkOf } from './repoLink'
|
||||
import { onMessage, postMessage } from './vscode'
|
||||
|
||||
/** 指针下能弹定义卡片的一处:注释里指到符号的链接,或页面上这份文档带下标的名字;浮窗里的不算。 */
|
||||
@@ -9,7 +10,7 @@ function peekTargetAt(target: EventTarget | null): PeekTarget<HTMLElement> | nul
|
||||
return null
|
||||
const link = target.closest<HTMLAnchorElement>('.cr-markdown a')
|
||||
if (link) {
|
||||
const source = linkPeekSource(link.getAttribute('href') ?? '')
|
||||
const source = linkPeekSource(commentLinkOf(link))
|
||||
return source ? { anchor: link, source } : null
|
||||
}
|
||||
const name = documentNameAt(target)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { RepoLinks } from './repoLink'
|
||||
import { commentLinkOf, RepoLinks } from './repoLink'
|
||||
import { onMessage, postMessage } from './vscode'
|
||||
|
||||
/** 坏链接悬停上的字,走页面上统一的 data-tooltip,不用原生 title。 */
|
||||
@@ -32,12 +32,13 @@ function paintLink(anchor: HTMLAnchorElement, broken: boolean): void {
|
||||
|
||||
function paint(links: RepoLinks): void {
|
||||
for (const anchor of document.querySelectorAll<HTMLAnchorElement>('.cr-markdown a'))
|
||||
paintLink(anchor, links.isBroken(anchor.getAttribute('href') ?? ''))
|
||||
paintLink(anchor, links.isBroken(commentLinkOf(anchor)))
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释、说明、问 AI 的回答里的仓库路径链接,和注释里不带路径的引用:点了发给扩展端打开(不走 webview 把 http 链接交给外部打开的那条),
|
||||
* 画出来之后把路径、引用发去问哪些文件没有、哪些引用找不到;文档换了版本再问一次(引用跟着源码变)。
|
||||
* 不带路径的引用都带着它写在哪块(见 commentLinkOf),扩展端从那块的声明起找。
|
||||
*/
|
||||
export function useRepoLinks(): void {
|
||||
const [links] = useState(() => new RepoLinks(postMessage))
|
||||
@@ -47,10 +48,10 @@ export function useRepoLinks(): void {
|
||||
const scan = (): void => {
|
||||
window.clearTimeout(timer)
|
||||
timer = window.setTimeout(() => {
|
||||
const anchors = [...document.querySelectorAll<HTMLAnchorElement>('.cr-markdown a')]
|
||||
for (const anchor of anchors)
|
||||
paintLink(anchor, links.isBroken(anchor.getAttribute('href') ?? ''))
|
||||
links.scan(anchors.map(anchor => anchor.getAttribute('href') ?? ''))
|
||||
const found = [...document.querySelectorAll<HTMLAnchorElement>('.cr-markdown a')].map(anchor => ({ anchor, link: commentLinkOf(anchor) }))
|
||||
for (const { anchor, link } of found)
|
||||
paintLink(anchor, links.isBroken(link))
|
||||
links.scan(found.flatMap(({ link }) => link ?? []))
|
||||
}, 50)
|
||||
}
|
||||
const off = onMessage((msg) => {
|
||||
@@ -65,19 +66,19 @@ export function useRepoLinks(): void {
|
||||
return
|
||||
}
|
||||
if (msg.type === 'repoLinkBroken') {
|
||||
links.broken(msg.path, msg.symbol)
|
||||
links.broken(msg)
|
||||
paint(links)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'repoLinkOpened') {
|
||||
links.opened(msg.path, msg.symbol)
|
||||
links.opened(msg)
|
||||
paint(links)
|
||||
}
|
||||
})
|
||||
// 捕获阶段:赶在 webview 把 <a> 交给外部打开之前拦下仓库链接
|
||||
const onClick = (event: MouseEvent): void => {
|
||||
const anchor = event.target instanceof Element ? event.target.closest('a') : null
|
||||
if (!anchor || !links.click(anchor.getAttribute('href')))
|
||||
if (!anchor || !links.click(commentLinkOf(anchor)))
|
||||
return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
Reference in new issue
Block a user