⚡ perf(vscode): 代码阅读预览的文档消息里源码和着色只带一份:块不再各带一份连子块在内的整段源码和上色,编辑框按块的编辑范围从整份源码和着色里取,5126 行的消息 4.3MB → 1.9MB

This commit is contained in:
cruldra committed 2026-10-13 15:50:33 +11:00
1 parent 0371712176
commit 2e3602242e
43 files changed
+501 -239

No files matched your search

@@ -120,8 +120,6 @@ function block(id: string, anchor: string): BlockView {
callTree: {},
startLine: 0,
endLine: 1,
code: '',
codeTokens: [],
attributes: { kind: 'function' },
concepts: [{ id: 'function', label: 'function', icon: 'braces' }],
children: [],
@@ -130,7 +128,7 @@ function block(id: string, anchor: string): BlockView {
}
function documentOf(version: number, blocks: BlockView[]): DocumentMessage {
return { type: 'document', version, languageId: 'python', blocks, attributes: [], translators: [], translatorId: '', names: [] }
return { type: 'document', version, languageId: 'python', blocks, source: '', colors: { roles: [], runs: [] }, attributes: [], translators: [], translatorId: '', names: [] }
}
function tree(id: string, name: string): CallTree {
@@ -6,10 +6,10 @@ import type { BlockView, KindBadge } from './types'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { fileAttributes } from '../../../src/codeReader/attributes'
import { toBlockViews } from '../../../src/codeReader/blockView'
import { readBlockViews, toBlockViews } from '../../../src/codeReader/blockView'
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
import { fastapiRule } from '../../../src/codeReader/rules/fastapi'
import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
import { runtime, WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
import { BlockTree, treeState } from './BlockTree'
import { applyFilter } from './filter'
@@ -31,8 +31,6 @@ function view(id: string, kind: string, text: string, extra: Partial<BlockView>
kind,
startLine: 0,
endLine: 0,
code: '',
codeTokens: [],
attributes: { kind },
concepts: [{ id: kind, label: badge?.label ?? kind, icon: badge?.icon ?? 'box', ...(badge && { color: badge.color }) }],
children: [],
@@ -68,7 +66,7 @@ function rewriteTree(state: ChatState): RewriteTree {
function tree(view: Partial<Omit<BlockTreeState, 'folds'>> = {}, rewrite: ChatState = { ...EMPTY_CHAT, available: true }): BlockTreeHandle {
return {
state: staticStore(treeState({
edit: { version: 1, editing: new Set(), focusId: null },
edit: { version: 1, sourceText: { source: '', colors: { roles: [], runs: [] } }, editing: new Set(), focusId: null },
collapsed: new Set(),
hidden: new Set(),
temporary: new Set(),
@@ -321,3 +319,17 @@ describe('一行译文下面拆开的各项', () => {
expect(before(byId(root, `${block.id}/i/1`), removed!) && before(removed!, byId(root, `${block.id}/i/2`))).toBe(true)
})
})
describe('编辑框', () => {
it('编辑着的块:编辑框里是文档源码按它的编辑范围取的整行、去掉共同缩进,着色取自整份源码的着色', async () => {
const source = 'def f(x):\n if x:\n return "a"\n'
const { blocks, colors } = await readBlockViews(runtime, createPythonAdapter(WASM_DIR), pythonZhTranslator, 'python', source)
const html = markup(blocks, tree({
edit: { version: 1, sourceText: { source, colors }, editing: new Set(['0.0']), focusId: null },
collapsed: new Set(),
}))
expect(html).toContain('>if x:\n return &quot;a&quot;</textarea>')
expect(html).toContain('<span class="cr-role-keyword">if</span>')
expect(html).toContain('<span class="cr-role-string">&quot;a&quot;</span>')
})
})
@@ -1,4 +1,5 @@
import type { CSSProperties, ReactElement, ReactNode } from 'react'
import type { SourceText } from './blockSource'
import type { RewriteTool } from './BlockTools'
import type { DiffTree, FoldedRuns, PlacedRemoved } from './diffView'
import type { GridSlots } from './PropertyGrid'
@@ -24,6 +25,8 @@ import { foldsByCard, listId, tableId } from './viewState'
/** 块树里改代码的那部分状态:哪些块在编辑。页面上的块树才有;定义卡片浮窗里的块树没有,卡片只读,不画右上角的按钮 */
export interface EditState {
version: number
/** 这一版文档的源码和着色:编辑框按块的编辑范围从里面取 */
sourceText: SourceText
editing: ReadonlySet<string>
/** 刚点了「编辑」、要把光标放进去的块 */
focusId: string | null
@@ -208,6 +211,8 @@ interface BlockShape {
focus: boolean
/** 编辑框每来一份新文档都要对一次内容;不在编辑的卡片不跟着版本重画 */
version?: number
/** 编辑着的卡片才有:这一版文档的源码和着色 */
sourceText?: SourceText
/** 调用树:扩展端标了能看调用树(BlockView.callTree)的块才有棵数 */
calls?: number
callOpen: boolean
@@ -229,6 +234,7 @@ function shapeOf(block: BlockView, state: BlockTreeState): BlockShape {
temporary: state.temporary.has(block.id),
focus: state.edit?.focusId === block.id,
version: editing ? state.edit?.version : undefined,
sourceText: editing ? state.edit?.sourceText : undefined,
calls: state.callTree && block.callTree ? state.callTree.counts.get(block.anchor) ?? 0 : undefined,
callOpen: state.callTree?.openAnchor === block.anchor,
}
@@ -493,10 +499,11 @@ function BlockCard({ block, shape, ask, rewrite }: { block: BlockView, shape: Bl
: <span className="w-3.5 shrink-0" />}
<KindIcon concepts={block.concepts} />
<div className="min-w-0 flex-1">
{edit && shape.version !== undefined
{edit && shape.version !== undefined && shape.sourceText
? (
<CodeEditor
block={block}
sourceText={shape.sourceText}
version={shape.version}
autoFocus={shape.focus}
onAutoFocused={edit.onFocused}
@@ -41,8 +41,6 @@ function block(kind: string, id = kind, callTree?: BlockView['callTree']): Block
...(callTree && { callTree }),
startLine: 0,
endLine: 1,
code: '',
codeTokens: [],
attributes: { kind },
concepts: [{ id: kind, label: kind, icon: kind === 'class' ? 'box' : 'braces' }],
children: [],
@@ -54,7 +52,7 @@ function block(kind: string, id = kind, callTree?: BlockView['callTree']): Block
function tree(callTree?: CallTreeState & { onOpen: (anchor: string) => void }, edit = true): BlockTreeHandle {
return {
state: staticStore(treeState({
...(edit && { edit: { version: 1, editing: new Set<string>(), focusId: null } }),
...(edit && { edit: { version: 1, sourceText: { source: '', colors: { roles: [], runs: [] } }, editing: new Set<string>(), focusId: null } }),
collapsed: new Set(),
hidden: new Set(),
temporary: new Set(),
@@ -1,6 +1,8 @@
import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react'
import type { SourceText } from './blockSource'
import type { BlockView } from './types'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { blockSource } from './blockSource'
import { recolor } from './segments'
import { SegmentText } from './SegmentText'
import { postMessage } from './vscode'
@@ -9,6 +11,8 @@ const EDIT_DEBOUNCE_MS = 200
interface CodeEditorProps {
block: BlockView
/** 这一版文档的源码和着色:这块的源码按它的编辑范围从里面取 */
sourceText: SourceText
/** 文档版本:每来一份新文档都要对一次输入框内容,哪怕这一块的代码没变 */
version: number
autoFocus: boolean
@@ -17,8 +21,9 @@ interface CodeEditorProps {
}
/** 一块源码的编辑状态:着色层显示,透明输入框接收输入,改动防抖后整行替换回文件。 */
export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }: CodeEditorProps): ReactElement {
const [text, setText] = useState(block.code)
export function CodeEditor({ block, sourceText, version, autoFocus, onAutoFocused, onExit }: CodeEditorProps): ReactElement {
const { code, tokens } = useMemo(() => blockSource(sourceText, block.startLine, block.endLine), [sourceText, block.startLine, block.endLine])
const [text, setText] = useState(code)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const blockRef = useRef(block)
blockRef.current = block
@@ -51,16 +56,16 @@ export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }:
useEffect(() => {
const el = textareaRef.current
const focused = el !== null && document.activeElement === el
if (focused && unconfirmed.current !== null && unconfirmed.current !== block.code)
if (focused && unconfirmed.current !== null && unconfirmed.current !== code)
return
unconfirmed.current = null
sentLineCount.current = null
if (block.code === textRef.current)
if (code === textRef.current)
return
if (focused)
restoreSelection.current = [el.selectionStart, el.selectionEnd]
setText(block.code)
}, [version, block.code])
setText(code)
}, [version, code])
useLayoutEffect(() => {
const el = textareaRef.current
@@ -121,7 +126,7 @@ export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }:
onBlur={flush}
/>
<pre className="cr-code" aria-hidden="true">
<SegmentText segments={recolor(block.codeTokens, block.code, text)} />
<SegmentText segments={recolor(tokens, code, text)} />
{text === '' || text.endsWith('\n') ? ' ' : ''}
</pre>
</div>
@@ -12,8 +12,6 @@ function block(id: string, kind: string, extra: Partial<BlockView>): BlockView {
kind,
startLine: 0,
endLine: 0,
code: '',
codeTokens: [],
attributes: { kind },
concepts: [{ id: kind, label: kind, icon: 'box' }],
children: [],
@@ -15,8 +15,6 @@ function block(kind: string, extra: Partial<BlockView>): BlockView {
kind,
startLine: 0,
endLine: 0,
code: '',
codeTokens: [],
attributes: { kind },
concepts: [{ id: kind, label: kind, icon: 'square-function' }],
children: [],
@@ -134,7 +134,7 @@ describe.each(['TextPreview', 'DiffText'] as const)('%s 的 Markdown 页面元
const peek: DefinitionPeek = {
path: 'client.py',
line: 1,
block: { id: '1', anchor: '1', kind: 'class', startLine: 1, endLine: 2, code: '', codeTokens: [], attributes: { kind: 'class' }, concepts: [], children: [], translation: [] },
block: { id: '1', anchor: '1', kind: 'class', startLine: 1, endLine: 2, attributes: { kind: 'class' }, concepts: [], children: [], translation: [] },
names: [],
}
await act(async () => controller.answer(1, peek))
@@ -0,0 +1,68 @@
import type { SourceColors, TokenRole } from './types'
import { describe, expect, it } from 'vitest'
import { blockSource } from './blockSource'
/** 按字给词性拼出整份源码的着色:pieces 是 [文字, 词性] 依次接起来,同词性相邻的连成一段 */
function colored(pieces: [string, TokenRole][]): { source: string, colors: SourceColors } {
const colors: SourceColors = { roles: [], runs: [] }
let last: TokenRole | undefined
for (const [text, role] of pieces) {
let index = colors.roles.indexOf(role)
if (index < 0)
index = colors.roles.push(role) - 1
if (role === last)
colors.runs[colors.runs.length - 2] += text.length
else
colors.runs.push(text.length, index)
last = role
}
return { source: pieces.map(([text]) => text).join(''), colors }
}
describe('编辑框里一块的源码和着色', () => {
const text = colored([
['def', 'storage'],
[' ', 'plain'],
['f', 'function'],
['():\n ', 'plain'],
['if', 'keyword'],
[' ', 'plain'],
['x', 'variable'],
[':\n ', 'plain'],
['return', 'keyword'],
[' ', 'plain'],
['"a\n b"', 'string'],
['\n', 'plain'],
])
it('编辑范围整行,去掉共同缩进,行间用 \\n', () => {
expect(blockSource(text, 1, 3).code).toBe('if x:\n return "a\nb"')
expect(blockSource(text, 0, 0).code).toBe('def f():')
})
it('着色按源码里每个字的词性,缩进去掉,相邻同词性的连成一段;行间的换行符取源码里那个换行符的词性', () => {
expect(blockSource(text, 1, 3).tokens).toEqual([
{ text: 'if', role: 'keyword' },
{ text: ' ', role: 'plain' },
{ text: 'x', role: 'variable' },
{ text: ':\n ', role: 'plain' },
{ text: 'return', role: 'keyword' },
{ text: ' ', role: 'plain' },
{ text: '"a\nb"', role: 'string' },
])
})
it('\\r\\n 换行:\\r 不进源码;制表符缩进按原样去掉;空行不算缩进', () => {
const crlf = colored([
['f() {\r\n\t', 'plain'],
['if', 'keyword'],
[' (a) {\r\n\r\n\t\t', 'plain'],
['return', 'keyword'],
['\r\n\t}\r\n}', 'plain'],
])
const { code, tokens } = blockSource(crlf, 1, 4)
expect(code).toBe('if (a) {\n\n\treturn\n}')
expect(tokens.map(token => token.text).join('')).toBe(code)
expect(tokens).toContainEqual({ text: 'return', role: 'keyword' })
})
})
@@ -0,0 +1,81 @@
import type { DocumentMessage, Segment, TokenRole } from './types'
/** 文档的整份源码和它的着色(DocumentMessage 只带这一份,各块按编辑范围取) */
export type SourceText = Pick<DocumentMessage, 'source' | 'colors'>
/** 一块在编辑框里的样子:编辑范围整行源码去掉共同缩进、行间用 \n,和它的着色(拼起来等于 code) */
export interface BlockSource {
code: string
tokens: Segment[]
}
/** 非空行共同的前导空白(空格 / 制表符按原样比,不换算),同扩展端写回时补缩进用的那份(editText.commonIndent) */
function commonIndent(rows: readonly string[]): number {
let common: string | undefined
for (const row of rows) {
if (!row.trim())
continue
const indent = row.slice(0, row.length - row.trimStart().length)
if (common === undefined) {
common = indent
continue
}
let i = 0
while (i < common.length && i < indent.length && common[i] === indent[i])
i++
common = common.slice(0, i)
}
return common?.length ?? 0
}
/**
* startLine..endLine 这几行在编辑框里的样子(见 BlockSource):从文档开头数到这几行,每行的字按整份着色里它那段的词性上色,
* 去掉的缩进不上色,行间的换行符取源码里那个换行符的词性;\r\n 的 \r 不算进行里。
*/
export function blockSource({ source, colors }: SourceText, startLine: number, endLine: number): BlockSource {
const rows: { start: number, text: string }[] = []
let start = 0
for (let line = 0; line <= endLine && start <= source.length; line++) {
const newline = source.indexOf('\n', start)
const end = newline < 0 ? source.length : newline
if (line >= startLine)
rows.push({ start, text: source.slice(start, end > start && source[end - 1] === '\r' ? end - 1 : end) })
start = end + 1
}
const indent = commonIndent(rows.map(row => row.text))
const { runs, roles } = colors
/** 整份着色里眼下这段:第几段(runs 里的下标),它从源码第几个字起、到第几个字前止 */
let run = 0
let runStart = 0
const roleAt = (at: number): TokenRole => {
while (run < runs.length && runStart + runs[run] <= at) {
runStart += runs[run]
run += 2
}
return run < runs.length ? roles[runs[run + 1]] : 'plain'
}
const tokens: Segment[] = []
const put = (from: number, to: number, text: string): void => {
for (let at = from; at < to;) {
const role = roleAt(at)
const until = Math.min(to, runStart + (runs[run] ?? to - runStart))
const piece = text.slice(at - from, until - from)
const prev = tokens.at(-1)
if (prev?.role === role)
prev.text += piece
else
tokens.push({ text: piece, role })
at = until
}
}
rows.forEach((row, index) => {
if (index > 0)
put(row.start - 1, row.start, '\n')
const from = row.start + Math.min(indent, row.text.length)
put(from, row.start + row.text.length, row.text.slice(from - row.start))
})
return {
code: rows.map(row => row.text.slice(Math.min(indent, row.text.length))).join('\n'),
tokens,
}
}
@@ -10,7 +10,7 @@ const HIDING = 500
const PEEK: DefinitionPeek = {
path: 'app/devices.py',
line: 41,
block: { id: '3', anchor: '3', kind: 'class', startLine: 41, endLine: 60, code: '', codeTokens: [], attributes: { kind: 'class' }, concepts: [], children: [], translation: [] },
block: { id: '3', anchor: '3', kind: 'class', startLine: 41, endLine: 60, attributes: { kind: 'class' }, concepts: [], children: [], translation: [] },
names: [],
}
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
import { changeStops, diffTree, foldUnchanged, nextStop, openedByDiff, removedTitle, statsParts } from './diffView'
function block(id: string, anchor: string, children: BlockView[] = [], extra: Partial<BlockView> = {}): BlockView {
return { id, anchor, kind: 'function', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'function' }, concepts: [{ id: 'function', label: '函数', icon: 'square-function' }], children, translation: [], ...extra } as BlockView
return { id, anchor, kind: 'function', startLine: 0, endLine: 0, attributes: { kind: 'function' }, concepts: [{ id: 'function', label: '函数', icon: 'square-function' }], children, translation: [], ...extra } as BlockView
}
function removed(parent: string, after: string | undefined, kind = 'function'): RemovedView {
@@ -4,7 +4,7 @@ import { coveredByEditing, startEditing } from './editing'
import { revealPlan, uncoveredPlan } from './reveal'
function block(id: string, kind: string, [startLine, endLine]: [number, number], children: BlockView[] = []): BlockView {
return { id, kind, startLine, endLine, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, properties: [] }
return { id, kind, startLine, endLine, attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, properties: [] }
}
/**
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
import { applyFilter, blockPath, excludedBy, facetLabel, facetsOf, keptWhileEditing, setChecked, toggleValue } from './filter'
function block(id: string, kind: BlockView['kind'], extra: Record<string, string> = {}, children: BlockView[] = []): BlockView {
return { id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind, ...extra }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation: [] }
return { id, kind, startLine: 0, endLine: 0, attributes: { kind, ...extra }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation: [] }
}
const NONE = new Set<string>()
@@ -9,8 +9,6 @@ function block(id: string, kind: string, extra: Partial<BlockView> = {}): BlockV
kind,
startLine: 0,
endLine: 0,
code: '',
codeTokens: [],
attributes: { kind },
concepts: [],
children: [],
@@ -1,8 +1,9 @@
import type { SourceText } from './blockSource'
import type { BlockTreeState } from './BlockTree'
import type { ChangeStop, DiffTree, FoldedRuns } from './diffView'
import type { FilterView } from './filter'
import type { ReadableStore } from './store'
import type { BlockView, CallTree, ContractMiss, DocumentDiff, DocumentMessage, Excluded, KindBadge, Reveal } from './types'
import type { BlockView, CallTree, ContractMiss, DocumentDiff, DocumentMessage, Excluded, KindBadge, Reveal, SourceColors } from './types'
import { changeStops, diffTree, foldUnchanged, openedByDiff } from './diffView'
import { coveredByEditing } from './editing'
import { applyFilter, excludedBy } from './filter'
@@ -72,6 +73,7 @@ export interface PageStore extends ReadableStore<Page> {
const NONE: ReadonlySet<string> = new Set()
const NO_BLOCKS: BlockView[] = []
const NO_COUNTS: ReadonlyMap<string, number> = new Map()
const NO_COLORS: SourceColors = { roles: [], runs: [] }
/** 只记上一次:参数(逐个同一个对象)和上次一样就交回上次的结果,同 memoize-one */
function memoizeOne<A extends unknown[], R>(compute: (...args: A) => R): (...args: A) => R {
@@ -108,8 +110,10 @@ export function createPageStore(initial: Pick<PageInputs, 'editing' | 'folds'>):
const countsOf = memoizeOne((version: number | undefined, callCounts: PageInputs['callCounts']) => callCounts && callCounts.version === version
? new Map(Object.entries(callCounts.counts))
: NO_COUNTS)
const sourceTextOf = memoizeOne((source: string, colors: SourceColors): SourceText => ({ source, colors }))
const treeOf = memoizeOne((
version: number,
sourceText: SourceText,
editing: ReadonlySet<string>,
focusId: string | null,
counts: ReadonlyMap<string, number>,
@@ -121,7 +125,7 @@ export function createPageStore(initial: Pick<PageInputs, 'editing' | 'folds'>):
changes: DiffTree | undefined,
folded: FoldedRuns | undefined,
): BlockTreeState => ({
edit: { version, editing, focusId },
edit: { version, sourceText, editing, focusId },
callTree: { counts, ...(openAnchor !== undefined && { openAnchor }) },
collapsed,
folds,
@@ -152,7 +156,7 @@ export function createPageStore(initial: Pick<PageInputs, 'editing' | 'folds'>):
folded,
unshown: unshownOf(filter, folded),
stops: stopsOf(blocks, changes, filter),
tree: treeOf(version, editing, inputs.focusId, countsOf(doc?.version, inputs.callCounts), openAnchor, collapsed, foldsOf(collapsed), filter, covered, changes, folded),
tree: treeOf(version, sourceTextOf(doc?.source ?? '', doc?.colors ?? NO_COLORS), editing, inputs.focusId, countsOf(doc?.version, inputs.callCounts), openAnchor, collapsed, foldsOf(collapsed), filter, covered, changes, folded),
}
}
@@ -21,11 +21,11 @@ function word(text: string): Segment {
}
function textBlock(id: string, kind: BlockKind, startLine: number, translation: Inline[], children: BlockView[] = []): BlockView {
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation }
return { id, kind, startLine, endLine: startLine, attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation }
}
function factBlock(id: string, kind: BlockKind, startLine: number, properties: PropertyRow[], children: BlockView[] = []): BlockView {
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, properties }
return { id, kind, startLine, endLine: startLine, attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, properties }
}
/**
@@ -162,7 +162,7 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
it('注释块、函数「说明」的 Markdown 里的名字不带下标,不算显示着定义', () => {
const blocks: BlockView[] = [
{ id: '0', kind: 'comment', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'comment' }, concepts: [{ id: 'comment', label: '注释', icon: 'message-square-text' }], children: [], translation: { markdown: '见 `get_db`' } },
{ id: '0', kind: 'comment', startLine: 0, endLine: 0, attributes: { kind: 'comment' }, concepts: [{ id: 'comment', label: '注释', icon: 'message-square-text' }], children: [], translation: { markdown: '见 `get_db`' } },
factBlock('1', 'function', 1, [
{ label: '说明', value: { markdown: '同 `get_db`' } },
{ label: '输入参数', value: [{ text: '1 个', role: 'hint' }], items: [{ cells: [[name('get_db', 30)]] }] },
@@ -302,7 +302,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
}
/** 闪的那一处:整块写成它的第一行源码,属性行写成「属性名: 值」,子行各列用 | 隔开。 */
function flashed(views: BlockView[], plan: RevealPlan | undefined): string {
function flashed(views: BlockView[], plan: RevealPlan | undefined, source: string): string {
const block = flatten(views).find(view => view.id === plan?.id)
if (!block || !plan)
return ''
@@ -316,7 +316,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
return item.cells.map(text).join(' | ')
}
}
return block.code.split('\n')[0]
return source.split('\n')[block.startLine].trimStart()
}
/** 定义提供者说定义在 source 里 needle 开头那个名字上:预览滚到哪、展开哪些、闪哪一处。 */
@@ -325,7 +325,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
expect(offset, needle).toBeGreaterThanOrEqual(0)
const views: BlockView[] = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator))
const plan = revealPlan(views, source.slice(0, offset).split('\n').length - 1, offset)
return { plan, flashed: flashed(views, plan) }
return { plan, flashed: flashed(views, plan, source) }
}
const PY = [
@@ -458,7 +458,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
const at = (needle: string): { plan: RevealPlan | undefined, flashed: string } => {
const offset = source.indexOf(needle)
const plan = revealPlan(views, source.slice(0, offset).split('\n').length - 1, offset)
return { plan, flashed: flashed(views, plan) }
return { plan, flashed: flashed(views, plan, source) }
}
expect(at('User')).toEqual({ plan: { id: '1', expand: [] }, flashed: 'class User(Base):' })
expect(at('name:')).toEqual({ plan: { id: '1', row: '1/p3/0', expand: ['1/p3'] }, flashed: 'name | 字符串 | | 名字' })
+13 -5
View File
@@ -133,13 +133,9 @@ interface BlockViewBase {
anchor: string
/** BlockKind,或库规则认领的块的种类 id(带点,例如 sqlalchemy.table)。筛选用它;图标不看它,看 concepts */
kind: string
/** 编辑范围的起止行,0 起:块的完整源码,头部连同各子孙块、结尾(块编辑时子孙都不画,见 coveredByEditing) */
/** 编辑范围的起止行,0 起:块的完整源码,头部连同各子孙块、结尾(块编辑时子孙都不画,见 coveredByEditing);编辑框里的源码按它从文档的源码里取(见 blockSource) */
startLine: number
endLine: number
/** 编辑范围的源码,去掉共同缩进 */
code: string
/** 源码着色(拼起来等于 code) */
codeTokens: Segment[]
/** 块自己的名字(函数名、类名、只声明一个变量时的变量名)在源码里的范围:跳到的定义就是它时闪整块 */
defines?: SourceRange
/** 筛选属性:属性 id → 值 id(含 kind),都是和语言无关的英文 id,显示名在 DocumentMessage.attributes 里 */
@@ -245,11 +241,23 @@ export interface DefinitionPeek {
names: ShownName[]
}
/**
* 整份源码的着色:按源码先后一段段排,相邻同词性的连成一段;runs 两个数一组:这段有几个字符(UTF-16,换行符也算),词性是 roles 里的第几个。
* 编辑框给一块源码上色时按行取(见 blockSource)。
*/
export interface SourceColors {
roles: TokenRole[]
runs: number[]
}
export interface DocumentMessage {
type: 'document'
version: number
languageId: string
blocks: BlockView[]
/** 整份源码和它的着色,只这一份:编辑框里一块的源码、着色按它的编辑范围取;解析失败、语言不支持时是空的(没有块) */
source: string
colors: SourceColors
/** 这份文件里出现过的筛选属性和值(带显示名、块数),按筛选栏的先后排:通用属性在前,语言自己的在后 */
attributes: FileAttribute[]
translators: TranslatorOption[]
@@ -15,7 +15,7 @@ function memoryStorage(initial?: unknown): ViewStateStorage {
}
function block(id: string, kind: BlockKind, children: BlockView[] = []): BlockView {
return { id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation: [] }
return { id, kind, startLine: 0, endLine: 0, attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation: [] }
}
/**