🎨 Modify code block logic

This commit is contained in:
2025-11-17 22:11:16 +08:00
parent 59db8dd177
commit a08c0d8448
11 changed files with 156 additions and 65 deletions

View File

@@ -0,0 +1,53 @@
import { Annotation, Transaction } from "@codemirror/state";
/**
* 统一的 CodeBlock 注解,用于标记内部触发的事务。
*/
export const codeBlockEvent = Annotation.define<string>();
export const LANGUAGE_CHANGE = "codeblock-language-change";
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";
/**
* 统一管理的 userEvent 常量,方便复用与检索。
*/
export const USER_EVENTS = {
INPUT: "input",
DELETE: "delete",
MOVE: "move",
SELECT: "select",
DELETE_LINE: "delete.line",
DELETE_CUT: "delete.cut",
INPUT_PASTE: "input.paste",
MOVE_LINE: "move.line",
MOVE_CHARACTER: "move.character",
SELECT_BLOCK_BOUNDARY: "select.block-boundary",
} as const;
/**
* 判断事务列表中是否包含指定注解。
*/
export function transactionsHasAnnotation(
transactions: readonly Transaction[],
annotation: string
) {
return transactions.some(
tr => tr.annotation(codeBlockEvent) === annotation
);
}
/**
* 判断事务列表中是否包含任一注解。
*/
export function transactionsHasAnnotationsAny(
transactions: readonly Transaction[],
annotations: readonly string[]
) {
return transactions.some(tr => {
const value = tr.annotation(codeBlockEvent);
return value ? annotations.includes(value) : false;
});
}

View File

