♻️ Refactor cursor position cache
This commit is contained in:
@@ -70,12 +70,11 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
// 在新窗口中打开文档
|
// 在新窗口中打开文档
|
||||||
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
await OpenDocumentWindow(docId);
|
|
||||||
const tabStore = useTabStore();
|
const tabStore = useTabStore();
|
||||||
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
|
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
|
||||||
tabStore.closeTab(docId);
|
tabStore.closeTab(docId);
|
||||||
}
|
}
|
||||||
|
await OpenDocumentWindow(docId);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to open document in new window:', error);
|
console.error('Failed to open document in new window:', error);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/b
|
|||||||
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||||
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
||||||
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
|
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
|
||||||
|
import {createCursorPositionExtension, scrollToCursor} from '@/views/editor/basic/cursorPositionExtension';
|
||||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||||
import {
|
import {
|
||||||
createDynamicExtensions,
|
createDynamicExtensions,
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
setExtensionManagerView
|
setExtensionManagerView
|
||||||
} from '@/views/editor/manager';
|
} from '@/views/editor/manager';
|
||||||
import {useExtensionStore} from './extensionStore';
|
import {useExtensionStore} from './extensionStore';
|
||||||
import createCodeBlockExtension, {blockState} from "@/views/editor/extensions/codeblock";
|
import createCodeBlockExtension 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";
|
||||||
@@ -35,7 +36,7 @@ export interface DocumentStats {
|
|||||||
selectedCharacters: number;
|
selectedCharacters: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复:只保存光标位置,恢复时自动滚动到光标处(更简单可靠)
|
// 修复:只保存光标位置,恢复时自动滚动到光标处
|
||||||
export interface EditorViewState {
|
export interface EditorViewState {
|
||||||
cursorPos: number;
|
cursorPos: number;
|
||||||
}
|
}
|
||||||
@@ -180,6 +181,9 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
enableAutoDetection: true
|
enableAutoDetection: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 光标位置持久化扩展
|
||||||
|
const cursorPositionExtension = createCursorPositionExtension(documentId);
|
||||||
|
|
||||||
// 再次检查操作有效性
|
// 再次检查操作有效性
|
||||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
@@ -212,13 +216,23 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
statsExtension,
|
statsExtension,
|
||||||
contentChangeExtension,
|
contentChangeExtension,
|
||||||
codeBlockExtension,
|
codeBlockExtension,
|
||||||
|
cursorPositionExtension,
|
||||||
...dynamicExtensions,
|
...dynamicExtensions,
|
||||||
];
|
];
|
||||||
|
|
||||||
// 创建编辑器状态
|
// 获取保存的光标位置
|
||||||
|
const savedState = documentStore.documentStates[documentId];
|
||||||
|
const docLength = content.length;
|
||||||
|
const initialCursorPos = savedState?.cursorPos !== undefined
|
||||||
|
? Math.min(savedState.cursorPos, docLength)
|
||||||
|
: docLength;
|
||||||
|
|
||||||
|
|
||||||
|
// 创建编辑器状态,设置初始光标位置
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: content,
|
doc: content,
|
||||||
extensions
|
extensions,
|
||||||
|
selection: { anchor: initialCursorPos, head: initialCursorPos }
|
||||||
});
|
});
|
||||||
|
|
||||||
return new EditorView({
|
return new EditorView({
|
||||||
@@ -316,6 +330,9 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染
|
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
// 滚动到当前光标位置
|
||||||
|
scrollToCursor(instance.view);
|
||||||
|
|
||||||
// 聚焦编辑器
|
// 聚焦编辑器
|
||||||
instance.view.focus();
|
instance.view.focus();
|
||||||
|
|
||||||
@@ -487,15 +504,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
await saveEditorContent(documentId);
|
await saveEditorContent(documentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存光标位置
|
|
||||||
if (instance.view && instance.view.state) {
|
|
||||||
const currentState: EditorViewState = {
|
|
||||||
cursorPos: instance.view.state.selection.main.head
|
|
||||||
};
|
|
||||||
// 保存到 documentStore 用于持久化
|
|
||||||
documentStore.documentStates[documentId] = currentState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除自动保存定时器
|
// 清除自动保存定时器
|
||||||
instance.autoSaveTimer.clear();
|
instance.autoSaveTimer.clear();
|
||||||
|
|
||||||
@@ -578,22 +586,10 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
operationManager.cancelAllOperations();
|
operationManager.cancelAllOperations();
|
||||||
|
|
||||||
editorCache.clear((_documentId, instance) => {
|
editorCache.clear((_documentId, instance) => {
|
||||||
// 修复:清空前只保存光标位置
|
|
||||||
if (instance.view) {
|
|
||||||
const currentState: EditorViewState = {
|
|
||||||
cursorPos: instance.view.state.selection.main.head
|
|
||||||
};
|
|
||||||
// 同时保存到实例和 documentStore
|
|
||||||
instance.editorState = currentState;
|
|
||||||
documentStore.documentStates[instance.documentId] = currentState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除自动保存定时器
|
// 清除自动保存定时器
|
||||||
instance.autoSaveTimer.clear();
|
instance.autoSaveTimer.clear();
|
||||||
|
|
||||||
// 从扩展管理器移除
|
// 从扩展管理器移除
|
||||||
removeExtensionManagerView(instance.documentId);
|
removeExtensionManagerView(instance.documentId);
|
||||||
|
|
||||||
// 移除DOM元素
|
// 移除DOM元素
|
||||||
if (instance.view.dom.parentElement) {
|
if (instance.view.dom.parentElement) {
|
||||||
instance.view.dom.remove();
|
instance.view.dom.remove();
|
||||||
|
|||||||
@@ -156,10 +156,10 @@ export const useTabStore = defineStore('tab', () => {
|
|||||||
*/
|
*/
|
||||||
const validateTabs = () => {
|
const validateTabs = () => {
|
||||||
const validDocIds = Object.keys(documentStore.documents).map(Number);
|
const validDocIds = Object.keys(documentStore.documents).map(Number);
|
||||||
|
|
||||||
// 找出无效的标签页(文档已被删除)
|
// 找出无效的标签页(文档已被删除)
|
||||||
const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId));
|
const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId));
|
||||||
|
|
||||||
if (invalidTabIds.length > 0) {
|
if (invalidTabIds.length > 0) {
|
||||||
// 批量清理无效标签页
|
// 批量清理无效标签页
|
||||||
invalidTabIds.forEach(docId => {
|
invalidTabIds.forEach(docId => {
|
||||||
@@ -175,7 +175,7 @@ export const useTabStore = defineStore('tab', () => {
|
|||||||
const initializeTab = () => {
|
const initializeTab = () => {
|
||||||
// 先验证并清理无效的标签页(处理持久化的脏数据)
|
// 先验证并清理无效的标签页(处理持久化的脏数据)
|
||||||
validateTabs();
|
validateTabs();
|
||||||
|
|
||||||
if (isTabsEnabled.value) {
|
if (isTabsEnabled.value) {
|
||||||
const currentDoc = documentStore.currentDocument;
|
const currentDoc = documentStore.currentDocument;
|
||||||
if (currentDoc) {
|
if (currentDoc) {
|
||||||
@@ -254,7 +254,7 @@ export const useTabStore = defineStore('tab', () => {
|
|||||||
return {
|
return {
|
||||||
tabsMap,
|
tabsMap,
|
||||||
tabOrder,
|
tabOrder,
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
tabs: readonly(tabs),
|
tabs: readonly(tabs),
|
||||||
draggedTabId,
|
draggedTabId,
|
||||||
@@ -283,9 +283,5 @@ export const useTabStore = defineStore('tab', () => {
|
|||||||
getTab
|
getTab
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
persist: {
|
persist: false,
|
||||||
key: 'voidraft-tabs',
|
|
||||||
storage: localStorage,
|
|
||||||
pick: ['tabsMap', 'tabOrder'],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
67
frontend/src/views/editor/basic/cursorPositionExtension.ts
Normal file
67
frontend/src/views/editor/basic/cursorPositionExtension.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
|
||||||
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
|
import {createDebounce} from '@/common/utils/debounce';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 光标位置持久化扩展
|
||||||
|
* 实时监听光标位置变化并持久化到 documentStore
|
||||||
|
*/
|
||||||
|
export function createCursorPositionExtension(documentId: number) {
|
||||||
|
return ViewPlugin.fromClass(
|
||||||
|
class CursorPositionPlugin {
|
||||||
|
private readonly documentStore = useDocumentStore();
|
||||||
|
private readonly debouncedSave;
|
||||||
|
|
||||||
|
constructor(private view: EditorView) {
|
||||||
|
const { debouncedFn, flush } = createDebounce(
|
||||||
|
() => this.saveCursorPosition(),
|
||||||
|
{ delay: 400 }
|
||||||
|
);
|
||||||
|
this.debouncedSave = { fn: debouncedFn, flush };
|
||||||
|
|
||||||
|
// 初始化时保存一次光标位置
|
||||||
|
this.saveCursorPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
// 只在选择变化时触发
|
||||||
|
if (!update.selectionSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防抖保存光标位置
|
||||||
|
this.debouncedSave.fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// 销毁时立即执行待保存的操作
|
||||||
|
this.debouncedSave.flush();
|
||||||
|
// 再保存一次确保最新状态
|
||||||
|
this.saveCursorPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveCursorPosition() {
|
||||||
|
const cursorPos = this.view.state.selection.main.head;
|
||||||
|
// 持久化到 documentStore
|
||||||
|
this.documentStore.documentStates[documentId] = {
|
||||||
|
cursorPos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滚动到当前光标位置(视口中心)
|
||||||
|
* @param view 编辑器视图
|
||||||
|
*/
|
||||||
|
export function scrollToCursor(view: EditorView) {
|
||||||
|
const cursorPos = view.state.selection.main.head;
|
||||||
|
view.dispatch({
|
||||||
|
effects: EditorView.scrollIntoView(cursorPos, {
|
||||||
|
y: 'center',
|
||||||
|
x: 'center'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user