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.
267 lines (231 loc) • 7.75 kB
text/typescript
/**
* Tree Builder Service - Constructs file tree structures
* Handles file system traversal and tree generation
*
* @module services
* @since 0.3.0
*/
import * as path from 'path';
import * as vscode from 'vscode';
import { FileTreeItem } from '../types';
import { getFileTypeInfo } from '../utils/fileUtils';
import { ExclusionService } from './exclusionService';
/**
* Progress callback type
*/
type ProgressCallback = (message: string) => void;
/**
* Service for building file tree structures
* Handles recursion, sorting, and progress reporting
*/
export class TreeBuilderService {
constructor(private exclusionService: ExclusionService) {}
/**
* Build FileTreeItem array from file system
*
* @param currentPath - Current directory path
* @param maxDepth - Maximum depth to traverse
* @param rootPath - Root path for exclusion checks
* @param depth - Current depth (default: 0)
* @param progressCallback - Optional progress callback
* @returns Array of FileTreeItem
*/
async buildFileTreeItems(
currentPath: string,
maxDepth: number,
rootPath: string,
depth: number = 0,
progressCallback?: ProgressCallback,
token?: vscode.CancellationToken
): Promise<FileTreeItem[]> {
// Check if operation was cancelled
if (token?.isCancellationRequested) {
return [];
}
if (depth > maxDepth) {
return [];
}
try {
// Pre-load .gitignore asynchronously (first time only)
if (depth === 0) {
await this.exclusionService.readGitignore(rootPath);
if (progressCallback) {
progressCallback(`Building tree: ${path.basename(currentPath)}`);
}
}
const items = await vscode.workspace.fs.readDirectory(vscode.Uri.file(currentPath));
const result: FileTreeItem[] = [];
// Sort items: folders first, then files
const folders: string[] = [];
const files: string[] = [];
for (const [item, fileType] of items) {
const itemPath = path.join(currentPath, item);
const isExcluded = this.exclusionService.shouldExclude(item, itemPath, rootPath);
if (!isExcluded) {
if (fileType === vscode.FileType.Directory) {
folders.push(item);
} else {
files.push(item);
}
}
}
// Sort alphabetically
folders.sort();
files.sort();
// Process folders recursively
for (const folder of folders) {
const folderPath = path.join(currentPath, folder);
const children = await this.buildFileTreeItems(
folderPath,
maxDepth,
rootPath,
depth + 1,
progressCallback,
token
);
result.push({
name: folder,
type: 'folder',
path: folderPath,
children,
});
}
// Process files
for (const file of files) {
const filePath = path.join(currentPath, file);
result.push({
name: file,
type: 'file',
path: filePath,
});
}
return result;
} catch (error) {
if (progressCallback) {
progressCallback(`Error reading directory: ${error}`);
}
return [];
}
}
/**
* Generate ASCII tree lines (for text-based representations)
*
* @param currentPath - Current directory path
* @param prefix - Line prefix for indentation
* @param lines - Output array to append lines
* @param depth - Current depth
* @param maxDepth - Maximum depth
* @param showIcons - Whether to show icons
* @param rootPath - Root path
* @param progressCallback - Optional progress callback
*/
async generateTreeLines(
currentPath: string,
prefix: string,
lines: string[],
depth: number,
maxDepth: number,
showIcons: boolean,
rootPath: string,
progressCallback?: ProgressCallback,
token?: vscode.CancellationToken
): Promise<void> {
// Check if operation was cancelled
if (token?.isCancellationRequested) {
return;
}
if (depth > maxDepth) {
return;
}
try {
// Pre-load .gitignore asynchronously (first time only)
if (depth === 0) {
await this.exclusionService.readGitignore(rootPath);
if (progressCallback) {
progressCallback(`Reading directory: ${path.basename(currentPath)}`);
}
}
const items = await vscode.workspace.fs.readDirectory(vscode.Uri.file(currentPath));
// Sort items: folders first, then files
const folders: string[] = [];
const files: string[] = [];
// Process items in batches to avoid memory issues
const batchSize = 100;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
// Process batch asynchronously
const batchPromises = batch.map(async ([item, fileType]) => {
const itemPath = path.join(currentPath, item);
const isExcluded = this.exclusionService.shouldExclude(item, itemPath, rootPath);
if (fileType === vscode.FileType.Directory) {
return { item, type: 'folder' as const, isExcluded };
} else {
return { item, type: 'file' as const, isExcluded };
}
});
const batchResults = await Promise.all(batchPromises);
for (const result of batchResults) {
if (result && !result.isExcluded) {
if (result.type === 'folder') {
folders.push(result.item);
} else {
files.push(result.item);
}
}
}
// Update progress for large directories
if (progressCallback && items.length > batchSize) {
const progress = Math.min(100, Math.round(((i + batchSize) / items.length) * 100));
progressCallback(`Processing items: ${progress}%`);
}
// Yield control periodically to prevent blocking
if (i % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// Sort alphabetically
folders.sort();
files.sort();
// Process folders with memory management
for (let i = 0; i < folders.length; i++) {
const folder = folders[i];
const isLast = i === folders.length - 1 && files.length === 0;
const connector = isLast ? '└── ' : '├── ';
const newPrefix = prefix + (isLast ? ' ' : '│ ');
lines.push(`${prefix}${connector}${showIcons ? '📁 ' : ''}${folder}/`);
const folderPath = path.join(currentPath, folder);
await this.generateTreeLines(
folderPath,
newPrefix,
lines,
depth + 1,
maxDepth,
showIcons,
rootPath,
progressCallback,
token
);
// Yield control periodically
if (i % 10 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// Process files with memory management
for (let i = 0; i < files.length; i++) {
const file = files[i];
const isLast = i === files.length - 1;
const connector = isLast ? '└── ' : '├── ';
// Get file icon based on extension
const fileInfo = getFileTypeInfo(file);
const icon = showIcons ? fileInfo.icon + ' ' : '';
lines.push(`${prefix}${connector}${icon}${file}`);
// Yield control periodically
if (i % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
} catch (error) {
lines.push(`${prefix}└── ❌ Error reading directory: ${error}`);
}
}
}