@@ -2,11 +2,12 @@
* Block 命令
*/
import { EditorSelection } from "@codemirror/state";
import { EditorSelection, Transaction } from "@codemirror/state";
import { Command } from "@codemirror/view";
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./state";
import { Block, EditorOptions, DELIMITER_REGEX } from "./types";
import { formatBlockContent } from "./formatCode";
import { codeBlockEvent, LANGUAGE_CHANGE, ADD_NEW_BLOCK, MOVE_BLOCK, DELETE_BLOCK, CURRENCIES_LOADED, USER_EVENTS } from "./annotation";
/**
* 获取块分隔符
@@ -32,7 +33,7 @@ export const insertNewBlockAtCursor = (options: EditorOptions): Command => ({ st
dispatch(state.replaceSelection(delimText), {
scrollIntoView: true,
userEvent: "input",
userEvent: USER_EVENTS.INPUT,
});
return true;
@@ -49,15 +50,16 @@ export const addNewBlockBeforeCurrent = (options: EditorOptions): Command => ({
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
dispatch(state.update({
dispatch(state.update({
changes: {
from: block.delimiter.from,
insert: delimText,
},
selection: EditorSelection.cursor(block.delimiter.from + delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: "input",
userEvent: USER_EVENTS.INPUT,
}));
return true;
@@ -74,15 +76,16 @@ export const addNewBlockAfterCurrent = (options: EditorOptions): Command => ({ s
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
dispatch(state.update({
dispatch(state.update({
changes: {
from: block.content.to,
insert: delimText,
},
selection: EditorSelection.cursor(block.content.to + delimText.length)
selection: EditorSelection.cursor(block.content.to + delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: "input",
userEvent: USER_EVENTS.INPUT,
}));
return true;
@@ -99,15 +102,16 @@ export const addNewBlockBeforeFirst = (options: EditorOptions): Command => ({ st
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
dispatch(state.update({
dispatch(state.update({
changes: {
from: block.delimiter.from,
insert: delimText,
},
selection: EditorSelection.cursor(delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: "input",
userEvent: USER_EVENTS.INPUT,
}));
return true;
@@ -124,15 +128,16 @@ export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ stat
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
dispatch(state.update({
dispatch(state.update({
changes: {
from: block.content.to,
insert: delimText,
},
selection: EditorSelection.cursor(block.content.to + delimText.length)
selection: EditorSelection.cursor(block.content.to + delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: "input",
userEvent: USER_EVENTS.INPUT,
}));
return true;
@@ -143,26 +148,19 @@ export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ stat
*/
export function changeLanguageTo(state: any, dispatch: any, block: Block, language: string, auto: boolean) {
if (state.readOnly) return false;
const currentDelimiter = state.doc.sliceString(block.delimiter.from, block.delimiter.to);
// 重置正则表达式的 lastIndex
DELIMITER_REGEX.lastIndex = 0;
if (currentDelimiter.match(DELIMITER_REGEX)) {
const newDelimiter = `\n∞∞∞${language}${auto ? '-a' : ''}\n`;
dispatch({
changes: {
from: block.delimiter.from,
to: block.delimiter.to,
insert: newDelimiter,
},
});
return true;
} else {
return false;
}
const newDelimiter = `\n∞∞∞${language}${auto ? '-a' : ''}\n`;
dispatch({
changes: {
from: block.delimiter.from,
to: block.delimiter.to,
insert: newDelimiter,
},
annotations: [codeBlockEvent.of(LANGUAGE_CHANGE)],
});
return true;
}
/**
@@ -189,7 +187,7 @@ function updateSel(sel: EditorSelection, by: (range: any) => any): EditorSelecti
}
function setSel(state: any, selection: EditorSelection) {
return state.update({ selection, scrollIntoView: true, userEvent: "select" });
return state.update({ selection, scrollIntoView: true, userEvent: USER_EVENTS.SELECT });
}
function extendSel(state: any, dispatch: any, how: (range: any) => any) {
@@ -303,10 +301,11 @@ export const deleteBlock = (_options: EditorOptions): Command => ({ state, dispa
to: block.range.to,
insert: ""
},
selection: EditorSelection.cursor(newCursorPos)
selection: EditorSelection.cursor(newCursorPos),
annotations: [codeBlockEvent.of(DELETE_BLOCK)]
}, {
scrollIntoView: true,
userEvent: "delete"
userEvent: USER_EVENTS.DELETE
}));
return true;
@@ -366,10 +365,11 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
dispatch(state.update({
changes,
selection: EditorSelection.cursor(newCursorPos)
selection: EditorSelection.cursor(newCursorPos),
annotations: [codeBlockEvent.of(MOVE_BLOCK)]
}, {
scrollIntoView: true,
userEvent: "move"
userEvent: USER_EVENTS.MOVE
}));
return true;
@@ -380,4 +380,21 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
*/
export const formatCurrentBlock: Command = (view) => {
return formatBlockContent(view);
};
};
/**
* 触发一次货币数据刷新,让数学块重新计算
*/
export function triggerCurrenciesLoaded({ state, dispatch }: { state: any; dispatch: any }) {
if (!dispatch || state.readOnly) {
return false;
}
dispatch(state.update({
changes: { from: 0, to: 0, insert: "" },
annotations: [
codeBlockEvent.of(CURRENCIES_LOADED),
Transaction.addToHistory.of(false)
],
}));
return true;
}

View File

