diff --git a/frontend/src/common/constant/editor.ts b/frontend/src/common/constant/editor.ts new file mode 100644 index 0000000..3ee6455 --- /dev/null +++ b/frontend/src/common/constant/editor.ts @@ -0,0 +1,13 @@ +/** + * 编辑器相关常量配置 + */ + +// 编辑器实例管理 +export const EDITOR_CONFIG = { + /** 最多缓存的编辑器实例数量 */ + MAX_INSTANCES: 5, + /** 语法树缓存过期时间(毫秒) */ + SYNTAX_TREE_CACHE_TIMEOUT: 30000, + /** 加载状态延迟时间(毫秒) */ + LOADING_DELAY: 800, +} as const; \ No newline at end of file diff --git a/frontend/src/common/utils/asyncManager.ts b/frontend/src/common/utils/asyncManager.ts new file mode 100644 index 0000000..2ad46a1 --- /dev/null +++ b/frontend/src/common/utils/asyncManager.ts @@ -0,0 +1,265 @@ +/** + * 操作信息接口 + */ +interface OperationInfo { + controller: AbortController; + createdAt: number; + timeout?: number; + timeoutId?: NodeJS.Timeout; +} + +/** + * 异步操作管理器 + * 用于管理异步操作的竞态条件,确保只有最新的操作有效 + * 支持操作超时和自动清理机制 + * + * @template T 操作上下文的类型 + */ +export class AsyncManager { + private operationSequence = 0; + private pendingOperations = new Map(); + private currentContext: T | null = null; + private defaultTimeout: number; + + /** + * 创建异步操作管理器 + * + * @param defaultTimeout 默认超时时间(毫秒),0表示不设置超时 + */ + constructor(defaultTimeout: number = 0) { + this.defaultTimeout = defaultTimeout; + } + + /** + * 生成新的操作ID + * + * @returns 新的操作ID + */ + getNextOperationId(): number { + return ++this.operationSequence; + } + + /** + * 开始新的操作 + * + * @param context 操作上下文 + * @param options 操作选项 + * @returns 操作ID和AbortController + */ + startOperation( + context: T, + options?: { + excludeId?: number; + timeout?: number; + } + ): { operationId: number; abortController: AbortController } { + const operationId = this.getNextOperationId(); + const abortController = new AbortController(); + const timeout = options?.timeout ?? this.defaultTimeout; + + // 取消之前的操作 + this.cancelPreviousOperations(options?.excludeId); + + // 创建操作信息 + const operationInfo: OperationInfo = { + controller: abortController, + createdAt: Date.now(), + timeout: timeout > 0 ? timeout : undefined + }; + + // 设置超时处理 + if (timeout > 0) { + operationInfo.timeoutId = setTimeout(() => { + this.cancelOperation(operationId, 'timeout'); + }, timeout); + } + + // 设置当前上下文和操作 + this.currentContext = context; + this.pendingOperations.set(operationId, operationInfo); + + return { operationId, abortController }; + } + + /** + * 检查操作是否仍然有效 + * + * @param operationId 操作ID + * @param context 操作上下文 + * @returns 操作是否有效 + */ + isOperationValid(operationId: number, context?: T): boolean { + const operationInfo = this.pendingOperations.get(operationId); + const contextValid = context === undefined || this.currentContext === context; + + return ( + operationInfo !== undefined && + !operationInfo.controller.signal.aborted && + contextValid + ); + } + + /** + * 完成操作 + * + * @param operationId 操作ID + */ + completeOperation(operationId: number): void { + const operationInfo = this.pendingOperations.get(operationId); + if (operationInfo) { + // 清理超时定时器 + if (operationInfo.timeoutId) { + clearTimeout(operationInfo.timeoutId); + } + this.pendingOperations.delete(operationId); + } + } + + /** + * 取消指定操作 + * + * @param operationId 操作ID + * @param reason 取消原因 + */ + cancelOperation(operationId: number, reason?: string): void { + const operationInfo = this.pendingOperations.get(operationId); + if (operationInfo) { + // 清理超时定时器 + if (operationInfo.timeoutId) { + clearTimeout(operationInfo.timeoutId); + } + // 取消操作 + operationInfo.controller.abort(reason); + this.pendingOperations.delete(operationId); + } + } + + /** + * 取消之前的操作(修复并发bug) + * + * @param excludeId 要排除的操作ID(不取消该操作) + */ + cancelPreviousOperations(excludeId?: number): void { + // 创建要取消的操作ID数组,避免在遍历时修改Map + const operationIdsToCancel: number[] = []; + + for (const [operationId] of this.pendingOperations) { + if (excludeId === undefined || operationId !== excludeId) { + operationIdsToCancel.push(operationId); + } + } + + // 批量取消操作 + for (const operationId of operationIdsToCancel) { + this.cancelOperation(operationId, 'superseded'); + } + } + + /** + * 取消所有操作 + */ + cancelAllOperations(): void { + // 创建要取消的操作ID数组,避免在遍历时修改Map + const operationIdsToCancel = Array.from(this.pendingOperations.keys()); + + // 批量取消操作 + for (const operationId of operationIdsToCancel) { + this.cancelOperation(operationId, 'cancelled'); + } + this.currentContext = null; + } + + /** + * 清理过期操作(手动清理超时操作) + * + * @param maxAge 最大存活时间(毫秒) + * @returns 清理的操作数量 + */ + cleanupExpiredOperations(maxAge: number): number { + const now = Date.now(); + const expiredOperationIds: number[] = []; + + for (const [operationId, operationInfo] of this.pendingOperations) { + if (now - operationInfo.createdAt > maxAge) { + expiredOperationIds.push(operationId); + } + } + + // 批量取消过期操作 + for (const operationId of expiredOperationIds) { + this.cancelOperation(operationId, 'expired'); + } + + return expiredOperationIds.length; + } + + /** + * 获取操作统计信息 + * + * @returns 操作统计信息 + */ + getOperationStats(): { + total: number; + withTimeout: number; + averageAge: number; + oldestAge: number; + } { + const now = Date.now(); + let withTimeout = 0; + let totalAge = 0; + let oldestAge = 0; + + for (const operationInfo of this.pendingOperations.values()) { + const age = now - operationInfo.createdAt; + totalAge += age; + oldestAge = Math.max(oldestAge, age); + + if (operationInfo.timeout) { + withTimeout++; + } + } + + return { + total: this.pendingOperations.size, + withTimeout, + averageAge: this.pendingOperations.size > 0 ? totalAge / this.pendingOperations.size : 0, + oldestAge + }; + } + + /** + * 获取当前上下文 + * + * @returns 当前上下文 + */ + getCurrentContext(): T | null { + return this.currentContext; + } + + /** + * 设置当前上下文 + * + * @param context 新的上下文 + */ + setCurrentContext(context: T | null): void { + this.currentContext = context; + } + + /** + * 获取待处理操作数量 + * + * @returns 待处理操作数量 + */ + get pendingCount(): number { + return this.pendingOperations.size; + } + + /** + * 检查是否有待处理的操作 + * + * @returns 是否有待处理的操作 + */ + hasPendingOperations(): boolean { + return this.pendingOperations.size > 0; + } +} \ No newline at end of file diff --git a/frontend/src/common/utils/doublyLinkedList.ts b/frontend/src/common/utils/doublyLinkedList.ts new file mode 100644 index 0000000..7be14a2 --- /dev/null +++ b/frontend/src/common/utils/doublyLinkedList.ts @@ -0,0 +1,280 @@ +/** + * 双向链表节点 + * + * @template T 节点数据的类型 + */ +export class DoublyLinkedListNode { + public data: T; + public prev: DoublyLinkedListNode | null = null; + public next: DoublyLinkedListNode | null = null; + + constructor(data: T) { + this.data = data; + } +} + +/** + * 双向链表实现 + * 提供 O(1) 时间复杂度的插入、删除和移动操作 + * + * @template T 链表数据的类型 + */ +export class DoublyLinkedList { + private head: DoublyLinkedListNode | null = null; + private tail: DoublyLinkedListNode | null = null; + private _size = 0; + + /** + * 获取链表大小 + * + * @returns 链表中节点的数量 + */ + get size(): number { + return this._size; + } + + /** + * 检查链表是否为空 + * + * @returns 链表是否为空 + */ + get isEmpty(): boolean { + return this._size === 0; + } + + /** + * 获取头节点 + * + * @returns 头节点,如果链表为空则返回null + */ + get first(): DoublyLinkedListNode | null { + return this.head; + } + + /** + * 获取尾节点 + * + * @returns 尾节点,如果链表为空则返回null + */ + get last(): DoublyLinkedListNode | null { + return this.tail; + } + + /** + * 在链表头部添加节点 + * + * @param data 要添加的数据 + * @returns 新创建的节点 + */ + addFirst(data: T): DoublyLinkedListNode { + const newNode = new DoublyLinkedListNode(data); + + if (this.head === null) { + this.head = this.tail = newNode; + } else { + newNode.next = this.head; + this.head.prev = newNode; + this.head = newNode; + } + + this._size++; + return newNode; + } + + /** + * 在链表尾部添加节点 + * + * @param data 要添加的数据 + * @returns 新创建的节点 + */ + addLast(data: T): DoublyLinkedListNode { + const newNode = new DoublyLinkedListNode(data); + + if (this.tail === null) { + this.head = this.tail = newNode; + } else { + newNode.prev = this.tail; + this.tail.next = newNode; + this.tail = newNode; + } + + this._size++; + return newNode; + } + + /** + * 删除指定节点 + * + * @param node 要删除的节点 + * @returns 被删除节点的数据 + */ + remove(node: DoublyLinkedListNode): T { + if (node.prev) { + node.prev.next = node.next; + } else { + this.head = node.next; + } + + if (node.next) { + node.next.prev = node.prev; + } else { + this.tail = node.prev; + } + + // 清理节点引用,防止内存泄漏 + const data = node.data; + node.prev = null; + node.next = null; + + this._size--; + return data; + } + + /** + * 删除头节点 + * + * @returns 被删除节点的数据,如果链表为空则返回undefined + */ + removeFirst(): T | undefined { + if (this.head === null) { + return undefined; + } + return this.remove(this.head); + } + + /** + * 删除尾节点 + * + * @returns 被删除节点的数据,如果链表为空则返回undefined + */ + removeLast(): T | undefined { + if (this.tail === null) { + return undefined; + } + return this.remove(this.tail); + } + + /** + * 将节点移动到链表头部 + * + * @param node 要移动的节点 + */ + moveToFirst(node: DoublyLinkedListNode): void { + if (node === this.head) { + return; // 已经在头部 + } + + // 从当前位置移除 + if (node.prev) { + node.prev.next = node.next; + } + if (node.next) { + node.next.prev = node.prev; + } else { + this.tail = node.prev; + } + + // 移动到头部 + node.prev = null; + node.next = this.head; + if (this.head) { + this.head.prev = node; + } + this.head = node; + + // 如果链表之前为空,更新尾节点 + if (this.tail === null) { + this.tail = node; + } + } + + /** + * 将节点移动到链表尾部 + * + * @param node 要移动的节点 + */ + moveToLast(node: DoublyLinkedListNode): void { + if (node === this.tail) { + return; // 已经在尾部 + } + + // 从当前位置移除 + if (node.prev) { + node.prev.next = node.next; + } else { + this.head = node.next; + } + if (node.next) { + node.next.prev = node.prev; + } + + // 移动到尾部 + node.next = null; + node.prev = this.tail; + if (this.tail) { + this.tail.next = node; + } + this.tail = node; + + // 如果链表之前为空,更新头节点 + if (this.head === null) { + this.head = node; + } + } + + /** + * 清空链表 + * + * @param onClear 清空时对每个节点数据的回调函数 + */ + clear(onClear?: (data: T) => void): void { + let current = this.head; + while (current) { + const next = current.next; + + if (onClear) { + onClear(current.data); + } + + // 清理节点引用,防止内存泄漏 + current.prev = null; + current.next = null; + + current = next; + } + + this.head = null; + this.tail = null; + this._size = 0; + } + + /** + * 将链表转换为数组 + * + * @returns 包含所有节点数据的数组,按从头到尾的顺序 + */ + toArray(): T[] { + const result: T[] = []; + let current = this.head; + while (current) { + result.push(current.data); + current = current.next; + } + return result; + } + + /** + * 遍历链表 + * + * @param callback 对每个节点数据执行的回调函数 + */ + forEach(callback: (data: T, index: number) => void): void { + let current = this.head; + let index = 0; + while (current) { + callback(current.data, index); + current = current.next; + index++; + } + } +} \ No newline at end of file diff --git a/frontend/src/common/utils/hashUtils.ts b/frontend/src/common/utils/hashUtils.ts new file mode 100644 index 0000000..d287a39 --- /dev/null +++ b/frontend/src/common/utils/hashUtils.ts @@ -0,0 +1,92 @@ +/** + * 高效哈希算法实现 + * 针对大量文本内容优化的哈希函数集合 + */ + +/** + * 使用优化的 xxHash32 算法生成字符串哈希值 + * 专为大量文本内容设计,性能优异 + * + * xxHash32 特点: + * - 极快的处理速度 + * - 优秀的分布质量,冲突率极低 + * - 对长文本友好,性能不会随长度线性下降 + * - 被广泛应用于数据库、压缩工具等 + * + * @param content 要哈希的字符串内容 + * @returns 32位哈希值的字符串表示 + */ +export const generateContentHash = (content: string): string => { + return (generateContentHashInternal(content) >>> 0).toString(36); +}; + +/** + * 从字符串中提取 32 位整数(模拟小端序) + */ +function getUint32(str: string, index: number): number { + return ( + (str.charCodeAt(index) & 0xff) | + ((str.charCodeAt(index + 1) & 0xff) << 8) | + ((str.charCodeAt(index + 2) & 0xff) << 16) | + ((str.charCodeAt(index + 3) & 0xff) << 24) + ); +} + +/** + * 32 位左旋转 + */ +function rotateLeft(value: number, shift: number): number { + return (value << shift) | (value >>> (32 - shift)); +} + + +/** + * 内部哈希计算函数,返回数值 + */ +function generateContentHashInternal(content: string): number { + const PRIME1 = 0x9e3779b1; + const PRIME2 = 0x85ebca77; + const PRIME3 = 0xc2b2ae3d; + const PRIME4 = 0x27d4eb2f; + const PRIME5 = 0x165667b1; + + const len = content.length; + let hash: number; + let i = 0; + + if (len >= 16) { + let acc1 = PRIME1 + PRIME2; + let acc2 = PRIME2; + let acc3 = 0; + let acc4 = -PRIME1; + + for (; i <= len - 16; i += 16) { + acc1 = Math.imul(rotateLeft(acc1 + Math.imul(getUint32(content, i), PRIME2), 13), PRIME1); + acc2 = Math.imul(rotateLeft(acc2 + Math.imul(getUint32(content, i + 4), PRIME2), 13), PRIME1); + acc3 = Math.imul(rotateLeft(acc3 + Math.imul(getUint32(content, i + 8), PRIME2), 13), PRIME1); + acc4 = Math.imul(rotateLeft(acc4 + Math.imul(getUint32(content, i + 12), PRIME2), 13), PRIME1); + } + + hash = rotateLeft(acc1, 1) + rotateLeft(acc2, 7) + rotateLeft(acc3, 12) + rotateLeft(acc4, 18); + } else { + hash = PRIME5; + } + + hash += len; + + for (; i <= len - 4; i += 4) { + hash = Math.imul(rotateLeft(hash + Math.imul(getUint32(content, i), PRIME3), 17), PRIME4); + } + + for (; i < len; i++) { + hash = Math.imul(rotateLeft(hash + Math.imul(content.charCodeAt(i), PRIME5), 11), PRIME1); + } + + hash ^= hash >>> 15; + hash = Math.imul(hash, PRIME2); + hash ^= hash >>> 13; + hash = Math.imul(hash, PRIME3); + hash ^= hash >>> 16; + + return hash; +} \ No newline at end of file diff --git a/frontend/src/common/utils/lruCache.ts b/frontend/src/common/utils/lruCache.ts new file mode 100644 index 0000000..b42fa3e --- /dev/null +++ b/frontend/src/common/utils/lruCache.ts @@ -0,0 +1,157 @@ +import { DoublyLinkedList, DoublyLinkedListNode } from './doublyLinkedList'; + +/** + * LRU缓存项 + * + * @template K 键的类型 + * @template V 值的类型 + */ +interface LruCacheItem { + key: K; + value: V; +} + +/** + * LRU (Least Recently Used) 缓存实现 + * 使用双向链表 + Map 实现 O(1) 时间复杂度的所有操作 + * + * @template K 键的类型 + * @template V 值的类型 + */ +export class LruCache { + private readonly maxSize: number; + private readonly cache = new Map>>(); + private readonly lru = new DoublyLinkedList>(); + + /** + * 创建LRU缓存实例 + * + * @param maxSize 最大缓存大小 + */ + constructor(maxSize: number) { + if (maxSize <= 0) { + throw new Error('Max size must be greater than 0'); + } + this.maxSize = maxSize; + } + + /** + * 获取缓存值 + * + * @param key 键 + * @returns 缓存的值,如果不存在则返回undefined + */ + get(key: K): V | undefined { + const node = this.cache.get(key); + if (node) { + // 将访问的节点移动到链表尾部(最近使用) + this.lru.moveToLast(node); + return node.data.value; + } + return undefined; + } + + /** + * 设置缓存值 + * + * @param key 键 + * @param value 值 + * @param onEvict 当有项目被驱逐时的回调函数 + */ + set(key: K, value: V, onEvict?: (evictedKey: K, evictedValue: V) => void): void { + const existingNode = this.cache.get(key); + + // 如果键已存在,更新值并移动到最近使用 + if (existingNode) { + existingNode.data.value = value; + this.lru.moveToLast(existingNode); + return; + } + + // 如果缓存已满,移除最少使用的项 + if (this.cache.size >= this.maxSize) { + const oldestNode = this.lru.first; + if (oldestNode) { + const { key: evictedKey, value: evictedValue } = oldestNode.data; + this.cache.delete(evictedKey); + this.lru.removeFirst(); + + if (onEvict) { + onEvict(evictedKey, evictedValue); + } + } + } + + // 添加新项到链表尾部(最近使用) + const newNode = this.lru.addLast({ key, value }); + this.cache.set(key, newNode); + } + + /** + * 检查键是否存在 + * + * @param key 键 + * @returns 是否存在 + */ + has(key: K): boolean { + return this.cache.has(key); + } + + /** + * 删除指定键的缓存 + * + * @param key 键 + * @returns 是否成功删除 + */ + delete(key: K): boolean { + const node = this.cache.get(key); + if (node) { + this.cache.delete(key); + this.lru.remove(node); + return true; + } + return false; + } + + /** + * 清空缓存 + * + * @param onEvict 清空时对每个项目的回调函数 + */ + clear(onEvict?: (key: K, value: V) => void): void { + if (onEvict) { + this.lru.forEach(item => { + onEvict(item.key, item.value); + }); + } + this.cache.clear(); + this.lru.clear(); + } + + /** + * 获取缓存大小 + * + * @returns 当前缓存项数量 + */ + get size(): number { + return this.cache.size; + } + + /** + * 获取所有键 + * + * @returns 所有键的数组,按最近使用顺序排列(从最少使用到最近使用) + */ + keys(): K[] { + return this.lru.toArray().map(item => item.key); + } + + /** + * 获取所有值 + * + * @returns 所有值的数组,按最近使用顺序排列(从最少使用到最近使用) + */ + values(): V[] { + return this.lru.toArray().map(item => item.value); + } +} \ No newline at end of file diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index 7217819..97f48d6 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -1,5 +1,5 @@ import {defineStore} from 'pinia'; -import {nextTick, ref, watch} from 'vue'; +import {computed, nextTick, ref, watch} from 'vue'; import {EditorView} from '@codemirror/view'; import {EditorState, Extension} from '@codemirror/state'; import {useConfigStore} from './configStore'; @@ -18,8 +18,11 @@ import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/edito import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager'; import {useExtensionStore} from './extensionStore'; import createCodeBlockExtension from "@/views/editor/extensions/codeblock"; - -const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例 +import {LruCache} from '@/common/utils/lruCache'; +import {AsyncManager} from '@/common/utils/asyncManager'; +import {generateContentHash} from "@/common/utils/hashUtils"; +import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils'; +import {EDITOR_CONFIG} from '@/common/constant/editor'; export interface DocumentStats { lines: number; @@ -33,7 +36,7 @@ interface EditorInstance { content: string; isDirty: boolean; lastModified: Date; - autoSaveTimer: number | null; + autoSaveTimer: TimerManager; syntaxTreeCache: { lastDocLength: number; lastContentHash: string; @@ -49,15 +52,8 @@ export const useEditorStore = defineStore('editor', () => { const extensionStore = useExtensionStore(); // === 核心状态 === - const editorCache = ref<{ - lru: number[]; - instances: Record; - containerElement: HTMLElement | null; - }>({ - lru: [], - instances: {}, - containerElement: null - }); + const editorCache = new LruCache(EDITOR_CONFIG.MAX_INSTANCES); + const containerElement = ref(null); const currentEditor = ref(null); const documentStats = ref({ @@ -69,52 +65,17 @@ export const useEditorStore = defineStore('editor', () => { // 编辑器加载状态 const isLoading = ref(false); - // 异步操作竞态条件控制 - const operationSequence = ref(0); - const pendingOperations = ref(new Map()); - const currentLoadingDocumentId = ref(null); + // 异步操作管理器 + const operationManager = new AsyncManager(); // 自动保存设置 - 从配置动态获取 const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay; - // 生成新的操作序列号 - const getNextOperationId = () => ++operationSequence.value; - - // 取消之前的操作 - const cancelPreviousOperations = (excludeId?: number) => { - pendingOperations.value.forEach((controller, id) => { - if (id !== excludeId) { - controller.abort(); - pendingOperations.value.delete(id); - } - }); - }; - - // 检查操作是否仍然有效 - const isOperationValid = (operationId: number, documentId: number) => { - return ( - pendingOperations.value.has(operationId) && - !pendingOperations.value.get(operationId)?.signal.aborted && - currentLoadingDocumentId.value === documentId - ); - }; - // === 私有方法 === - // 生成内容哈希 - const generateContentHash = (content: string): string => { - let hash = 0; - for (let i = 0; i < content.length; i++) { - const char = content.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; // Convert to 32bit integer - } - return hash.toString(); - }; - // 缓存化的语法树确保方法 const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => { - const instance = editorCache.value.instances[documentId]; + const instance = editorCache.get(documentId); if (!instance) return; const docLength = view.state.doc.length; @@ -127,7 +88,7 @@ export const useEditorStore = defineStore('editor', () => { const shouldRebuild = !cache || cache.lastDocLength !== docLength || cache.lastContentHash !== contentHash || - (now.getTime() - cache.lastParsed.getTime()) > 30000; // 30秒过期 + (now.getTime() - cache.lastParsed.getTime()) > EDITOR_CONFIG.SYNTAX_TREE_CACHE_TIMEOUT; if (shouldRebuild) { try { @@ -151,12 +112,12 @@ export const useEditorStore = defineStore('editor', () => { operationId: number, documentId: number ): Promise => { - if (!editorCache.value.containerElement) { + if (!containerElement.value) { throw new Error('Editor container not set'); } // 检查操作是否仍然有效 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { throw new Error('Operation cancelled'); } @@ -196,7 +157,7 @@ export const useEditorStore = defineStore('editor', () => { }); // 再次检查操作有效性 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { throw new Error('Operation cancelled'); } @@ -204,7 +165,7 @@ export const useEditorStore = defineStore('editor', () => { const keymapExtension = await createDynamicKeymapExtension(); // 检查操作有效性 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { throw new Error('Operation cancelled'); } @@ -212,7 +173,7 @@ export const useEditorStore = defineStore('editor', () => { const dynamicExtensions = await createDynamicExtensions(documentId); // 最终检查操作有效性 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { throw new Error('Operation cancelled'); } @@ -252,52 +213,31 @@ export const useEditorStore = defineStore('editor', () => { // 添加编辑器到缓存 const addEditorToCache = (documentId: number, view: EditorView, content: string) => { - // 如果缓存已满,移除最少使用的编辑器 - if (editorCache.value.lru.length >= NUM_EDITOR_INSTANCES) { - const oldestId = editorCache.value.lru.shift(); - if (oldestId && editorCache.value.instances[oldestId]) { - const oldInstance = editorCache.value.instances[oldestId]; - // 清除自动保存定时器 - if (oldInstance.autoSaveTimer) { - clearTimeout(oldInstance.autoSaveTimer); - } - // 移除DOM元素 - if (oldInstance.view.dom.parentElement) { - oldInstance.view.dom.remove(); - } - oldInstance.view.destroy(); - delete editorCache.value.instances[oldestId]; - } - } - - // 添加新的编辑器实例 - editorCache.value.instances[documentId] = { + const instance: EditorInstance = { view, documentId, content, isDirty: false, lastModified: new Date(), - autoSaveTimer: null, + autoSaveTimer: createTimerManager(), syntaxTreeCache: null }; - // 添加到LRU列表 - editorCache.value.lru.push(documentId); + // 使用LRU缓存的onEvict回调处理被驱逐的实例 + editorCache.set(documentId, instance, (_evictedKey, evictedInstance) => { + // 清除自动保存定时器 + evictedInstance.autoSaveTimer.clear(); + // 移除DOM元素 + if (evictedInstance.view.dom.parentElement) { + evictedInstance.view.dom.remove(); + } + evictedInstance.view.destroy(); + }); // 初始化语法树缓存 ensureSyntaxTreeCached(view, documentId); }; - // 更新LRU - const updateLRU = (documentId: number) => { - const lru = editorCache.value.lru; - const index = lru.indexOf(documentId); - if (index > -1) { - lru.splice(index, 1); - } - lru.push(documentId); - }; - // 获取或创建编辑器 const getOrCreateEditor = async ( documentId: number, @@ -305,14 +245,13 @@ export const useEditorStore = defineStore('editor', () => { operationId: number ): Promise => { // 检查缓存 - const cached = editorCache.value.instances[documentId]; + const cached = editorCache.get(documentId); if (cached) { - updateLRU(documentId); return cached.view; } // 检查操作是否仍然有效 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { throw new Error('Operation cancelled'); } @@ -320,7 +259,7 @@ export const useEditorStore = defineStore('editor', () => { const view = await createEditorInstance(content, operationId, documentId); // 最终检查操作有效性 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { // 如果操作已取消,清理创建的实例 view.destroy(); throw new Error('Operation cancelled'); @@ -333,8 +272,8 @@ export const useEditorStore = defineStore('editor', () => { // 显示编辑器 const showEditor = (documentId: number) => { - const instance = editorCache.value.instances[documentId]; - if (!instance || !editorCache.value.containerElement) return; + const instance = editorCache.get(documentId); + if (!instance || !containerElement.value) return; try { // 移除当前编辑器DOM @@ -343,18 +282,15 @@ export const useEditorStore = defineStore('editor', () => { } // 确保容器为空 - editorCache.value.containerElement.innerHTML = ''; + containerElement.value.innerHTML = ''; // 将目标编辑器DOM添加到容器 - editorCache.value.containerElement.appendChild(instance.view.dom); + containerElement.value.appendChild(instance.view.dom); currentEditor.value = instance.view; // 设置扩展管理器视图 setExtensionManagerView(instance.view, documentId); - // 更新LRU - updateLRU(documentId); - // 重新测量和聚焦编辑器 nextTick(() => { // 将光标定位到文档末尾并滚动到该位置 @@ -377,7 +313,7 @@ export const useEditorStore = defineStore('editor', () => { // 保存编辑器内容 const saveEditorContent = async (documentId: number): Promise => { - const instance = editorCache.value.instances[documentId]; + const instance = editorCache.get(documentId); if (!instance || !instance.isDirty) return true; try { @@ -403,7 +339,7 @@ export const useEditorStore = defineStore('editor', () => { // 内容变化处理 const onContentChange = (documentId: number) => { - const instance = editorCache.value.instances[documentId]; + const instance = editorCache.get(documentId); if (!instance) return; instance.isDirty = true; @@ -412,13 +348,8 @@ export const useEditorStore = defineStore('editor', () => { // 清理语法树缓存,下次访问时重新构建 instance.syntaxTreeCache = null; - // 清除之前的定时器 - if (instance.autoSaveTimer) { - clearTimeout(instance.autoSaveTimer); - } - - // 设置新的自动保存定时器 - instance.autoSaveTimer = window.setTimeout(() => { + // 设置自动保存定时器 + instance.autoSaveTimer.set(() => { saveEditorContent(documentId); }, getAutoSaveDelay()); }; @@ -427,7 +358,7 @@ export const useEditorStore = defineStore('editor', () => { // 设置编辑器容器 const setEditorContainer = (container: HTMLElement | null) => { - editorCache.value.containerElement = container; + containerElement.value = container; // 如果设置容器时已有当前文档,立即加载编辑器 if (container && documentStore.currentDocument) { @@ -439,9 +370,9 @@ export const useEditorStore = defineStore('editor', () => { const loadEditor = async (documentId: number, content: string) => { // 设置加载状态 isLoading.value = true; - // 生成新的操作ID - const operationId = getNextOperationId(); - const abortController = new AbortController(); + + // 开始新的操作 + const { operationId } = operationManager.startOperation(documentId); try { // 验证参数 @@ -449,11 +380,6 @@ export const useEditorStore = defineStore('editor', () => { throw new Error('Invalid parameters for loadEditor'); } - // 取消之前的操作并设置当前操作 - cancelPreviousOperations(); - currentLoadingDocumentId.value = documentId; - pendingOperations.value.set(operationId, abortController); - // 保存当前编辑器内容 if (currentEditor.value) { const currentDocId = documentStore.currentDocumentId; @@ -461,7 +387,7 @@ export const useEditorStore = defineStore('editor', () => { await saveEditorContent(currentDocId); // 检查操作是否仍然有效 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { return; } } @@ -471,12 +397,12 @@ export const useEditorStore = defineStore('editor', () => { const view = await getOrCreateEditor(documentId, content, operationId); // 检查操作是否仍然有效 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { return; } // 更新内容(如果需要) - const instance = editorCache.value.instances[documentId]; + const instance = editorCache.get(documentId); if (instance && instance.content !== content) { // 确保编辑器视图有效 if (view && view.state && view.dispatch) { @@ -495,7 +421,7 @@ export const useEditorStore = defineStore('editor', () => { } // 最终检查操作有效性 - if (!isOperationValid(operationId, documentId)) { + if (!operationManager.isOperationValid(operationId, documentId)) { return; } @@ -509,35 +435,28 @@ export const useEditorStore = defineStore('editor', () => { console.error('Failed to load editor:', error); } } finally { - // 清理操作记录 - pendingOperations.value.delete(operationId); - if (currentLoadingDocumentId.value === documentId) { - currentLoadingDocumentId.value = null; - } + // 完成操作 + operationManager.completeOperation(operationId); // 延迟一段时间后再取消加载状态 setTimeout(() => { isLoading.value = false; - }, 800); + }, EDITOR_CONFIG.LOADING_DELAY); } }; // 移除编辑器 const removeEditor = (documentId: number) => { - const instance = editorCache.value.instances[documentId]; + const instance = editorCache.get(documentId); if (instance) { try { // 如果正在加载这个文档,取消操作 - if (currentLoadingDocumentId.value === documentId) { - cancelPreviousOperations(); - currentLoadingDocumentId.value = null; + if (operationManager.getCurrentContext() === documentId) { + operationManager.cancelAllOperations(); } // 清除自动保存定时器 - if (instance.autoSaveTimer) { - clearTimeout(instance.autoSaveTimer); - instance.autoSaveTimer = null; - } + instance.autoSaveTimer.clear(); // 从扩展管理器中移除视图 removeExtensionManagerView(documentId); @@ -557,12 +476,8 @@ export const useEditorStore = defineStore('editor', () => { currentEditor.value = null; } - delete editorCache.value.instances[documentId]; - - const lruIndex = editorCache.value.lru.indexOf(documentId); - if (lruIndex > -1) { - editorCache.value.lru.splice(lruIndex, 1); - } + // 从缓存中删除 + editorCache.delete(documentId); } catch (error) { console.error('Error removing editor:', error); } @@ -576,7 +491,7 @@ export const useEditorStore = defineStore('editor', () => { // 应用字体设置 const applyFontSettings = () => { - Object.values(editorCache.value.instances).forEach(instance => { + editorCache.values().forEach(instance => { updateFontConfig(instance.view, { fontFamily: configStore.config.editing.fontFamily, fontSize: configStore.config.editing.fontSize, @@ -588,7 +503,7 @@ export const useEditorStore = defineStore('editor', () => { // 应用主题设置 const applyThemeSettings = () => { - Object.values(editorCache.value.instances).forEach(instance => { + editorCache.values().forEach(instance => { updateEditorTheme(instance.view, themeStore.currentTheme || SystemThemeType.SystemThemeAuto ); @@ -597,7 +512,7 @@ export const useEditorStore = defineStore('editor', () => { // 应用Tab设置 const applyTabSettings = () => { - Object.values(editorCache.value.instances).forEach(instance => { + editorCache.values().forEach(instance => { updateTabConfig( instance.view, configStore.config.editing.tabSize, @@ -611,7 +526,7 @@ export const useEditorStore = defineStore('editor', () => { const applyKeymapSettings = async () => { // 确保所有编辑器实例的快捷键都更新 await Promise.all( - Object.values(editorCache.value.instances).map(instance => + editorCache.values().map(instance => updateKeymapExtension(instance.view) ) ); @@ -620,14 +535,11 @@ export const useEditorStore = defineStore('editor', () => { // 清空所有编辑器 const clearAllEditors = () => { // 取消所有挂起的操作 - cancelPreviousOperations(); - currentLoadingDocumentId.value = null; + operationManager.cancelAllOperations(); - Object.values(editorCache.value.instances).forEach(instance => { + editorCache.clear((_documentId, instance) => { // 清除自动保存定时器 - if (instance.autoSaveTimer) { - clearTimeout(instance.autoSaveTimer); - } + instance.autoSaveTimer.clear(); // 从扩展管理器移除 removeExtensionManagerView(instance.documentId); @@ -640,8 +552,6 @@ export const useEditorStore = defineStore('editor', () => { instance.view.destroy(); }); - editorCache.value.instances = {}; - editorCache.value.lru = []; currentEditor.value = null; }; @@ -665,29 +575,36 @@ export const useEditorStore = defineStore('editor', () => { // 重新加载扩展配置 await extensionStore.loadExtensions(); - // 不再需要单独更新当前编辑器的快捷键映射,因为扩展管理器会更新所有实例 - // 但我们仍需要确保快捷键配置在所有编辑器上更新 await applyKeymapSettings(); }; // 监听文档切换 - watch(() => documentStore.currentDocument, (newDoc) => { - if (newDoc && editorCache.value.containerElement) { + watch(() => documentStore.currentDocument, async (newDoc) => { + if (newDoc && containerElement.value) { // 使用 nextTick 确保DOM更新完成后再加载编辑器 - nextTick(() => { + await nextTick(() => { loadEditor(newDoc.id, newDoc.content); }); } }); - // 监听配置变化 - watch(() => configStore.config.editing.fontSize, applyFontSettings); - watch(() => configStore.config.editing.fontFamily, applyFontSettings); - watch(() => configStore.config.editing.lineHeight, applyFontSettings); - watch(() => configStore.config.editing.fontWeight, applyFontSettings); - watch(() => configStore.config.editing.tabSize, applyTabSettings); - watch(() => configStore.config.editing.enableTabIndent, applyTabSettings); - watch(() => configStore.config.editing.tabType, applyTabSettings); + // 创建字体配置的计算属性 + const fontConfig = computed(() => ({ + fontSize: configStore.config.editing.fontSize, + fontFamily: configStore.config.editing.fontFamily, + lineHeight: configStore.config.editing.lineHeight, + fontWeight: configStore.config.editing.fontWeight + })); + // 创建Tab配置的计算属性 + const tabConfig = computed(() => ({ + tabSize: configStore.config.editing.tabSize, + enableTabIndent: configStore.config.editing.enableTabIndent, + tabType: configStore.config.editing.tabType + })); + // 监听字体配置变化 + watch(fontConfig, applyFontSettings, { deep: true }); + // 监听Tab配置变化 + watch(tabConfig, applyTabSettings, { deep: true }); return { // 状态 diff --git a/frontend/src/views/editor/Editor.vue b/frontend/src/views/editor/Editor.vue index 7e895f3..e9fc233 100644 --- a/frontend/src/views/editor/Editor.vue +++ b/frontend/src/views/editor/Editor.vue @@ -26,10 +26,10 @@ onMounted(async () => { // 从URL查询参数中获取documentId const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined; - + // 初始化文档存储,优先使用URL参数中的文档ID await documentStore.initialize(urlDocumentId); - + // 设置编辑器容器 editorStore.setEditorContainer(editorElement.value); @@ -42,12 +42,14 @@ onBeforeUnmount(() => { if (editorElement.value) { editorElement.value.removeEventListener('wheel', wheelHandler); } + editorStore.clearAllEditors(); + });