initial public release.
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
import { KGCommand } from './KGCommand';
|
||||
import { DEBUG_MODE } from '../../constants';
|
||||
|
||||
/**
|
||||
* Manages the command history for undo/redo functionality
|
||||
* Implements a singleton pattern to ensure single source of truth
|
||||
*/
|
||||
export class KGCommandHistory {
|
||||
private static _instance: KGCommandHistory | null = null;
|
||||
|
||||
private undoStack: KGCommand[] = [];
|
||||
private redoStack: KGCommand[] = [];
|
||||
private maxHistorySize: number = 30;
|
||||
|
||||
// Callbacks for UI updates
|
||||
private onHistoryChanged?: () => void;
|
||||
|
||||
private constructor() {
|
||||
// Private constructor for singleton pattern
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance
|
||||
*/
|
||||
public static instance(): KGCommandHistory {
|
||||
if (!this._instance) {
|
||||
this._instance = new KGCommandHistory();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command and add it to the history
|
||||
* @param command The command to execute
|
||||
*/
|
||||
public executeCommand(command: KGCommand): void {
|
||||
try {
|
||||
// Execute the command
|
||||
command.execute();
|
||||
|
||||
// Check if we can merge with the last command
|
||||
if (this.undoStack.length > 0) {
|
||||
const lastCommand = this.undoStack[this.undoStack.length - 1];
|
||||
if (lastCommand.canMergeWith(command)) {
|
||||
const mergedCommand = lastCommand.mergeWith(command);
|
||||
if (mergedCommand) {
|
||||
// Replace the last command with the merged one
|
||||
this.undoStack[this.undoStack.length - 1] = mergedCommand;
|
||||
|
||||
if (DEBUG_MODE.CORE) {
|
||||
console.log(`Merged command: ${mergedCommand.getDescription()}`);
|
||||
}
|
||||
|
||||
this.notifyHistoryChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to undo stack
|
||||
this.undoStack.push(command);
|
||||
|
||||
// Clear redo stack since we're executing a new command
|
||||
this.redoStack = [];
|
||||
|
||||
// Maintain history size limit
|
||||
if (this.undoStack.length > this.maxHistorySize) {
|
||||
this.undoStack.shift();
|
||||
}
|
||||
|
||||
if (DEBUG_MODE.CORE) {
|
||||
console.log(`Executed command: ${command.getDescription()}`);
|
||||
console.log(`Undo stack size: ${this.undoStack.length}`);
|
||||
}
|
||||
|
||||
this.notifyHistoryChanged();
|
||||
} catch (error) {
|
||||
console.error('Failed to execute command:', error);
|
||||
// Don't add failed commands to history
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last command
|
||||
* @returns true if undo was successful, false otherwise
|
||||
*/
|
||||
public undo(): boolean {
|
||||
if (this.undoStack.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const command = this.undoStack.pop()!;
|
||||
|
||||
try {
|
||||
command.undo();
|
||||
this.redoStack.push(command);
|
||||
|
||||
if (DEBUG_MODE.CORE) {
|
||||
console.log(`Undid command: ${command.getDescription()}`);
|
||||
console.log(`Undo stack size: ${this.undoStack.length}, Redo stack size: ${this.redoStack.length}`);
|
||||
}
|
||||
|
||||
this.notifyHistoryChanged();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to undo command:', error);
|
||||
// Put the command back on the stack if undo fails
|
||||
this.undoStack.push(command);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last undone command
|
||||
* @returns true if redo was successful, false otherwise
|
||||
*/
|
||||
public redo(): boolean {
|
||||
if (this.redoStack.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const command = this.redoStack.pop()!;
|
||||
|
||||
try {
|
||||
command.execute();
|
||||
this.undoStack.push(command);
|
||||
|
||||
if (DEBUG_MODE.CORE) {
|
||||
console.log(`Redid command: ${command.getDescription()}`);
|
||||
console.log(`Undo stack size: ${this.undoStack.length}, Redo stack size: ${this.redoStack.length}`);
|
||||
}
|
||||
|
||||
this.notifyHistoryChanged();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to redo command:', error);
|
||||
// Put the command back on the redo stack if redo fails
|
||||
this.redoStack.push(command);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if undo is available
|
||||
*/
|
||||
public canUndo(): boolean {
|
||||
return this.undoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if redo is available
|
||||
*/
|
||||
public canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description of the next command that would be undone
|
||||
*/
|
||||
public getUndoDescription(): string | null {
|
||||
if (this.undoStack.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return this.undoStack[this.undoStack.length - 1].getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description of the next command that would be redone
|
||||
*/
|
||||
public getRedoDescription(): string | null {
|
||||
if (this.redoStack.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return this.redoStack[this.redoStack.length - 1].getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all command history
|
||||
*/
|
||||
public clear(): void {
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
|
||||
if (DEBUG_MODE.CORE) {
|
||||
console.log('Cleared command history');
|
||||
}
|
||||
|
||||
this.notifyHistoryChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of commands to keep in history
|
||||
* @param size Maximum history size (default: 30)
|
||||
*/
|
||||
public setMaxHistorySize(size: number): void {
|
||||
this.maxHistorySize = Math.max(1, size);
|
||||
|
||||
// Trim history if needed
|
||||
while (this.undoStack.length > this.maxHistorySize) {
|
||||
this.undoStack.shift();
|
||||
}
|
||||
|
||||
this.notifyHistoryChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current history size limit
|
||||
*/
|
||||
public getMaxHistorySize(): number {
|
||||
return this.maxHistorySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback for when history changes (for UI updates)
|
||||
*/
|
||||
public setOnHistoryChanged(callback: () => void): void {
|
||||
this.onHistoryChanged = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current history statistics for debugging
|
||||
*/
|
||||
public getHistoryStats(): { undoCount: number; redoCount: number; maxSize: number } {
|
||||
return {
|
||||
undoCount: this.undoStack.length,
|
||||
redoCount: this.redoStack.length,
|
||||
maxSize: this.maxHistorySize
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify listeners that history has changed
|
||||
*/
|
||||
private notifyHistoryChanged(): void {
|
||||
if (this.onHistoryChanged) {
|
||||
this.onHistoryChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user