import type { FindResult, FindUnit } from './find' import type { WrittenBlock } from './testBlocks' import type { BlockKind, Inline, Markdown, PropertyRow, Segment } from './types' import { describe, expect, it } from 'vitest' import { nameMap } from './displayNames' import { findCount, findMatches, findUnits, PageFind, paintPlan, shownText, spansOf } from './find' import { placementOf } from './placement' import { placed } from './testBlocks' function name(text: string, offset: number): Segment { return { text, role: 'variable', offset } } function word(text: string): Segment { return { text, role: 'keyword' } } function textBlock(id: string, kind: BlockKind, line: number, translation: Inline[], children: WrittenBlock[] = []): WrittenBlock { return { id, anchor: id, kind, lines: [line, line], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation } } function factBlock(id: string, kind: BlockKind, line: number, properties: PropertyRow[], children: WrittenBlock[] = []): WrittenBlock { return { id, anchor: id, kind, lines: [line, line], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, properties } } /** Markdown 显示成的字:测试里就是原文 */ const markdownText = (markdown: Markdown): string => markdown.markdown /** * 0 def load(path): 函数默认折叠 * 1 return get_db(path) * 3 LIMIT = load(3) */ const LOAD_ROWS: PropertyRow[] = [ { label: '名称', value: [name('load', 4)] }, { label: '输入参数', value: [word('1 个')], items: [{ cells: [[name('path', 9)]] }] }, ] const BLOCKS: WrittenBlock[] = [ factBlock('0', 'function', 0, LOAD_ROWS, [ textBlock('0.0', 'return', 1, [word('返回'), name('get_db', 30), name('path', 37)]), ]), textBlock('1', 'const', 3, [word('设'), name('LIMIT', 50), word('为'), name('load', 58)]), ] function unitsOf(blocks: WrittenBlock[], names = nameMap([])): FindUnit[] { const page = placed(blocks) return findUnits(page, placementOf(page), names, markdownText) } const ANYWHERE = (): boolean => true describe('查找栏:页面上能查的字', () => { it('整份文件按页面先后排,折叠着的块里的也在;每段记着在哪块、画在哪、要展开什么、块在第几行', () => { expect(unitsOf(BLOCKS).map(({ block, key, expand, line, text }) => ({ block, key, expand, line, text }))).toEqual([ { block: '0', key: '0/p0#label', expand: [], line: 0, text: '名称' }, { block: '0', key: '0/p0#value', expand: [], line: 0, text: 'load' }, { block: '0', key: '0/p1#label', expand: [], line: 0, text: '输入参数' }, { block: '0', key: '0/p1#value', expand: [], line: 0, text: '1 个' }, { block: '0', key: '0/p1/0#0', expand: ['0/p1'], line: 0, text: 'path' }, { block: '0.0', key: '0.0#text', expand: ['0'], line: 1, text: '返回get_dbpath' }, { block: '1', key: '1#text', expand: [], line: 3, text: '设LIMIT为load' }, ]) }) it('按页面上显示的字:起了中文名的名字是中文名(定义处后面还跟着原名),框是框头加框里的字,收起的类名只有小标', () => { const names = nameMap([{ offset: 58, length: 4, name: '加载' }, { offset: 50, length: 5, name: '上限', definition: true }]) const shown = shownText([word('设'), name('LIMIT', 50), word('为'), name('load', 58)], 0, names) expect(shown).toEqual({ text: '设上限LIMIT为加载', renamed: [{ start: 9, end: 11, original: 'load' }] }) const boxed: Inline[] = [ word('对'), { style: 'iteration', label: '每个', segments: [name('item', 3), { style: 'tag', label: '', segments: [word('x')] }] }, { style: 'classes', label: '样式 3 个类', detail: 'a\nb\nc', segments: [word('a b c')] }, ] expect(shownText(boxed, 0, nameMap([])).text).toBe('对每个itemx样式 3 个类') expect(unitsOf(BLOCKS, names).at(-1)?.text).toBe('设上限LIMIT为加载') }) it('注释、说明的 Markdown 是渲染出来的字;属性名、表头照原样', () => { const blocks = [factBlock('0', 'class', 0, [ { label: '说明', value: { markdown: '**加载**配置' } }, { label: '字段', value: [word('1 个')], columns: ['名称', '类型'], items: [{ cells: [[name('path', 9)], [word('str')]] }] }, ])] const page = placed(blocks) const units = findUnits(page, placementOf(page), nameMap([]), markdown => markdown.markdown.replaceAll('**', '')) expect(units.map(unit => [unit.key, unit.text])).toEqual([ ['0/p0#label', '说明'], ['0/p0#value', '加载配置'], ['0/p1#label', '字段'], ['0/p1#value', '1 个'], ['0/p1#h0', '名称'], ['0/p1#h1', '类型'], ['0/p1/0#0', 'path'], ['0/p1/0#1', 'str'], ]) }) }) describe('查找栏:命中', () => { it('纯文字、不分大小写,一段字里从前往后不重叠地找;特殊字符照字面', () => { expect(spansOf('ab')('xAbaBab')).toEqual([{ start: 1, end: 3 }, { start: 3, end: 5 }, { start: 5, end: 7 }]) expect(spansOf('aa')('aaaa')).toEqual([{ start: 0, end: 2 }, { start: 2, end: 4 }]) expect(spansOf('a.(b')('xa.(b a-(b')).toEqual([{ start: 1, end: 5 }]) expect(spansOf('')('abc')).toEqual([]) }) it('各段字分开找,不跨段;按页面先后排', () => { const units = unitsOf(BLOCKS) expect(findMatches(units, 'LOAD')).toEqual([{ unit: 1, start: 0, end: 4 }, { unit: 6, start: 7, end: 11 }]) expect(findMatches(units, 'namepath')).toEqual([]) expect(findMatches(units, 'path').map(match => units[match.unit].key)).toEqual(['0/p1/0#0', '0.0#text']) }) it('显示成中文名的名字,输入源码原名也算命中,命中的是整个中文名;中文名自己也命中时不重复算', () => { const names = nameMap([{ offset: 58, length: 4, name: '加载' }, { offset: 4, length: 4, name: 'load加载' }]) const units = unitsOf(BLOCKS, names) expect(findMatches(units, 'Load')).toEqual([{ unit: 1, start: 0, end: 4 }, { unit: 6, start: 7, end: 9 }]) expect(findMatches(units, '加载')).toEqual([{ unit: 1, start: 4, end: 6 }, { unit: 6, start: 7, end: 9 }]) }) it('查找栏上的数:没写字时不显示,没有命中写「无结果」,有当前那处写「第几处/共几处」,一处都跳不到时写「N 处」', () => { const matches = [{ unit: 0, start: 0, end: 1 }, { unit: 1, start: 0, end: 1 }] expect(findCount({ query: '', matches: [] })).toBeUndefined() expect(findCount({ query: 'x', matches: [] })).toBe('无结果') expect(findCount({ query: 'x', matches, current: 1 })).toBe('2/2') expect(findCount({ query: 'x', matches })).toBe('2 处') }) it('画高亮要的:按段字的记号分好各段的字和命中,当前那处是它那段里的第几个', () => { const units = unitsOf(BLOCKS) const result: FindResult = { query: 't', matches: findMatches(units, 't'), current: 2 } const plan = paintPlan(units, result) expect(plan.query).toBe('t') expect(plan.texts.get('0/p1/0#0')).toEqual({ text: 'path', spans: [{ start: 2, end: 3 }] }) expect(plan.texts.get('0.0#text')).toEqual({ text: '返回get_dbpath', spans: [{ start: 4, end: 5 }, { start: 10, end: 11 }] }) expect(plan.texts.has('0/p0#value')).toBe(false) expect(plan.current).toEqual({ key: '0.0#text', index: 1 }) expect(paintPlan(units, { ...result, current: 1 }).current).toEqual({ key: '0.0#text', index: 0 }) expect(paintPlan(units, { ...result, current: undefined }).current).toBeUndefined() }) }) describe('查找栏:当前那处', () => { function session(units: FindUnit[]): { find: PageFind, results: FindResult[] } { const results: FindResult[] = [] const find = new PageFind(result => results.push(result)) find.setUnits(units, ANYWHERE) return { find, results } } it('写了字:从给的那处(视口顶部那块的行、选中的字)往后第一处能跳到的是当前那处;被筛掉的块里的跳过,总数照旧', () => { const units = unitsOf(BLOCKS) const { find, results } = session(units) expect(find.search('load', ANYWHERE, { line: 1, start: 0 })).toEqual({ unit: 6, start: 7, end: 11 }) expect(results.at(-1)).toEqual({ query: 'load', matches: [{ unit: 1, start: 0, end: 4 }, { unit: 6, start: 7, end: 11 }], current: 1 }) const other = session(units).find expect(other.search('load', unit => unit.block !== '1', { line: 1, start: 0 })).toEqual({ unit: 1, start: 0, end: 4 }) expect(other.state.current).toBe(0) expect(session(units).find.search('load', () => false, { line: 1, start: 0 })).toBeUndefined() const selected = session(units).find expect(selected.search('load', ANYWHERE, { key: '0/p0#value', line: 0, start: 1 })).toEqual({ unit: 6, start: 7, end: 11 }) expect(selected.search('load', ANYWHERE, { key: '0/p0#value', line: 0, start: 0 })).toEqual({ unit: 1, start: 0, end: 4 }) }) it('改了字:从原来那处往后接着找,原来那处还命中就停在原地', () => { const units = unitsOf(BLOCKS) const { find } = session(units) find.search('pat', ANYWHERE) expect(find.move(1, ANYWHERE)).toEqual({ unit: 5, start: 8, end: 11 }) expect(find.search('path', ANYWHERE)).toEqual({ unit: 5, start: 8, end: 12 }) expect(find.search('load', ANYWHERE)).toEqual({ unit: 6, start: 7, end: 11 }) expect(find.search('', ANYWHERE)).toBeUndefined() expect(find.state).toEqual({ query: '', matches: [] }) }) it('下一处、上一处按页面先后,到头了从另一头接着;跳不到的跳过;没有命中时不动', () => { const units = unitsOf(BLOCKS) const { find } = session(units) expect(find.move(1, ANYWHERE)).toBeUndefined() find.search('a', ANYWHERE) expect(find.state.current).toBe(0) expect(find.state.matches).toHaveLength(4) expect(find.move(-1, ANYWHERE)).toBe(find.state.matches[3]) expect(find.move(1, ANYWHERE)).toBe(find.state.matches[0]) const shown = (unit: FindUnit): boolean => unit.block !== '0.0' expect(find.move(1, shown)).toBe(find.state.matches[1]) expect(find.move(1, shown)).toBe(find.state.matches[3]) expect(find.state.current).toBe(3) }) it('换了一版文档:照旧按原来的字找,当前那处停在原来那段字里原来的位置往后;那段字没了,按原来那块的行往后找', () => { const { find, results } = session(unitsOf(BLOCKS)) find.search('path', ANYWHERE) find.move(1, ANYWHERE) const before = results.length const added = [textBlock('9', 'statement', 0, [word('path')]), ...BLOCKS] find.setUnits(unitsOf(added), ANYWHERE) expect(results).toHaveLength(before + 1) expect(find.state.matches).toHaveLength(3) expect(find.state.current).toBe(2) const renumbered = [factBlock('0', 'function', 0, LOAD_ROWS), textBlock('1', 'return', 1, [name('path', 37)])] find.setUnits(unitsOf(renumbered), ANYWHERE) expect(find.state.current).toBe(1) }) })