@@ -7,6 +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";
/**
* 构建块分隔符正则表达式
@@ -89,7 +90,7 @@ export const codeBlockCopyCut = EditorView.domEventHandlers({
view.dispatch({
changes: ranges,
scrollIntoView: true,
userEvent: "delete.cut"
userEvent: USER_EVENTS.DELETE_CUT
});
}
}
@@ -111,7 +112,7 @@ const copyCut = (view: EditorView, cut: boolean): boolean => {
view.dispatch({
changes: ranges,
scrollIntoView: true,
userEvent: "delete.cut"
userEvent: USER_EVENTS.DELETE_CUT
});
}
@@ -142,7 +143,7 @@ function doPaste(view: EditorView, input: string) {
}
view.dispatch(changes, {
userEvent: "input.paste",
userEvent: USER_EVENTS.INPUT_PASTE,
scrollIntoView: true
});
}
@@ -186,4 +187,4 @@ export function getCopyPasteExtensions() {
return [
codeBlockCopyCut,
];
}
}

View File

@@ -7,6 +7,7 @@ import { EditorView } from '@codemirror/view';
import { EditorSelection } from '@codemirror/state';
import { blockState } from './state';
import { Block } from './types';
import { USER_EVENTS } from './annotation';
/**
* 二分查找:找到包含指定位置的块
@@ -136,7 +137,7 @@ export function createCursorProtection() {
view.dispatch({
selection: EditorSelection.cursor(adjustedPos),
scrollIntoView: true,
userEvent: 'select'
userEvent: USER_EVENTS.SELECT
});
// 阻止默认行为
@@ -148,4 +149,3 @@ export function createCursorProtection() {
}
});
}

View File

@@ -5,6 +5,7 @@
import { ViewPlugin, EditorView, Decoration, WidgetType, layer, RectangleMarker } from "@codemirror/view";
import { StateField, RangeSetBuilder, EditorState } from "@codemirror/state";
import { blockState } from "./state";
import { codeBlockEvent } from "./annotation";
/**
* 块开始装饰组件
@@ -180,10 +181,11 @@ const blockLayer = layer({
*/
const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any) => {
const protect: number[] = [];
const internalEvent = tr.annotation(codeBlockEvent);
// 获取块状态并获取第一个块的分隔符大小
const blocks = tr.startState.field(blockState);
if (blocks && blocks.length > 0) {
if (!internalEvent && blocks && blocks.length > 0) {
const firstBlock = blocks[0];
const firstBlockDelimiterSize = firstBlock.delimiter.to;
@@ -195,22 +197,25 @@ const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any)
// 如果是搜索替换操作,保护所有块分隔符
if (tr.annotations.some((a: any) => a.value === "input.replace" || a.value === "input.replace.all")) {
blocks.forEach((block: any) => {
blocks?.forEach((block: any) => {
if (block.delimiter) {
protect.push(block.delimiter.from, block.delimiter.to);
}
});
}
// 返回保护范围数组,如果没有需要保护的范围则返回 false
return protect.length > 0 ? protect : false;
});
// 返回保护范围数组;若无需保护则返回 true 放行事务
return protect.length > 0 ? protect : true;
})
/**
* 防止选择在第一个块之前
* 使用 transactionFilter 来确保选择不会在第一个块之前
*/
const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr: any) => {
if (tr.annotation(codeBlockEvent)) {
return tr;
}
// 获取块状态并获取第一个块的分隔符大小
const blocks = tr.startState.field(blockState);
if (!blocks || blocks.length === 0) {
@@ -261,4 +266,4 @@ export function getBlockDecorationExtensions(options: {
}
return extensions;
}
}

View File

@@ -6,6 +6,7 @@
import { EditorSelection, SelectionRange } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { getNoteBlockFromPos } from "./state";
import { USER_EVENTS } from "./annotation";
interface LineBlock {
from: number;
@@ -87,7 +88,7 @@ export const deleteLine = (view: EditorView): boolean => {
changes,
selection,
scrollIntoView: true,
userEvent: "delete.line"
userEvent: USER_EVENTS.DELETE_LINE
});
return true;
@@ -127,8 +128,8 @@ export const deleteLineCommand = ({ state, dispatch }: { state: any; dispatch: a
changes,
selection,
scrollIntoView: true,
userEvent: "delete.line"
userEvent: USER_EVENTS.DELETE_LINE
}));
return true;
};
};

View File

@@ -4,6 +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";
export const formatBlockContent = (view) => {
if (!view || view.state.readOnly)
@@ -87,7 +88,7 @@ export const formatBlockContent = (view) => {
},
selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
scrollIntoView: true,
userEvent: "input"
userEvent: USER_EVENTS.INPUT
});
return true;
@@ -100,4 +101,4 @@ export const formatBlockContent = (view) => {
// 执行异步格式化
performFormat();
return true; // 立即返回 true表示命令已开始执行
};
};

