/** * 块隔离选择功能 */ import { ViewPlugin, Decoration } from "@codemirror/view"; import { StateField, StateEffect, RangeSetBuilder, EditorSelection, EditorState, Transaction } from "@codemirror/state"; import { selectAll as defaultSelectAll } from "@codemirror/commands"; import { Command } from "@codemirror/view"; import { getActiveNoteBlock, blockState } from "./state"; /** * 当用户按下 Ctrl+A 时,我们希望首先选择整个块。但如果整个块已经被选中, * 我们希望改为选择整个文档。这对于空块不起作用,因为整个块已经被选中(因为它是空的)。 * 因此我们使用 StateField 来跟踪空块是否被选中,并添加手动行装饰来视觉上指示空块被选中。 */ /** * 空块选择状态字段 */ export const emptyBlockSelected = StateField.define({ create: () => { return null; }, update(value, tr) { if (tr.selection) { // 如果选择改变,重置状态 return null; } else { for (let e of tr.effects) { if (e.is(setEmptyBlockSelected)) { // 切换状态为 true return e.value; } } } return value; }, provide() { return ViewPlugin.fromClass(class { decorations: any; constructor(view: any) { this.decorations = emptyBlockSelectedDecorations(view); } update(update: any) { this.decorations = emptyBlockSelectedDecorations(update.view); } }, { decorations: v => v.decorations }); } }); /** * 可以分派的效果来设置空块选择状态 */ const setEmptyBlockSelected = StateEffect.define(); /** * 空块选择装饰 */ const decoration = Decoration.line({ attributes: { class: "code-block-empty-selected" } }); function emptyBlockSelectedDecorations(view: any) { const selectionPos = view.state.field(emptyBlockSelected); const builder = new RangeSetBuilder(); if (selectionPos !== null) { const line = view.state.doc.lineAt(selectionPos); builder.add(line.from, line.from, decoration); } return builder.finish(); } /** * 块隔离的选择全部功能 */ export const selectAll: Command = ({ state, dispatch }) => { const range = state.selection.asSingle().ranges[0]; const block = getActiveNoteBlock(state); // 如果没有找到块,使用默认的全选 if (!block) { return defaultSelectAll({ state, dispatch }); } // 单独处理空块 if (block.content.from === block.content.to) { // 检查是否已经按过 Ctrl+A if (state.field(emptyBlockSelected)) { // 如果活动块已经标记为选中,我们想要选择整个缓冲区 return defaultSelectAll({ state, dispatch }); } else if (range.empty) { // 如果空块没有被选中,标记为选中 // 我们检查 range.empty 的原因是如果文档末尾有一个空块 // 用户按两次 Ctrl+A 使整个缓冲区被选中,活动块仍然是空的 // 但我们不想标记它为选中 dispatch({ effects: setEmptyBlockSelected.of(block.content.from) }); return true; } return true; } // 检查是否已经选中了块的所有文本,在这种情况下我们想要选择整个文档的所有文本 if (range.from === block.content.from && range.to === block.content.to) { return defaultSelectAll({ state, dispatch }); } // 选择当前块的所有内容 dispatch(state.update({ selection: { anchor: block.content.from, head: block.content.to }, userEvent: "select" })); return true; }; /** * 块感知的选择扩展功能 * 使用事务过滤器来确保选择不会跨越块边界 */ export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) => { // 只处理选择变化的事务,并且忽略我们自己生成的事务 if (!tr.selection || !tr.selection.ranges || tr.annotation?.(Transaction.userEvent) === "select.block-boundary") { return tr; } const state = tr.startState; try { const blocks = state.field(blockState); if (blocks.length === 0) { return tr; } // 检查是否需要修正选择 let needsCorrection = false; const correctedRanges = tr.selection.ranges.map((range: any) => { // 为选择范围的开始和结束位置找到对应的块 const fromBlock = getBlockAtPos(state, range.from); const toBlock = getBlockAtPos(state, range.to); // 如果选择开始或结束在分隔符内,跳过边界检查 if (isInDelimiter(state, range.from) || isInDelimiter(state, range.to)) { return range; } // 如果选择跨越了多个块,需要限制选择 if (fromBlock && toBlock && fromBlock !== toBlock) { // 选择跨越了多个块,限制到起始块 needsCorrection = true; return EditorSelection.range( Math.max(range.from, fromBlock.content.from), Math.min(range.to, fromBlock.content.to) ); } // 如果选择在一个块内,确保不超出块边界 if (fromBlock) { let newFrom = Math.max(range.from, fromBlock.content.from); let newTo = Math.min(range.to, fromBlock.content.to); if (newFrom !== range.from || newTo !== range.to) { needsCorrection = true; return EditorSelection.range(newFrom, newTo); } } return range; }); if (needsCorrection) { // 返回修正后的事务 return { ...tr, selection: EditorSelection.create(correctedRanges, tr.selection.mainIndex), annotations: tr.annotations.concat(Transaction.userEvent.of("select.block-boundary")) }; } } catch (error) { // 如果出现错误,返回原始事务 console.warn("Block boundary check failed:", error); } return tr; }); /** * 辅助函数:根据位置获取块 */ function getBlockAtPos(state: any, pos: number) { const blocks = state.field(blockState); return blocks.find((block: any) => block.content.from <= pos && block.content.to >= pos ); } /** * 辅助函数:检查位置是否在块分隔符内 */ function isInDelimiter(state: any, pos: number) { const blocks = state.field(blockState); return blocks.some((block: any) => block.delimiter.from <= pos && block.delimiter.to >= pos ); } /** * 获取块选择扩展 */ export function getBlockSelectExtensions() { return [ emptyBlockSelected, // 禁用块边界检查以避免递归更新问题 // blockAwareSelection, ]; }