✨ feat(vscode): 代码阅读预览单击名字高亮它在文件里的各处出现
This commit is contained in:
1 parent
5872cf7958
commit
9c0f05362c
12 files changed
+558
-9
No files matched your search
@@ -0,0 +1,224 @@
|
||||
import type { Click, Highlight } from './occurrences'
|
||||
import type { BlockView, Occurrence, PreviewText, PropertyItem, Segment, WebviewToExtension } from './types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../../../src/codeReader/blockView'
|
||||
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
|
||||
import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
|
||||
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
|
||||
import { DefinitionLinks } from './definitionLinks'
|
||||
import { clickAction, OccurrenceHighlight, occurrenceStyle, segmentOccurrence } from './occurrences'
|
||||
|
||||
function name(text: string, offset: number): Segment {
|
||||
return { text, role: 'variable', offset }
|
||||
}
|
||||
|
||||
function at(start: number, end: number, kind: Occurrence['kind'] = 'read'): Occurrence {
|
||||
return { start, end, kind }
|
||||
}
|
||||
|
||||
function click(overrides: Partial<Click> = {}): Click {
|
||||
return { modified: false, count: 1, selecting: false, control: false, ...overrides }
|
||||
}
|
||||
|
||||
function setup(version = 1, translatorId = 'zh'): { occ: OccurrenceHighlight, asked: () => { id: number, version: number, offset: number }[], changes: (Highlight | undefined)[] } {
|
||||
const sent: WebviewToExtension[] = []
|
||||
const changes: (Highlight | undefined)[] = []
|
||||
const occ = new OccurrenceHighlight(msg => sent.push(msg), highlight => changes.push(highlight))
|
||||
occ.setDocument(version, translatorId)
|
||||
const asked = (): { id: number, version: number, offset: number }[] => sent.flatMap(msg => msg.type === 'findOccurrences' ? [{ id: msg.id, version: msg.version, offset: msg.offset }] : [])
|
||||
return { occ, asked, changes }
|
||||
}
|
||||
|
||||
function find(occ: OccurrenceHighlight, offset: number, text: string): void {
|
||||
occ.click(clickAction(click({ name: { offset, text } })))
|
||||
}
|
||||
|
||||
describe('occurrences:哪些段画高亮', () => {
|
||||
it('段在源码里的范围和一处出现重叠就画,边界相接不算,没有下标的不画', () => {
|
||||
const occurrences = [at(10, 18)]
|
||||
expect(segmentOccurrence(name('ALPHABET', 10), occurrences)).toBe('normal')
|
||||
expect(segmentOccurrence(name('x', 2), occurrences)).toBeUndefined()
|
||||
expect(segmentOccurrence(name('ab', 8), occurrences)).toBeUndefined()
|
||||
expect(segmentOccurrence(name('ab', 18), occurrences)).toBeUndefined()
|
||||
expect(segmentOccurrence(name('ab', 17), occurrences)).toBe('normal')
|
||||
expect(segmentOccurrence({ text: '常量', role: 'storage' }, occurrences)).toBeUndefined()
|
||||
// 整段字符串比名字长:出现落在字符串里面也算
|
||||
expect(segmentOccurrence({ text: '\'./util\'', role: 'string', offset: 9 }, occurrences)).toBe('normal')
|
||||
})
|
||||
|
||||
it('写入用强高亮色,读取和按文字认出的用普通高亮色;同一段碰上两种按写入画', () => {
|
||||
expect(occurrenceStyle('write')).toBe('strong')
|
||||
expect(occurrenceStyle('read')).toBe('normal')
|
||||
expect(occurrenceStyle('text')).toBe('normal')
|
||||
expect(segmentOccurrence(name('a', 0), [at(0, 1, 'text')])).toBe('normal')
|
||||
expect(segmentOccurrence(name('a', 0), [at(0, 1, 'read'), at(0, 1, 'write')])).toBe('strong')
|
||||
})
|
||||
|
||||
it('真实的 Python 译文和属性表:定义处按写入、用到的地方按读取画;点分模块名每一节各按自己的范围认', async () => {
|
||||
const source = [
|
||||
'import luffy_agent.core.db',
|
||||
'import secrets',
|
||||
'',
|
||||
'ALPHABET = "abcdefgh"',
|
||||
'',
|
||||
'def token():',
|
||||
' return "".join(secrets.choice(ALPHABET) for _ in range(8))',
|
||||
].join('\n')
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, source, py, pythonZhTranslator))
|
||||
const highlighted = (occurrences: Occurrence[]): string[] => allSegments(views).flatMap((segment) => {
|
||||
const style = segmentOccurrence(segment, occurrences)
|
||||
return style ? [`${segment.text}:${style}`] : []
|
||||
})
|
||||
const range = (needle: string, kind: Occurrence['kind'], from = 0): Occurrence => {
|
||||
const start = source.indexOf(needle, from)
|
||||
expect(start, needle).toBeGreaterThanOrEqual(0)
|
||||
return at(start, start + needle.length, kind)
|
||||
}
|
||||
|
||||
const definition = range('ALPHABET', 'write')
|
||||
expect(highlighted([definition, range('ALPHABET', 'read', definition.end)])).toEqual(['ALPHABET:strong', 'ALPHABET:normal'])
|
||||
expect(highlighted([range('core', 'read')])).toEqual(['core:normal'])
|
||||
expect(highlighted([range('luffy_agent.core.db', 'read')])).toEqual(['luffy_agent:normal', 'core:normal', 'db:normal'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('occurrences:点击', () => {
|
||||
it('单击名字问它的各处出现,名字在折叠箭头这类按钮里也算;点空白处清掉,点在按钮、链接、筛选栏上不动', () => {
|
||||
expect(clickAction(click({ name: { offset: 3, text: 'x' } }))).toEqual({ kind: 'find', offset: 3, name: 'x' })
|
||||
expect(clickAction(click({ name: { offset: 3, text: 'x' }, control: true }))).toEqual({ kind: 'find', offset: 3, name: 'x' })
|
||||
expect(clickAction(click())).toEqual({ kind: 'clear' })
|
||||
expect(clickAction(click({ control: true }))).toEqual({ kind: 'ignore' })
|
||||
})
|
||||
|
||||
it('拖着选字、双击、带修饰键的点击都不动高亮', () => {
|
||||
const onName = { name: { offset: 3, text: 'x' } }
|
||||
expect(clickAction(click({ ...onName, selecting: true }))).toEqual({ kind: 'ignore' })
|
||||
expect(clickAction(click({ selecting: true }))).toEqual({ kind: 'ignore' })
|
||||
expect(clickAction(click({ ...onName, count: 2 }))).toEqual({ kind: 'ignore' })
|
||||
expect(clickAction(click({ count: 2 }))).toEqual({ kind: 'ignore' })
|
||||
expect(clickAction(click({ ...onName, modified: true }))).toEqual({ kind: 'ignore' })
|
||||
expect(clickAction(click({ modified: true }))).toEqual({ kind: 'ignore' })
|
||||
})
|
||||
|
||||
it('⌘ / Ctrl + 点击名字照旧跳到定义,不问出现处', () => {
|
||||
const sent: WebviewToExtension[] = []
|
||||
const links = new DefinitionLinks(msg => sent.push(msg))
|
||||
links.setVersion(1)
|
||||
const occ = new OccurrenceHighlight(msg => sent.push(msg), () => {})
|
||||
occ.setDocument(1, 'zh')
|
||||
|
||||
expect(links.click(42)).toBe(true)
|
||||
occ.click(clickAction(click({ modified: true, name: { offset: 42, text: 'x' } })))
|
||||
expect(sent).toEqual([{ type: 'goToDefinition', version: 1, offset: 42 }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('occurrences:高亮的状态', () => {
|
||||
it('问到的各处出现连同点中的名字一起显示;回答是空的不高亮', () => {
|
||||
const { occ, asked, changes } = setup()
|
||||
find(occ, 20, 'ALPHABET')
|
||||
expect(asked()).toEqual([{ id: asked()[0].id, version: 1, offset: 20 }])
|
||||
const occurrences = [at(20, 28, 'write'), at(60, 68)]
|
||||
occ.answer(asked()[0].id, occurrences)
|
||||
expect(occ.highlight).toEqual({ name: 'ALPHABET', occurrences })
|
||||
expect(changes).toEqual([{ name: 'ALPHABET', occurrences }])
|
||||
|
||||
find(occ, 5, 'secrets')
|
||||
occ.answer(asked()[1].id, [])
|
||||
expect(occ.highlight).toBeUndefined()
|
||||
})
|
||||
|
||||
it('点另一个名字:回答到之前旧高亮留着,到了就换成新的', () => {
|
||||
const { occ, asked } = setup()
|
||||
find(occ, 20, 'ALPHABET')
|
||||
occ.answer(asked()[0].id, [at(20, 28)])
|
||||
find(occ, 5, 'secrets')
|
||||
expect(occ.highlight?.name).toBe('ALPHABET')
|
||||
occ.answer(asked()[1].id, [at(5, 12), at(40, 47)])
|
||||
expect(occ.highlight).toEqual({ name: 'secrets', occurrences: [at(5, 12), at(40, 47)] })
|
||||
})
|
||||
|
||||
it('只认最近一次询问的回答,迟到的丢掉', () => {
|
||||
const { occ, asked } = setup()
|
||||
find(occ, 20, 'ALPHABET')
|
||||
find(occ, 5, 'secrets')
|
||||
const [first, second] = asked()
|
||||
occ.answer(second.id, [at(5, 12)])
|
||||
occ.answer(first.id, [at(20, 28)])
|
||||
expect(occ.highlight).toEqual({ name: 'secrets', occurrences: [at(5, 12)] })
|
||||
occ.answer(second.id, [at(99, 100)])
|
||||
expect(occ.highlight).toEqual({ name: 'secrets', occurrences: [at(5, 12)] })
|
||||
})
|
||||
|
||||
it('回答不留到下次用:答过「没有」后再点同一个名字照样再问', () => {
|
||||
const { occ, asked } = setup()
|
||||
find(occ, 20, 'ALPHABET')
|
||||
occ.answer(asked()[0].id, [])
|
||||
find(occ, 20, 'ALPHABET')
|
||||
expect(asked()).toHaveLength(2)
|
||||
occ.answer(asked()[1].id, [at(20, 28)])
|
||||
expect(occ.highlight?.name).toBe('ALPHABET')
|
||||
})
|
||||
|
||||
it('点空白处、Esc 清掉高亮,清掉后才到的回答也丢掉', () => {
|
||||
const { occ, asked } = setup()
|
||||
find(occ, 20, 'ALPHABET')
|
||||
occ.answer(asked()[0].id, [at(20, 28)])
|
||||
occ.click(clickAction(click()))
|
||||
expect(occ.highlight).toBeUndefined()
|
||||
|
||||
find(occ, 20, 'ALPHABET')
|
||||
occ.answer(asked()[1].id, [at(20, 28)])
|
||||
find(occ, 5, 'secrets')
|
||||
occ.clear()
|
||||
occ.answer(asked()[2].id, [at(5, 12)])
|
||||
expect(occ.highlight).toBeUndefined()
|
||||
})
|
||||
|
||||
it('换了文档版本或翻译就清掉,同一版本同一翻译重发不清;换版本前问的回答丢掉', () => {
|
||||
const { occ, asked } = setup(1, 'zh')
|
||||
find(occ, 20, 'ALPHABET')
|
||||
occ.answer(asked()[0].id, [at(20, 28)])
|
||||
occ.setDocument(1, 'zh')
|
||||
expect(occ.highlight?.name).toBe('ALPHABET')
|
||||
occ.setDocument(1, 'ai')
|
||||
expect(occ.highlight).toBeUndefined()
|
||||
|
||||
find(occ, 20, 'ALPHABET')
|
||||
occ.answer(asked()[1].id, [at(20, 28)])
|
||||
find(occ, 5, 'secrets')
|
||||
occ.setDocument(2, 'ai')
|
||||
expect(occ.highlight).toBeUndefined()
|
||||
occ.answer(asked()[2].id, [at(5, 12)])
|
||||
expect(occ.highlight).toBeUndefined()
|
||||
|
||||
find(occ, 20, 'ALPHABET')
|
||||
expect(asked()[3].version).toBe(2)
|
||||
})
|
||||
|
||||
it('还没有文档时点名字不问', () => {
|
||||
const sent: WebviewToExtension[] = []
|
||||
const occ = new OccurrenceHighlight(msg => sent.push(msg), () => {})
|
||||
find(occ, 20, 'ALPHABET')
|
||||
expect(sent).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
function textSegments(text: PreviewText): Segment[] {
|
||||
return 'markdown' in text ? [] : text
|
||||
}
|
||||
|
||||
function itemSegments(items: PropertyItem[] | undefined): Segment[] {
|
||||
return (items ?? []).flatMap(item => [...item.cells.flat(), ...itemSegments(item.items)])
|
||||
}
|
||||
|
||||
/** 预览里画出来的所有段,按页面上从上到下的顺序:每块的属性表或译文,再是它的子块。 */
|
||||
function allSegments(views: BlockView[]): Segment[] {
|
||||
return views.flatMap(view => [
|
||||
...('properties' in view
|
||||
? view.properties.flatMap(row => [...textSegments(row.value), ...itemSegments(row.items)])
|
||||
: textSegments(view.translation)),
|
||||
...allSegments(view.children),
|
||||
])
|
||||
}
|
||||
Reference in new issue
Block a user