✨ Added cursor protection extension
This commit is contained in:
@@ -21,7 +21,7 @@ import {
|
|||||||
setExtensionManagerView
|
setExtensionManagerView
|
||||||
} from '@/views/editor/manager';
|
} from '@/views/editor/manager';
|
||||||
import {useExtensionStore} from './extensionStore';
|
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 {LruCache} from '@/common/utils/lruCache';
|
||||||
import {AsyncManager} from '@/common/utils/asyncManager';
|
import {AsyncManager} from '@/common/utils/asyncManager';
|
||||||
import {generateContentHash} from "@/common/utils/hashUtils";
|
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 ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
||||||
const instance = editorCache.get(documentId);
|
const instance = editorCache.get(documentId);
|
||||||
@@ -205,8 +280,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
extensions
|
extensions
|
||||||
});
|
});
|
||||||
|
|
||||||
// 不再强制定位到文档末尾,保持默认位置(开头)或恢复保存的位置
|
|
||||||
|
|
||||||
return new EditorView({
|
return new EditorView({
|
||||||
state
|
state
|
||||||
});
|
});
|
||||||
@@ -314,27 +387,10 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 重新测量和聚焦编辑器
|
// 重新测量和聚焦编辑器
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 恢复保存的状态
|
// 恢复编辑器状态(光标位置和滚动位置)
|
||||||
const savedState = instance.editorState || documentStore.documentStates[documentId];
|
restoreEditorState(instance, 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 聚焦编辑器
|
||||||
instance.view.focus();
|
instance.view.focus();
|
||||||
|
|
||||||
// 使用缓存的语法树确保方法
|
// 使用缓存的语法树确保方法
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ import {getCodeBlockLanguageExtension} from './lang-parser';
|
|||||||
import {createLanguageDetection} from './lang-detect';
|
import {createLanguageDetection} from './lang-detect';
|
||||||
import {SupportedLanguage} from './types';
|
import {SupportedLanguage} from './types';
|
||||||
import {getMathBlockExtensions} from './mathBlock';
|
import {getMathBlockExtensions} from './mathBlock';
|
||||||
|
import {createCursorProtection} from './cursorProtection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代码块扩展配置选项
|
* 代码块扩展配置选项
|
||||||
@@ -108,6 +109,9 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
|
|||||||
showBackground
|
showBackground
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 光标保护(防止方向键移动到分隔符上)
|
||||||
|
createCursorProtection(),
|
||||||
|
|
||||||
// 块选择功能
|
// 块选择功能
|
||||||
...getBlockSelectExtensions(),
|
...getBlockSelectExtensions(),
|
||||||
|
|
||||||
@@ -207,6 +211,11 @@ export {
|
|||||||
getMathBlockExtensions
|
getMathBlockExtensions
|
||||||
} from './mathBlock';
|
} from './mathBlock';
|
||||||
|
|
||||||
|
// 光标保护功能
|
||||||
|
export {
|
||||||
|
createCursorProtection
|
||||||
|
} from './cursorProtection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认导出
|
* 默认导出
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user