UNPKG

@phroun/paged-buffer

Version:

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

892 lines 38.2 kB
"use strict"; /** * @fileoverview Line and Marks Manager - Page coordinate-based marks with CORRECTED logic * @description Manages line positions and named marks using page coordinates for efficiency * @author Jeffrey R. Day * @version 2.1.1 - Fixed mark update logic for deletions and page operations */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ExtractedContent = exports.LineOperationResult = exports.LineAndMarksManager = void 0; const logger_1 = require("./logger"); /** * Represents the result of line-related operations */ class LineOperationResult { constructor(lineNumber, byteStart, byteEnd, marks = [], isExact = true) { this.lineNumber = lineNumber; this.byteStart = byteStart; this.byteEnd = byteEnd; this.length = byteEnd - byteStart; this.marks = marks; this.isExact = isExact; } } exports.LineOperationResult = LineOperationResult; /** * Represents extracted content with marks - FIXED to use tuples consistently */ class ExtractedContent { constructor(data, marks = []) { this.data = data; this.marks = marks; } } exports.ExtractedContent = ExtractedContent; /** * Page coordinate-based marks and line manager */ class LineAndMarksManager { constructor(virtualPageManager) { this.globalMarks = new Map(); // markName -> [pageKey, offset] this.pageToMarks = new Map(); // pageKey -> Set<markName> (for performance) this.vpm = virtualPageManager; } // =================== INTERNAL COORDINATE METHODS =================== /** * Convert virtual address to page coordinates */ _virtualToPageCoord(virtualAddress) { // Handle address at the very end of buffer const totalSize = this.vpm.getTotalSize(); if (virtualAddress === totalSize && totalSize > 0) { const allPages = this.vpm.addressIndex.getAllPages(); if (allPages.length > 0) { const lastPage = allPages[allPages.length - 1]; return [lastPage.pageKey, lastPage.virtualSize]; } } const descriptor = this.vpm.addressIndex.findPageAt(virtualAddress); if (!descriptor) { throw new Error(`No page found for virtual address ${virtualAddress}`); } const offset = virtualAddress - descriptor.virtualStart; return [descriptor.pageKey, offset]; } /** * Convert page coordinates to virtual address */ _pageCoordToVirtual(pageKey, offset) { const descriptor = this.vpm.addressIndex.pages.find(p => p.pageKey === pageKey); if (!descriptor) { throw new Error(`Page ${pageKey} not found`); } return descriptor.virtualStart + offset; } /** * Set mark using page coordinates */ _setMarkByCoord(markName, pageKey, offset) { // Remove from old page index if exists const oldCoord = this.globalMarks.get(markName); if (oldCoord) { this._removeFromPageIndex(markName, oldCoord[0]); } // Set new coordinates (using array for performance) this.globalMarks.set(markName, [pageKey, offset]); // Update page index if (!this.pageToMarks.has(pageKey)) { this.pageToMarks.set(pageKey, new Set()); } this.pageToMarks.get(pageKey).add(markName); } /** * Remove mark from page index */ _removeFromPageIndex(markName, pageKey) { const markSet = this.pageToMarks.get(pageKey); if (markSet) { markSet.delete(markName); if (markSet.size === 0) { this.pageToMarks.delete(pageKey); } } } /** * Helper method to update mark coordinate and page index */ _updateMarkCoordinate(markName, coord, newCoord) { const [oldPageKey] = coord; const [newPageKey, newOffset] = newCoord; // Update the coordinate in place coord[0] = newPageKey; coord[1] = newOffset; // Update page index if page changed if (oldPageKey !== newPageKey) { // Remove from old page index this._removeFromPageIndex(markName, oldPageKey); // Add to new page index if (!this.pageToMarks.has(newPageKey)) { this.pageToMarks.set(newPageKey, new Set()); } this.pageToMarks.get(newPageKey).add(markName); } } // =================== PAGE STRUCTURE UPDATE OPERATIONS (Page Coordinate Based) =================== // These handle page splits/merges - only called by VPM for structural changes /** * Handle page split - transfer marks to appropriate pages */ handlePageSplit(originalPageKey, newPageKey, splitOffset) { const markNames = this.pageToMarks.get(originalPageKey); if (!markNames) return; const marksToMove = []; // Find marks that need to move to the new page for (const markName of markNames) { const coord = this.globalMarks.get(markName); const [_pageKey, offset] = coord; if (offset >= splitOffset) { marksToMove.push(markName); } } // Move marks to new page for (const markName of marksToMove) { const coord = this.globalMarks.get(markName); coord[0] = newPageKey; // Update pageKey coord[1] -= splitOffset; // Adjust offset // Update page index this.pageToMarks.get(originalPageKey).delete(markName); if (!this.pageToMarks.has(newPageKey)) { this.pageToMarks.set(newPageKey, new Set()); } this.pageToMarks.get(newPageKey).add(markName); } } /** * Handle page merge - transfer marks from absorbed page */ handlePageMerge(absorbedPageKey, targetPageKey, insertOffset) { const markNames = this.pageToMarks.get(absorbedPageKey); if (!markNames) return; // Move all marks from absorbed page to target page for (const markName of markNames) { const coord = this.globalMarks.get(markName); coord[0] = targetPageKey; // Update pageKey coord[1] = insertOffset + coord[1]; // Adjust offset // Update page index if (!this.pageToMarks.has(targetPageKey)) { this.pageToMarks.set(targetPageKey, new Set()); } this.pageToMarks.get(targetPageKey).add(markName); } // Remove absorbed page from index this.pageToMarks.delete(absorbedPageKey); } /** * Validate and clean up orphaned marks */ validateAndCleanupMarks() { const orphanedMarks = []; for (const [markName, coord] of this.globalMarks) { const [pageKey, offset] = coord; // Check if page still exists const descriptor = this.vpm.addressIndex.pages.find(p => p.pageKey === pageKey); if (!descriptor) { orphanedMarks.push(markName); continue; } // Check if offset is within page bounds if (offset > descriptor.virtualSize) { // Try to move mark to next page const nextPage = this._findNextPage(descriptor); if (nextPage) { coord[0] = nextPage.pageKey; coord[1] = 0; // Move to start of next page // Update page index this._removeFromPageIndex(markName, pageKey); if (!this.pageToMarks.has(nextPage.pageKey)) { this.pageToMarks.set(nextPage.pageKey, new Set()); } this.pageToMarks.get(nextPage.pageKey).add(markName); } else { // No next page - clamp to end of current page coord[1] = Math.max(0, descriptor.virtualSize - 1); } } } // Remove orphaned marks for (const markName of orphanedMarks) { this.removeMark(markName); } return orphanedMarks; } /** * Find the next page after the given page */ _findNextPage(currentPage) { const pages = this.vpm.addressIndex.getAllPages(); const currentIndex = pages.findIndex(p => p.pageKey === currentPage.pageKey); return currentIndex >= 0 && currentIndex < pages.length - 1 ? pages[currentIndex + 1] : null; } // =================== PUBLIC MARKS API =================== /** * Set a named mark at a virtual address */ setMark(markName, virtualAddress) { const totalSize = this.vpm.getTotalSize(); if (virtualAddress < 0 || virtualAddress > totalSize) { throw new Error(`Mark address ${virtualAddress} is out of range`); } // Handle the special case of marking at the very end of the buffer if (virtualAddress === totalSize) { // Find the last page or create one if empty const allPages = this.vpm.addressIndex.getAllPages(); if (allPages.length > 0) { const lastPage = allPages[allPages.length - 1]; this._setMarkByCoord(markName, lastPage.pageKey, lastPage.virtualSize); return; } } const coord = this._virtualToPageCoord(virtualAddress); this._setMarkByCoord(markName, coord[0], coord[1]); } /** * Get the virtual address of a named mark */ getMark(markName) { const coord = this.globalMarks.get(markName); if (!coord) return null; try { return this._pageCoordToVirtual(coord[0], coord[1]); } catch (error) { // Page might have been deleted - mark is orphaned return null; } } /** * Remove a named mark */ removeMark(markName) { const coord = this.globalMarks.get(markName); if (!coord) return false; // Remove from page index this._removeFromPageIndex(markName, coord[0]); // Remove from global registry this.globalMarks.delete(markName); return true; } /** * Get all marks between two virtual addresses */ getMarksInRange(startAddress, endAddress) { const result = []; for (const [markName, coord] of this.globalMarks) { try { const virtualAddress = this._pageCoordToVirtual(coord[0], coord[1]); if (virtualAddress >= startAddress && virtualAddress <= endAddress) { result.push([markName, virtualAddress]); } } catch (error) { // Skip orphaned marks continue; } } return result.sort((a, b) => a[1] - b[1]); } /** * Get all marks in the buffer */ getAllMarks() { const result = []; for (const [markName, coord] of this.globalMarks) { try { const virtualAddress = this._pageCoordToVirtual(coord[0], coord[1]); result.push([markName, virtualAddress]); } catch (error) { // Skip orphaned marks continue; } } return result.sort((a, b) => a[1] - b[1]); } /** * Get information about marks in content that will be deleted * This reports what marks were in the deleted content (for paste operations) * but does NOT remove the marks - they get consolidated to deletion start */ getMarksInDeletedContent(startAddress, endAddress) { const marksInfo = []; for (const [markName, coord] of this.globalMarks) { try { const virtualAddress = this._pageCoordToVirtual(coord[0], coord[1]); if (virtualAddress >= startAddress && virtualAddress < endAddress) { marksInfo.push([markName, virtualAddress - startAddress]); } } catch (error) { // Skip orphaned marks continue; } } return marksInfo.sort((a, b) => a[1] - b[1]); } /** * Remove marks from a range entirely (for true extraction/cut operations) * This actually removes marks from the buffer - used when marks should disappear */ removeMarksFromRange(startAddress, endAddress) { const removed = []; const marksToRemove = []; for (const [markName, coord] of this.globalMarks) { try { const virtualAddress = this._pageCoordToVirtual(coord[0], coord[1]); if (virtualAddress >= startAddress && virtualAddress < endAddress) { removed.push([markName, virtualAddress - startAddress]); marksToRemove.push(markName); } } catch (error) { // Mark is orphaned - remove it marksToRemove.push(markName); } } // Actually remove the marks for (const markName of marksToRemove) { this.removeMark(markName); } return removed.sort((a, b) => a[1] - b[1]); } /** * Insert marks from relative positions (for insert operations) */ insertMarksFromRelative(insertAddress, marks) { for (const markData of marks) { const virtualAddress = insertAddress + markData[1]; this.setMark(markData[0], virtualAddress); } } // =================== PERSISTENCE API =================== /** * Get all marks as a key-value object with virtual addresses (for persistence) */ getAllMarksForPersistence() { const result = {}; for (const [markName, coord] of this.globalMarks) { try { const virtualAddress = this._pageCoordToVirtual(coord[0], coord[1]); result[markName] = virtualAddress; } catch (error) { // Skip orphaned marks continue; } } return result; } /** * Set marks from a key-value object (for persistence) * Updates/overwrites conflicting marks, retains others */ setMarksFromPersistence(marksObject) { for (const [markName, virtualAddress] of Object.entries(marksObject)) { if (typeof virtualAddress === 'number' && virtualAddress >= 0) { try { this.setMark(markName, virtualAddress); } catch (error) { // Skip invalid addresses continue; } } } } /** * Clear all marks */ clearAllMarks() { this.globalMarks.clear(); this.pageToMarks.clear(); } // =================== ENHANCED OPERATIONS WITH MARKS =================== /** * Enhanced getBytes that includes marks in the result */ async getBytesWithMarks(start, end, includeMarks = false) { const data = await this.vpm.readRange(start, end); if (!includeMarks) { return data; } const marks = this.getMarksInRange(start, end - 1); const relativeMarks = marks.map(mark => [ mark[0], // name mark[1] - start // relative offset ]); return new ExtractedContent(data, relativeMarks); } /** * CORRECTED: Enhanced insertBytes - handles marks correctly with page operations */ async insertBytesWithMarks(position, data, marks = []) { logger_1.logger.debug(`[DEBUG] insertBytesWithMarks: position=${position}, dataLen=${data.length}`); logger_1.logger.debug('[DEBUG] Marks before operation:', this.getAllMarks()); // STEP 1: Capture marks that need to be shifted (AFTER insertion point, not AT) const marksToShift = []; for (const [markName, coord] of this.globalMarks) { try { const markVirtualPos = this._pageCoordToVirtual(coord[0], coord[1]); if (markVirtualPos > position) { // FIXED: Only marks AFTER insertion point get shifted marksToShift.push({ name: markName, originalPos: markVirtualPos }); } } catch (error) { // Skip invalid marks continue; } } logger_1.logger.debug('[DEBUG] Marks to shift:', marksToShift); // STEP 2: Let VPM handle the insertion first (including any page splits) await this.vpm.insertAt(position, data); logger_1.logger.debug('[DEBUG] Marks after VPM insertAt:', this.getAllMarks()); // STEP 3: Now update the captured marks to their new positions for (const markInfo of marksToShift) { const newPos = markInfo.originalPos + data.length; try { this.setMark(markInfo.name, newPos); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger_1.logger.warn(`Failed to update mark ${markInfo.name} to position ${newPos}: ${errorMessage}`); } } logger_1.logger.debug('[DEBUG] Marks after shifting:', this.getAllMarks()); // STEP 4: Insert new marks if (marks.length > 0) { this.insertMarksFromRelative(position, marks); logger_1.logger.debug('[DEBUG] Marks after inserting new marks:', this.getAllMarks()); } } /** * CORRECTED: Enhanced deleteBytes - reports marks in deleted content but consolidates them */ async deleteBytesWithMarks(start, end, reportMarks = false) { logger_1.logger.debug(`[DEBUG] deleteBytesWithMarks: start=${start}, end=${end}, reportMarks=${reportMarks}`); logger_1.logger.debug('[DEBUG] Marks before operation:', this.getAllMarks()); // STEP 1: If requested, get info about marks in the deleted content (for paste operations) let marksInDeletedContent = []; if (reportMarks) { marksInDeletedContent = this.getMarksInDeletedContent(start, end); logger_1.logger.debug('[DEBUG] Marks in deleted content (for reporting):', marksInDeletedContent); } // STEP 2: Update marks for the content change (this will move marks in deletion range to deletion start) this.updateMarksAfterModification(start, end - start, 0); logger_1.logger.debug('[DEBUG] Marks after consolidating to deletion start:', this.getAllMarks()); // STEP 3: Let VPM handle the actual deletion (VPM will handle any page structure changes) const deletedData = await this.vpm.deleteRange(start, end); logger_1.logger.debug('[DEBUG] Marks after VPM deleteRange:', this.getAllMarks()); // Return deleted data with marks report (if requested) - using tuples directly return new ExtractedContent(deletedData, marksInDeletedContent); } /** * Enhanced overwriteBytes with marks support */ async overwriteBytesWithMarks(position, data, marks = []) { const endPosition = Math.min(position + data.length, this.vpm.getTotalSize()); const originalSize = endPosition - position; const netSizeChange = data.length - originalSize; logger_1.logger.debug(`[DEBUG] overwriteBytesWithMarks: position=${position}, dataLen=${data.length}, originalSize=${originalSize}, netChange=${netSizeChange}`); logger_1.logger.debug('[DEBUG] Marks before operation:', this.getAllMarks()); // Get overwritten data before modification const overwrittenData = await this.vpm.readRange(position, endPosition); // Handle marks based on the type of overwrite let marksInOverwrittenContent = []; if (data.length < originalSize) { // Content is shrinking - report marks that will be in the removed portion marksInOverwrittenContent = this.getMarksInDeletedContent(position + data.length, endPosition); } // Capture marks that need to be shifted (after the overwrite region) const marksToShift = []; if (netSizeChange !== 0) { for (const [markName, coord] of this.globalMarks) { try { const markVirtualPos = this._pageCoordToVirtual(coord[0], coord[1]); if (markVirtualPos >= endPosition) { marksToShift.push({ name: markName, originalPos: markVirtualPos }); } } catch (error) { // Skip invalid marks continue; } } } // If content is shrinking, consolidate marks in the removed portion if (data.length < originalSize) { this.updateMarksAfterModification(position + data.length, originalSize - data.length, 0); } logger_1.logger.debug('[DEBUG] Marks after consolidation, before VPM:', this.getAllMarks()); // Let VPM handle the actual overwrite (VPM will handle any page structure changes) await this.vpm.deleteRange(position, endPosition); await this.vpm.insertAt(position, data); logger_1.logger.debug('[DEBUG] Marks after VPM operations:', this.getAllMarks()); // Update marks that were after the overwrite region for (const markInfo of marksToShift) { const newPos = markInfo.originalPos + netSizeChange; try { this.setMark(markInfo.name, newPos); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger_1.logger.warn(`Failed to update mark ${markInfo.name} to position ${newPos}: ${errorMessage}`); } } // Insert new marks if (marks.length > 0) { this.insertMarksFromRelative(position, marks); logger_1.logger.debug('[DEBUG] Marks after inserting new marks:', this.getAllMarks()); } // Return overwritten data with marks report - using tuples directly return new ExtractedContent(overwrittenData, marksInOverwrittenContent); } /** * CORRECTED: Update marks after a modification using virtual addresses * This method handles logical mark movement for content changes */ updateMarksAfterModification(virtualStart, deletedBytes, insertedBytes) { const virtualEnd = virtualStart + deletedBytes; const netChange = insertedBytes - deletedBytes; logger_1.logger.debug(`[DEBUG] updateMarksAfterModification: start=${virtualStart}, deleted=${deletedBytes}, inserted=${insertedBytes}, netChange=${netChange}`); // Create a list of marks to update (avoid modifying map during iteration) const marksToUpdate = []; for (const [markName, coord] of this.globalMarks) { try { const markVirtualPos = this._pageCoordToVirtual(coord[0], coord[1]); marksToUpdate.push({ name: markName, virtualPos: markVirtualPos, coord }); // bespoke three part object for this task logger_1.logger.debug(`[DEBUG] Mark ${markName} at position ${markVirtualPos}`); } catch (error) { // Mark coordinate is invalid - skip for now, don't remove yet logger_1.logger.warn(`Mark ${markName} has invalid coordinates, skipping update`); continue; } } // Update marks based on their position relative to the modification for (const mark of marksToUpdate) { const { name, virtualPos, coord } = mark; if (virtualPos < virtualStart) { // Mark before modification - no change logger_1.logger.debug(`[DEBUG] Mark ${name}: before modification, no change`); continue; } else if (virtualPos === virtualStart) { // CORRECTED: Mark exactly at modification start if (deletedBytes === 0) { // Pure insertion at this point - mark stays at insertion point logger_1.logger.debug(`[DEBUG] Mark ${name}: at insertion point, stays put`); continue; } else { // Deletion starting at this point - mark stays at deletion start logger_1.logger.debug(`[DEBUG] Mark ${name}: at deletion start, stays put`); continue; } } else if (deletedBytes > 0 && virtualPos > virtualStart && virtualPos < virtualEnd) { // CORRECTED: Mark within deleted region - move to deletion start (don't remove!) logger_1.logger.debug(`[DEBUG] Mark ${name}: within deletion range [${virtualStart}, ${virtualEnd}), moving to deletion start`); try { const newCoord = this._virtualToPageCoord(virtualStart); this._updateMarkCoordinate(name, coord, newCoord); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger_1.logger.warn(`Failed to move mark ${name} to deletion start: ${errorMessage}`); // Don't remove the mark, leave it where it is } } else if (virtualPos >= virtualEnd) { // CORRECTED: Mark after deletion end - shift by net change logger_1.logger.debug(`[DEBUG] Mark ${name}: after modification, shifting by ${netChange} (${virtualPos} + ${netChange} = ${virtualPos + netChange})`); try { const newVirtualPos = virtualPos + netChange; const newCoord = this._virtualToPageCoord(newVirtualPos); this._updateMarkCoordinate(name, coord, newCoord); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger_1.logger.warn(`Failed to shift mark ${name}: ${errorMessage}`); // Don't remove the mark, leave it where it is } } else { logger_1.logger.debug(`[DEBUG] Mark ${name}: no condition matched - virtualPos=${virtualPos}, virtualStart=${virtualStart}, virtualEnd=${virtualEnd}, deletedBytes=${deletedBytes}`); } } // Invalidate line caches after mark updates this.invalidateLineCaches(); } // =================== LINE TRACKING (UNCHANGED) =================== /** * Invalidate line caches (called when buffer content changes) */ invalidateLineCaches() { this.invalidatePageLineCaches(); } /** * Invalidate line caches in pages (called when buffer content changes) */ invalidatePageLineCaches() { // Mark all page line caches as invalid in the VPM for (const descriptor of this.vpm.addressIndex.getAllPages()) { descriptor.lineInfoCached = false; } } /** * Ensure page containing address is loaded (ASYNC) */ async seekAddress(address) { if (address < 0 || address > this.vpm.getTotalSize()) { return false; } const descriptor = this.vpm.addressIndex.findPageAt(address); if (!descriptor) { return false; } try { if (this.vpm._ensurePageLoaded) { await this.vpm._ensurePageLoaded(descriptor); } return true; } catch (error) { return false; } } /** * Get the total number of lines in the buffer (SYNCHRONOUS) */ getTotalLineCount() { const totalSize = this.vpm.getTotalSize(); if (totalSize === 0) { return 1; // Empty content has 1 line } let lineCount = 1; // Start with first line for (const descriptor of this.vpm.addressIndex.getAllPages()) { if (descriptor.virtualSize === 0) { continue; // Skip empty pages } // Use cached newline count if available if (descriptor.lineInfoCached) { lineCount += descriptor.newlineCount; } else if (this.vpm?.pageCache.has(descriptor.pageKey)) { // Page is loaded - count newlines and cache the result const pageInfo = this.vpm.pageCache.get(descriptor.pageKey); pageInfo.ensureLineCacheValid(); descriptor.cacheLineInfo(pageInfo); lineCount += descriptor.newlineCount; } else { // Page not loaded and no cached count - we can't know exactly // This is a limitation of keeping it synchronous // For now, assume worst case of 0 newlines in unloaded pages // (Total will be underestimated but won't crash) } } return lineCount; } /** * Get line information by line number (SYNCHRONOUS) */ getLineInfo(lineNumber) { if (lineNumber < 1) { return null; } const totalSize = this.vpm.getTotalSize(); if (totalSize === 0) { return lineNumber === 1 ? new LineOperationResult(1, 0, 0, [], true) : null; } let currentLine = 1; for (const descriptor of this.vpm.addressIndex.getAllPages()) { if (descriptor.virtualSize === 0) { continue; } const pageEndAddress = descriptor.virtualStart + descriptor.virtualSize; // Count lines in this page let pageLinesCount = 0; let exactPositions = null; if (this.vpm.pageCache.has(descriptor.pageKey)) { // Page is loaded - get exact line positions const pageInfo = this.vpm.pageCache.get(descriptor.pageKey); pageInfo.ensureLineCacheValid(); descriptor.cacheLineInfo(pageInfo); pageLinesCount = descriptor.newlineCount; exactPositions = pageInfo.newlinePositions; } else if (descriptor.lineInfoCached) { // Use cached count pageLinesCount = descriptor.newlineCount; } // Check if target line is in this page if (lineNumber >= currentLine && lineNumber < currentLine + pageLinesCount + 1) { if (exactPositions && lineNumber < currentLine + pageLinesCount) { // Target line ends with a newline in this page - exact position const lineIndex = lineNumber - currentLine; const lineStart = lineIndex === 0 ? descriptor.virtualStart : descriptor.virtualStart + exactPositions[lineIndex - 1] + 1; const lineEnd = descriptor.virtualStart + exactPositions[lineIndex] + 1; const marks = this.getMarksInRange(lineStart, lineEnd - 1); return new LineOperationResult(lineNumber, lineStart, lineEnd, marks, true); } else if (exactPositions) { // Target line is the last line in this page (no trailing newline) const lastNewlinePos = exactPositions.length > 0 ? descriptor.virtualStart + exactPositions[exactPositions.length - 1] + 1 : descriptor.virtualStart; const lineStart = exactPositions.length > 0 ? lastNewlinePos : descriptor.virtualStart; const lineEnd = pageEndAddress; const marks = this.getMarksInRange(lineStart, lineEnd - 1); return new LineOperationResult(lineNumber, lineStart, lineEnd, marks, true); } else { // Page not loaded - return page boundaries as approximation const marks = this.getMarksInRange(descriptor.virtualStart, pageEndAddress - 1); return new LineOperationResult(lineNumber, descriptor.virtualStart, pageEndAddress, marks, false); } } currentLine += pageLinesCount; } // Line not found or beyond end return null; } /** * Get information about multiple lines at once (SYNCHRONOUS) */ getMultipleLines(startLine, endLine) { const result = []; const clampedStart = Math.max(1, startLine); const clampedEnd = Math.max(clampedStart, endLine); for (let lineNum = clampedStart; lineNum <= clampedEnd; lineNum++) { const lineInfo = this.getLineInfo(lineNum); if (lineInfo) { result.push(lineInfo); } else { break; // No more lines } } return result; } /** * Convert virtual byte address to line number (SYNCHRONOUS) */ getLineNumberFromAddress(virtualAddress) { if (virtualAddress < 0) { return 0; } const totalSize = this.vpm.getTotalSize(); if (virtualAddress > totalSize) { return 0; } if (totalSize === 0) { return 1; // Empty buffer has line 1 } let currentLine = 1; for (const descriptor of this.vpm.addressIndex.getAllPages()) { if (descriptor.virtualSize === 0) { continue; } // Check if address is in this page if (virtualAddress >= descriptor.virtualStart && virtualAddress < descriptor.virtualEnd) { if (this.vpm.pageCache.has(descriptor.pageKey)) { // Page is loaded - get exact line const pageInfo = this.vpm.pageCache.get(descriptor.pageKey); pageInfo.ensureLineCacheValid(); descriptor.cacheLineInfo(pageInfo); const relativeAddress = virtualAddress - descriptor.virtualStart; let linesInPage = 0; for (const nlPos of pageInfo.newlinePositions) { if (relativeAddress <= nlPos) { break; } linesInPage++; } return currentLine + linesInPage; } else { // Page not loaded - return start of page's line range return currentLine; } } // Count lines in this page and continue if (this.vpm.pageCache.has(descriptor.pageKey)) { const pageInfo = this.vpm.pageCache.get(descriptor.pageKey); pageInfo.ensureLineCacheValid(); descriptor.cacheLineInfo(pageInfo); currentLine += descriptor.newlineCount; } else if (descriptor.lineInfoCached) { currentLine += descriptor.newlineCount; } } return currentLine; // Address is at end of buffer } /** * Convert line/character position to absolute byte position (SYNCHRONOUS) */ lineCharToBytePosition(pos) { const lineInfo = this.getLineInfo(pos.line); if (!lineInfo) { return this.vpm.getTotalSize(); } const character = pos.character - 1; // Convert to 0-based if (character <= 0) { return lineInfo.byteStart; } // Simple byte arithmetic - character position is byte offset within line const targetByte = lineInfo.byteStart + character; // Clamp to line boundaries return Math.min(targetByte, lineInfo.byteEnd - 1); } /** * Convert absolute byte position to line/character position (SYNCHRONOUS) */ byteToLineCharPosition(bytePos) { const lineNumber = this.getLineNumberFromAddress(bytePos); if (lineNumber === 0) { return { line: 1, character: 1 }; } const lineInfo = this.getLineInfo(lineNumber); if (!lineInfo) { return { line: lineNumber, character: 1 }; } const byteOffsetInLine = bytePos - lineInfo.byteStart; // Simple byte arithmetic - character position is byte offset + 1 (for 1-based indexing) return { line: lineNumber, character: byteOffsetInLine + 1 }; } /** * Get memory usage statistics */ getMemoryStats() { // Calculate marks memory more accurately let marksMemory = 0; for (const [markName, coord] of this.globalMarks) { marksMemory += markName.length * 2; // String storage (UTF-16) marksMemory += 16; // Array overhead marksMemory += coord[0].length * 2; // pageKey string marksMemory += 8; // offset number } // Add page index memory for (const markSet of this.pageToMarks.values()) { marksMemory += markSet.size * 16; // Set overhead per mark } return { globalMarksCount: this.globalMarks.size, pageIndexSize: this.pageToMarks.size, totalLines: -1, // No longer cached globally lineStartsCacheSize: 0, // No global cache lineStartsCacheValid: true, // Always valid since no cache estimatedMarksMemory: marksMemory, estimatedLinesCacheMemory: 0 // No global cache }; } } exports.LineAndMarksManager = LineAndMarksManager; //# sourceMappingURL=line-marks-manager.js.map