View File

@@ -6,6 +6,7 @@
import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view";
import { RangeSetBuilder } from "@codemirror/state";
import { getNoteBlockFromPos } from "./state";
import { transactionsHasAnnotation, CURRENCIES_LOADED } from "./annotation";
// 声明全局math对象
declare global {
interface Window {
@@ -75,6 +76,11 @@ function mathDeco(view: any): any {
// get math.js parser and cache it for this block
let { parser, prev } = mathParsers.get(block) || {};
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 });
@@ -148,8 +154,12 @@ export const mathBlock = ViewPlugin.fromClass(class {
}
update(update: any) {
// If the document changed, the viewport changed, update the decorations
if (update.docChanged || update.viewportChanged) {
// 需要在文档/视口变化或收到 CURRENCIES_LOADED 注解时重新渲染
if (
update.docChanged ||
update.viewportChanged ||
transactionsHasAnnotation(update.transactions, CURRENCIES_LOADED)
) {
this.decorations = mathDeco(update.view);
}
}

View File

@@ -6,6 +6,7 @@
import { EditorSelection, SelectionRange } from "@codemirror/state";
import { blockState } from "./state";
import { LANGUAGES } from "./lang-parser/languages";
import { USER_EVENTS } from "./annotation";
interface LineBlock {
from: number;
@@ -131,7 +132,7 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
changes,
scrollIntoView: true,
selection: EditorSelection.create(ranges, state.selection.mainIndex),
userEvent: "move.line"
userEvent: USER_EVENTS.MOVE_LINE
}));
return true;
@@ -157,4 +158,4 @@ export const moveLineUp = ({ state, dispatch }: { state: any; dispatch: any }):
*/
export const moveLineDown = ({ state, dispatch }: { state: any; dispatch: any }): boolean => {
return moveLine(state, dispatch, true);
};
};

View File

@@ -7,6 +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";
/**
* 当用户按下 Ctrl+A 时,我们希望首先选择整个块。但如果整个块已经被选中,
@@ -115,7 +116,7 @@ export const selectAll: Command = ({ state, dispatch }) => {
// 选择当前块的所有内容
dispatch(state.update({
selection: { anchor: block.content.from, head: block.content.to },
userEvent: "select"
userEvent: USER_EVENTS.SELECT
}));
return true;
@@ -127,7 +128,7 @@ export const selectAll: Command = ({ state, dispatch }) => {
*/
export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) => {
// 只处理选择变化的事务,并且忽略我们自己生成的事务
if (!tr.selection || !tr.selection.ranges || tr.annotation?.(Transaction.userEvent) === "select.block-boundary") {
if (!tr.selection || !tr.selection.ranges || tr.annotation?.(Transaction.userEvent) === USER_EVENTS.SELECT_BLOCK_BOUNDARY) {
return tr;
}
@@ -181,7 +182,7 @@ export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) =>
return {
...tr,
selection: EditorSelection.create(correctedRanges, tr.selection.mainIndex),
annotations: tr.annotations.concat(Transaction.userEvent.of("select.block-boundary"))
annotations: tr.annotations.concat(Transaction.userEvent.of(USER_EVENTS.SELECT_BLOCK_BOUNDARY))
};
}
} catch (error) {
@@ -219,4 +220,4 @@ export function getBlockSelectExtensions() {
return [
emptyBlockSelected,
];
}
}

View File

@@ -5,6 +5,7 @@
import { EditorSelection, findClusterBreak } from "@codemirror/state";
import { getNoteBlockFromPos } from "./state";
import { USER_EVENTS } from "./annotation";
/**
* 交换光标前后的字符
@@ -46,8 +47,8 @@ export const transposeChars = ({ state, dispatch }: { state: any; dispatch: any
dispatch(state.update(changes, {
scrollIntoView: true,
userEvent: "move.character"
userEvent: USER_EVENTS.MOVE_CHARACTER
}));
return true;
};
};