UNPKG

@phroun/paged-buffer

Version:

High-performance buffer system for editing massive files with intelligent memory management and undo/redo capabilities

661 lines 27.1 kB
"use strict"; /** * @fileoverview Enhanced Buffer Undo/Redo System with Line and Marks Integration * @author Jeffrey R. Day * @version 2.2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.OperationTransaction = exports.OperationGroup = exports.BufferUndoSystem = void 0; const common_1 = require("./types/common"); const buffer_operation_1 = require("./buffer-operation"); const logger_1 = require("./utils/logger"); /** * Groups related operations together for undo/redo */ class OperationGroup { constructor(id, name = null) { this.operations = []; this.isFromTransaction = false; // Enhanced: Store marks state before and after group execution this.marksSnapshot = null; // Will be set when group is recorded this.linesSnapshot = null; // Line count snapshot for verification this.id = id; this.name = name; this.timestamp = Date.now(); } /** * Calculate total memory usage of this group */ getMemoryUsage() { let total = 0; for (const op of this.operations) { if (op.data) total += op.data.length; if (op.originalData) total += op.originalData.length; } // Add marks snapshot memory (rough estimate) if (this.marksSnapshot) { total += this.marksSnapshot.length * 64; // Rough estimate per mark } return total; } /** * Set marks snapshot for this group */ setMarksSnapshot(marks) { this.marksSnapshot = marks.map(mark => [...mark]); // Deep copy } /** * Set lines snapshot for this group */ setLinesSnapshot(lineCount) { this.linesSnapshot = lineCount; } } exports.OperationGroup = OperationGroup; /** * Transaction for grouping operations */ class OperationTransaction { constructor(name, options = {}) { this.operations = []; // Enhanced: Track marks state at transaction start this.initialMarksSnapshot = null; this.initialLinesSnapshot = null; this.name = name; this.startTime = Date.now(); this.options = options; } /** * Set initial state snapshots */ setInitialState(marks, lineCount) { this.initialMarksSnapshot = marks.map(mark => [...mark]); this.initialLinesSnapshot = lineCount; } /** * Get info about this transaction */ getInfo() { return { name: this.name, operationCount: this.operations.length, startTime: this.startTime, duration: Date.now() - this.startTime, options: this.options, hasMarksSnapshot: this.initialMarksSnapshot !== null, hasLinesSnapshot: this.initialLinesSnapshot !== null }; } } exports.OperationTransaction = OperationTransaction; /** * Enhanced Buffer Undo/Redo System with Line and Marks Integration */ class BufferUndoSystem { constructor(buffer, maxUndoLevels = 50) { // Undo/Redo stacks - contain only OperationGroup objects this.undoStack = []; this.redoStack = []; // Transaction support this.activeTransaction = null; // IMPROVED DEFAULTS: More conservative merge settings this.mergeTimeWindow = 5000; // Keep reasonable time window for rapid typing this.mergePositionWindow = 0; // DEFAULT TO ZERO - merge adjacent only // State tracking this.isUndoing = false; this.groupIdCounter = 0; // Clock function (can be mocked for testing) this.clockFunction = () => Date.now(); this.buffer = buffer; this.maxUndoLevels = maxUndoLevels; } /** * Configure the undo system */ configure(config) { if (config.maxUndoLevels !== undefined) { this.maxUndoLevels = config.maxUndoLevels; } if (config.mergeTimeWindow !== undefined) { this.mergeTimeWindow = config.mergeTimeWindow; } if (config.mergePositionWindow !== undefined) { this.mergePositionWindow = config.mergePositionWindow; } } /** * Set custom clock function (for testing) */ setClock(clockFn) { this.clockFunction = clockFn; } /** * Get current time from clock function */ getClock() { return this.clockFunction(); } /** * Generate unique group ID */ _generateGroupId() { return `group_${++this.groupIdCounter}_${this.getClock()}`; } /** * CRITICAL FIX: Capture current marks state for snapshot BEFORE any operation recording * This must be called by buffer operations BEFORE they execute */ captureCurrentMarksState() { if (!this.buffer.lineAndMarksManager) { return []; } try { return this.buffer.lineAndMarksManager.getAllMarks(); } catch (error) { logger_1.logger.warn('Failed to capture current marks state:', error.message); return []; } } /** * Record an insert operation with enhanced tracking */ recordInsert(position, data, timestamp = null, preOpMarksSnapshot = null) { const operation = new buffer_operation_1.BufferOperation(common_1.OperationType.INSERT, position, Buffer.from(data), null, timestamp || this.getClock()); operation.setPostExecutionPosition(position); this._recordOperation(operation, preOpMarksSnapshot); return operation; } /** * Record a delete operation with enhanced tracking */ recordDelete(position, deletedData, timestamp = null, preOpMarksSnapshot = null) { const operation = new buffer_operation_1.BufferOperation(common_1.OperationType.DELETE, position, Buffer.alloc(0), Buffer.from(deletedData), timestamp || this.getClock()); operation.setPostExecutionPosition(position); this._recordOperation(operation, preOpMarksSnapshot); return operation; } /** * Record an overwrite operation with enhanced tracking */ recordOverwrite(position, newData, originalData, timestamp = null, preOpMarksSnapshot = null) { const operation = new buffer_operation_1.BufferOperation(common_1.OperationType.OVERWRITE, position, Buffer.from(newData), Buffer.from(originalData), timestamp || this.getClock()); operation.setPostExecutionPosition(position); this._recordOperation(operation, preOpMarksSnapshot); return operation; } /** * Enhanced operation recording with marks and lines tracking - FIXED SNAPSHOT TIMING */ _recordOperation(operation, preOpMarksSnapshot = null) { // Don't record operations during undo/redo if (this.isUndoing) { return; } // Clear redo stack when new operations are performed this.redoStack = []; // Handle transactions if (this.activeTransaction) { this.activeTransaction.operations.push(operation); return; } // Try to merge with the top group on the stack first if (this.undoStack.length > 0) { const topGroup = this.undoStack[this.undoStack.length - 1]; // Don't merge across transaction boundaries if (!topGroup.isFromTransaction && topGroup.operations.length > 0) { const lastOp = topGroup.operations[topGroup.operations.length - 1]; // Check if operations can be merged if (lastOp.canMergeWith(operation, this.mergeTimeWindow, this.mergePositionWindow)) { // IMPROVED: Only do physical merges for truly contiguous same-type operations const distance = this._getOperationDistance(lastOp, operation); if (distance === 0 && this._areContiguousOperations(lastOp, operation) && lastOp.type === operation.type) { // PHYSICAL MERGE: Operations are truly contiguous and same type lastOp.mergeWith(operation); } else { // LOGICAL MERGE: Operations should undo together but remain separate topGroup.operations.push(operation); } // NOTE: Don't update snapshot for merged operations - keep original pre-state return; // Either way, we're done } } } // Cannot merge - create NEW group and push to stack // CRITICAL FIX: Use the provided pre-operation snapshot, or capture current state const newGroup = new OperationGroup(this._generateGroupId()); // Use provided snapshot or capture current state if none provided const snapshotToUse = preOpMarksSnapshot || this.captureCurrentMarksState(); newGroup.setMarksSnapshot(snapshotToUse); // Capture line count snapshot if (this.buffer.lineAndMarksManager) { try { const lineCount = this.buffer.lineAndMarksManager.getTotalLineCount(); newGroup.setLinesSnapshot(lineCount); } catch (error) { logger_1.logger.warn('Failed to capture line count snapshot:', error.message); } } // Now add the operation to the group newGroup.operations.push(operation); this.undoStack.push(newGroup); // Enforce maximum undo levels while (this.undoStack.length > this.maxUndoLevels) { this.undoStack.shift(); } } /** * Helper method to get distance between operations */ _getOperationDistance(op1, op2) { try { return op1.getLogicalDistance(op2); } catch (error) { // Fallback to simple distance return Math.abs(op1.preExecutionPosition - op2.preExecutionPosition); } } /** * More conservative contiguous operation detection */ _areContiguousOperations(op1, op2) { // Only insert operations of the same type can be physically merged if (op1.type !== 'insert' || op2.type !== 'insert') { return false; } // Check if they're truly adjacent with no gap and in correct order const distance = this._getOperationDistance(op1, op2); if (distance !== 0) { return false; } // Additional check: second operation should start where first ends const op1End = op1.preExecutionPosition + (op1.data ? op1.data.length : 0); return Math.abs(op2.preExecutionPosition - op1End) <= 1; } // =================== ENHANCED TRANSACTION SUPPORT =================== /** * Begin a new transaction with state tracking */ beginUndoTransaction(name, options = {}) { if (this.activeTransaction) { throw new Error('Cannot start transaction - another transaction is already active'); } this.activeTransaction = new OperationTransaction(name, options); this.activeTransaction.startTime = this.getClock(); // FIXED: Capture initial state using virtual addresses BEFORE any operations if (this.buffer.lineAndMarksManager) { try { const allMarks = this.buffer.lineAndMarksManager.getAllMarks(); const lineCount = this.buffer.lineAndMarksManager.getTotalLineCount(); this.activeTransaction.setInitialState(allMarks, lineCount); } catch (error) { logger_1.logger.warn('Failed to capture initial transaction state:', error.message); } } } /** * Commit the current transaction with enhanced state tracking */ commitUndoTransaction(finalName = null) { if (!this.activeTransaction) { return false; } if (this.activeTransaction.operations.length > 0) { // Create group from transaction operations const group = new OperationGroup(this._generateGroupId(), finalName || this.activeTransaction.name); group.operations = [...this.activeTransaction.operations]; group.isFromTransaction = true; // FIXED: Set snapshots using virtual addresses from transaction INITIAL state (pre-operations) if (this.activeTransaction.initialMarksSnapshot) { group.setMarksSnapshot(this.activeTransaction.initialMarksSnapshot); } if (this.activeTransaction.initialLinesSnapshot !== null) { group.setLinesSnapshot(this.activeTransaction.initialLinesSnapshot); } this.undoStack.push(group); // Enforce maximum undo levels while (this.undoStack.length > this.maxUndoLevels) { this.undoStack.shift(); } } this.activeTransaction = null; return true; } /** * Enhanced rollback with marks and lines restoration */ async rollbackUndoTransaction() { if (!this.activeTransaction) { return false; } // Undo all operations in reverse order using VPM this.isUndoing = true; try { for (let i = this.activeTransaction.operations.length - 1; i >= 0; i--) { const operation = this.activeTransaction.operations[i]; await this._undoOperationVPM(operation); } // FIXED: Restore marks state using virtual addresses if (this.activeTransaction.initialMarksSnapshot && this.buffer.lineAndMarksManager) { await this._restoreMarksState(this.activeTransaction.initialMarksSnapshot); } } finally { this.isUndoing = false; } this.activeTransaction = null; return true; } /** * Check if currently in a transaction */ inTransaction() { return this.activeTransaction !== null; } /** * Get current transaction info */ getCurrentTransaction() { return this.activeTransaction ? this.activeTransaction.getInfo() : null; } // =================== ENHANCED UNDO/REDO OPERATIONS =================== /** * Enhanced undo with marks and lines restoration */ async undo() { // Handle undo during active transaction as rollback if (this.activeTransaction) { return await this.rollbackUndoTransaction(); } if (this.undoStack.length === 0) { return false; } const group = this.undoStack.pop(); this.isUndoing = true; try { // Undo operations in reverse order using VPM for (let i = group.operations.length - 1; i >= 0; i--) { const operation = group.operations[i]; await this._undoOperationVPM(operation); } // FIXED: Restore marks state using virtual addresses if (group.marksSnapshot && this.buffer.lineAndMarksManager) { await this._restoreMarksState(group.marksSnapshot); } this.redoStack.push(group); return true; } catch (error) { // If undo fails, restore the group to undo stack this.undoStack.push(group); throw error; } finally { this.isUndoing = false; } } /** * Enhanced redo with marks and lines restoration */ async redo() { if (this.redoStack.length === 0) { return false; } const group = this.redoStack.pop(); this.isUndoing = true; try { // CRITICAL FIX: Capture current marks state BEFORE redo operations let currentMarksSnapshot = null; if (this.buffer.lineAndMarksManager) { try { currentMarksSnapshot = this.buffer.lineAndMarksManager.getAllMarks(); } catch (error) { logger_1.logger.warn('Failed to capture current marks state for redo:', error.message); } } // Redo operations in forward order using VPM for (const operation of group.operations) { await this._redoOperationVPM(operation); } // FIXED: Update the group's snapshot to current post-redo state for future undo if (currentMarksSnapshot) { group.setMarksSnapshot(currentMarksSnapshot); } this.undoStack.push(group); return true; } catch (error) { // If redo fails, restore the group to redo stack this.redoStack.push(group); throw error; } finally { this.isUndoing = false; } } /** * CORRECTED: Restore marks state from snapshot using virtual addresses */ async _restoreMarksState(marksSnapshot) { if (!this.buffer.lineAndMarksManager) { return; } logger_1.logger.debug(`[DEBUG] Restoring ${marksSnapshot.length} marks from snapshot`); try { // Clear all current marks const currentMarks = this.buffer.lineAndMarksManager.getAllMarks(); logger_1.logger.debug('[DEBUG] Current marks before clear:', currentMarks); this.buffer.lineAndMarksManager.clearAllMarks(); // Restore marks using virtual addresses from snapshot for (const mark of marksSnapshot) { logger_1.logger.debug(`[DEBUG] Restoring mark ${mark[0]} at address ${mark[1]}`); // Validate that the address is still within bounds const totalSize = this.buffer.getTotalSize(); if (mark[1] >= 0 && mark[1] <= totalSize) { this.buffer.lineAndMarksManager.setMark(mark[0], mark[1]); logger_1.logger.debug(`[DEBUG] Successfully restored mark ${mark[0]}`); } else { logger_1.logger.debug(`[DEBUG] Skipping mark ${mark[0]} - address ${mark[1]} out of bounds (buffer size: ${totalSize})`); } } const restoredMarks = this.buffer.lineAndMarksManager.getAllMarks(); logger_1.logger.debug('[DEBUG] Marks after restoration:', restoredMarks); } catch (error) { logger_1.logger.warn('Failed to restore marks state:', error.message); } } /** * Undo a single operation using Virtual Page Manager with enhanced tracking */ async _undoOperationVPM(operation) { const vpm = this.buffer.virtualPageManager; switch (operation.type) { case common_1.OperationType.INSERT: // Undo insert by deleting the inserted data await vpm.deleteRange(operation.preExecutionPosition, operation.preExecutionPosition + operation.data.length); // FORCE mark update for undo if (this.buffer.lineAndMarksManager) { this.buffer.lineAndMarksManager.updateMarksAfterModification(operation.preExecutionPosition, operation.data.length, 0); } this.buffer.totalSize -= operation.data.length; break; case common_1.OperationType.DELETE: // Undo delete by inserting the original data back await vpm.insertAt(operation.preExecutionPosition, operation.originalData); // FORCE mark update for undo if (this.buffer.lineAndMarksManager) { this.buffer.lineAndMarksManager.updateMarksAfterModification(operation.preExecutionPosition, 0, operation.originalData.length); } this.buffer.totalSize += operation.originalData.length; break; case common_1.OperationType.OVERWRITE: // Undo overwrite by deleting new data and inserting original data await vpm.deleteRange(operation.preExecutionPosition, operation.preExecutionPosition + operation.data.length); await vpm.insertAt(operation.preExecutionPosition, operation.originalData); // FORCE mark update for undo (net change) if (this.buffer.lineAndMarksManager) { this.buffer.lineAndMarksManager.updateMarksAfterModification(operation.preExecutionPosition, operation.data.length, operation.originalData.length); } const sizeChange = operation.originalData.length - operation.data.length; this.buffer.totalSize += sizeChange; break; default: throw new Error(`Unknown operation type: ${operation.type}`); } this.buffer.markAsModified(); } async _redoOperationVPM(operation) { const vpm = this.buffer.virtualPageManager; switch (operation.type) { case common_1.OperationType.INSERT: // Redo insert await vpm.insertAt(operation.preExecutionPosition, operation.data); // FORCE mark update for redo if (this.buffer.lineAndMarksManager) { this.buffer.lineAndMarksManager.updateMarksAfterModification(operation.preExecutionPosition, 0, operation.data.length); } this.buffer.totalSize += operation.data.length; break; case common_1.OperationType.DELETE: // Redo delete await vpm.deleteRange(operation.preExecutionPosition, operation.preExecutionPosition + operation.originalData.length); // FORCE mark update for redo if (this.buffer.lineAndMarksManager) { this.buffer.lineAndMarksManager.updateMarksAfterModification(operation.preExecutionPosition, operation.originalData.length, 0); } this.buffer.totalSize -= operation.originalData.length; break; case common_1.OperationType.OVERWRITE: // Redo overwrite by deleting original data and inserting new data await vpm.deleteRange(operation.preExecutionPosition, operation.preExecutionPosition + operation.originalData.length); await vpm.insertAt(operation.preExecutionPosition, operation.data); // FORCE mark update for redo (net change) if (this.buffer.lineAndMarksManager) { this.buffer.lineAndMarksManager.updateMarksAfterModification(operation.preExecutionPosition, operation.originalData.length, operation.data.length); } const sizeChange = operation.data.length - operation.originalData.length; this.buffer.totalSize += sizeChange; break; default: throw new Error(`Unknown operation type: ${operation.type}`); } this.buffer.markAsModified(); } // =================== STATE QUERIES =================== /** * Check if undo is available */ canUndo() { // During active transaction, undo should be available for rollback if (this.activeTransaction) { return true; } return this.undoStack.length > 0; } /** * Check if redo is available */ canRedo() { // Block redo during active transactions if (this.activeTransaction) { return false; } return this.redoStack.length > 0; } /** * Get enhanced undo/redo statistics */ getStats() { let totalUndoOperations = 0; let totalRedoOperations = 0; let memoryUsage = 0; let groupsWithMarksSnapshots = 0; let groupsWithLinesSnapshots = 0; for (const group of this.undoStack) { totalUndoOperations += group.operations.length; memoryUsage += group.getMemoryUsage(); if (group.marksSnapshot) groupsWithMarksSnapshots++; if (group.linesSnapshot !== null) groupsWithLinesSnapshots++; } for (const group of this.redoStack) { totalRedoOperations += group.operations.length; memoryUsage += group.getMemoryUsage(); if (group.marksSnapshot) groupsWithMarksSnapshots++; if (group.linesSnapshot !== null) groupsWithLinesSnapshots++; } const currentTransactionOperations = this.activeTransaction ? this.activeTransaction.operations.length : 0; return { undoGroups: this.undoStack.length, redoGroups: this.redoStack.length, totalUndoOperations, totalRedoOperations, currentGroupOperations: 0, // No more current group currentTransactionOperations, memoryUsage, maxUndoLevels: this.maxUndoLevels, groupsWithMarksSnapshots, groupsWithLinesSnapshots, hasEnhancedTracking: true }; } /** * Clear all undo/redo history */ clear() { this.undoStack = []; this.redoStack = []; this.activeTransaction = null; this.groupIdCounter = 0; } /** * Get enhanced debug information */ getDebugInfo() { return { undoStack: this.undoStack.map(group => ({ id: group.id, name: group.name, operationCount: group.operations.length, isFromTransaction: group.isFromTransaction, hasMarksSnapshot: group.marksSnapshot !== null, hasLinesSnapshot: group.linesSnapshot !== null, marksCount: group.marksSnapshot ? group.marksSnapshot.length : 0, operations: group.operations.map(op => ({ type: op.type, position: op.preExecutionPosition, dataLength: op.data ? op.data.length : 0, originalDataLength: op.originalData ? op.originalData.length : 0 })) })), redoStack: this.redoStack.map(group => ({ id: group.id, name: group.name, operationCount: group.operations.length, isFromTransaction: group.isFromTransaction, hasMarksSnapshot: group.marksSnapshot !== null, hasLinesSnapshot: group.linesSnapshot !== null, marksCount: group.marksSnapshot ? group.marksSnapshot.length : 0 })), activeTransaction: this.activeTransaction ? this.activeTransaction.getInfo() : null, stats: this.getStats() }; } } exports.BufferUndoSystem = BufferUndoSystem; //# sourceMappingURL=undo-system.js.map