UNPKG

filetree-pro

Version:

A powerful file tree generator for VS Code and Cursor. Generate beautiful file trees in multiple formats with smart exclusions and custom configurations.

315 lines (259 loc) 9.77 kB
import * as vscode from 'vscode'; import { AnalyticsService } from '../services/analyticsService'; import { CopilotService } from '../services/copilotService'; import { FileSystemService } from '../services/fileSystemService'; import { FileTreeItem } from '../types'; import { formatDate, formatFileSize, getFileIcon } from '../utils/fileUtils'; export class FileTreeProvider implements vscode.TreeDataProvider<FileTreeItem> { // Event emitter for tree changes (Official VS Code API pattern) private _onDidChangeTreeData: vscode.EventEmitter<FileTreeItem | undefined | null | void> = new vscode.EventEmitter<FileTreeItem | undefined | null | void>(); readonly onDidChangeTreeData: vscode.Event<FileTreeItem | undefined | null | void> = this._onDidChangeTreeData.event; // Chunked loading configuration private static readonly CHUNK_SIZE = 100; // Load 100 items at a time private loadingChunks: Map<string, boolean> = new Map(); // Track loading state constructor( private fileSystemService: FileSystemService, private copilotService: CopilotService, private analyticsService: AnalyticsService ) {} getTreeItem(element: FileTreeItem): vscode.TreeItem { const treeItem = new vscode.TreeItem(element.name); // Set collapsible state if (element.type === 'folder') { treeItem.collapsibleState = element.children && element.children.length > 0 ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; } else { treeItem.collapsibleState = vscode.TreeItemCollapsibleState.None; } // Set icon and color - will be used for custom icons later // const icon = getFileIcon(element.name); // const color = getFileColor(element.name); treeItem.iconPath = new vscode.ThemeIcon('file'); treeItem.description = this.getDescription(element); treeItem.tooltip = this.getTooltip(element); treeItem.contextValue = element.contextValue || element.type; if (element.uri) { treeItem.resourceUri = element.uri; } // Add command for files if (element.type === 'file' && element.uri) { treeItem.command = { command: 'filetree-pro.openFile', title: 'Open File', arguments: [element.uri], }; } return treeItem; } /** * ✅ Get children for tree node - implements lazy loading * Called by VS Code when user expands a node (on-demand) * Implements chunked loading for large directories */ async getChildren(element?: FileTreeItem): Promise<FileTreeItem[]> { if (!vscode.workspace.workspaceFolders) { return []; } try { if (!element) { // Root level - get workspace folders const workspaceFolders = vscode.workspace.workspaceFolders; const items: FileTreeItem[] = []; for (const folder of workspaceFolders) { const children = await this.fileSystemService.getFileTree(folder.uri); items.push({ uri: folder.uri, name: folder.name, type: 'folder', children, }); } return items; } else { // Get children of the current element with chunked loading if (element.type === 'folder' && element.uri) { return await this.getChildrenChunked(element); } return []; } } catch (error) { console.error('Error getting children:', error); vscode.window.showErrorMessage(`Failed to load children: ${error}`); return []; } } /** * ✅ Load children in chunks for large directories * Shows first chunk immediately, loads rest progressively */ private async getChildrenChunked(element: FileTreeItem): Promise<FileTreeItem[]> { if (!element.uri) { return []; } const cacheKey = element.uri.fsPath; // If already loaded, return from cache if (element.children && element.children.length > 0) { return element.children; } // Load all children const allChildren = await this.fileSystemService.getFileTree(element.uri); // If small directory, return all at once if (allChildren.length <= FileTreeProvider.CHUNK_SIZE) { return allChildren; } // Large directory - return first chunk immediately const firstChunk = allChildren.slice(0, FileTreeProvider.CHUNK_SIZE); // Schedule loading remaining chunks in background if (!this.loadingChunks.get(cacheKey)) { this.loadingChunks.set(cacheKey, true); this.loadRemainingChunks(element, allChildren.slice(FileTreeProvider.CHUNK_SIZE)); } return firstChunk; } /** * ✅ Load remaining chunks progressively in background */ private async loadRemainingChunks( parent: FileTreeItem, remaining: FileTreeItem[] ): Promise<void> { // Load in chunks with small delay between each for (let i = 0; i < remaining.length; i += FileTreeProvider.CHUNK_SIZE) { const chunk = remaining.slice(i, i + FileTreeProvider.CHUNK_SIZE); // Add chunk to parent's children if (!parent.children) { parent.children = []; } parent.children.push(...chunk); // Fire refresh event for this parent (incremental update) this._onDidChangeTreeData.fire(parent); // Small delay to avoid blocking UI await new Promise(resolve => setTimeout(resolve, 50)); } // Clear loading flag if (parent.uri) { this.loadingChunks.delete(parent.uri.fsPath); } } private getDescription(element: FileTreeItem): string { const parts: string[] = []; if (element.size !== undefined) { parts.push(formatFileSize(element.size)); } if (element.modifiedDate) { parts.push(formatDate(element.modifiedDate)); } return parts.join(' • '); } private getTooltip(element: FileTreeItem): string { const parts: string[] = []; parts.push(`Name: ${element.name}`); parts.push(`Type: ${element.type}`); if (element.uri) { parts.push(`Path: ${element.uri.fsPath}`); } if (element.size !== undefined) { parts.push(`Size: ${formatFileSize(element.size)}`); } if (element.modifiedDate) { parts.push(`Modified: ${formatDate(element.modifiedDate)}`); } // Add Copilot analysis if available if (element.type === 'file' && element.uri && this.copilotService.isAvailable()) { const analysis = this.copilotService.getFileAnalysis(element.uri); if (analysis?.summary) { parts.push(`AI Summary: ${analysis.summary}`); } } return parts.join('\n'); } /** * ✅ Refresh entire tree or specific element (Official VS Code API pattern) * @param element - Optional element to refresh (undefined = refresh all) */ refresh(element?: FileTreeItem): void { // Clear chunk loading state if refreshing if (!element) { this.loadingChunks.clear(); } else if (element.uri) { this.loadingChunks.delete(element.uri.fsPath); } this._onDidChangeTreeData.fire(element); } setRootPath(path: string): void { // This method allows setting a specific root path for the tree // The tree will refresh and show the contents of the specified path this._onDidChangeTreeData.fire(); } async expandItem(element: FileTreeItem): Promise<void> { if (element.type === 'folder') { const children = await this.getChildren(element); element.children = children; this._onDidChangeTreeData.fire(element); } } async collapseItem(element: FileTreeItem): Promise<void> { if (element.type === 'folder') { element.children = []; this._onDidChangeTreeData.fire(element); } } async search(query: string): Promise<FileTreeItem[]> { return await this.fileSystemService.searchFiles(query); } async getAnalytics(): Promise<any> { return await this.analyticsService.getProjectAnalytics(); } async exportTree(format: string): Promise<string> { const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders) { throw new Error('No workspace folders found'); } const rootItems = await this.getChildren(); return this.exportToFormat(rootItems, format); } private exportToFormat(items: FileTreeItem[], format: string): string { switch (format.toLowerCase()) { case 'json': return JSON.stringify(items, null, 2); case 'markdown': return this.exportToMarkdown(items); case 'ascii': return this.exportToAscii(items); default: throw new Error(`Unsupported format: ${format}`); } } private exportToMarkdown(items: FileTreeItem[], level: number = 0): string { let result = ''; const indent = ' '.repeat(level); for (const item of items) { const icon = getFileIcon(item.name); const size = item.size ? ` (${formatFileSize(item.size)})` : ''; result += `${indent}- ${icon} ${item.name}${size}\n`; if (item.children && item.children.length > 0) { result += this.exportToMarkdown(item.children, level + 1); } } return result; } private exportToAscii(items: FileTreeItem[], prefix: string = ''): string { let result = ''; for (let i = 0; i < items.length; i++) { const item = items[i]; const isLast = i === items.length - 1; const connector = isLast ? '└── ' : '├── '; const icon = getFileIcon(item.name); result += `${prefix}${connector}${icon} ${item.name}\n`; if (item.children && item.children.length > 0) { const newPrefix = prefix + (isLast ? ' ' : '│ '); result += this.exportToAscii(item.children, newPrefix); } } return result; } }