Files
voidraft/frontend/src/views/editor/extensions/codeblock/commands.ts
2025-11-17 22:11:16 +08:00

401 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Block 命令
*/
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";
/**
* 获取块分隔符
*/
export function getBlockDelimiter(defaultToken: string, autoDetect: boolean): string {
return `\n∞∞∞${autoDetect ? defaultToken + '-a' : defaultToken}\n`;
}
/**
* 在光标处插入新块
*/
export const insertNewBlockAtCursor = (options: EditorOptions): Command => ({ state, dispatch }) => {
if (state.readOnly) return false;
const currentBlock = getActiveNoteBlock(state);
let delimText: string;
if (currentBlock) {
delimText = `\n∞∞∞${currentBlock.language.name}${currentBlock.language.auto ? "-a" : ""}\n`;
} else {
delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
}
dispatch(state.replaceSelection(delimText), {
scrollIntoView: true,
userEvent: USER_EVENTS.INPUT,
});
return true;
};
/**
* 在当前块之前添加新块
*/
export const addNewBlockBeforeCurrent = (options: EditorOptions): Command => ({ state, dispatch }) => {
if (state.readOnly) return false;
const block = getActiveNoteBlock(state);
if (!block) return false;
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
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: USER_EVENTS.INPUT,
}));
return true;
};
/**
* 在当前块之后添加新块
*/
export const addNewBlockAfterCurrent = (options: EditorOptions): Command => ({ state, dispatch }) => {
if (state.readOnly) return false;
const block = getActiveNoteBlock(state);
if (!block) return false;
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
dispatch(state.update({
changes: {
from: block.content.to,
insert: delimText,
},
selection: EditorSelection.cursor(block.content.to + delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: USER_EVENTS.INPUT,
}));
return true;
};
/**
* 在第一个块之前添加新块
*/
export const addNewBlockBeforeFirst = (options: EditorOptions): Command => ({ state, dispatch }) => {
if (state.readOnly) return false;
const block = getFirstNoteBlock(state);
if (!block) return false;
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
dispatch(state.update({
changes: {
from: block.delimiter.from,
insert: delimText,
},
selection: EditorSelection.cursor(delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: USER_EVENTS.INPUT,
}));
return true;
};
/**
* 在最后一个块之后添加新块
*/
export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ state, dispatch }) => {
if (state.readOnly) return false;
const block = getLastNoteBlock(state);
if (!block) return false;
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
dispatch(state.update({
changes: {
from: block.content.to,
insert: delimText,
},
selection: EditorSelection.cursor(block.content.to + delimText.length),
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: USER_EVENTS.INPUT,
}));
return true;
};
/**
* 更改块语言
*/
export function changeLanguageTo(state: any, dispatch: any, block: Block, language: string, auto: boolean) {
if (state.readOnly) 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;
}
/**
* 更改当前块语言
*/
export function changeCurrentBlockLanguage(state: any, dispatch: any, language: string | null, auto: boolean) {
const block = getActiveNoteBlock(state);
if (!block) {
console.warn("No active block found");
return false;
}
// 如果 language 为 null我们只想更改自动检测标志
if (language === null) {
language = block.language.name;
}
return changeLanguageTo(state, dispatch, block, language, auto);
}
// 选择和移动辅助函数
function updateSel(sel: EditorSelection, by: (range: any) => any): EditorSelection {
return EditorSelection.create(sel.ranges.map(by), sel.mainIndex);
}
function setSel(state: any, selection: EditorSelection) {
return state.update({ selection, scrollIntoView: true, userEvent: USER_EVENTS.SELECT });
}
function extendSel(state: any, dispatch: any, how: (range: any) => any) {
const selection = updateSel(state.selection, range => {
const head = how(range);
return EditorSelection.range(range.anchor, head.head, head.goalColumn, head.bidiLevel || undefined);
});
if (selection.eq(state.selection)) return false;
dispatch(setSel(state, selection));
return true;
}
function moveSel(state: any, dispatch: any, how: (range: any) => any) {
const selection = updateSel(state.selection, how);
if (selection.eq(state.selection)) return false;
dispatch(setSel(state, selection));
return true;
}
function previousBlock(state: any, range: any) {
const blocks = state.field(blockState);
const block = getNoteBlockFromPos(state, range.head);
if (!block) return EditorSelection.cursor(0);
if (range.head === block.content.from) {
const index = blocks.indexOf(block);
const previousBlockIndex = index > 0 ? index - 1 : 0;
return EditorSelection.cursor(blocks[previousBlockIndex].content.from);
} else {
return EditorSelection.cursor(block.content.from);
}
}
function nextBlock(state: any, range: any) {
const blocks = state.field(blockState);
const block = getNoteBlockFromPos(state, range.head);
if (!block) return EditorSelection.cursor(state.doc.length);
if (range.head === block.content.to) {
const index = blocks.indexOf(block);
const nextBlockIndex = index < blocks.length - 1 ? index + 1 : index;
return EditorSelection.cursor(blocks[nextBlockIndex].content.to);
} else {
return EditorSelection.cursor(block.content.to);
}
}
/**
* 跳转到下一个块
*/
export function gotoNextBlock({ state, dispatch }: any) {
return moveSel(state, dispatch, (range: any) => nextBlock(state, range));
}
/**
* 选择到下一个块
*/
export function selectNextBlock({ state, dispatch }: any) {
return extendSel(state, dispatch, (range: any) => nextBlock(state, range));
}
/**
* 跳转到上一个块
*/
export function gotoPreviousBlock({ state, dispatch }: any) {
return moveSel(state, dispatch, (range: any) => previousBlock(state, range));
}
/**
* 选择到上一个块
*/
export function selectPreviousBlock({ state, dispatch }: any) {
return extendSel(state, dispatch, (range: any) => previousBlock(state, range));
}
/**
* 删除块
*/
export const deleteBlock = (_options: EditorOptions): Command => ({ state, dispatch }) => {
if (state.readOnly) return false;
const block = getActiveNoteBlock(state);
if (!block) return false;
const blocks = state.field(blockState);
if (blocks.length <= 1) return false; // 不能删除最后一个块
const blockIndex = blocks.indexOf(block);
let newCursorPos: number;
if (blockIndex === blocks.length - 1) {
// 如果是最后一个块,将光标移到前一个块的末尾
// 需要计算删除后的位置
const prevBlock = blocks[blockIndex - 1];
newCursorPos = prevBlock.content.to;
} else {
// 否则移到下一个块的开始
// 需要计算删除后的位置,下一个块会向前移动
const nextBlock = blocks[blockIndex + 1];
const blockLength = block.range.to - block.range.from;
newCursorPos = nextBlock.content.from - blockLength;
}
// 确保光标位置在有效范围内
const docLengthAfterDelete = state.doc.length - (block.range.to - block.range.from);
newCursorPos = Math.max(0, Math.min(newCursorPos, docLengthAfterDelete));
dispatch(state.update({
changes: {
from: block.range.from,
to: block.range.to,
insert: ""
},
selection: EditorSelection.cursor(newCursorPos),
annotations: [codeBlockEvent.of(DELETE_BLOCK)]
}, {
scrollIntoView: true,
userEvent: USER_EVENTS.DELETE
}));
return true;
};
/**
* 向上移动当前块
*/
export function moveCurrentBlockUp({ state, dispatch }: any) {
return moveCurrentBlock(state, dispatch, true);
}
/**
* 向下移动当前块
*/
export function moveCurrentBlockDown({ state, dispatch }: any) {
return moveCurrentBlock(state, dispatch, false);
}
function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
if (state.readOnly) return false;
const block = getActiveNoteBlock(state);
if (!block) return false;
const blocks = state.field(blockState);
const blockIndex = blocks.indexOf(block);
const targetIndex = up ? blockIndex - 1 : blockIndex + 1;
if (targetIndex < 0 || targetIndex >= blocks.length) return false;
const targetBlock = blocks[targetIndex];
// 获取两个块的完整内容
const currentBlockContent = state.doc.sliceString(block.range.from, block.range.to);
const targetBlockContent = state.doc.sliceString(targetBlock.range.from, targetBlock.range.to);
// 交换块的位置
const changes = up ? [
{
from: targetBlock.range.from,
to: block.range.to,
insert: currentBlockContent + targetBlockContent
}
] : [
{
from: block.range.from,
to: targetBlock.range.to,
insert: targetBlockContent + currentBlockContent
}
];
// 计算新的光标位置
const newCursorPos = up ?
targetBlock.range.from + (block.range.to - block.range.from) + (block.content.from - block.range.from) :
block.range.from + (targetBlock.range.to - targetBlock.range.from) + (block.content.from - block.range.from);
dispatch(state.update({
changes,
selection: EditorSelection.cursor(newCursorPos),
annotations: [codeBlockEvent.of(MOVE_BLOCK)]
}, {
scrollIntoView: true,
userEvent: USER_EVENTS.MOVE
}));
return true;
}
/**
* 格式化当前块
*/
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;
}