|
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