🐛 fix(vscode): 代码阅读 ⌘ 悬停每次现问定义,语言扩展启动后就能链上、点击总去跳
This commit is contained in:
1 parent
d412f4f01e
commit
ea5935135b
7 files changed
+204
-71
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import type { WebviewToExtension } from './types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { DefinitionLinks } from './definitionLinks'
|
||||
|
||||
function setup(version: number | undefined): { links: DefinitionLinks, sent: WebviewToExtension[], probes: () => { id: number, version: number, offset: number }[] } {
|
||||
const sent: WebviewToExtension[] = []
|
||||
const links = new DefinitionLinks(msg => sent.push(msg))
|
||||
links.setVersion(version)
|
||||
const probes = (): { id: number, version: number, offset: number }[] => sent.flatMap(msg => msg.type === 'probeDefinition' ? [{ id: msg.id, version: msg.version, offset: msg.offset }] : [])
|
||||
return { links, sent, probes }
|
||||
}
|
||||
|
||||
describe('definitionLinks', () => {
|
||||
it('问到「没有」后移开再指回来会重新问,语言扩展启动好了就能链上', () => {
|
||||
const { links, probes } = setup(1)
|
||||
links.point(54)
|
||||
links.answer(probes()[0].id, false)
|
||||
expect(links.linked).toBeUndefined()
|
||||
|
||||
links.point(undefined)
|
||||
links.point(54)
|
||||
const [first, second] = probes()
|
||||
expect(second).toEqual({ id: second.id, version: 1, offset: 54 })
|
||||
expect(second.id).not.toBe(first.id)
|
||||
|
||||
links.answer(second.id, true)
|
||||
expect(links.linked).toBe(54)
|
||||
})
|
||||
|
||||
it('修饰键 + 点击名字总交给扩展端去跳,问到过「没有」也一样', () => {
|
||||
const { links, sent, probes } = setup(1)
|
||||
links.point(54)
|
||||
links.answer(probes()[0].id, false)
|
||||
|
||||
expect(links.click(54)).toBe(true)
|
||||
expect(sent.at(-1)).toEqual({ type: 'goToDefinition', version: 1, offset: 54 })
|
||||
})
|
||||
|
||||
it('还指着同一个名字不重问', () => {
|
||||
const { links, probes } = setup(1)
|
||||
links.point(54)
|
||||
links.point(54)
|
||||
expect(probes()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('迟到的旧回答丢掉,只认最近一次询问的回答', () => {
|
||||
const { links, probes } = setup(1)
|
||||
links.point(54)
|
||||
links.point(79)
|
||||
links.point(54)
|
||||
const [stale, , latest] = probes()
|
||||
links.answer(latest.id, true)
|
||||
links.answer(stale.id, false)
|
||||
expect(links.linked).toBe(54)
|
||||
})
|
||||
|
||||
it('松开修饰键或移开就不链接,之后到的回答也不再挂上', () => {
|
||||
const { links, probes } = setup(1)
|
||||
links.point(54)
|
||||
const [probe] = probes()
|
||||
links.answer(probe.id, true)
|
||||
expect(links.linked).toBe(54)
|
||||
|
||||
links.point(undefined)
|
||||
expect(links.linked).toBeUndefined()
|
||||
links.answer(probe.id, true)
|
||||
expect(links.linked).toBeUndefined()
|
||||
})
|
||||
|
||||
it('换了文档版本:正指着的名字按新版本重问,旧版本的回答丢掉', () => {
|
||||
const { links, probes } = setup(1)
|
||||
links.point(54)
|
||||
links.setVersion(2)
|
||||
const [old, renewed] = probes()
|
||||
expect(renewed).toEqual({ id: renewed.id, version: 2, offset: 54 })
|
||||
|
||||
links.answer(old.id, true)
|
||||
expect(links.linked).toBeUndefined()
|
||||
links.answer(renewed.id, true)
|
||||
expect(links.linked).toBe(54)
|
||||
})
|
||||
|
||||
it('还没收到文档时不问,点击也不拦', () => {
|
||||
const { links, sent } = setup(undefined)
|
||||
links.point(54)
|
||||
expect(links.click(54)).toBe(false)
|
||||
expect(sent).toEqual([])
|
||||
})
|
||||
|
||||
it('没指着名字的点击不拦', () => {
|
||||
const { links } = setup(1)
|
||||
expect(links.click(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { WebviewToExtension } from './types'
|
||||
|
||||
/**
|
||||
* 按住 ⌘ / Ctrl 指着名字时问不问定义、链接显示在哪:和编辑器一样,每回指到一个名字上都现问。
|
||||
* 语言扩展(Pylance、tsserver)刚启动那几秒会答「没有」,所以回答不留到下次指过来时用。
|
||||
* 只管下标,不碰 DOM;按住修饰键时指着的名字下标由调用方给,不按就给 undefined。
|
||||
*/
|
||||
export class DefinitionLinks {
|
||||
private version: number | undefined
|
||||
/** 按住修饰键时指着的名字的下标 */
|
||||
private target: number | undefined
|
||||
/** 为 target 发出的最近一次询问;它的回答之外的迟到回答都丢掉 */
|
||||
private request = 0
|
||||
private found = false
|
||||
|
||||
constructor(private readonly post: (msg: WebviewToExtension) => void) {}
|
||||
|
||||
/** 链接该显示在哪个下标的名字上;没有就是 undefined */
|
||||
get linked(): number | undefined {
|
||||
return this.found ? this.target : undefined
|
||||
}
|
||||
|
||||
/** 换了一份文档:下标换了意思,正指着的名字按新版本重问 */
|
||||
setVersion(version: number | undefined): void {
|
||||
if (version === this.version)
|
||||
return
|
||||
this.version = version
|
||||
this.ask(this.target)
|
||||
}
|
||||
|
||||
/** 指着的名字(或修饰键)变了;还指着同一个名字就不重问 */
|
||||
point(offset: number | undefined): void {
|
||||
if (offset !== this.target)
|
||||
this.ask(offset)
|
||||
}
|
||||
|
||||
answer(id: number, found: boolean): void {
|
||||
if (id === this.request && this.target !== undefined)
|
||||
this.found = found
|
||||
}
|
||||
|
||||
/** 修饰键 + 点击名字:交给扩展端去跳,返回 true 表示这次点击归跳转,不再给块上的按钮 */
|
||||
click(offset: number | undefined): boolean {
|
||||
if (offset === undefined || this.version === undefined)
|
||||
return false
|
||||
this.post({ type: 'goToDefinition', version: this.version, offset })
|
||||
return true
|
||||
}
|
||||
|
||||
private ask(offset: number | undefined): void {
|
||||
this.target = offset
|
||||
this.found = false
|
||||
this.request++
|
||||
if (offset !== undefined && this.version !== undefined)
|
||||
this.post({ type: 'probeDefinition', id: this.request, version: this.version, offset })
|
||||
}
|
||||
}
|
||||
@@ -69,17 +69,18 @@ export interface DocumentMessage {
|
||||
|
||||
/**
|
||||
* reveal:已经开着的预览滚到某处(同 DocumentMessage.reveal)
|
||||
* definitionProbed:probeDefinition 的回答,found 表示查得到定义
|
||||
* definitionProbed:id 那次 probeDefinition 的回答,found 表示查得到定义
|
||||
*/
|
||||
export type ExtensionToWebview
|
||||
= | DocumentMessage
|
||||
| ({ type: 'reveal' } & Reveal)
|
||||
| { type: 'definitionProbed', version: number, offset: number, found: boolean }
|
||||
| { type: 'definitionProbed', id: number, found: boolean }
|
||||
|
||||
/**
|
||||
* edit:把 startLine..endLine 这几行整行替换成 text(扩展端给每行补回原来第一行的缩进)
|
||||
* visibleLine:视口顶部那块的起始行,切回文本编辑器时扩展端把这行放到顶部
|
||||
* probeDefinition:按住 ⌘ / Ctrl 悬停到带下标的名字上,问 offset 处有没有定义;version 是这份下标所属的文档版本
|
||||
* probeDefinition:按住 ⌘ / Ctrl 指到带下标的名字上,问 offset 处有没有定义;id 标出是哪一次问,回答带回来;
|
||||
* version 是这份下标所属的文档版本
|
||||
* goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义
|
||||
*/
|
||||
export type WebviewToExtension
|
||||
@@ -87,5 +88,5 @@ export type WebviewToExtension
|
||||
| { type: 'edit', startLine: number, endLine: number, text: string }
|
||||
| { type: 'selectTranslator', id: string }
|
||||
| { type: 'visibleLine', line: number }
|
||||
| { type: 'probeDefinition', version: number, offset: number }
|
||||
| { type: 'probeDefinition', id: number, version: number, offset: number }
|
||||
| { type: 'goToDefinition', version: number, offset: number }
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WebviewToExtension } from './types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { DefinitionLinks } from './definitionLinks'
|
||||
import { onMessage, postMessage } from './vscode'
|
||||
|
||||
/** 跳到定义用的修饰键,按平台习惯:macOS 是 ⌘,其余是 Ctrl。 */
|
||||
@@ -18,63 +18,30 @@ function offsetOf(el: HTMLElement | null): number | undefined {
|
||||
return el?.dataset.offset === undefined ? undefined : Number(el.dataset.offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* 和 VS Code 编辑器里一样:按住 ⌘ / Ctrl、鼠标停在查得到定义的名字上时,名字显示成链接(data-link),点一下跳到定义。
|
||||
* 有没有定义问扩展端,回答按文档版本缓存:同一版本里每个名字只问一次,换了版本清空,迟到的旧版本回答丢掉。
|
||||
*/
|
||||
class DefinitionLinks {
|
||||
private version: number | undefined
|
||||
/** 这个版本里问过的下标:true / false 是回答,undefined 是问了还没回 */
|
||||
private readonly answers = new Map<number, boolean | undefined>()
|
||||
/** 指针下的名字、修饰键按没按;把链接样式(data-link)挂到指着的那个名字上。 */
|
||||
class Pointer {
|
||||
private hovered: HTMLElement | null = null
|
||||
private held = false
|
||||
private linked: HTMLElement | null = null
|
||||
|
||||
constructor(private readonly post: (msg: WebviewToExtension) => void) {}
|
||||
constructor(private readonly links: DefinitionLinks) {}
|
||||
|
||||
setVersion(version: number | undefined): void {
|
||||
if (version === this.version)
|
||||
return
|
||||
this.version = version
|
||||
this.answers.clear()
|
||||
this.update()
|
||||
}
|
||||
|
||||
point(target: EventTarget | null, held: boolean): void {
|
||||
move(target: EventTarget | null, held: boolean): void {
|
||||
this.hovered = nameAt(target)
|
||||
this.held = held
|
||||
this.update()
|
||||
this.sync()
|
||||
}
|
||||
|
||||
hold(held: boolean): void {
|
||||
this.held = held
|
||||
this.update()
|
||||
this.sync()
|
||||
}
|
||||
|
||||
answer(version: number, offset: number, found: boolean): void {
|
||||
if (version !== this.version)
|
||||
return
|
||||
this.answers.set(offset, found)
|
||||
this.update()
|
||||
}
|
||||
|
||||
/** ⌘ / Ctrl + 点击名字:跳到定义,并拦下这次点击,不让它再触发块上的切换。已知没有定义的名字照常点。 */
|
||||
click(event: MouseEvent): void {
|
||||
const offset = modifierHeld(event) ? offsetOf(nameAt(event.target)) : undefined
|
||||
if (offset === undefined || this.version === undefined || this.answers.get(offset) === false)
|
||||
return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.post({ type: 'goToDefinition', version: this.version, offset })
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
const offset = this.held ? offsetOf(this.hovered) : undefined
|
||||
if (offset !== undefined && this.version !== undefined && !this.answers.has(offset)) {
|
||||
this.answers.set(offset, undefined)
|
||||
this.post({ type: 'probeDefinition', version: this.version, offset })
|
||||
}
|
||||
const link = offset !== undefined && this.answers.get(offset) === true ? this.hovered : null
|
||||
/** 把按住修饰键时指着的名字告诉 links,再按它的回答挂上或摘掉链接样式 */
|
||||
sync(): void {
|
||||
const offset = offsetOf(this.hovered)
|
||||
this.links.point(this.held ? offset : undefined)
|
||||
const link = offset !== undefined && offset === this.links.linked ? this.hovered : null
|
||||
if (link === this.linked)
|
||||
return
|
||||
this.linked?.removeAttribute('data-link')
|
||||
@@ -83,26 +50,39 @@ class DefinitionLinks {
|
||||
}
|
||||
}
|
||||
|
||||
/** 预览里的名字能 ⌘ / Ctrl + 点击跳到定义;version 是当前显示的文档版本,名字的下标属于它。 */
|
||||
/**
|
||||
* 和 VS Code 编辑器里一样:按住 ⌘ / Ctrl、鼠标停在查得到定义的名字上时,名字显示成链接,点一下跳到定义。
|
||||
* version 是当前显示的文档版本,名字的下标属于它。
|
||||
*/
|
||||
export function useDefinitionLinks(version: number | undefined): void {
|
||||
const [links] = useState(() => new DefinitionLinks(postMessage))
|
||||
const [pointer] = useState(() => new Pointer(links))
|
||||
|
||||
useEffect(() => {
|
||||
links.setVersion(version)
|
||||
}, [links, version])
|
||||
pointer.sync()
|
||||
}, [links, pointer, version])
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (event: MouseEvent): void => links.point(event.target, modifierHeld(event))
|
||||
const onMove = (event: MouseEvent): void => pointer.move(event.target, modifierHeld(event))
|
||||
const onOut = (event: MouseEvent): void => {
|
||||
if (event.relatedTarget === null)
|
||||
links.point(null, false)
|
||||
pointer.move(null, false)
|
||||
}
|
||||
const onKey = (event: KeyboardEvent): void => pointer.hold(modifierHeld(event))
|
||||
const onBlur = (): void => pointer.hold(false)
|
||||
// ⌘ / Ctrl + 点击名字归跳转:拦下这次点击,不让它再触发块上的编辑切换、折叠
|
||||
const onClick = (event: MouseEvent): void => {
|
||||
if (!links.click(modifierHeld(event) ? offsetOf(nameAt(event.target)) : undefined))
|
||||
return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
const onKey = (event: KeyboardEvent): void => links.hold(modifierHeld(event))
|
||||
const onBlur = (): void => links.hold(false)
|
||||
const onClick = (event: MouseEvent): void => links.click(event)
|
||||
const off = onMessage((msg) => {
|
||||
if (msg.type === 'definitionProbed')
|
||||
links.answer(msg.version, msg.offset, msg.found)
|
||||
if (msg.type !== 'definitionProbed')
|
||||
return
|
||||
links.answer(msg.id, msg.found)
|
||||
pointer.sync()
|
||||
})
|
||||
document.addEventListener('mousemove', onMove)
|
||||
document.addEventListener('mouseout', onOut)
|
||||
@@ -120,5 +100,5 @@ export function useDefinitionLinks(version: number | undefined): void {
|
||||
window.removeEventListener('keyup', onKey)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
}
|
||||
}, [links])
|
||||
}, [links, pointer])
|
||||
}
|
||||
Reference in new issue
Block a user