⚡ 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

+56 -54
View File
@@ -1,5 +1,5 @@
import type { Tree } from '@vscode/tree-sitter-wasm'
import type { BlockAttributes, BlockConcept, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, HttpEndpoint, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, NameBinding, Node, PreviewTable, PreviewText, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, SourceDocument, SourceRange, SourceSpan, StepBlocks, TextBlock, TokenRole, Translator } from './contract'
import type { BlockAttributes, BlockConcept, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, HttpEndpoint, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, NameBinding, Node, PreviewTable, PreviewText, PropertyItem, PropertyRow, RuleCard, RuleContext, SourceDocument, SourceRange, SourceSpan, StepBlocks, TextBlock, TokenRole, Translator } from './contract'
import type { TreeSitterRuntime } from './runtime'
import type { ImportedName } from './symbolLinks'
import { blockAttributes } from './attributes'
@@ -25,10 +25,6 @@ interface BlockViewBase {
endLine: number
/** 块自己那部分(头部)的最后一行:复合语句到主体开始之前,库规则认领的块是整条语句。问 AI 时贴外层块的开头用。 */
headerEndLine: number
/** 编辑范围的整行源码,去掉共同缩进。 */
code: string
/** 源码着色(拼起来等于 code)。 */
codeTokens: Segment[]
/** 块自己的名字(见 ownName)在源码里的范围:跳到的定义就是它时闪整块,不闪属性表里的某一行。 */
defines?: SourceRange
/** 筛选属性(含 kind),见 blockAttributes;库规则认领的块叠上规则给的,kind 是规则的种类 id。 */
@@ -77,17 +73,26 @@ export type CommentTarget
*/
export type BlockView = BlockViewBase & ({ translation: PreviewText, items?: PropertyItem[], table?: PreviewTable, doc?: Markdown, rows?: PropertyRow[] } | { properties: PropertyRow[] })
interface Line { start: number, end: number }
/**
* 整份源码的着色,发给页面:页面编辑框给块的源码上色时按行取,父块、子块共用这一份。
* 按源码先后一段段排,相邻同词性的连成一段;runs 两个数一组:这段有几个字符(UTF-16,换行符也算),词性是 roles 里的第几个。
*/
export interface SourceColors {
roles: TokenRole[]
runs: number[]
}
/** 每行在源码里的起止下标(end 不含换行符)。 */
function lineRanges(source: string): Line[] {
const lines: Line[] = []
let start = 0
for (const text of splitLines(source)) {
lines.push({ start, end: start + text.length })
start += text.length + (source[start + text.length] === '\r' ? 2 : 1)
/**
* 一份源码里各块的编辑范围(startLine..endLine 整行)去掉共同缩进的样子,同页面编辑框里显示的(写回见 editText.reindentBlock):
* 源码按行切一次,各块按行取。
*/
export function blockCodes(source: string): (block: Pick<BlockView, 'startLine' | 'endLine'>) => string {
const texts = splitLines(source)
return ({ startLine, endLine }) => {
const rows = texts.slice(startLine, endLine + 1)
const indent = commonIndent(rows).length
return rows.map(row => row.slice(Math.min(indent, row.length))).join('\n')
}
return lines
}
function leavesIn(node: Node, from: number, to: number, out: Node[]): void {
@@ -130,27 +135,33 @@ function coloring(layers: readonly ColorLayer[], source: string): Coloring {
return { owner, roles }
}
/** 按语法树叶子给 [startRow, endRow] 这几行着色:每个有词性的叶子一段,空白、标点等 plain 的连成一段。 */
function colorLines({ owner, roles }: Coloring, source: string, lines: Line[], startRow: number, endRow: number, indent: number): Segment[] {
const segments: Segment[] = []
let current = -2
const put = (char: string, who: number): void => {
const prev = segments.at(-1)
/** 各着色层铺好后整份源码的着色(见 SourceColors):每个字符取它所在叶子的词性,不归哪个叶子的空白、标点是 plain。 */
function sourceColors({ owner, roles }: Coloring): SourceColors {
const colors: SourceColors = { roles: [], runs: [] }
const order = new Map<TokenRole, number>()
let current: TokenRole | undefined
let length = 0
const close = (): void => {
if (current === undefined)
return
let index = order.get(current)
if (index === undefined) {
index = colors.roles.push(current) - 1
order.set(current, index)
}
colors.runs.push(length, index)
}
for (const who of owner) {
const role = who < 0 ? 'plain' : roles[who]
if (prev && (who === current || (role === 'plain' && prev.role === 'plain')))
prev.text += char
else
segments.push({ text: char, role })
current = who
if (role !== current) {
close()
current = role
length = 0
}
length++
}
for (let row = startRow; row <= endRow; row++) {
const line = lines[row]
if (row > startRow)
put('\n', owner[line.start - 1])
for (let at = line.start + Math.min(indent, line.end - line.start); at < line.end; at++)
put(source[at], owner[at])
}
return segments
close()
return colors
}
/**
@@ -291,7 +302,7 @@ function withCommentRow(rows: readonly PropertyRow[], row: PropertyRow): Propert
}
/**
* 代码块树 → 视图数据:块的完整范围整行源码去共同缩进、按语法树着色,调用翻译插件。
* 代码块树 → 视图数据:各块的编辑范围、翻译插件给的译文;块的源码、着色不按块带,页面从整份源码里按编辑范围取(见 SourceColors)。
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock):规则的卡片,或认成的属性表块(交给翻译插件排,同语言插件报的这种块),
* 并进去的子块不再单独成块。讲块的注释(BlockShape.comments)排成卡片上的「注释」一行(见 withCommentRow)。
*/
@@ -303,24 +314,9 @@ export function toBlockViews(
embedded: EmbeddedParts = new Map(),
): BlockView[] {
const { source } = document
const lines = lineRanges(source)
const texts = splitLines(source)
const outer = blocks[0]?.node.tree.rootNode
const colors = coloring([
...(outer ? [{ root: outer, tokenRole: document.language.tokenRole }] : []),
...[...embedded.values()].map(part => ({ root: part.root, tokenRole: part.document.language.tokenRole })),
], source)
/** 一段占的整行源码去共同缩进,和它的着色(嵌着的代码按它那种语言着色)。 */
const sourceRows = ({ startRow, endRow }: SourceSpan): Pick<BlockView, 'startLine' | 'endLine' | 'code' | 'codeTokens'> => {
const rows = texts.slice(startRow, endRow + 1)
const indent = commonIndent(rows).length
return {
startLine: startRow,
endLine: endRow,
code: rows.map(row => row.slice(Math.min(indent, row.length))).join('\n'),
codeTokens: colorLines(colors, source, lines, startRow, endRow, indent),
}
}
/** 一段占的行:块的编辑范围 */
const spanLines = ({ startRow, endRow }: SourceSpan): Pick<BlockView, 'startLine' | 'endLine'> => ({ startLine: startRow, endLine: endRow })
/** 一段占的整行源码:没名字的块按它认(见 BlockAnchors.place)。 */
const rowsText = ({ startRow, endRow }: SourceSpan): string => texts.slice(startRow, endRow + 1).join('\n')
/** 按源码先后逐块给位置记号:先序遍历,块的记号在它的子块之前要 */
@@ -437,7 +433,7 @@ export function toBlockViews(
id,
anchor,
kind: block.kind,
...sourceRows(span),
...spanLines(span),
headerEndLine: block.header.endRow,
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
...(name && (block.kind === 'function' || block.kind === 'class') && { callTree: {} }),
@@ -474,7 +470,7 @@ export function toBlockViews(
id,
anchor,
kind,
...sourceRows(span),
...spanLines(span),
headerEndLine: statement.endRow,
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
...(defines && endpoint && { callTree: { endpoint } }),
@@ -512,6 +508,8 @@ export function toBlockViews(
*/
export interface DocumentViews {
blocks: BlockView[]
/** 整份源码的着色(嵌着的代码按它那种语言着色) */
colors: SourceColors
displayNames: DisplayName[]
symbols: LocalSymbol[]
imports: ImportedName[]
@@ -585,12 +583,16 @@ export async function readBlockViews(
}
const codeBlocks = adapter.toBlocks(tree.rootNode, source, languageId)
const blocks = toBlockViews(codeBlocks, document, translator, extras.rules, parts)
const colors = sourceColors(coloring([
{ root: tree.rootNode, tokenRole: adapter.tokenRole },
...[...parts.values()].map(part => ({ root: part.root, tokenRole: part.document.language.tokenRole })),
], source))
const symbols = [
...adapter.symbols?.(tree.rootNode, source, languageId) ?? [],
...[...parts.values()].flatMap(part => part.document.language.symbols?.(part.root, source, part.code.languageId) ?? []),
]
const bindings = adapter.bindings?.(tree.rootNode, source, languageId) ?? []
return { blocks, displayNames: names, symbols, imports: importedNames(importsIn(codeBlocks)), kinds: namedKinds(codeBlocks), bindings }
return { blocks, colors, displayNames: names, symbols, imports: importedNames(importsIn(codeBlocks)), kinds: namedKinds(codeBlocks), bindings }
}
finally {
for (const parsed of trees)