From dec3ef5ef4cfd2eb0fba2d1ccaca2ac5bd84d2c7 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Thu, 13 Nov 2025 21:00:35 +0800 Subject: [PATCH] :sparkles: Added cursor protection extension --- frontend/src/stores/editorStore.ts | 102 +++++++++++---- .../extensions/codeblock/cursorProtection.ts | 123 ++++++++++++++++++ .../editor/extensions/codeblock/index.ts | 9 ++ 3 files changed, 211 insertions(+), 23 deletions(-) create mode 100644 frontend/src/views/editor/extensions/codeblock/cursorProtection.ts diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index 9aba0c6..a3e831d 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -21,7 +21,7 @@ import { setExtensionManagerView } from '@/views/editor/manager'; import {useExtensionStore} from './extensionStore'; -import createCodeBlockExtension from "@/views/editor/extensions/codeblock"; +import createCodeBlockExtension, {blockState} from "@/views/editor/extensions/codeblock"; import {LruCache} from '@/common/utils/lruCache'; import {AsyncManager} from '@/common/utils/asyncManager'; import {generateContentHash} from "@/common/utils/hashUtils"; @@ -81,6 +81,81 @@ export const useEditorStore = defineStore('editor', () => { // === 私有方法 === + /** + * 检查位置是否在代码块分隔符区域内 + */ + const isPositionInDelimiter = (view: EditorView, pos: number): boolean => { + try { + const blocks = view.state.field(blockState, false); + if (!blocks) return false; + + for (const block of blocks) { + if (pos >= block.delimiter.from && pos < block.delimiter.to) { + return true; + } + } + return false; + } catch { + return false; + } + }; + + /** + * 调整光标位置到有效的内容区域 + * 如果位置在分隔符内,移动到该块的内容开始位置 + */ + const adjustCursorPosition = (view: EditorView, pos: number): number => { + try { + const blocks = view.state.field(blockState, false); + if (!blocks || blocks.length === 0) return pos; + + // 如果位置在分隔符内,移动到该块的内容开始位置 + for (const block of blocks) { + if (pos >= block.delimiter.from && pos < block.delimiter.to) { + return block.content.from; + } + } + + return pos; + } catch { + return pos; + } + }; + + /** + * 恢复编辑器的光标和滚动位置 + */ + const restoreEditorState = (instance: EditorInstance, documentId: number): void => { + const savedState = instance.editorState || documentStore.documentStates[documentId]; + + if (savedState) { + // 有保存的状态,恢复光标位置和滚动位置 + let pos = Math.min(savedState.cursorPos, instance.view.state.doc.length); + + // 确保位置不在分隔符上 + if (isPositionInDelimiter(instance.view, pos)) { + pos = adjustCursorPosition(instance.view, pos); + } + + instance.view.dispatch({ + selection: {anchor: pos, head: pos} + }); + + // 恢复滚动位置 + instance.view.scrollDOM.scrollTop = savedState.scrollTop; + + // 更新实例状态 + instance.editorState = savedState; + } else { + // 首次打开或没有记录,光标在文档末尾 + const docLength = instance.view.state.doc.length; + instance.view.dispatch({ + selection: {anchor: docLength, head: docLength}, + scrollIntoView: true + }); + } + }; + // 缓存化的语法树确保方法 const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => { const instance = editorCache.get(documentId); @@ -205,8 +280,6 @@ export const useEditorStore = defineStore('editor', () => { extensions }); - // 不再强制定位到文档末尾,保持默认位置(开头)或恢复保存的位置 - return new EditorView({ state }); @@ -314,27 +387,10 @@ export const useEditorStore = defineStore('editor', () => { // 重新测量和聚焦编辑器 nextTick(() => { - // 恢复保存的状态 - const savedState = instance.editorState || documentStore.documentStates[documentId]; - if (savedState) { - // 有保存的状态,恢复光标位置和滚动位置 - const pos = Math.min(savedState.cursorPos, instance.view.state.doc.length); - instance.view.dispatch({ - selection: {anchor: pos, head: pos} - }); - // 恢复滚动位置 - instance.view.scrollDOM.scrollTop = savedState.scrollTop; - // 更新实例状态 - instance.editorState = savedState; - } else { - // 首次打开或没有记录,光标在文档末尾 - const docLength = instance.view.state.doc.length; - instance.view.dispatch({ - selection: {anchor: docLength, head: docLength}, - scrollIntoView: true - }); - } + // 恢复编辑器状态(光标位置和滚动位置) + restoreEditorState(instance, documentId); + // 聚焦编辑器 instance.view.focus(); // 使用缓存的语法树确保方法 diff --git a/frontend/src/views/editor/extensions/codeblock/cursorProtection.ts b/frontend/src/views/editor/extensions/codeblock/cursorProtection.ts new file mode 100644 index 0000000..dfd4e91 --- /dev/null +++ b/frontend/src/views/editor/extensions/codeblock/cursorProtection.ts @@ -0,0 +1,123 @@ +/** + * 光标保护扩展 + * 防止光标通过方向键移动到分隔符区域 + */ + +import { EditorView } from '@codemirror/view'; +import { EditorSelection } from '@codemirror/state'; +import { blockState } from './state'; + +/** + * 检查位置是否在分隔符区域内 + */ +function isInDelimiter(view: EditorView, pos: number): boolean { + try { + const blocks = view.state.field(blockState, false); + if (!blocks) return false; + + for (const block of blocks) { + if (pos >= block.delimiter.from && pos < block.delimiter.to) { + return true; + } + } + return false; + } catch { + return false; + } +} + +/** + * 调整光标位置,跳过分隔符区域 + */ +function adjustPosition(view: EditorView, pos: number, forward: boolean): number { + try { + const blocks = view.state.field(blockState, false); + if (!blocks || blocks.length === 0) return pos; + + for (const block of blocks) { + // 如果位置在分隔符内 + if (pos >= block.delimiter.from && pos < block.delimiter.to) { + // 向前移动:跳到该块内容的开始 + // 向后移动:跳到前一个块的内容末尾 + if (forward) { + return block.content.from; + } else { + // 找到前一个块 + const blockIndex = blocks.indexOf(block); + if (blockIndex > 0) { + const prevBlock = blocks[blockIndex - 1]; + return prevBlock.content.to; + } + return block.delimiter.from; + } + } + } + + return pos; + } catch { + return pos; + } +} + +/** + * 光标保护扩展 + * 拦截方向键移动,防止光标进入分隔符区域 + */ +export function createCursorProtection() { + return EditorView.domEventHandlers({ + keydown(event, view) { + // 只处理方向键 + if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) { + return false; + } + + // 获取当前光标位置 + const selection = view.state.selection.main; + const currentPos = selection.head; + + // 计算目标位置 + let targetPos = currentPos; + + if (event.key === 'ArrowLeft') { + targetPos = Math.max(0, currentPos - 1); + } else if (event.key === 'ArrowRight') { + targetPos = Math.min(view.state.doc.length, currentPos + 1); + } else if (event.key === 'ArrowUp') { + const line = view.state.doc.lineAt(currentPos); + if (line.number > 1) { + const prevLine = view.state.doc.line(line.number - 1); + const col = currentPos - line.from; + targetPos = Math.min(prevLine.from + col, prevLine.to); + } + } else if (event.key === 'ArrowDown') { + const line = view.state.doc.lineAt(currentPos); + if (line.number < view.state.doc.lines) { + const nextLine = view.state.doc.line(line.number + 1); + const col = currentPos - line.from; + targetPos = Math.min(nextLine.from + col, nextLine.to); + } + } + + // 检查目标位置是否在分隔符内 + if (isInDelimiter(view, targetPos)) { + // 调整位置 + const forward = event.key === 'ArrowRight' || event.key === 'ArrowDown'; + const adjustedPos = adjustPosition(view, targetPos, forward); + + // 移动光标到调整后的位置 + view.dispatch({ + selection: EditorSelection.cursor(adjustedPos), + scrollIntoView: true, + userEvent: 'select' + }); + + // 阻止默认行为 + event.preventDefault(); + return true; + } + + return false; + } + }); +} + diff --git a/frontend/src/views/editor/extensions/codeblock/index.ts b/frontend/src/views/editor/extensions/codeblock/index.ts index 8294570..a9003c4 100644 --- a/frontend/src/views/editor/extensions/codeblock/index.ts +++ b/frontend/src/views/editor/extensions/codeblock/index.ts @@ -25,6 +25,7 @@ import {getCodeBlockLanguageExtension} from './lang-parser'; import {createLanguageDetection} from './lang-detect'; import {SupportedLanguage} from './types'; import {getMathBlockExtensions} from './mathBlock'; +import {createCursorProtection} from './cursorProtection'; /** * 代码块扩展配置选项 @@ -108,6 +109,9 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens showBackground }), + // 光标保护(防止方向键移动到分隔符上) + createCursorProtection(), + // 块选择功能 ...getBlockSelectExtensions(), @@ -207,6 +211,11 @@ export { getMathBlockExtensions } from './mathBlock'; +// 光标保护功能 +export { + createCursorProtection +} from './cursorProtection'; + /** * 默认导出 */