🎨 Optimize code structure
This commit is contained in:
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
@@ -16,6 +16,8 @@ declare module 'vue' {
|
|||||||
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
|
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default']
|
||||||
|
TabItem: typeof import('./src/components/tabs/TabItem.vue')['default']
|
||||||
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
|
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
|
||||||
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
|
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
|
||||||
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']
|
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']
|
||||||
|
|||||||
3
frontend/src/common/async-operation/index.ts
Normal file
3
frontend/src/common/async-operation/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { AsyncOperationManager } from './manager';
|
||||||
|
export * from './types';
|
||||||
|
export { AsyncOperationManager as default } from './manager';
|
||||||
296
frontend/src/common/async-operation/manager.ts
Normal file
296
frontend/src/common/async-operation/manager.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import {
|
||||||
|
OperationStatus,
|
||||||
|
OperationInfo,
|
||||||
|
AsyncOperationManagerConfig,
|
||||||
|
OperationCallbacks,
|
||||||
|
OperationExecutor,
|
||||||
|
OperationResult
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步操作管理器
|
||||||
|
* 用于控制异步操作的竞态条件,确保操作的正确性和一致性
|
||||||
|
*/
|
||||||
|
export class AsyncOperationManager {
|
||||||
|
private operationSequence = 0;
|
||||||
|
private pendingOperations = new Map<number, OperationInfo>();
|
||||||
|
private currentResourceOperation = new Map<string | number, number>();
|
||||||
|
private config: Required<AsyncOperationManagerConfig>;
|
||||||
|
private callbacks: OperationCallbacks;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: AsyncOperationManagerConfig = {},
|
||||||
|
callbacks: OperationCallbacks = {}
|
||||||
|
) {
|
||||||
|
this.config = {
|
||||||
|
timeout: 0,
|
||||||
|
autoCleanup: true,
|
||||||
|
maxConcurrent: 0,
|
||||||
|
debug: false,
|
||||||
|
...config
|
||||||
|
};
|
||||||
|
this.callbacks = callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成新的操作ID
|
||||||
|
*/
|
||||||
|
private getNextOperationId(): number {
|
||||||
|
return ++this.operationSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录日志(调试模式下)
|
||||||
|
*/
|
||||||
|
private log(message: string, ...args: any[]): void {
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log(`[AsyncOperationManager] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建操作信息
|
||||||
|
*/
|
||||||
|
private createOperation(
|
||||||
|
resourceId: string | number,
|
||||||
|
type?: string
|
||||||
|
): OperationInfo {
|
||||||
|
const operation: OperationInfo = {
|
||||||
|
id: this.getNextOperationId(),
|
||||||
|
resourceId,
|
||||||
|
status: OperationStatus.PENDING,
|
||||||
|
abortController: new AbortController(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
type
|
||||||
|
};
|
||||||
|
|
||||||
|
this.log(`Created operation ${operation.id} for resource ${resourceId}`, operation);
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理已完成的操作
|
||||||
|
*/
|
||||||
|
private cleanupCompletedOperations(): void {
|
||||||
|
if (!this.config.autoCleanup) return;
|
||||||
|
|
||||||
|
const completedStatuses = [
|
||||||
|
OperationStatus.COMPLETED,
|
||||||
|
OperationStatus.CANCELLED,
|
||||||
|
OperationStatus.FAILED
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [id, operation] of this.pendingOperations.entries()) {
|
||||||
|
if (completedStatuses.includes(operation.status)) {
|
||||||
|
this.pendingOperations.delete(id);
|
||||||
|
this.log(`Cleaned up operation ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消指定资源的所有操作(除了指定的操作ID)
|
||||||
|
*/
|
||||||
|
public cancelResourceOperations(
|
||||||
|
resourceId: string | number,
|
||||||
|
excludeOperationId?: number
|
||||||
|
): void {
|
||||||
|
this.log(`Cancelling operations for resource ${resourceId}, exclude: ${excludeOperationId}`);
|
||||||
|
|
||||||
|
for (const [id, operation] of this.pendingOperations.entries()) {
|
||||||
|
if (
|
||||||
|
operation.resourceId === resourceId &&
|
||||||
|
id !== excludeOperationId &&
|
||||||
|
operation.status === OperationStatus.RUNNING
|
||||||
|
) {
|
||||||
|
this.cancelOperation(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消指定操作
|
||||||
|
*/
|
||||||
|
public cancelOperation(operationId: number): boolean {
|
||||||
|
const operation = this.pendingOperations.get(operationId);
|
||||||
|
if (!operation) return false;
|
||||||
|
|
||||||
|
if (operation.status === OperationStatus.RUNNING) {
|
||||||
|
operation.abortController.abort();
|
||||||
|
operation.status = OperationStatus.CANCELLED;
|
||||||
|
this.log(`Cancelled operation ${operationId}`);
|
||||||
|
|
||||||
|
this.callbacks.onCancel?.(operation);
|
||||||
|
this.cleanupCompletedOperations();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消所有操作
|
||||||
|
*/
|
||||||
|
public cancelAllOperations(): void {
|
||||||
|
this.log('Cancelling all operations');
|
||||||
|
|
||||||
|
for (const [id] of this.pendingOperations.entries()) {
|
||||||
|
this.cancelOperation(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentResourceOperation.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查操作是否仍然有效
|
||||||
|
*/
|
||||||
|
public isOperationValid(
|
||||||
|
operationId: number,
|
||||||
|
resourceId?: string | number
|
||||||
|
): boolean {
|
||||||
|
const operation = this.pendingOperations.get(operationId);
|
||||||
|
|
||||||
|
if (!operation) return false;
|
||||||
|
if (operation.abortController.signal.aborted) return false;
|
||||||
|
if (operation.status !== OperationStatus.RUNNING) return false;
|
||||||
|
|
||||||
|
// 如果指定了资源ID,检查是否为当前资源的活跃操作
|
||||||
|
if (resourceId !== undefined) {
|
||||||
|
const currentOperationId = this.currentResourceOperation.get(resourceId);
|
||||||
|
if (currentOperationId !== operationId) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行异步操作
|
||||||
|
*/
|
||||||
|
public async executeOperation<T = any>(
|
||||||
|
resourceId: string | number,
|
||||||
|
executor: OperationExecutor<T>,
|
||||||
|
operationType?: string
|
||||||
|
): Promise<OperationResult<T>> {
|
||||||
|
// 检查并发限制
|
||||||
|
if (this.config.maxConcurrent > 0) {
|
||||||
|
const runningCount = Array.from(this.pendingOperations.values())
|
||||||
|
.filter(op => op.status === OperationStatus.RUNNING).length;
|
||||||
|
|
||||||
|
if (runningCount >= this.config.maxConcurrent) {
|
||||||
|
throw new Error(`Maximum concurrent operations limit reached: ${this.config.maxConcurrent}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const operation = this.createOperation(resourceId, operationType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 取消同一资源的其他操作
|
||||||
|
this.cancelResourceOperations(resourceId, operation.id);
|
||||||
|
|
||||||
|
// 设置当前资源的活跃操作
|
||||||
|
this.currentResourceOperation.set(resourceId, operation.id);
|
||||||
|
|
||||||
|
// 添加到待处理操作列表
|
||||||
|
this.pendingOperations.set(operation.id, operation);
|
||||||
|
|
||||||
|
// 设置超时
|
||||||
|
if (this.config.timeout > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.isOperationValid(operation.id)) {
|
||||||
|
this.cancelOperation(operation.id);
|
||||||
|
}
|
||||||
|
}, this.config.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态为运行中
|
||||||
|
operation.status = OperationStatus.RUNNING;
|
||||||
|
this.callbacks.onStart?.(operation);
|
||||||
|
this.log(`Started operation ${operation.id} for resource ${resourceId}`);
|
||||||
|
|
||||||
|
// 执行操作
|
||||||
|
const result = await executor(operation.abortController.signal, operation.id);
|
||||||
|
|
||||||
|
// 检查操作是否仍然有效
|
||||||
|
if (!this.isOperationValid(operation.id, resourceId)) {
|
||||||
|
throw new Error('Operation was cancelled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作成功完成
|
||||||
|
operation.status = OperationStatus.COMPLETED;
|
||||||
|
this.callbacks.onComplete?.(operation);
|
||||||
|
this.log(`Completed operation ${operation.id}`);
|
||||||
|
|
||||||
|
this.cleanupCompletedOperations();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
operation
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
|
if (operation.abortController.signal.aborted) {
|
||||||
|
operation.status = OperationStatus.CANCELLED;
|
||||||
|
this.log(`Operation ${operation.id} was cancelled`);
|
||||||
|
} else {
|
||||||
|
operation.status = OperationStatus.FAILED;
|
||||||
|
this.callbacks.onError?.(operation, err);
|
||||||
|
this.log(`Operation ${operation.id} failed:`, err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupCompletedOperations();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: err,
|
||||||
|
operation
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
// 清理当前资源操作记录
|
||||||
|
if (this.currentResourceOperation.get(resourceId) === operation.id) {
|
||||||
|
this.currentResourceOperation.delete(resourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取操作信息
|
||||||
|
*/
|
||||||
|
public getOperation(operationId: number): OperationInfo | undefined {
|
||||||
|
return this.pendingOperations.get(operationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源的当前操作ID
|
||||||
|
*/
|
||||||
|
public getCurrentOperationId(resourceId: string | number): number | undefined {
|
||||||
|
return this.currentResourceOperation.get(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有待处理操作
|
||||||
|
*/
|
||||||
|
public getPendingOperations(): OperationInfo[] {
|
||||||
|
return Array.from(this.pendingOperations.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行中的操作数量
|
||||||
|
*/
|
||||||
|
public getRunningOperationsCount(): number {
|
||||||
|
return Array.from(this.pendingOperations.values())
|
||||||
|
.filter(op => op.status === OperationStatus.RUNNING).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁管理器,取消所有操作
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
this.log('Destroying AsyncOperationManager');
|
||||||
|
this.cancelAllOperations();
|
||||||
|
this.pendingOperations.clear();
|
||||||
|
this.currentResourceOperation.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
82
frontend/src/common/async-operation/types.ts
Normal file
82
frontend/src/common/async-operation/types.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* 异步操作竞态条件控制相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作状态枚举
|
||||||
|
*/
|
||||||
|
export enum OperationStatus {
|
||||||
|
PENDING = 'pending',
|
||||||
|
RUNNING = 'running',
|
||||||
|
COMPLETED = 'completed',
|
||||||
|
CANCELLED = 'cancelled',
|
||||||
|
FAILED = 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作信息接口
|
||||||
|
*/
|
||||||
|
export interface OperationInfo {
|
||||||
|
/** 操作ID */
|
||||||
|
id: number;
|
||||||
|
/** 资源ID(如文档ID、用户ID等) */
|
||||||
|
resourceId: string | number;
|
||||||
|
/** 操作状态 */
|
||||||
|
status: OperationStatus;
|
||||||
|
/** 取消控制器 */
|
||||||
|
abortController: AbortController;
|
||||||
|
/** 创建时间 */
|
||||||
|
createdAt: number;
|
||||||
|
/** 操作类型(可选,用于调试) */
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作管理器配置
|
||||||
|
*/
|
||||||
|
export interface AsyncOperationManagerConfig {
|
||||||
|
/** 操作超时时间(毫秒),0表示不超时 */
|
||||||
|
timeout?: number;
|
||||||
|
/** 是否自动清理已完成的操作 */
|
||||||
|
autoCleanup?: boolean;
|
||||||
|
/** 最大并发操作数,0表示无限制 */
|
||||||
|
maxConcurrent?: number;
|
||||||
|
/** 调试模式 */
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作回调函数类型
|
||||||
|
*/
|
||||||
|
export interface OperationCallbacks {
|
||||||
|
/** 操作开始回调 */
|
||||||
|
onStart?: (operation: OperationInfo) => void;
|
||||||
|
/** 操作完成回调 */
|
||||||
|
onComplete?: (operation: OperationInfo) => void;
|
||||||
|
/** 操作取消回调 */
|
||||||
|
onCancel?: (operation: OperationInfo) => void;
|
||||||
|
/** 操作失败回调 */
|
||||||
|
onError?: (operation: OperationInfo, error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作执行器函数类型
|
||||||
|
*/
|
||||||
|
export type OperationExecutor<T = any> = (
|
||||||
|
signal: AbortSignal,
|
||||||
|
operationId: number
|
||||||
|
) => Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作结果类型
|
||||||
|
*/
|
||||||
|
export interface OperationResult<T = any> {
|
||||||
|
/** 操作是否成功 */
|
||||||
|
success: boolean;
|
||||||
|
/** 操作结果数据 */
|
||||||
|
data?: T;
|
||||||
|
/** 错误信息 */
|
||||||
|
error?: Error;
|
||||||
|
/** 操作信息 */
|
||||||
|
operation: OperationInfo;
|
||||||
|
}
|
||||||
106
frontend/src/common/cache/README.md
vendored
Normal file
106
frontend/src/common/cache/README.md
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# 缓存系统
|
||||||
|
|
||||||
|
简洁高效的 LRU 缓存实现,支持泛型和自动清理。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 🚀 高性能 LRU 缓存算法
|
||||||
|
- 🔧 TypeScript 泛型支持
|
||||||
|
- 🧹 自动资源清理
|
||||||
|
- 📊 缓存统计信息
|
||||||
|
- ⏰ TTL 过期支持
|
||||||
|
- 🎯 简洁易用的 API
|
||||||
|
|
||||||
|
## 基础用法
|
||||||
|
|
||||||
|
### 创建缓存
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { LRUCache, CacheManager, createCacheItem } from '@/common/cache';
|
||||||
|
|
||||||
|
// 直接创建缓存
|
||||||
|
const cache = new LRUCache({
|
||||||
|
maxSize: 100,
|
||||||
|
ttl: 5 * 60 * 1000, // 5 分钟
|
||||||
|
onEvict: (item) => console.log('Evicted:', item)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用缓存管理器
|
||||||
|
const manager = new CacheManager();
|
||||||
|
const myCache = manager.createCache('myCache', { maxSize: 50 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存操作
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 创建缓存项
|
||||||
|
const item = createCacheItem('key1', {
|
||||||
|
name: 'example',
|
||||||
|
data: { foo: 'bar' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置缓存
|
||||||
|
cache.set('key1', item);
|
||||||
|
|
||||||
|
// 获取缓存
|
||||||
|
const cached = cache.get('key1');
|
||||||
|
|
||||||
|
// 检查存在
|
||||||
|
if (cache.has('key1')) {
|
||||||
|
// 处理逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除缓存
|
||||||
|
cache.remove('key1');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动清理资源
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MyItem extends CacheItem, DisposableCacheItem {
|
||||||
|
resource: SomeResource;
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item: MyItem = {
|
||||||
|
id: 'resource1',
|
||||||
|
lastAccessed: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
resource: new SomeResource(),
|
||||||
|
dispose() {
|
||||||
|
this.resource.cleanup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当项被驱逐或移除时,dispose 方法会自动调用
|
||||||
|
cache.set('resource1', item);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参考
|
||||||
|
|
||||||
|
### LRUCache
|
||||||
|
|
||||||
|
- `get(id)` - 获取缓存项
|
||||||
|
- `set(id, item)` - 设置缓存项
|
||||||
|
- `remove(id)` - 移除缓存项
|
||||||
|
- `has(id)` - 检查是否存在
|
||||||
|
- `clear()` - 清空缓存
|
||||||
|
- `size()` - 获取缓存大小
|
||||||
|
- `getStats()` - 获取统计信息
|
||||||
|
- `cleanup()` - 清理过期项
|
||||||
|
|
||||||
|
### CacheManager
|
||||||
|
|
||||||
|
- `getCache(name, config?)` - 获取或创建缓存
|
||||||
|
- `createCache(name, config)` - 创建新缓存
|
||||||
|
- `removeCache(name)` - 删除缓存
|
||||||
|
- `clearAll()` - 清空所有缓存
|
||||||
|
- `getAllStats()` - 获取所有统计信息
|
||||||
|
|
||||||
|
## 工具函数
|
||||||
|
|
||||||
|
- `generateCacheKey(...parts)` - 生成缓存键
|
||||||
|
- `createCacheItem(id, data)` - 创建缓存项
|
||||||
|
- `createContentHash(content)` - 创建内容哈希
|
||||||
|
- `debounce(func, wait)` - 防抖函数
|
||||||
|
- `throttle(func, limit)` - 节流函数
|
||||||
133
frontend/src/common/cache/cache-manager.ts
vendored
Normal file
133
frontend/src/common/cache/cache-manager.ts
vendored
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import type { CacheItem, CacheConfig, CacheStats } from './types';
|
||||||
|
import { LRUCache } from './lru-cache';
|
||||||
|
|
||||||
|
export class CacheManager {
|
||||||
|
private caches = new Map<string, LRUCache<any>>();
|
||||||
|
private cleanupInterval?: number;
|
||||||
|
|
||||||
|
constructor(options?: {
|
||||||
|
/** 自动清理间隔(毫秒),默认 5 分钟 */
|
||||||
|
cleanupInterval?: number;
|
||||||
|
}) {
|
||||||
|
if (options?.cleanupInterval) {
|
||||||
|
this.startAutoCleanup(options.cleanupInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建或获取缓存实例
|
||||||
|
*/
|
||||||
|
getCache<T extends CacheItem>(name: string, config?: CacheConfig): LRUCache<T> {
|
||||||
|
if (!this.caches.has(name)) {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Cache "${name}" does not exist and no config provided`);
|
||||||
|
}
|
||||||
|
this.caches.set(name, new LRUCache<T>(config));
|
||||||
|
}
|
||||||
|
return this.caches.get(name)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新的缓存实例
|
||||||
|
*/
|
||||||
|
createCache<T extends CacheItem>(name: string, config: CacheConfig): LRUCache<T> {
|
||||||
|
if (this.caches.has(name)) {
|
||||||
|
throw new Error(`Cache "${name}" already exists`);
|
||||||
|
}
|
||||||
|
const cache = new LRUCache<T>(config);
|
||||||
|
this.caches.set(name, cache);
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除缓存实例
|
||||||
|
*/
|
||||||
|
removeCache(name: string): boolean {
|
||||||
|
const cache = this.caches.get(name);
|
||||||
|
if (cache) {
|
||||||
|
cache.clear();
|
||||||
|
this.caches.delete(name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存是否存在
|
||||||
|
*/
|
||||||
|
hasCache(name: string): boolean {
|
||||||
|
return this.caches.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有缓存名称
|
||||||
|
*/
|
||||||
|
getCacheNames(): string[] {
|
||||||
|
return Array.from(this.caches.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有缓存
|
||||||
|
*/
|
||||||
|
clearAll(): void {
|
||||||
|
for (const cache of this.caches.values()) {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有缓存的统计信息
|
||||||
|
*/
|
||||||
|
getAllStats(): Record<string, CacheStats> {
|
||||||
|
const stats: Record<string, CacheStats> = {};
|
||||||
|
for (const [name, cache] of this.caches.entries()) {
|
||||||
|
stats[name] = cache.getStats();
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理所有缓存中的过期项
|
||||||
|
*/
|
||||||
|
cleanupAll(): Record<string, number> {
|
||||||
|
const results: Record<string, number> = {};
|
||||||
|
for (const [name, cache] of this.caches.entries()) {
|
||||||
|
results[name] = cache.cleanup();
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动自动清理
|
||||||
|
*/
|
||||||
|
startAutoCleanup(interval: number): void {
|
||||||
|
this.stopAutoCleanup();
|
||||||
|
this.cleanupInterval = window.setInterval(() => {
|
||||||
|
this.cleanupAll();
|
||||||
|
}, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止自动清理
|
||||||
|
*/
|
||||||
|
stopAutoCleanup(): void {
|
||||||
|
if (this.cleanupInterval) {
|
||||||
|
clearInterval(this.cleanupInterval);
|
||||||
|
this.cleanupInterval = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁管理器
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
this.stopAutoCleanup();
|
||||||
|
this.clearAll();
|
||||||
|
this.caches.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局缓存管理器实例
|
||||||
|
export const globalCacheManager = new CacheManager({
|
||||||
|
cleanupInterval: 5 * 60 * 1000 // 5 分钟
|
||||||
|
});
|
||||||
19
frontend/src/common/cache/index.ts
vendored
Normal file
19
frontend/src/common/cache/index.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export type {
|
||||||
|
CacheItem,
|
||||||
|
DisposableCacheItem,
|
||||||
|
CacheConfig,
|
||||||
|
CacheStats
|
||||||
|
} from './types';
|
||||||
|
export { LRUCache } from './lru-cache';
|
||||||
|
export { CacheManager, globalCacheManager } from './cache-manager';
|
||||||
|
|
||||||
|
export {
|
||||||
|
generateCacheKey,
|
||||||
|
createCacheItem,
|
||||||
|
calculateHitRate,
|
||||||
|
formatCacheSize,
|
||||||
|
isExpired,
|
||||||
|
createContentHash,
|
||||||
|
debounce,
|
||||||
|
throttle
|
||||||
|
} from './utils';
|
||||||
200
frontend/src/common/cache/lru-cache.ts
vendored
Normal file
200
frontend/src/common/cache/lru-cache.ts
vendored
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import type { CacheItem, CacheConfig, CacheStats, DisposableCacheItem } from './types';
|
||||||
|
|
||||||
|
export class LRUCache<T extends CacheItem> {
|
||||||
|
private items = new Map<string | number, T>();
|
||||||
|
private accessOrder: (string | number)[] = [];
|
||||||
|
private config: CacheConfig;
|
||||||
|
private stats = {
|
||||||
|
hits: 0,
|
||||||
|
misses: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(config: CacheConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存项
|
||||||
|
*/
|
||||||
|
get(id: string | number): T | null {
|
||||||
|
const item = this.items.get(id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
this.stats.misses++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (this.isExpired(item)) {
|
||||||
|
this.remove(id);
|
||||||
|
this.stats.misses++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新访问时间和顺序
|
||||||
|
item.lastAccessed = new Date();
|
||||||
|
this.updateAccessOrder(id);
|
||||||
|
this.stats.hits++;
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存项
|
||||||
|
*/
|
||||||
|
set(id: string | number, item: T): void {
|
||||||
|
// 如果已存在,先移除旧的
|
||||||
|
if (this.items.has(id)) {
|
||||||
|
this.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查容量,必要时驱逐最旧的项
|
||||||
|
while (this.items.size >= this.config.maxSize) {
|
||||||
|
const oldestId = this.accessOrder.shift();
|
||||||
|
if (oldestId !== undefined) {
|
||||||
|
this.evict(oldestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新项
|
||||||
|
this.items.set(id, item);
|
||||||
|
this.accessOrder.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除缓存项
|
||||||
|
*/
|
||||||
|
remove(id: string | number): boolean {
|
||||||
|
const item = this.items.get(id);
|
||||||
|
if (!item) return false;
|
||||||
|
|
||||||
|
this.items.delete(id);
|
||||||
|
const index = this.accessOrder.indexOf(id);
|
||||||
|
if (index > -1) {
|
||||||
|
this.accessOrder.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用清理逻辑
|
||||||
|
this.disposeItem(item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否存在
|
||||||
|
*/
|
||||||
|
has(id: string | number): boolean {
|
||||||
|
const item = this.items.get(id);
|
||||||
|
if (!item) return false;
|
||||||
|
|
||||||
|
if (this.isExpired(item)) {
|
||||||
|
this.remove(id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空缓存
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
for (const item of this.items.values()) {
|
||||||
|
this.disposeItem(item);
|
||||||
|
}
|
||||||
|
this.items.clear();
|
||||||
|
this.accessOrder = [];
|
||||||
|
this.stats.hits = 0;
|
||||||
|
this.stats.misses = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有项
|
||||||
|
*/
|
||||||
|
getAll(): T[] {
|
||||||
|
return Array.from(this.items.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存大小
|
||||||
|
*/
|
||||||
|
size(): number {
|
||||||
|
return this.items.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计信息
|
||||||
|
*/
|
||||||
|
getStats(): CacheStats {
|
||||||
|
const total = this.stats.hits + this.stats.misses;
|
||||||
|
return {
|
||||||
|
size: this.items.size,
|
||||||
|
maxSize: this.config.maxSize,
|
||||||
|
hits: this.stats.hits,
|
||||||
|
misses: this.stats.misses,
|
||||||
|
hitRate: total > 0 ? this.stats.hits / total : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期项
|
||||||
|
*/
|
||||||
|
cleanup(): number {
|
||||||
|
let cleanedCount = 0;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!this.config.ttl) return cleanedCount;
|
||||||
|
|
||||||
|
for (const [id, item] of this.items.entries()) {
|
||||||
|
if (now - item.lastAccessed.getTime() > this.config.ttl) {
|
||||||
|
this.remove(id);
|
||||||
|
cleanedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 私有方法
|
||||||
|
|
||||||
|
private isExpired(item: T): boolean {
|
||||||
|
if (!this.config.ttl) return false;
|
||||||
|
return Date.now() - item.lastAccessed.getTime() > this.config.ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateAccessOrder(id: string | number): void {
|
||||||
|
const index = this.accessOrder.indexOf(id);
|
||||||
|
if (index > -1) {
|
||||||
|
this.accessOrder.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.accessOrder.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private evict(id: string | number): void {
|
||||||
|
const item = this.items.get(id);
|
||||||
|
if (item) {
|
||||||
|
if (this.config.onEvict) {
|
||||||
|
this.config.onEvict(item);
|
||||||
|
}
|
||||||
|
this.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private disposeItem(item: T): void {
|
||||||
|
if (this.isDisposable(item)) {
|
||||||
|
try {
|
||||||
|
const result = item.dispose();
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
result.catch(error => {
|
||||||
|
console.warn('Failed to dispose cache item:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to dispose cache item:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDisposable(item: T): item is T & DisposableCacheItem {
|
||||||
|
return 'dispose' in item && typeof (item as any).dispose === 'function';
|
||||||
|
}
|
||||||
|
}
|
||||||
39
frontend/src/common/cache/types.ts
vendored
Normal file
39
frontend/src/common/cache/types.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// 缓存项基础接口
|
||||||
|
export interface CacheItem {
|
||||||
|
/** 缓存项的唯一标识 */
|
||||||
|
id: string | number;
|
||||||
|
/** 最后访问时间 */
|
||||||
|
lastAccessed: Date;
|
||||||
|
/** 创建时间 */
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可清理的缓存项接口
|
||||||
|
export interface DisposableCacheItem extends CacheItem {
|
||||||
|
/** 清理资源的方法 */
|
||||||
|
dispose(): void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存配置
|
||||||
|
export interface CacheConfig {
|
||||||
|
/** 最大缓存数量 */
|
||||||
|
maxSize: number;
|
||||||
|
/** 生存时间(毫秒),可选 */
|
||||||
|
ttl?: number;
|
||||||
|
/** 驱逐回调函数,可选 */
|
||||||
|
onEvict?: (item: any) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存统计信息
|
||||||
|
export interface CacheStats {
|
||||||
|
/** 当前缓存项数量 */
|
||||||
|
size: number;
|
||||||
|
/** 最大容量 */
|
||||||
|
maxSize: number;
|
||||||
|
/** 命中次数 */
|
||||||
|
hits: number;
|
||||||
|
/** 未命中次数 */
|
||||||
|
misses: number;
|
||||||
|
/** 命中率 */
|
||||||
|
hitRate: number;
|
||||||
|
}
|
||||||
98
frontend/src/common/cache/utils.ts
vendored
Normal file
98
frontend/src/common/cache/utils.ts
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { CacheItem } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成缓存键
|
||||||
|
*/
|
||||||
|
export function generateCacheKey(...parts: (string | number)[]): string {
|
||||||
|
return parts.join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建基础缓存项
|
||||||
|
*/
|
||||||
|
export function createCacheItem<T extends Record<string, any>>(
|
||||||
|
id: string | number,
|
||||||
|
data: T
|
||||||
|
): CacheItem & T {
|
||||||
|
const now = new Date();
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
lastAccessed: now,
|
||||||
|
createdAt: now,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算缓存命中率
|
||||||
|
*/
|
||||||
|
export function calculateHitRate(hits: number, misses: number): number {
|
||||||
|
const total = hits + misses;
|
||||||
|
return total > 0 ? hits / total : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化缓存大小
|
||||||
|
*/
|
||||||
|
export function formatCacheSize(size: number): string {
|
||||||
|
if (size < 1024) return `${size} B`;
|
||||||
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||||
|
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查项是否过期
|
||||||
|
*/
|
||||||
|
export function isExpired(item: CacheItem, ttl: number): boolean {
|
||||||
|
return Date.now() - item.lastAccessed.getTime() > ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建内容哈希(简单实现)
|
||||||
|
*/
|
||||||
|
export function createContentHash(content: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
if (content.length === 0) return hash.toString();
|
||||||
|
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
|
const char = content.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // 转换为 32 位整数
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.abs(hash).toString(36);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数,用于缓存操作
|
||||||
|
*/
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: number | undefined;
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = window.setTimeout(() => func(...args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节流函数,用于缓存清理
|
||||||
|
*/
|
||||||
|
export function throttle<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
limit: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let inThrottle: boolean;
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
if (!inThrottle) {
|
||||||
|
func(...args);
|
||||||
|
inThrottle = true;
|
||||||
|
setTimeout(() => inThrottle = false, limit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
257
frontend/src/stores/editorCacheStore.ts
Normal file
257
frontend/src/stores/editorCacheStore.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { ensureSyntaxTree } from '@codemirror/language';
|
||||||
|
import { LRUCache, type CacheItem, type DisposableCacheItem, createContentHash } from '@/common/cache';
|
||||||
|
import { removeExtensionManagerView } from '@/views/editor/manager';
|
||||||
|
|
||||||
|
// 编辑器缓存项接口
|
||||||
|
export interface EditorCacheItem extends CacheItem, DisposableCacheItem {
|
||||||
|
view: EditorView;
|
||||||
|
documentId: number;
|
||||||
|
content: string;
|
||||||
|
isDirty: boolean;
|
||||||
|
lastModified: Date;
|
||||||
|
autoSaveTimer: number | null;
|
||||||
|
syntaxTreeCache: {
|
||||||
|
lastDocLength: number;
|
||||||
|
lastContentHash: string;
|
||||||
|
lastParsed: Date;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEditorCacheStore = defineStore('editorCache', () => {
|
||||||
|
// 清理编辑器实例的函数
|
||||||
|
const cleanupEditorInstance = (item: EditorCacheItem) => {
|
||||||
|
try {
|
||||||
|
// 清除自动保存定时器
|
||||||
|
if (item.autoSaveTimer) {
|
||||||
|
clearTimeout(item.autoSaveTimer);
|
||||||
|
item.autoSaveTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从扩展管理器中移除视图
|
||||||
|
removeExtensionManagerView(item.documentId);
|
||||||
|
|
||||||
|
// 移除DOM元素
|
||||||
|
if (item.view && item.view.dom && item.view.dom.parentElement) {
|
||||||
|
item.view.dom.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销毁编辑器
|
||||||
|
if (item.view && item.view.destroy) {
|
||||||
|
item.view.destroy();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning up editor instance:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建编辑器缓存项
|
||||||
|
const createEditorCacheItem = (
|
||||||
|
documentId: number,
|
||||||
|
view: EditorView,
|
||||||
|
content: string
|
||||||
|
): EditorCacheItem => {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const item: EditorCacheItem = {
|
||||||
|
id: documentId,
|
||||||
|
lastAccessed: now,
|
||||||
|
createdAt: now,
|
||||||
|
view,
|
||||||
|
documentId,
|
||||||
|
content,
|
||||||
|
isDirty: false,
|
||||||
|
lastModified: now,
|
||||||
|
autoSaveTimer: null,
|
||||||
|
syntaxTreeCache: null,
|
||||||
|
dispose: () => cleanupEditorInstance(item)
|
||||||
|
};
|
||||||
|
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑器缓存配置
|
||||||
|
const EDITOR_CACHE_CONFIG = {
|
||||||
|
maxSize: 5, // 最多缓存5个编辑器实例
|
||||||
|
onEvict: (item: EditorCacheItem) => {
|
||||||
|
// 清理被驱逐的编辑器实例
|
||||||
|
cleanupEditorInstance(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 编辑器缓存实例
|
||||||
|
const cache = new LRUCache<EditorCacheItem>(EDITOR_CACHE_CONFIG);
|
||||||
|
|
||||||
|
// 容器元素
|
||||||
|
const containerElement = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// 设置容器元素
|
||||||
|
const setContainer = (element: HTMLElement | null) => {
|
||||||
|
containerElement.value = element;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取容器元素
|
||||||
|
const getContainer = () => containerElement.value;
|
||||||
|
|
||||||
|
// 添加编辑器到缓存
|
||||||
|
const addEditor = (documentId: number, view: EditorView, content: string) => {
|
||||||
|
const item = createEditorCacheItem(documentId, view, content);
|
||||||
|
cache.set(documentId, item);
|
||||||
|
|
||||||
|
// 初始化语法树缓存
|
||||||
|
ensureSyntaxTreeCached(view, documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取编辑器实例
|
||||||
|
const getEditor = (documentId: number): EditorCacheItem | null => {
|
||||||
|
return cache.get(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查编辑器是否存在
|
||||||
|
const hasEditor = (documentId: number): boolean => {
|
||||||
|
return cache.has(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除编辑器
|
||||||
|
const removeEditor = (documentId: number): boolean => {
|
||||||
|
return cache.remove(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有编辑器实例
|
||||||
|
const getAllEditors = (): EditorCacheItem[] => {
|
||||||
|
return cache.getAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空所有编辑器
|
||||||
|
const clearAll = () => {
|
||||||
|
cache.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取缓存大小
|
||||||
|
const size = (): number => {
|
||||||
|
return cache.size();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取缓存统计信息
|
||||||
|
const getStats = () => {
|
||||||
|
return cache.getStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 缓存化的语法树确保方法
|
||||||
|
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
||||||
|
const item = cache.get(documentId);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const docLength = view.state.doc.length;
|
||||||
|
const content = view.state.doc.toString();
|
||||||
|
const contentHash = createContentHash(content);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 检查是否需要重新构建语法树
|
||||||
|
const syntaxCache = item.syntaxTreeCache;
|
||||||
|
const shouldRebuild = !syntaxCache ||
|
||||||
|
syntaxCache.lastDocLength !== docLength ||
|
||||||
|
syntaxCache.lastContentHash !== contentHash ||
|
||||||
|
(now.getTime() - syntaxCache.lastParsed.getTime()) > 30000; // 30秒过期
|
||||||
|
|
||||||
|
if (shouldRebuild) {
|
||||||
|
try {
|
||||||
|
ensureSyntaxTree(view.state, docLength, 5000);
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
item.syntaxTreeCache = {
|
||||||
|
lastDocLength: docLength,
|
||||||
|
lastContentHash: contentHash,
|
||||||
|
lastParsed: now
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to ensure syntax tree:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新编辑器内容
|
||||||
|
const updateEditorContent = (documentId: number, content: string) => {
|
||||||
|
const item = cache.get(documentId);
|
||||||
|
if (item) {
|
||||||
|
item.content = content;
|
||||||
|
item.isDirty = false;
|
||||||
|
item.lastModified = new Date();
|
||||||
|
// 清理语法树缓存,因为内容已更新
|
||||||
|
item.syntaxTreeCache = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记编辑器为脏状态
|
||||||
|
const markEditorDirty = (documentId: number) => {
|
||||||
|
const item = cache.get(documentId);
|
||||||
|
if (item) {
|
||||||
|
item.isDirty = true;
|
||||||
|
item.lastModified = new Date();
|
||||||
|
// 清理语法树缓存,下次访问时重新构建
|
||||||
|
item.syntaxTreeCache = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置自动保存定时器
|
||||||
|
const setAutoSaveTimer = (documentId: number, timer: number) => {
|
||||||
|
const item = cache.get(documentId);
|
||||||
|
if (item) {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (item.autoSaveTimer) {
|
||||||
|
clearTimeout(item.autoSaveTimer);
|
||||||
|
}
|
||||||
|
item.autoSaveTimer = timer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除自动保存定时器
|
||||||
|
const clearAutoSaveTimer = (documentId: number) => {
|
||||||
|
const item = cache.get(documentId);
|
||||||
|
if (item && item.autoSaveTimer) {
|
||||||
|
clearTimeout(item.autoSaveTimer);
|
||||||
|
item.autoSaveTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取脏状态的编辑器
|
||||||
|
const getDirtyEditors = (): EditorCacheItem[] => {
|
||||||
|
return cache.getAll().filter(item => item.isDirty);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理过期的语法树缓存
|
||||||
|
const cleanupExpiredSyntaxTrees = () => {
|
||||||
|
const now = new Date();
|
||||||
|
cache.getAll().forEach(item => {
|
||||||
|
if (item.syntaxTreeCache &&
|
||||||
|
(now.getTime() - item.syntaxTreeCache.lastParsed.getTime()) > 30000) {
|
||||||
|
item.syntaxTreeCache = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 容器管理
|
||||||
|
setContainer,
|
||||||
|
getContainer,
|
||||||
|
|
||||||
|
// 基础缓存操作
|
||||||
|
addEditor,
|
||||||
|
getEditor,
|
||||||
|
hasEditor,
|
||||||
|
removeEditor,
|
||||||
|
getAllEditors,
|
||||||
|
clearAll,
|
||||||
|
size,
|
||||||
|
getStats,
|
||||||
|
|
||||||
|
ensureSyntaxTreeCached,
|
||||||
|
updateEditorContent,
|
||||||
|
markEditorDirty,
|
||||||
|
setAutoSaveTimer,
|
||||||
|
clearAutoSaveTimer,
|
||||||
|
getDirtyEditors,
|
||||||
|
cleanupExpiredSyntaxTrees
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -5,9 +5,9 @@ import {EditorState, Extension} from '@codemirror/state';
|
|||||||
import {useConfigStore} from './configStore';
|
import {useConfigStore} from './configStore';
|
||||||
import {useDocumentStore} from './documentStore';
|
import {useDocumentStore} from './documentStore';
|
||||||
import {useThemeStore} from './themeStore';
|
import {useThemeStore} from './themeStore';
|
||||||
|
import {useEditorCacheStore} from './editorCacheStore';
|
||||||
import {ExtensionID, SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
import {ExtensionID, SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||||
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
|
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
|
||||||
import {ensureSyntaxTree} from "@codemirror/language";
|
|
||||||
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
|
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
|
||||||
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
|
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
|
||||||
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
|
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
|
||||||
@@ -18,8 +18,7 @@ import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/edito
|
|||||||
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
|
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
|
||||||
import {useExtensionStore} from './extensionStore';
|
import {useExtensionStore} from './extensionStore';
|
||||||
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
||||||
|
import {AsyncOperationManager} from '@/common/async-operation';
|
||||||
const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例
|
|
||||||
|
|
||||||
export interface DocumentStats {
|
export interface DocumentStats {
|
||||||
lines: number;
|
lines: number;
|
||||||
@@ -27,38 +26,15 @@ export interface DocumentStats {
|
|||||||
selectedCharacters: number;
|
selectedCharacters: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditorInstance {
|
|
||||||
view: EditorView;
|
|
||||||
documentId: number;
|
|
||||||
content: string;
|
|
||||||
isDirty: boolean;
|
|
||||||
lastModified: Date;
|
|
||||||
autoSaveTimer: number | null;
|
|
||||||
syntaxTreeCache: {
|
|
||||||
lastDocLength: number;
|
|
||||||
lastContentHash: string;
|
|
||||||
lastParsed: Date;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useEditorStore = defineStore('editor', () => {
|
export const useEditorStore = defineStore('editor', () => {
|
||||||
// === 依赖store ===
|
// === 依赖store ===
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const extensionStore = useExtensionStore();
|
const extensionStore = useExtensionStore();
|
||||||
|
const editorCacheStore = useEditorCacheStore();
|
||||||
|
|
||||||
// === 核心状态 ===
|
// === 核心状态 ===
|
||||||
const editorCache = ref<{
|
|
||||||
lru: number[];
|
|
||||||
instances: Record<number, EditorInstance>;
|
|
||||||
containerElement: HTMLElement | null;
|
|
||||||
}>({
|
|
||||||
lru: [],
|
|
||||||
instances: {},
|
|
||||||
containerElement: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentEditor = ref<EditorView | null>(null);
|
const currentEditor = ref<EditorView | null>(null);
|
||||||
const documentStats = ref<DocumentStats>({
|
const documentStats = ref<DocumentStats>({
|
||||||
lines: 0,
|
lines: 0,
|
||||||
@@ -69,94 +45,29 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 编辑器加载状态
|
// 编辑器加载状态
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// 异步操作竞态条件控制
|
// 异步操作管理器
|
||||||
const operationSequence = ref(0);
|
const operationManager = new AsyncOperationManager({
|
||||||
const pendingOperations = ref(new Map<number, AbortController>());
|
debug: false,
|
||||||
const currentLoadingDocumentId = ref<number | null>(null);
|
autoCleanup: true
|
||||||
|
});
|
||||||
|
|
||||||
// 自动保存设置 - 从配置动态获取
|
// 自动保存设置
|
||||||
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
|
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];
|
|
||||||
if (!instance) return;
|
|
||||||
|
|
||||||
const docLength = view.state.doc.length;
|
|
||||||
const content = view.state.doc.toString();
|
|
||||||
const contentHash = generateContentHash(content);
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
// 检查是否需要重新构建语法树
|
|
||||||
const cache = instance.syntaxTreeCache;
|
|
||||||
const shouldRebuild = !cache ||
|
|
||||||
cache.lastDocLength !== docLength ||
|
|
||||||
cache.lastContentHash !== contentHash ||
|
|
||||||
(now.getTime() - cache.lastParsed.getTime()) > 30000; // 30秒过期
|
|
||||||
|
|
||||||
if (shouldRebuild) {
|
|
||||||
try {
|
|
||||||
ensureSyntaxTree(view.state, docLength, 5000);
|
|
||||||
|
|
||||||
// 更新缓存
|
|
||||||
instance.syntaxTreeCache = {
|
|
||||||
lastDocLength: docLength,
|
|
||||||
lastContentHash: contentHash,
|
|
||||||
lastParsed: now
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to ensure syntax tree:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建编辑器实例
|
// 创建编辑器实例
|
||||||
const createEditorInstance = async (
|
const createEditorInstance = async (
|
||||||
content: string,
|
content: string,
|
||||||
operationId: number,
|
signal: AbortSignal,
|
||||||
documentId: number
|
documentId: number
|
||||||
): Promise<EditorView> => {
|
): Promise<EditorView> => {
|
||||||
if (!editorCache.value.containerElement) {
|
if (!editorCacheStore.getContainer()) {
|
||||||
throw new Error('Editor container not set');
|
throw new Error('Editor container not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
// 检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,24 +106,24 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
enableAutoDetection: true
|
enableAutoDetection: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// 再次检查操作有效性
|
// 再次检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快捷键扩展
|
// 快捷键扩展
|
||||||
const keymapExtension = await createDynamicKeymapExtension();
|
const keymapExtension = await createDynamicKeymapExtension();
|
||||||
|
|
||||||
// 检查操作有效性
|
// 检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态扩展,传递文档ID以便扩展管理器可以预初始化
|
// 动态扩展,传递文档ID以便扩展管理器可以预初始化
|
||||||
const dynamicExtensions = await createDynamicExtensions(documentId);
|
const dynamicExtensions = await createDynamicExtensions(documentId);
|
||||||
|
|
||||||
// 最终检查操作有效性
|
// 最终检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,91 +161,43 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加编辑器到缓存
|
|
||||||
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] = {
|
|
||||||
view,
|
|
||||||
documentId,
|
|
||||||
content,
|
|
||||||
isDirty: false,
|
|
||||||
lastModified: new Date(),
|
|
||||||
autoSaveTimer: null,
|
|
||||||
syntaxTreeCache: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加到LRU列表
|
|
||||||
editorCache.value.lru.push(documentId);
|
|
||||||
|
|
||||||
// 初始化语法树缓存
|
|
||||||
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 (
|
const getOrCreateEditor = async (
|
||||||
documentId: number,
|
documentId: number,
|
||||||
content: string,
|
content: string,
|
||||||
operationId: number
|
signal: AbortSignal
|
||||||
): Promise<EditorView> => {
|
): Promise<EditorView> => {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
const cached = editorCache.value.instances[documentId];
|
const cached = editorCacheStore.getEditor(documentId);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
updateLRU(documentId);
|
|
||||||
return cached.view;
|
return cached.view;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
// 检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新的编辑器实例
|
// 创建新的编辑器实例
|
||||||
const view = await createEditorInstance(content, operationId, documentId);
|
const view = await createEditorInstance(content, signal, documentId);
|
||||||
|
|
||||||
// 最终检查操作有效性
|
// 最终检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
// 如果操作已取消,清理创建的实例
|
// 如果操作已取消,清理创建的实例
|
||||||
view.destroy();
|
view.destroy();
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
addEditorToCache(documentId, view, content);
|
// 添加到缓存
|
||||||
|
editorCacheStore.addEditor(documentId, view, content);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 显示编辑器
|
// 显示编辑器
|
||||||
const showEditor = (documentId: number) => {
|
const showEditor = (documentId: number) => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCacheStore.getEditor(documentId);
|
||||||
if (!instance || !editorCache.value.containerElement) return;
|
if (!instance || !editorCacheStore.getContainer()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 移除当前编辑器DOM
|
// 移除当前编辑器DOM
|
||||||
@@ -343,18 +206,19 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保容器为空
|
// 确保容器为空
|
||||||
editorCache.value.containerElement.innerHTML = '';
|
const container = editorCacheStore.getContainer();
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
// 将目标编辑器DOM添加到容器
|
// 将目标编辑器DOM添加到容器
|
||||||
editorCache.value.containerElement.appendChild(instance.view.dom);
|
container.appendChild(instance.view.dom);
|
||||||
|
}
|
||||||
|
|
||||||
currentEditor.value = instance.view;
|
currentEditor.value = instance.view;
|
||||||
|
|
||||||
// 设置扩展管理器视图
|
// 设置扩展管理器视图
|
||||||
setExtensionManagerView(instance.view, documentId);
|
setExtensionManagerView(instance.view, documentId);
|
||||||
|
|
||||||
// 更新LRU
|
|
||||||
updateLRU(documentId);
|
|
||||||
|
|
||||||
// 重新测量和聚焦编辑器
|
// 重新测量和聚焦编辑器
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 将光标定位到文档末尾并滚动到该位置
|
// 将光标定位到文档末尾并滚动到该位置
|
||||||
@@ -364,11 +228,11 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
scrollIntoView: true
|
scrollIntoView: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// 滚动到文档底部(将光标位置滚动到可见区域)
|
// 滚动到文档底部
|
||||||
instance.view.focus();
|
instance.view.focus();
|
||||||
|
|
||||||
// 使用缓存的语法树确保方法
|
// 使用缓存的语法树确保方法
|
||||||
ensureSyntaxTreeCached(instance.view, documentId);
|
editorCacheStore.ensureSyntaxTreeCached(instance.view, documentId);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error showing editor:', error);
|
console.error('Error showing editor:', error);
|
||||||
@@ -377,7 +241,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 保存编辑器内容
|
// 保存编辑器内容
|
||||||
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCacheStore.getEditor(documentId);
|
||||||
if (!instance || !instance.isDirty) return true;
|
if (!instance || !instance.isDirty) return true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -388,9 +252,8 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 检查在保存期间内容是否又被修改了
|
// 检查在保存期间内容是否又被修改了
|
||||||
if (instance.lastModified === lastModified) {
|
if (instance.lastModified === lastModified) {
|
||||||
instance.content = content;
|
editorCacheStore.updateEditorContent(documentId, content);
|
||||||
instance.isDirty = false;
|
// isDirty 已在 updateEditorContent 中设置为 false
|
||||||
instance.lastModified = new Date();
|
|
||||||
}
|
}
|
||||||
// 如果内容在保存期间被修改了,保持 isDirty 状态
|
// 如果内容在保存期间被修改了,保持 isDirty 状态
|
||||||
|
|
||||||
@@ -403,31 +266,23 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 内容变化处理
|
// 内容变化处理
|
||||||
const onContentChange = (documentId: number) => {
|
const onContentChange = (documentId: number) => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCacheStore.getEditor(documentId);
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
|
|
||||||
instance.isDirty = true;
|
editorCacheStore.markEditorDirty(documentId);
|
||||||
instance.lastModified = new Date();
|
|
||||||
|
|
||||||
// 清理语法树缓存,下次访问时重新构建
|
// 清除之前的定时器并设置新的自动保存定时器
|
||||||
instance.syntaxTreeCache = null;
|
const timer = setTimeout(() => {
|
||||||
|
|
||||||
// 清除之前的定时器
|
|
||||||
if (instance.autoSaveTimer) {
|
|
||||||
clearTimeout(instance.autoSaveTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的自动保存定时器
|
|
||||||
instance.autoSaveTimer = window.setTimeout(() => {
|
|
||||||
saveEditorContent(documentId);
|
saveEditorContent(documentId);
|
||||||
}, getAutoSaveDelay());
|
}, getAutoSaveDelay());
|
||||||
|
editorCacheStore.setAutoSaveTimer(documentId, timer as unknown as number);
|
||||||
};
|
};
|
||||||
|
|
||||||
// === 公共API ===
|
// === 公共API ===
|
||||||
|
|
||||||
// 设置编辑器容器
|
// 设置编辑器容器
|
||||||
const setEditorContainer = (container: HTMLElement | null) => {
|
const setEditorContainer = (container: HTMLElement | null) => {
|
||||||
editorCache.value.containerElement = container;
|
editorCacheStore.setContainer(container);
|
||||||
|
|
||||||
// 如果设置容器时已有当前文档,立即加载编辑器
|
// 如果设置容器时已有当前文档,立即加载编辑器
|
||||||
if (container && documentStore.currentDocument) {
|
if (container && documentStore.currentDocument) {
|
||||||
@@ -439,9 +294,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const loadEditor = async (documentId: number, content: string) => {
|
const loadEditor = async (documentId: number, content: string) => {
|
||||||
// 设置加载状态
|
// 设置加载状态
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
// 生成新的操作ID
|
|
||||||
const operationId = getNextOperationId();
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 验证参数
|
// 验证参数
|
||||||
@@ -449,34 +301,33 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
throw new Error('Invalid parameters for loadEditor');
|
throw new Error('Invalid parameters for loadEditor');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消之前的操作并设置当前操作
|
// 使用异步操作管理器执行加载操作
|
||||||
cancelPreviousOperations();
|
const result = await operationManager.executeOperation(
|
||||||
currentLoadingDocumentId.value = documentId;
|
documentId,
|
||||||
pendingOperations.value.set(operationId, abortController);
|
async (signal) => {
|
||||||
|
|
||||||
// 保存当前编辑器内容
|
// 保存当前编辑器内容
|
||||||
if (currentEditor.value) {
|
if (currentEditor.value) {
|
||||||
const currentDocId = documentStore.currentDocumentId;
|
const currentDocId = documentStore.currentDocumentId;
|
||||||
if (currentDocId && currentDocId !== documentId) {
|
if (currentDocId && currentDocId !== documentId) {
|
||||||
await saveEditorContent(currentDocId);
|
await saveEditorContent(currentDocId);
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
// 检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
return;
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取或创建编辑器
|
// 获取或创建编辑器
|
||||||
const view = await getOrCreateEditor(documentId, content, operationId);
|
const view = await getOrCreateEditor(documentId, content, signal);
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
// 检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
return;
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新内容(如果需要)
|
// 更新内容
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCacheStore.getEditor(documentId);
|
||||||
if (instance && instance.content !== content) {
|
if (instance && instance.content !== content) {
|
||||||
// 确保编辑器视图有效
|
// 确保编辑器视图有效
|
||||||
if (view && view.state && view.dispatch) {
|
if (view && view.state && view.dispatch) {
|
||||||
@@ -487,34 +338,32 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
insert: content
|
insert: content
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
instance.content = content;
|
editorCacheStore.updateEditorContent(documentId, content);
|
||||||
instance.isDirty = false;
|
|
||||||
// 清理语法树缓存,因为内容已更新
|
|
||||||
instance.syntaxTreeCache = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终检查操作有效性
|
// 最终检查操作是否被取消
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (signal.aborted) {
|
||||||
return;
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示编辑器
|
// 显示编辑器
|
||||||
showEditor(documentId);
|
showEditor(documentId);
|
||||||
|
|
||||||
} catch (error) {
|
return view;
|
||||||
if (error instanceof Error && error.message === 'Operation cancelled') {
|
},
|
||||||
console.log(`Editor loading cancelled for document ${documentId}`);
|
'loadEditor'
|
||||||
} else {
|
);
|
||||||
console.error('Failed to load editor:', error);
|
|
||||||
|
if (!result.success) {
|
||||||
|
if (result.error?.message !== 'Operation cancelled') {
|
||||||
|
console.error('Failed to load editor:', result.error);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
// 清理操作记录
|
|
||||||
pendingOperations.value.delete(operationId);
|
|
||||||
if (currentLoadingDocumentId.value === documentId) {
|
|
||||||
currentLoadingDocumentId.value = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load editor:', error);
|
||||||
|
} finally {
|
||||||
// 延迟一段时间后再取消加载状态
|
// 延迟一段时间后再取消加载状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@@ -524,49 +373,20 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 移除编辑器
|
// 移除编辑器
|
||||||
const removeEditor = (documentId: number) => {
|
const removeEditor = (documentId: number) => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
// 取消该文档的所有操作
|
||||||
if (instance) {
|
operationManager.cancelResourceOperations(documentId);
|
||||||
try {
|
|
||||||
// 如果正在加载这个文档,取消操作
|
|
||||||
if (currentLoadingDocumentId.value === documentId) {
|
|
||||||
cancelPreviousOperations();
|
|
||||||
currentLoadingDocumentId.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除自动保存定时器
|
|
||||||
if (instance.autoSaveTimer) {
|
|
||||||
clearTimeout(instance.autoSaveTimer);
|
|
||||||
instance.autoSaveTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从扩展管理器中移除视图
|
// 从扩展管理器中移除视图
|
||||||
removeExtensionManagerView(documentId);
|
removeExtensionManagerView(documentId);
|
||||||
|
|
||||||
// 移除DOM元素
|
// 清除当前编辑器引用
|
||||||
if (instance.view && instance.view.dom && instance.view.dom.parentElement) {
|
const instance = editorCacheStore.getEditor(documentId);
|
||||||
instance.view.dom.remove();
|
if (instance && currentEditor.value === instance.view) {
|
||||||
}
|
|
||||||
|
|
||||||
// 销毁编辑器
|
|
||||||
if (instance.view && instance.view.destroy) {
|
|
||||||
instance.view.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理引用
|
|
||||||
if (currentEditor.value === instance.view) {
|
|
||||||
currentEditor.value = null;
|
currentEditor.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete editorCache.value.instances[documentId];
|
// 从缓存中移除编辑器
|
||||||
|
editorCacheStore.removeEditor(documentId);
|
||||||
const lruIndex = editorCache.value.lru.indexOf(documentId);
|
|
||||||
if (lruIndex > -1) {
|
|
||||||
editorCache.value.lru.splice(lruIndex, 1);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error removing editor:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新文档统计
|
// 更新文档统计
|
||||||
@@ -576,7 +396,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用字体设置
|
// 应用字体设置
|
||||||
const applyFontSettings = () => {
|
const applyFontSettings = () => {
|
||||||
Object.values(editorCache.value.instances).forEach(instance => {
|
editorCacheStore.getAllEditors().forEach(instance => {
|
||||||
updateFontConfig(instance.view, {
|
updateFontConfig(instance.view, {
|
||||||
fontFamily: configStore.config.editing.fontFamily,
|
fontFamily: configStore.config.editing.fontFamily,
|
||||||
fontSize: configStore.config.editing.fontSize,
|
fontSize: configStore.config.editing.fontSize,
|
||||||
@@ -588,7 +408,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用主题设置
|
// 应用主题设置
|
||||||
const applyThemeSettings = () => {
|
const applyThemeSettings = () => {
|
||||||
Object.values(editorCache.value.instances).forEach(instance => {
|
editorCacheStore.getAllEditors().forEach(instance => {
|
||||||
updateEditorTheme(instance.view,
|
updateEditorTheme(instance.view,
|
||||||
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
||||||
);
|
);
|
||||||
@@ -597,7 +417,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用Tab设置
|
// 应用Tab设置
|
||||||
const applyTabSettings = () => {
|
const applyTabSettings = () => {
|
||||||
Object.values(editorCache.value.instances).forEach(instance => {
|
editorCacheStore.getAllEditors().forEach(instance => {
|
||||||
updateTabConfig(
|
updateTabConfig(
|
||||||
instance.view,
|
instance.view,
|
||||||
configStore.config.editing.tabSize,
|
configStore.config.editing.tabSize,
|
||||||
@@ -611,37 +431,21 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const applyKeymapSettings = async () => {
|
const applyKeymapSettings = async () => {
|
||||||
// 确保所有编辑器实例的快捷键都更新
|
// 确保所有编辑器实例的快捷键都更新
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.values(editorCache.value.instances).map(instance =>
|
editorCacheStore.getAllEditors().map(instance =>
|
||||||
updateKeymapExtension(instance.view)
|
updateKeymapExtension(instance.view)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清空所有编辑器
|
// 清理所有编辑器
|
||||||
const clearAllEditors = () => {
|
const clearAllEditors = () => {
|
||||||
// 取消所有挂起的操作
|
// 取消所有挂起的操作
|
||||||
cancelPreviousOperations();
|
operationManager.cancelAllOperations();
|
||||||
currentLoadingDocumentId.value = null;
|
|
||||||
|
|
||||||
Object.values(editorCache.value.instances).forEach(instance => {
|
// 清理所有编辑器
|
||||||
// 清除自动保存定时器
|
editorCacheStore.clearAll();
|
||||||
if (instance.autoSaveTimer) {
|
|
||||||
clearTimeout(instance.autoSaveTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从扩展管理器移除
|
// 清除当前编辑器引用
|
||||||
removeExtensionManagerView(instance.documentId);
|
|
||||||
|
|
||||||
// 移除DOM元素
|
|
||||||
if (instance.view.dom.parentElement) {
|
|
||||||
instance.view.dom.remove();
|
|
||||||
}
|
|
||||||
// 销毁编辑器
|
|
||||||
instance.view.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
editorCache.value.instances = {};
|
|
||||||
editorCache.value.lru = [];
|
|
||||||
currentEditor.value = null;
|
currentEditor.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -672,7 +476,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 监听文档切换
|
// 监听文档切换
|
||||||
watch(() => documentStore.currentDocument, (newDoc) => {
|
watch(() => documentStore.currentDocument, (newDoc) => {
|
||||||
if (newDoc && editorCache.value.containerElement) {
|
if (newDoc && editorCacheStore.getContainer()) {
|
||||||
// 使用 nextTick 确保DOM更新完成后再加载编辑器
|
// 使用 nextTick 确保DOM更新完成后再加载编辑器
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
loadEditor(newDoc.id, newDoc.content);
|
loadEditor(newDoc.id, newDoc.content);
|
||||||
|
|||||||
Reference in New Issue
Block a user