|  | 
| 1 | 1 | import * as vscode from 'vscode'; | 
| 2 | 2 | import * as path from 'path'; | 
| 3 | 3 | import * as fs from 'fs'; | 
|  | 4 | +import { promises as fsPromises } from 'fs'; | 
| 4 | 5 | import { LogViewerProvider, ReportViewerProvider, LogItem } from './logViewer'; | 
| 5 | 6 | 
 | 
| 6 | 7 | // Prompts the user to confirm if the current project is a Magento project. | 
| @@ -108,18 +109,18 @@ export function registerCommands(context: vscode.ExtensionContext, logViewerProv | 
| 108 | 109 |     const message = `Cache: ${stats.size}/${stats.maxSize} files | Memory: ${stats.memoryUsage} | Max file size: ${Math.round(stats.maxFileSize / 1024 / 1024)} MB`; | 
| 109 | 110 |     vscode.window.showInformationMessage(message); | 
| 110 | 111 |   });  // Improved command registration for openFile | 
| 111 |  | -  vscode.commands.registerCommand('magento-log-viewer.openFile', (filePath: string | unknown, lineNumber?: number) => { | 
|  | 112 | +  vscode.commands.registerCommand('magento-log-viewer.openFile', async (filePath: string | unknown, lineNumber?: number) => { | 
| 112 | 113 |     // If filePath is not a string, show a selection box with available log files | 
| 113 | 114 |     if (typeof filePath !== 'string') { | 
| 114 |  | -      handleOpenFileWithoutPath(magentoRoot); | 
|  | 115 | +      await handleOpenFileWithoutPathAsync(magentoRoot); | 
| 115 | 116 |       return; | 
| 116 | 117 |     } | 
| 117 | 118 | 
 | 
| 118 | 119 |     // If it's just a line number (e.g. "/20") | 
| 119 | 120 |     if (filePath.startsWith('/') && !filePath.includes('/')) { | 
| 120 | 121 |       const possibleLineNumber = parseInt(filePath.substring(1)); | 
| 121 | 122 |       if (!isNaN(possibleLineNumber)) { | 
| 122 |  | -        handleOpenFileWithoutPath(magentoRoot, possibleLineNumber); | 
|  | 123 | +        await handleOpenFileWithoutPathAsync(magentoRoot, possibleLineNumber); | 
| 123 | 124 |         return; | 
| 124 | 125 |       } | 
| 125 | 126 |     } | 
| @@ -176,7 +177,7 @@ export function clearAllLogFiles(logViewerProvider: LogViewerProvider, magentoRo | 
| 176 | 177 |   vscode.window.showWarningMessage('Are you sure you want to delete all log files?', 'Yes', 'No').then(selection => { | 
| 177 | 178 |     if (selection === 'Yes') { | 
| 178 | 179 |       const logPath = path.join(magentoRoot, 'var', 'log'); | 
| 179 |  | -      if (logViewerProvider.pathExists(logPath)) { | 
|  | 180 | +      if (pathExists(logPath)) { | 
| 180 | 181 |         const files = fs.readdirSync(logPath); | 
| 181 | 182 |         files.forEach(file => fs.unlinkSync(path.join(logPath, file))); | 
| 182 | 183 |         logViewerProvider.refresh(); | 
| @@ -317,7 +318,25 @@ export function isValidPath(filePath: string): boolean { | 
| 317 | 318 |   } | 
| 318 | 319 | } | 
| 319 | 320 | 
 | 
| 320 |  | -// Checks if the given path exists. | 
|  | 321 | +/** | 
|  | 322 | + * Checks if the given path exists (asynchronous version) | 
|  | 323 | + * @param p Path to check | 
|  | 324 | + * @returns Promise<boolean> - true if path exists, false otherwise | 
|  | 325 | + */ | 
|  | 326 | +export async function pathExistsAsync(p: string): Promise<boolean> { | 
|  | 327 | +  try { | 
|  | 328 | +    await fsPromises.access(p); | 
|  | 329 | +    return true; | 
|  | 330 | +  } catch (err) { | 
|  | 331 | +    return false; | 
|  | 332 | +  } | 
|  | 333 | +} | 
|  | 334 | + | 
|  | 335 | +/** | 
|  | 336 | + * Checks if the given path exists (synchronous fallback for compatibility) | 
|  | 337 | + * @param p Path to check | 
|  | 338 | + * @returns boolean - true if path exists, false otherwise | 
|  | 339 | + */ | 
| 321 | 340 | export function pathExists(p: string): boolean { | 
| 322 | 341 |   try { | 
| 323 | 342 |     fs.accessSync(p); | 
| @@ -401,6 +420,63 @@ export function getIconForLogLevel(level: string): vscode.ThemeIcon { | 
| 401 | 420 |   } | 
| 402 | 421 | } | 
| 403 | 422 | 
 | 
|  | 423 | +// Asynchronous version of getLogItems for better performance | 
|  | 424 | +export async function getLogItemsAsync(dir: string, parseTitle: (filePath: string) => string, getIcon: (filePath: string) => vscode.ThemeIcon): Promise<LogItem[]> { | 
|  | 425 | +  if (!(await pathExistsAsync(dir))) { | 
|  | 426 | +    return []; | 
|  | 427 | +  } | 
|  | 428 | + | 
|  | 429 | +  const items: LogItem[] = []; | 
|  | 430 | + | 
|  | 431 | +  try { | 
|  | 432 | +    const files = await fsPromises.readdir(dir); | 
|  | 433 | + | 
|  | 434 | +    // Process files in batches to avoid overwhelming the system | 
|  | 435 | +    const batchSize = 10; | 
|  | 436 | +    for (let i = 0; i < files.length; i += batchSize) { | 
|  | 437 | +      const batch = files.slice(i, i + batchSize); | 
|  | 438 | + | 
|  | 439 | +      const batchPromises = batch.map(async (file) => { | 
|  | 440 | +        const filePath = path.join(dir, file); | 
|  | 441 | + | 
|  | 442 | +        try { | 
|  | 443 | +          const stats = await fsPromises.stat(filePath); | 
|  | 444 | + | 
|  | 445 | +          if (stats.isDirectory()) { | 
|  | 446 | +            const subItems = await getLogItemsAsync(filePath, parseTitle, getIcon); | 
|  | 447 | +            return subItems.length > 0 ? subItems : []; | 
|  | 448 | +          } else if (stats.isFile()) { | 
|  | 449 | +            const title = parseTitle(filePath); | 
|  | 450 | +            const logFile = new LogItem(title, vscode.TreeItemCollapsibleState.None, { | 
|  | 451 | +              command: 'magento-log-viewer.openFile', | 
|  | 452 | +              title: 'Open Log File', | 
|  | 453 | +              arguments: [filePath] | 
|  | 454 | +            }); | 
|  | 455 | +            logFile.iconPath = getIcon(filePath); | 
|  | 456 | +            return [logFile]; | 
|  | 457 | +          } | 
|  | 458 | +        } catch (error) { | 
|  | 459 | +          console.error(`Error processing file ${filePath}:`, error); | 
|  | 460 | +        } | 
|  | 461 | + | 
|  | 462 | +        return []; | 
|  | 463 | +      }); | 
|  | 464 | + | 
|  | 465 | +      const batchResults = await Promise.all(batchPromises); | 
|  | 466 | +      items.push(...batchResults.flat()); | 
|  | 467 | + | 
|  | 468 | +      // Small delay between batches to prevent blocking | 
|  | 469 | +      if (i + batchSize < files.length) { | 
|  | 470 | +        await new Promise(resolve => setTimeout(resolve, 1)); | 
|  | 471 | +      } | 
|  | 472 | +    } | 
|  | 473 | +  } catch (error) { | 
|  | 474 | +    console.error(`Error reading directory ${dir}:`, error); | 
|  | 475 | +  } | 
|  | 476 | + | 
|  | 477 | +  return items; | 
|  | 478 | +} | 
|  | 479 | + | 
| 404 | 480 | export function getLogItems(dir: string, parseTitle: (filePath: string) => string, getIcon: (filePath: string) => vscode.ThemeIcon): LogItem[] { | 
| 405 | 481 |   if (!pathExists(dir)) { | 
| 406 | 482 |     return []; | 
| @@ -498,7 +574,82 @@ function getReportContent(filePath: string): unknown | null { | 
| 498 | 574 |   } | 
| 499 | 575 | } | 
| 500 | 576 | 
 | 
| 501 |  | -// Enhanced file content caching function | 
|  | 577 | +// Enhanced file content caching function (asynchronous) | 
|  | 578 | +export async function getCachedFileContentAsync(filePath: string): Promise<string | null> { | 
|  | 579 | +  try { | 
|  | 580 | +    // Check if file exists first | 
|  | 581 | +    if (!(await pathExistsAsync(filePath))) { | 
|  | 582 | +      return null; | 
|  | 583 | +    } | 
|  | 584 | + | 
|  | 585 | +    const stats = await fsPromises.stat(filePath); | 
|  | 586 | + | 
|  | 587 | +    // For very large files (>50MB), use streaming | 
|  | 588 | +    if (stats.size > 50 * 1024 * 1024) { | 
|  | 589 | +      return readLargeFileAsync(filePath); | 
|  | 590 | +    } | 
|  | 591 | + | 
|  | 592 | +    // Don't cache files larger than configured limit to prevent memory issues | 
|  | 593 | +    if (stats.size > CACHE_CONFIG.maxFileSize) { | 
|  | 594 | +      return await fsPromises.readFile(filePath, 'utf-8'); | 
|  | 595 | +    } | 
|  | 596 | + | 
|  | 597 | +    const cachedContent = fileContentCache.get(filePath); | 
|  | 598 | + | 
|  | 599 | +    // Return cached content if it's still valid | 
|  | 600 | +    if (cachedContent && cachedContent.timestamp >= stats.mtime.getTime()) { | 
|  | 601 | +      return cachedContent.content; | 
|  | 602 | +    } | 
|  | 603 | + | 
|  | 604 | +    // Read file content asynchronously | 
|  | 605 | +    const content = await fsPromises.readFile(filePath, 'utf-8'); | 
|  | 606 | + | 
|  | 607 | +    // Manage cache size - remove oldest entries if cache is full | 
|  | 608 | +    if (fileContentCache.size >= CACHE_CONFIG.maxSize) { | 
|  | 609 | +      // Remove multiple old entries if we're significantly over the limit | 
|  | 610 | +      const entriesToRemove = Math.max(1, Math.floor(CACHE_CONFIG.maxSize * 0.1)); | 
|  | 611 | +      const keys = Array.from(fileContentCache.keys()); | 
|  | 612 | + | 
|  | 613 | +      for (let i = 0; i < entriesToRemove && keys.length > 0; i++) { | 
|  | 614 | +        fileContentCache.delete(keys[i]); | 
|  | 615 | +      } | 
|  | 616 | +    } | 
|  | 617 | + | 
|  | 618 | +    // Cache the content | 
|  | 619 | +    fileContentCache.set(filePath, { | 
|  | 620 | +      content, | 
|  | 621 | +      timestamp: stats.mtime.getTime() | 
|  | 622 | +    }); | 
|  | 623 | + | 
|  | 624 | +    return content; | 
|  | 625 | +  } catch (error) { | 
|  | 626 | +    console.error(`Error reading file ${filePath}:`, error); | 
|  | 627 | +    return null; | 
|  | 628 | +  } | 
|  | 629 | +} | 
|  | 630 | + | 
|  | 631 | +// Stream-based reading for very large files | 
|  | 632 | +async function readLargeFileAsync(filePath: string): Promise<string> { | 
|  | 633 | +  return new Promise((resolve, reject) => { | 
|  | 634 | +    const chunks: Buffer[] = []; | 
|  | 635 | +    const stream = fs.createReadStream(filePath, { encoding: 'utf8' }); | 
|  | 636 | + | 
|  | 637 | +    stream.on('data', (chunk: Buffer) => { | 
|  | 638 | +      chunks.push(chunk); | 
|  | 639 | +    }); | 
|  | 640 | + | 
|  | 641 | +    stream.on('end', () => { | 
|  | 642 | +      resolve(Buffer.concat(chunks).toString('utf8')); | 
|  | 643 | +    }); | 
|  | 644 | + | 
|  | 645 | +    stream.on('error', (error) => { | 
|  | 646 | +      console.error(`Error reading large file ${filePath}:`, error); | 
|  | 647 | +      reject(error); | 
|  | 648 | +    }); | 
|  | 649 | +  }); | 
|  | 650 | +} | 
|  | 651 | + | 
|  | 652 | +// Enhanced file content caching function (synchronous - for compatibility) | 
| 502 | 653 | export function getCachedFileContent(filePath: string): string | null { | 
| 503 | 654 |   try { | 
| 504 | 655 |     // Check if file exists first | 
| @@ -731,7 +882,90 @@ export function formatTimestamp(timestamp: string): string { | 
| 731 | 882 |   } | 
| 732 | 883 | } | 
| 733 | 884 | 
 | 
| 734 |  | -// Shows a dialog to select a log file when no path is provided | 
|  | 885 | +// Shows a dialog to select a log file when no path is provided (async version) | 
|  | 886 | +export async function handleOpenFileWithoutPathAsync(magentoRoot: string, lineNumber?: number): Promise<void> { | 
|  | 887 | +  try { | 
|  | 888 | +    // Collect log and report files asynchronously | 
|  | 889 | +    const logPath = path.join(magentoRoot, 'var', 'log'); | 
|  | 890 | +    const reportPath = path.join(magentoRoot, 'var', 'report'); | 
|  | 891 | +    const logFiles: string[] = []; | 
|  | 892 | +    const reportFiles: string[] = []; | 
|  | 893 | + | 
|  | 894 | +    // Check directories and read files in parallel | 
|  | 895 | +    const [logExists, reportExists] = await Promise.all([ | 
|  | 896 | +      pathExistsAsync(logPath), | 
|  | 897 | +      pathExistsAsync(reportPath) | 
|  | 898 | +    ]); | 
|  | 899 | + | 
|  | 900 | +    const fileReadPromises: Promise<void>[] = []; | 
|  | 901 | + | 
|  | 902 | +    if (logExists) { | 
|  | 903 | +      fileReadPromises.push( | 
|  | 904 | +        fsPromises.readdir(logPath).then(files => { | 
|  | 905 | +          return Promise.all(files.map(async file => { | 
|  | 906 | +            const filePath = path.join(logPath, file); | 
|  | 907 | +            const stats = await fsPromises.stat(filePath); | 
|  | 908 | +            if (stats.isFile()) { | 
|  | 909 | +              logFiles.push(filePath); | 
|  | 910 | +            } | 
|  | 911 | +          })); | 
|  | 912 | +        }).then(() => {}) | 
|  | 913 | +      ); | 
|  | 914 | +    } | 
|  | 915 | + | 
|  | 916 | +    if (reportExists) { | 
|  | 917 | +      fileReadPromises.push( | 
|  | 918 | +        fsPromises.readdir(reportPath).then(files => { | 
|  | 919 | +          return Promise.all(files.map(async file => { | 
|  | 920 | +            const filePath = path.join(reportPath, file); | 
|  | 921 | +            const stats = await fsPromises.stat(filePath); | 
|  | 922 | +            if (stats.isFile()) { | 
|  | 923 | +              reportFiles.push(filePath); | 
|  | 924 | +            } | 
|  | 925 | +          })); | 
|  | 926 | +        }).then(() => {}) | 
|  | 927 | +      ); | 
|  | 928 | +    } | 
|  | 929 | + | 
|  | 930 | +    await Promise.all(fileReadPromises); | 
|  | 931 | + | 
|  | 932 | +    // Create a list of options for the quick pick | 
|  | 933 | +    const options: { label: string; description: string; filePath: string }[] = [ | 
|  | 934 | +      ...logFiles.map(filePath => ({ | 
|  | 935 | +        label: path.basename(filePath), | 
|  | 936 | +        description: 'Log File', | 
|  | 937 | +        filePath | 
|  | 938 | +      })), | 
|  | 939 | +      ...reportFiles.map(filePath => ({ | 
|  | 940 | +        label: path.basename(filePath), | 
|  | 941 | +        description: 'Report File', | 
|  | 942 | +        filePath | 
|  | 943 | +      })) | 
|  | 944 | +    ]; | 
|  | 945 | + | 
|  | 946 | +    // If no files were found | 
|  | 947 | +    if (options.length === 0) { | 
|  | 948 | +      showErrorMessage('No log or report files found.'); | 
|  | 949 | +      return; | 
|  | 950 | +    } | 
|  | 951 | + | 
|  | 952 | +    // Show a quick pick dialog | 
|  | 953 | +    const selection = await vscode.window.showQuickPick(options, { | 
|  | 954 | +      placeHolder: lineNumber !== undefined ? | 
|  | 955 | +        `Select a file to navigate to line ${lineNumber}` : | 
|  | 956 | +        'Select a log or report file' | 
|  | 957 | +    }); | 
|  | 958 | + | 
|  | 959 | +    if (selection) { | 
|  | 960 | +      openFile(selection.filePath, lineNumber); | 
|  | 961 | +    } | 
|  | 962 | +  } catch (error) { | 
|  | 963 | +    showErrorMessage(`Error fetching log files: ${error instanceof Error ? error.message : String(error)}`); | 
|  | 964 | +    console.error('Error fetching log files:', error); | 
|  | 965 | +  } | 
|  | 966 | +} | 
|  | 967 | + | 
|  | 968 | +// Shows a dialog to select a log file when no path is provided (sync fallback) | 
| 735 | 969 | export function handleOpenFileWithoutPath(magentoRoot: string, lineNumber?: number): void { | 
| 736 | 970 |   try { | 
| 737 | 971 |     // Collect log and report files | 
|  | 
0 commit comments