✨ feat(vscode): 代码阅读的名字高亮更显眼(当前那处描框、别处画细线),⌘↓ / ⌘↑ 在各处之间跳,跳到折叠处自动展开,筛选栏写第几处

This commit is contained in:
cruldra committed 2026-09-26 23:47:43 +08:00
1 parent c5c5d2b517
commit 49f253b80f
18 files changed
+731 -53

No files matched your search

@@ -1,12 +1,12 @@
import type { Click, Highlight } from './occurrences'
import type { BlockView, Inline, Occurrence, PreviewText, PropertyItem, Segment, WebviewToExtension } from './types'
import type { BlockView, Inline, KeyCombo, 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'
import { clickAction, isCurrent, matchesKey, occurrenceCount, OccurrenceHighlight, OccurrenceNavigation, occurrenceStyle, segmentOccurrence, stepIndex } from './occurrences'
function name(text: string, offset: number): Segment {
return { text, role: 'variable', offset }
@@ -121,8 +121,8 @@ describe('occurrences:高亮的状态', () => {
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 }])
expect(occ.highlight).toEqual({ name: 'ALPHABET', occurrences, current: 0 })
expect(changes).toEqual([{ name: 'ALPHABET', occurrences, current: 0 }])
find(occ, 5, 'secrets')
occ.answer(asked()[1].id, [])
@@ -136,7 +136,7 @@ describe('occurrences:高亮的状态', () => {
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)] })
expect(occ.highlight).toEqual({ name: 'secrets', occurrences: [at(5, 12), at(40, 47)], current: 0 })
})
it('只认最近一次询问的回答,迟到的丢掉', () => {
@@ -146,9 +146,9 @@ describe('occurrences:高亮的状态', () => {
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)] })
expect(occ.highlight).toEqual({ name: 'secrets', occurrences: [at(5, 12)], current: 0 })
occ.answer(second.id, [at(99, 100)])
expect(occ.highlight).toEqual({ name: 'secrets', occurrences: [at(5, 12)] })
expect(occ.highlight).toEqual({ name: 'secrets', occurrences: [at(5, 12)], current: 0 })
})
it('回答不留到下次用:答过「没有」后再点同一个名字照样再问', () => {
@@ -227,3 +227,103 @@ function allSegments(views: BlockView[]): Segment[] {
...allSegments(view.children),
])
}
const ANYWHERE = (): boolean => true
describe('occurrences:用键盘在各处出现之间跳', () => {
it('回答照源码先后排好,点中的那处是当前那一处;筛选栏写「第几处/共几处」,点中的不在回答里时写「N 处」', () => {
const { occ, asked } = setup()
find(occ, 40, 'ALPHABET')
occ.answer(asked()[0].id, [at(90, 98), at(40, 48), at(10, 18, 'write')])
expect(occ.highlight).toEqual({ name: 'ALPHABET', occurrences: [at(10, 18, 'write'), at(40, 48), at(90, 98)], current: 1 })
expect(occurrenceCount(occ.highlight!)).toBe('2/3')
find(occ, 60, 'ALPHABET')
occ.answer(asked()[1].id, [at(10, 18), at(40, 48), at(90, 98)])
expect(occ.highlight?.current).toBeUndefined()
expect(occurrenceCount(occ.highlight!)).toBe('3 处')
})
it('往后、往前按源码先后跳,到头了从另一头接着跳;当前那一处跟着换', () => {
const { occ, asked, changes } = setup()
find(occ, 40, 'ALPHABET')
occ.answer(asked()[0].id, [at(10, 18), at(40, 48), at(90, 98)])
expect(occ.move(1, ANYWHERE)).toEqual(at(90, 98))
expect(occ.highlight?.current).toBe(2)
expect(occ.move(1, ANYWHERE)).toEqual(at(10, 18))
expect(occ.highlight?.current).toBe(0)
expect(occ.move(-1, ANYWHERE)).toEqual(at(90, 98))
expect(occ.move(-1, ANYWHERE)).toEqual(at(40, 48))
expect(occurrenceCount(occ.highlight!)).toBe('2/3')
expect(changes.at(-1)).toEqual({ name: 'ALPHABET', occurrences: [at(10, 18), at(40, 48), at(90, 98)], current: 1 })
})
it('页面上找不到的(在被筛掉的块里)跳过,总数照旧;一处都找不到、没有高亮时不动', () => {
const { occ, asked, changes } = setup()
expect(occ.move(1, ANYWHERE)).toBeUndefined()
find(occ, 10, 'ALPHABET')
occ.answer(asked()[0].id, [at(10, 18), at(40, 48), at(90, 98)])
const shown = (occurrence: Occurrence): boolean => occurrence.start !== 40
expect(occ.move(1, shown)).toEqual(at(90, 98))
expect(occ.move(1, shown)).toEqual(at(10, 18))
expect(occ.move(-1, shown)).toEqual(at(90, 98))
expect(occurrenceCount(occ.highlight!)).toBe('3/3')
const before = changes.length
expect(occ.move(1, () => false)).toBeUndefined()
expect(changes).toHaveLength(before)
expect(occ.highlight?.current).toBe(2)
})
it('点中的名字不在回答里:从点中的地方往后是它后面第一处,往前是它前面最后一处', () => {
const occurrences = [at(10, 18), at(40, 48), at(90, 98)]
expect(stepIndex(occurrences, undefined, 60, 1, ANYWHERE)).toBe(2)
expect(stepIndex(occurrences, undefined, 60, -1, ANYWHERE)).toBe(1)
expect(stepIndex(occurrences, undefined, 5, -1, ANYWHERE)).toBe(2)
expect(stepIndex(occurrences, undefined, 95, 1, ANYWHERE)).toBe(0)
expect(stepIndex(occurrences, 0, 0, -1, ANYWHERE)).toBe(2)
expect(stepIndex(occurrences, 1, 0, 1, occurrence => occurrence.start === 40)).toBe(1)
expect(stepIndex([], undefined, 0, 1, ANYWHERE)).toBeUndefined()
})
it('当前那一处:和它重叠的段描边,别的出现、没下标的段不描', () => {
const highlight = { name: 'ALPHABET', occurrences: [at(10, 18), at(40, 48)], current: 1 }
expect(isCurrent(name('ALPHABET', 40), highlight)).toBe(true)
expect(isCurrent(name('ALPHABET', 10), highlight)).toBe(false)
expect(isCurrent({ text: 'ALPHABET', role: 'plain' }, highlight)).toBe(false)
expect(isCurrent(name('ALPHABET', 40), { ...highlight, current: undefined })).toBe(false)
})
it('能不能用键盘跳:有高亮、页面有焦点、焦点不在输入框里;第一次都告诉扩展端,之后变了才告诉', () => {
const sent: WebviewToExtension[] = []
const navigation = new OccurrenceNavigation(msg => sent.push(msg), { focused: true, typing: false })
const reports = (): boolean[] => sent.flatMap(msg => msg.type === 'occurrenceNavigation' ? [msg.available] : [])
navigation.update({})
expect(reports()).toEqual([false])
navigation.update({ highlighted: true })
expect(navigation.available).toBe(true)
navigation.update({ highlighted: true })
navigation.update({ typing: true })
navigation.update({ typing: false })
navigation.update({ focused: false })
navigation.update({ highlighted: false })
navigation.update({ focused: true })
expect(reports()).toEqual([false, true, false, true, false])
})
it('命令绑的键:能跳时拦下浏览器的默认动作(⌘↓ 滚到底),不能跳、别的键、修饰键对不上都不拦', () => {
const keys: KeyCombo[] = [{ key: 'ArrowDown', ctrl: false, shift: false, alt: false, meta: true }, { key: 'j', ctrl: true, shift: false, alt: false, meta: false }]
const press = (key: string, modifiers: Partial<Record<'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey', boolean>> = {}): Pick<KeyboardEvent, 'key' | 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'> =>
({ key, ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, ...modifiers })
const navigation = new OccurrenceNavigation(() => {}, { focused: true, typing: false })
expect(navigation.blocks(press('ArrowDown', { metaKey: true }), keys)).toBe(false)
navigation.update({ highlighted: true })
expect(navigation.blocks(press('ArrowDown', { metaKey: true }), keys)).toBe(true)
expect(navigation.blocks(press('J', { ctrlKey: true }), keys)).toBe(true)
expect(navigation.blocks(press('ArrowDown', { metaKey: true, shiftKey: true }), keys)).toBe(false)
expect(navigation.blocks(press('ArrowDown'), keys)).toBe(false)
expect(navigation.blocks(press('ArrowUp', { metaKey: true }), keys)).toBe(false)
navigation.update({ typing: true })
expect(navigation.blocks(press('ArrowDown', { metaKey: true }), keys)).toBe(false)
expect(matchesKey(press('ArrowDown', { metaKey: true }), keys[0])).toBe(true)
})
})