Files
PathEditor/src/core/undo-redo.ts
T
Serendipity 3aed03f599 fix: 修复 5 个 bug + 备份警告丢失
- BUG 1: undo/redo 后持久化 disabled 状态到 disabled.json
- BUG 2: expand_env_vars 增加缓冲区不足检测(result > required)
- BUG 3: E2E mock load_disabled_state 返回格式从对象改为数组
- BUG 4: 双 hive 保存失败时同时显示两个错误原因
- BUG 5: 导入 both 合并为单条 undo 记录(新增 IMPORT_BOTH 操作类型)
- 备份失败后保存成功时显示"保存成功(备份失败)"而非覆盖警告

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 00:38:32 +08:00

167 lines
5.1 KiB
TypeScript

/**
* 撤销/重做管理器 — 纯逻辑,操作不可变 PathEntry[]
*/
import type { PathEntry } from './path-entry';
export const OperationType = {
ADD: 0, DELETE: 1, EDIT: 2, MOVE_UP: 3, MOVE_DOWN: 4, CLEAN: 5, CLEAR: 6, IMPORT: 7, TOGGLE: 8, IMPORT_BOTH: 9,
} as const;
export type OperationType = (typeof OperationType)[keyof typeof OperationType];
export const TargetType = { SYSTEM: 0, USER: 1 } as const;
export type TargetType = (typeof TargetType)[keyof typeof TargetType];
export interface OpRecord {
type: OperationType;
target: TargetType;
index: number;
count: number;
oldPaths: PathEntry[];
newPaths: PathEntry[];
/** DELETE 操作专用:被删除的各路径的原始 index(升序) */
indices?: number[];
/** IMPORT_BOTH 专用:用户 hive 的旧路径 */
oldPathsOther?: PathEntry[];
/** IMPORT_BOTH 专用:用户 hive 的新路径 */
newPathsOther?: PathEntry[];
}
const DEFAULT_MAX_SIZE = 50;
export class UndoRedoManager {
private records: OpRecord[] = [];
private current: number = -1;
private readonly maxSize: number;
constructor(maxSize: number = DEFAULT_MAX_SIZE) {
this.maxSize = maxSize;
}
push(record: OpRecord): void {
this.records = this.records.slice(0, this.current + 1);
if (this.records.length >= this.maxSize) {
this.records.shift();
}
this.records.push(record);
this.current = this.records.length - 1;
}
undo(sysPaths: readonly PathEntry[], userPaths: readonly PathEntry[]): [PathEntry[], PathEntry[]] | null {
if (this.current < 0) return null;
const rec = this.records[this.current];
this.current--;
const sys = [...sysPaths];
const user = [...userPaths];
const target = rec.target === TargetType.SYSTEM ? sys : user;
switch (rec.type) {
case OperationType.ADD:
target.splice(target.length - rec.count, rec.count);
break;
case OperationType.DELETE:
if (rec.indices) {
for (let i = 0; i < rec.indices.length; i++) {
target.splice(rec.indices[i], 0, rec.oldPaths[i]);
}
} else {
for (let i = 0; i < rec.count; i++) {
target.splice(rec.index + i, 0, rec.oldPaths[i]);
}
}
break;
case OperationType.EDIT:
target[rec.index] = rec.oldPaths[0];
break;
case OperationType.MOVE_UP:
[target[rec.index - 1], target[rec.index]] = [target[rec.index], target[rec.index - 1]];
break;
case OperationType.MOVE_DOWN:
[target[rec.index], target[rec.index + 1]] = [target[rec.index + 1], target[rec.index]];
break;
case OperationType.CLEAN:
case OperationType.IMPORT:
target.length = 0;
target.push(...rec.oldPaths);
break;
case OperationType.CLEAR:
target.push(...rec.oldPaths);
break;
case OperationType.TOGGLE:
target[rec.index] = rec.oldPaths[0];
break;
case OperationType.IMPORT_BOTH:
sys.length = 0;
sys.push(...rec.oldPaths);
user.length = 0;
user.push(...(rec.oldPathsOther || []));
return [sys, user];
}
return [sys, user];
}
redo(sysPaths: readonly PathEntry[], userPaths: readonly PathEntry[]): [PathEntry[], PathEntry[]] | null {
if (this.current >= this.records.length - 1) return null;
this.current++;
const rec = this.records[this.current];
const sys = [...sysPaths];
const user = [...userPaths];
const target = rec.target === TargetType.SYSTEM ? sys : user;
switch (rec.type) {
case OperationType.ADD:
target.push(...rec.newPaths);
break;
case OperationType.DELETE:
if (rec.indices) {
for (let i = rec.indices.length - 1; i >= 0; i--) {
target.splice(rec.indices[i], 1);
}
} else {
for (let i = rec.count - 1; i >= 0; i--) {
target.splice(rec.index + i, 1);
}
}
break;
case OperationType.EDIT:
target[rec.index] = rec.newPaths[0];
break;
case OperationType.MOVE_UP:
[target[rec.index - 1], target[rec.index]] = [target[rec.index], target[rec.index - 1]];
break;
case OperationType.MOVE_DOWN:
[target[rec.index], target[rec.index + 1]] = [target[rec.index + 1], target[rec.index]];
break;
case OperationType.CLEAN:
case OperationType.IMPORT:
target.length = 0;
target.push(...rec.newPaths);
break;
case OperationType.CLEAR:
target.length = 0;
break;
case OperationType.TOGGLE:
target[rec.index] = rec.newPaths[0];
break;
case OperationType.IMPORT_BOTH:
sys.length = 0;
sys.push(...rec.newPaths);
user.length = 0;
user.push(...(rec.newPathsOther || []));
return [sys, user];
}
return [sys, user];
}
canUndo(): boolean { return this.current >= 0; }
canRedo(): boolean { return this.current < this.records.length - 1; }
clear(): void { this.records = []; this.current = -1; }
get historyLength(): number { return this.records.length; }
}