From 991a89147e6d0ff5625b2848c5229b8d236a31dc Mon Sep 17 00:00:00 2001 From: landaiqing Date: Mon, 17 Nov 2025 23:14:58 +0800 Subject: [PATCH] :art: Optimize code --- .../editor/extensions/codeblock/annotation.ts | 5 +- .../editor/extensions/codeblock/copyPaste.ts | 11 +- .../extensions/codeblock/decorations.ts | 7 +- .../editor/extensions/codeblock/deleteLine.ts | 7 +- .../editor/extensions/codeblock/formatCode.ts | 5 +- .../editor/extensions/codeblock/mathBlock.ts | 43 ++- .../editor/extensions/codeblock/moveLines.ts | 4 +- .../editor/extensions/codeblock/parser.ts | 268 +++++------------- .../editor/extensions/codeblock/selectAll.ts | 5 +- .../extensions/codeblock/transposeChars.ts | 5 +- 10 files changed, 136 insertions(+), 224 deletions(-) diff --git a/frontend/src/views/editor/extensions/codeblock/annotation.ts b/frontend/src/views/editor/extensions/codeblock/annotation.ts index 7d937fa..0bbb44a 100644 --- a/frontend/src/views/editor/extensions/codeblock/annotation.ts +++ b/frontend/src/views/editor/extensions/codeblock/annotation.ts @@ -10,9 +10,10 @@ export const ADD_NEW_BLOCK = "codeblock-add-new-block"; export const MOVE_BLOCK = "codeblock-move-block"; export const DELETE_BLOCK = "codeblock-delete-block"; export const CURRENCIES_LOADED = "codeblock-currencies-loaded"; +export const CONTENT_EDIT = "codeblock-content-edit"; /** - * 统一管理的 userEvent 常量,方便复用与检索。 + * 统一管理的 userEvent 常量。 */ export const USER_EVENTS = { INPUT: "input", @@ -25,6 +26,8 @@ export const USER_EVENTS = { MOVE_LINE: "move.line", MOVE_CHARACTER: "move.character", SELECT_BLOCK_BOUNDARY: "select.block-boundary", + INPUT_REPLACE: "input.replace", + INPUT_REPLACE_ALL: "input.replace.all", } as const; /** diff --git a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts index c5088b3..0f51a62 100644 --- a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts +++ b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts @@ -7,7 +7,7 @@ import { EditorState, EditorSelection } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { Command } from "@codemirror/view"; import { LANGUAGES } from "./lang-parser/languages"; -import { USER_EVENTS } from "./annotation"; +import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation"; /** * 构建块分隔符正则表达式 @@ -90,7 +90,8 @@ export const codeBlockCopyCut = EditorView.domEventHandlers({ view.dispatch({ changes: ranges, scrollIntoView: true, - userEvent: USER_EVENTS.DELETE_CUT + userEvent: USER_EVENTS.DELETE_CUT, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], }); } } @@ -112,7 +113,8 @@ const copyCut = (view: EditorView, cut: boolean): boolean => { view.dispatch({ changes: ranges, scrollIntoView: true, - userEvent: USER_EVENTS.DELETE_CUT + userEvent: USER_EVENTS.DELETE_CUT, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], }); } @@ -144,7 +146,8 @@ function doPaste(view: EditorView, input: string) { view.dispatch(changes, { userEvent: USER_EVENTS.INPUT_PASTE, - scrollIntoView: true + scrollIntoView: true, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], }); } diff --git a/frontend/src/views/editor/extensions/codeblock/decorations.ts b/frontend/src/views/editor/extensions/codeblock/decorations.ts index 9d84d28..1ecf639 100644 --- a/frontend/src/views/editor/extensions/codeblock/decorations.ts +++ b/frontend/src/views/editor/extensions/codeblock/decorations.ts @@ -3,9 +3,9 @@ */ import { ViewPlugin, EditorView, Decoration, WidgetType, layer, RectangleMarker } from "@codemirror/view"; -import { StateField, RangeSetBuilder, EditorState } from "@codemirror/state"; +import { StateField, RangeSetBuilder, EditorState, Transaction } from "@codemirror/state"; import { blockState } from "./state"; -import { codeBlockEvent } from "./annotation"; +import { codeBlockEvent, USER_EVENTS } from "./annotation"; /** * 块开始装饰组件 @@ -196,7 +196,8 @@ const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any) } // 如果是搜索替换操作,保护所有块分隔符 - if (tr.annotations.some((a: any) => a.value === "input.replace" || a.value === "input.replace.all")) { + const userEvent = tr.annotation(Transaction.userEvent); + if (userEvent && (userEvent === USER_EVENTS.INPUT_REPLACE || userEvent === USER_EVENTS.INPUT_REPLACE_ALL)) { blocks?.forEach((block: any) => { if (block.delimiter) { protect.push(block.delimiter.from, block.delimiter.to); diff --git a/frontend/src/views/editor/extensions/codeblock/deleteLine.ts b/frontend/src/views/editor/extensions/codeblock/deleteLine.ts index 45bf059..fe1b633 100644 --- a/frontend/src/views/editor/extensions/codeblock/deleteLine.ts +++ b/frontend/src/views/editor/extensions/codeblock/deleteLine.ts @@ -6,6 +6,7 @@ import { EditorSelection, SelectionRange } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { getNoteBlockFromPos } from "./state"; +import { codeBlockEvent, CONTENT_EDIT } from "./annotation"; import { USER_EVENTS } from "./annotation"; interface LineBlock { @@ -88,7 +89,8 @@ export const deleteLine = (view: EditorView): boolean => { changes, selection, scrollIntoView: true, - userEvent: USER_EVENTS.DELETE_LINE + userEvent: USER_EVENTS.DELETE_LINE, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], }); return true; @@ -128,7 +130,8 @@ export const deleteLineCommand = ({ state, dispatch }: { state: any; dispatch: a changes, selection, scrollIntoView: true, - userEvent: USER_EVENTS.DELETE_LINE + userEvent: USER_EVENTS.DELETE_LINE, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], })); return true; diff --git a/frontend/src/views/editor/extensions/codeblock/formatCode.ts b/frontend/src/views/editor/extensions/codeblock/formatCode.ts index 58487b4..df7c324 100644 --- a/frontend/src/views/editor/extensions/codeblock/formatCode.ts +++ b/frontend/src/views/editor/extensions/codeblock/formatCode.ts @@ -4,7 +4,7 @@ import * as prettier from "prettier/standalone"; import { getActiveNoteBlock } from "./state"; import { getLanguage } from "./lang-parser/languages"; import { SupportedLanguage } from "./types"; -import { USER_EVENTS } from "./annotation"; +import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation"; export const formatBlockContent = (view) => { if (!view || view.state.readOnly) @@ -88,7 +88,8 @@ export const formatBlockContent = (view) => { }, selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)), scrollIntoView: true, - userEvent: USER_EVENTS.INPUT + userEvent: USER_EVENTS.INPUT, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], }); return true; diff --git a/frontend/src/views/editor/extensions/codeblock/mathBlock.ts b/frontend/src/views/editor/extensions/codeblock/mathBlock.ts index ee6440a..8bee9e0 100644 --- a/frontend/src/views/editor/extensions/codeblock/mathBlock.ts +++ b/frontend/src/views/editor/extensions/codeblock/mathBlock.ts @@ -7,6 +7,11 @@ import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view"; import { RangeSetBuilder } from "@codemirror/state"; import { getNoteBlockFromPos } from "./state"; import { transactionsHasAnnotation, CURRENCIES_LOADED } from "./annotation"; + +type MathParserEntry = { + parser: any; + prev?: any; +}; // 声明全局math对象 declare global { interface Window { @@ -63,8 +68,7 @@ class MathResult extends WidgetType { /** * 数学装饰函数 */ -function mathDeco(view: any): any { - const mathParsers = new WeakMap(); +function mathDeco(view: any, parserCache: WeakMap): any { const builder = new RangeSetBuilder(); for (const { from, to } of view.visibleRanges) { @@ -73,17 +77,17 @@ function mathDeco(view: any): any { const block = getNoteBlockFromPos(view.state, pos); if (block && block.language.name === "math") { - // get math.js parser and cache it for this block - let { parser, prev } = mathParsers.get(block) || {}; + let entry = parserCache.get(block); + let parser = entry?.parser; if (!parser) { - // 如果当前可见行不是该 math 块的第一行,为了正确累计 prev,需要从块头开始重新扫描 if (line.from > block.content.from) { pos = block.content.from; continue; } if (typeof window.math !== 'undefined') { parser = window.math.parser(); - mathParsers.set(block, { parser, prev }); + entry = { parser, prev: undefined }; + parserCache.set(block, entry); } } @@ -91,10 +95,15 @@ function mathDeco(view: any): any { let result: any; try { if (parser) { - parser.set("prev", prev); + if (entry && line.from === block.content.from && typeof parser.clear === "function") { + parser.clear(); + entry.prev = undefined; + } + const prevValue = entry?.prev; + parser.set("prev", prevValue); result = parser.evaluate(line.text); - if (result !== undefined) { - mathParsers.set(block, { parser, prev: result }); + if (entry && result !== undefined) { + entry.prev = result; } } } catch (e) { @@ -103,7 +112,7 @@ function mathDeco(view: any): any { // if we got a result from math.js, add the result decoration if (result !== undefined) { - const format = parser?.get("format"); + const format = parser?.get?.("format"); let resultWidget: MathResult | undefined; if (typeof(result) === "string") { @@ -148,19 +157,25 @@ function mathDeco(view: any): any { */ export const mathBlock = ViewPlugin.fromClass(class { decorations: any; + mathParsers: WeakMap; constructor(view: any) { - this.decorations = mathDeco(view); + this.mathParsers = new WeakMap(); + this.decorations = mathDeco(view, this.mathParsers); } update(update: any) { - // 需要在文档/视口变化或收到 CURRENCIES_LOADED 注解时重新渲染 + const hasCurrencyUpdate = transactionsHasAnnotation(update.transactions, CURRENCIES_LOADED); + if (update.docChanged || hasCurrencyUpdate) { + // 文档结构或汇率变化时重置解析缓存 + this.mathParsers = new WeakMap(); + } if ( update.docChanged || update.viewportChanged || - transactionsHasAnnotation(update.transactions, CURRENCIES_LOADED) + hasCurrencyUpdate ) { - this.decorations = mathDeco(update.view); + this.decorations = mathDeco(update.view, this.mathParsers); } } }, { diff --git a/frontend/src/views/editor/extensions/codeblock/moveLines.ts b/frontend/src/views/editor/extensions/codeblock/moveLines.ts index 6090e10..eef6edb 100644 --- a/frontend/src/views/editor/extensions/codeblock/moveLines.ts +++ b/frontend/src/views/editor/extensions/codeblock/moveLines.ts @@ -6,6 +6,7 @@ import { EditorSelection, SelectionRange } from "@codemirror/state"; import { blockState } from "./state"; import { LANGUAGES } from "./lang-parser/languages"; +import { codeBlockEvent, CONTENT_EDIT } from "./annotation"; import { USER_EVENTS } from "./annotation"; interface LineBlock { @@ -132,7 +133,8 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean { changes, scrollIntoView: true, selection: EditorSelection.create(ranges, state.selection.mainIndex), - userEvent: USER_EVENTS.MOVE_LINE + userEvent: USER_EVENTS.MOVE_LINE, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], })); return true; diff --git a/frontend/src/views/editor/extensions/codeblock/parser.ts b/frontend/src/views/editor/extensions/codeblock/parser.ts index 8f70d26..07a67ed 100644 --- a/frontend/src/views/editor/extensions/codeblock/parser.ts +++ b/frontend/src/views/editor/extensions/codeblock/parser.ts @@ -3,7 +3,8 @@ */ import { EditorState } from '@codemirror/state'; -import { syntaxTree, syntaxTreeAvailable } from '@codemirror/language'; +import { syntaxTree, ensureSyntaxTree } from '@codemirror/language'; +import type { Tree } from '@lezer/common'; import { Block as BlockNode, BlockDelimiter, BlockContent, BlockLanguage } from './lang-parser/parser.terms.js'; import { SupportedLanguage, @@ -15,51 +16,47 @@ import { } from './types'; import { LANGUAGES } from './lang-parser/languages'; +const DEFAULT_LANGUAGE = (LANGUAGES[0]?.token || 'text') as string; + /** * 从语法树解析代码块 */ export function getBlocksFromSyntaxTree(state: EditorState): Block[] | null { - if (!syntaxTreeAvailable(state)) { + const tree = syntaxTree(state); + if (!tree) { return null; } + return collectBlocksFromTree(tree, state); +} - const tree = syntaxTree(state); +function collectBlocksFromTree(tree: Tree, state: EditorState): Block[] | null { const blocks: Block[] = []; const doc = state.doc; - // 遍历语法树中的所有块 tree.iterate({ enter(node) { if (node.type.id === BlockNode) { - // 查找块的分隔符和内容 let delimiter: { from: number; to: number } | null = null; let content: { from: number; to: number } | null = null; - let language = 'text'; + let language: string = DEFAULT_LANGUAGE; let auto = false; - // 遍历块的子节点 const blockNode = node.node; blockNode.firstChild?.cursor().iterate(child => { if (child.type.id === BlockDelimiter) { delimiter = { from: child.from, to: child.to }; - - // 解析整个分隔符文本来获取语言和自动检测标记 const delimiterText = doc.sliceString(child.from, child.to); - - // 使用正则表达式解析分隔符 const match = delimiterText.match(/∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/); if (match) { - language = match[1] || 'text'; + language = match[1] || DEFAULT_LANGUAGE; auto = match[2] === '-a'; } else { - // 回退到逐个解析子节点 child.node.firstChild?.cursor().iterate(langChild => { if (langChild.type.id === BlockLanguage) { const langText = doc.sliceString(langChild.from, langChild.to); - language = langText || 'text'; + language = langText || DEFAULT_LANGUAGE; } - // 检查是否有自动检测标记 - if (doc.sliceString(langChild.from, langChild.to) === '-a') { + if (doc.sliceString(langChild.from, langChild.to) === AUTO_DETECT_SUFFIX) { auto = true; } }); @@ -88,7 +85,6 @@ export function getBlocksFromSyntaxTree(state: EditorState): Block[] | null { }); if (blocks.length > 0) { - // 设置第一个块分隔符的大小 firstBlockDelimiterSize = blocks[0].delimiter.to; return blocks; } @@ -104,203 +100,78 @@ export let firstBlockDelimiterSize: number | undefined; */ export function getBlocksFromString(state: EditorState): Block[] { const blocks: Block[] = []; - const doc = state.doc; + const doc = state.doc; if (doc.length === 0) { - // 如果文档为空,创建一个默认的文本块 - return [{ - language: { - name: 'text', - auto: false, - }, - content: { - from: 0, - to: 0, - }, - delimiter: { - from: 0, - to: 0, - }, - range: { - from: 0, - to: 0, - }, - }]; - } + return [createPlainTextBlock(0, 0)]; + } const content = doc.sliceString(0, doc.length); - const delim = "\n∞∞∞"; - let pos = 0; + const delimiter = DELIMITER_PREFIX; + const suffixLength = DELIMITER_SUFFIX.length; - // 检查文档是否以分隔符开始(不带前导换行符) - if (content.startsWith("∞∞∞")) { - // 文档直接以分隔符开始,调整为标准格式 - pos = 0; - } else if (content.startsWith("\n∞∞∞")) { - // 文档以换行符+分隔符开始,这是标准格式,从位置0开始解析 - pos = 0; - } else { - // 如果文档不以分隔符开始,查找第一个分隔符 - const firstDelimPos = content.indexOf(delim); - - if (firstDelimPos === -1) { - // 如果没有找到分隔符,整个文档作为一个文本块 - firstBlockDelimiterSize = 0; - return [{ - language: { - name: 'text', - auto: false, - }, - content: { - from: 0, - to: doc.length, - }, - delimiter: { - from: 0, - to: 0, - }, - range: { - from: 0, - to: doc.length, - }, - }]; - } + let pos = content.indexOf(delimiter); + + if (pos === -1) { + firstBlockDelimiterSize = 0; + return [createPlainTextBlock(0, doc.length)]; + } + + if (pos > 0) { + blocks.push(createPlainTextBlock(0, pos)); + } + + while (pos !== -1 && pos < doc.length) { + const blockStart = pos; + const langStart = blockStart + delimiter.length; + const delimiterEnd = content.indexOf(DELIMITER_SUFFIX, langStart); + if (delimiterEnd === -1) break; + + const delimiterText = content.slice(blockStart, delimiterEnd + suffixLength); + const delimiterInfo = parseDelimiter(delimiterText); + if (!delimiterInfo) break; + + const contentStart = delimiterEnd + suffixLength; + const nextDelimiter = content.indexOf(delimiter, contentStart); + const contentEnd = nextDelimiter === -1 ? doc.length : nextDelimiter; - // 创建第一个块(分隔符之前的内容) blocks.push({ - language: { - name: 'text', - auto: false, - }, - content: { - from: 0, - to: firstDelimPos, - }, - delimiter: { - from: 0, - to: 0, - }, - range: { - from: 0, - to: firstDelimPos, - }, + language: { name: delimiterInfo.language, auto: delimiterInfo.auto }, + content: { from: contentStart, to: contentEnd }, + delimiter: { from: blockStart, to: delimiterEnd + suffixLength }, + range: { from: blockStart, to: contentEnd }, }); - - pos = firstDelimPos; - firstBlockDelimiterSize = 0; + + pos = nextDelimiter; } - - while (pos < doc.length) { - let blockStart: number; - - if (pos === 0 && content.startsWith("∞∞∞")) { - // 处理文档开头直接是分隔符的情况(不带前导换行符) - blockStart = 0; - } else if (pos === 0 && content.startsWith("\n∞∞∞")) { - // 处理文档开头是换行符+分隔符的情况(标准格式) - blockStart = 0; - } else { - blockStart = content.indexOf(delim, pos); - if (blockStart !== pos) { - // 如果在当前位置没有找到分隔符,可能是文档结尾 - break; - } - } - - // 确定语言开始位置 - let langStart: number; - if (pos === 0 && content.startsWith("∞∞∞")) { - // 文档直接以分隔符开始,跳过 ∞∞∞ - langStart = blockStart + 3; - } else { - // 标准情况,跳过 \n∞∞∞ - langStart = blockStart + delim.length; - } - - const delimiterEnd = content.indexOf("\n", langStart); - if (delimiterEnd < 0) { - console.error("Error parsing blocks. Delimiter didn't end with newline"); - break; - } - - const langFull = content.substring(langStart, delimiterEnd); - let auto = false; - let lang = langFull; - - if (langFull.endsWith("-a")) { - auto = true; - lang = langFull.substring(0, langFull.length - 2); - } - - const contentFrom = delimiterEnd + 1; - let blockEnd = content.indexOf(delim, contentFrom); - if (blockEnd < 0) { - blockEnd = doc.length; - } - - const block: Block = { - language: { - name: lang || 'text', - auto: auto, - }, - content: { - from: contentFrom, - to: blockEnd, - }, - delimiter: { - from: blockStart, - to: delimiterEnd + 1, - }, - range: { - from: blockStart, - to: blockEnd, - }, - }; - - blocks.push(block); - pos = blockEnd; - } - - // 如果没有找到任何块,创建一个默认块 + if (blocks.length === 0) { - blocks.push({ - language: { - name: 'text', - auto: false, - }, - content: { - from: 0, - to: doc.length, - }, - delimiter: { - from: 0, - to: 0, - }, - range: { - from: 0, - to: doc.length, - }, - }); + blocks.push(createPlainTextBlock(0, doc.length)); firstBlockDelimiterSize = 0; } else { - // 设置第一个块分隔符的大小 firstBlockDelimiterSize = blocks[0].delimiter.to; } - return blocks; + return blocks; } /** * 获取文档中的所有块 */ export function getBlocks(state: EditorState): Block[] { - // 优先使用语法树解析 - const syntaxTreeBlocks = getBlocksFromSyntaxTree(state); - if (syntaxTreeBlocks) { - return syntaxTreeBlocks; + let blocks = getBlocksFromSyntaxTree(state); + if (blocks) { + return blocks; + } + + const ensuredTree = ensureSyntaxTree(state, state.doc.length, 200); + if (ensuredTree) { + blocks = collectBlocksFromTree(ensuredTree, state); + if (blocks) { + return blocks; + } } - // 如果语法树不可用,回退到字符串解析 return getBlocksFromString(state); } @@ -396,10 +267,21 @@ export function parseDelimiter(delimiterText: string): { language: SupportedLang const validLanguage = LANGUAGES.some(lang => lang.token === languageName) ? languageName as SupportedLanguage - : 'text'; + : DEFAULT_LANGUAGE as SupportedLanguage; return { language: validLanguage, auto: isAuto }; -} \ No newline at end of file +} + +function createPlainTextBlock(from: number, to: number): Block { + return { + language: { name: DEFAULT_LANGUAGE, auto: false }, + content: { from, to }, + delimiter: { from: 0, to: 0 }, + range: { from, to }, + }; +} + + diff --git a/frontend/src/views/editor/extensions/codeblock/selectAll.ts b/frontend/src/views/editor/extensions/codeblock/selectAll.ts index b913db9..1784fcb 100644 --- a/frontend/src/views/editor/extensions/codeblock/selectAll.ts +++ b/frontend/src/views/editor/extensions/codeblock/selectAll.ts @@ -7,7 +7,7 @@ import { StateField, StateEffect, RangeSetBuilder, EditorSelection, EditorState, import { selectAll as defaultSelectAll } from "@codemirror/commands"; import { Command } from "@codemirror/view"; import { getActiveNoteBlock, blockState } from "./state"; -import { USER_EVENTS } from "./annotation"; +import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation"; /** * 当用户按下 Ctrl+A 时,我们希望首先选择整个块。但如果整个块已经被选中, @@ -116,7 +116,8 @@ export const selectAll: Command = ({ state, dispatch }) => { // 选择当前块的所有内容 dispatch(state.update({ selection: { anchor: block.content.from, head: block.content.to }, - userEvent: USER_EVENTS.SELECT + userEvent: USER_EVENTS.SELECT, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], })); return true; diff --git a/frontend/src/views/editor/extensions/codeblock/transposeChars.ts b/frontend/src/views/editor/extensions/codeblock/transposeChars.ts index ffa0f35..72ff746 100644 --- a/frontend/src/views/editor/extensions/codeblock/transposeChars.ts +++ b/frontend/src/views/editor/extensions/codeblock/transposeChars.ts @@ -5,7 +5,7 @@ import { EditorSelection, findClusterBreak } from "@codemirror/state"; import { getNoteBlockFromPos } from "./state"; -import { USER_EVENTS } from "./annotation"; +import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation"; /** * 交换光标前后的字符 @@ -47,7 +47,8 @@ export const transposeChars = ({ state, dispatch }: { state: any; dispatch: any dispatch(state.update(changes, { scrollIntoView: true, - userEvent: USER_EVENTS.MOVE_CHARACTER + userEvent: USER_EVENTS.MOVE_CHARACTER, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], })); return true;