⚡ perf(vscode): 代码阅读预览的文档消息里源码和着色只带一份:块不再各带一份连子块在内的整段源码和上色,编辑框按块的编辑范围从整份源码和着色里取,5126 行的消息 4.3MB → 1.9MB
This commit is contained in:
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 "a"</textarea>')
|
||||
expect(html).toContain('<span class="cr-role-keyword">if</span>')
|
||||
expect(html).toContain('<span class="cr-role-string">"a"</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 | 字符串 | | 名字' })
|
||||
|
||||
@@ -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: [] }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user