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.

986 lines (839 loc) 25.1 kB
import * as path from 'path'; import * as vscode from 'vscode'; export function registerCommands(): vscode.Disposable[] { const disposables: vscode.Disposable[] = []; // Generate File Tree command (main command) disposables.push( vscode.commands.registerCommand('filetree-pro.generateFileTree', async (uri: vscode.Uri) => { try { if (!uri) { vscode.window.showErrorMessage('Please right-click on a folder to generate file tree'); return; } // Get the folder path const folderPath = uri.fsPath; const folderName = path.basename(folderPath); // Ask user for format preference const formatChoice = await vscode.window.showQuickPick( [ { label: '📄 Markdown', value: 'markdown' }, { label: '📊 JSON', value: 'json' }, { label: '🎨 SVG', value: 'svg' }, { label: '📝 ASCII', value: 'ascii' }, ], { placeHolder: 'Choose output format', canPickMany: false, } ); if (!formatChoice) { return; // User cancelled } // Ask user for icon preference const iconChoice = await vscode.window.showQuickPick(['With Icons', 'Without Icons'], { placeHolder: 'Choose tree style', canPickMany: false, }); if (!iconChoice) { return; // User cancelled } const useIcons = iconChoice === 'With Icons'; // Show progress with loading indicator const progressOptions = { location: vscode.ProgressLocation.Notification, title: `Generating file tree for ${folderName}`, cancellable: false, }; await vscode.window.withProgress(progressOptions, async progress => { progress.report({ message: 'Starting tree generation...' }); // Generate the file tree based on format with progress updates const treeContent = await generateFileTree( folderPath, 10, useIcons, formatChoice.value, message => { progress.report({ message }); } ); progress.report({ message: 'Creating document...' }); // Create an untitled document with the tree content const document = await vscode.workspace.openTextDocument({ content: treeContent, language: formatChoice.value === 'json' ? 'json' : formatChoice.value === 'svg' ? 'xml' : formatChoice.value === 'ascii' ? 'plaintext' : 'markdown', }); // Show the document in a new tab (unsaved mode) await vscode.window.showTextDocument(document); vscode.window.showInformationMessage( `File tree generated successfully! Ready to save when you're ready.` ); }); } catch (error) { vscode.window.showErrorMessage(`Failed to generate file tree: ${error}`); } }) ); return disposables; } async function generateFileTree( rootPath: string, maxDepth: number = 10, forceShowIcons?: boolean, format: string = 'markdown', progressCallback?: (message: string) => void ): Promise<string> { const lines: string[] = []; // Get user settings or use forced value const config = vscode.workspace.getConfiguration('filetree-pro'); const showIcons = forceShowIcons !== undefined ? forceShowIcons : config.get<boolean>('showIcons', true); // Generate based on format switch (format) { case 'json': return await generateJsonTree(rootPath, maxDepth, showIcons, progressCallback); case 'svg': return await generateSvgTree(rootPath, maxDepth, showIcons, progressCallback); case 'ascii': return await generateAsciiTree(rootPath, maxDepth, showIcons, progressCallback); case 'markdown': default: return await generateMarkdownTree(rootPath, maxDepth, showIcons, progressCallback); } } export async function generateTreeLines( currentPath: string, prefix: string, lines: string[], depth: number, maxDepth: number, showIcons: boolean, progressCallback?: (message: string) => void ): Promise<void> { if (depth > maxDepth) { return; } try { // Update progress for large directories if (progressCallback && depth === 0) { 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 isExcluded = shouldExclude(item); 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) { 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 ? ' ' : '│ '); // Check if folder is excluded const isExcluded = shouldExclude(folder); const exclusionIndicator = isExcluded ? ' 🚫 (auto-hidden)' : ''; lines.push(`${prefix}${connector}${showIcons ? '📁 ' : ''}${folder}/${exclusionIndicator}`); if (!isExcluded) { const folderPath = path.join(currentPath, folder); await generateTreeLines( folderPath, newPrefix, lines, depth + 1, maxDepth, showIcons, progressCallback ); } // Yield control periodically to prevent blocking 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 ? '└── ' : '├── '; // Check if file is excluded const isExcluded = shouldExclude(file); const exclusionIndicator = isExcluded ? ' 🚫 (auto-hidden)' : ''; // Get file icon based on extension const icon = showIcons ? getFileIcon(file) + ' ' : ''; lines.push(`${prefix}${connector}${icon}${file}${exclusionIndicator}`); // Yield control periodically to prevent blocking if (i % 50 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); } } } catch (error) { lines.push(`${prefix}└── ❌ Error reading directory: ${error}`); } } function getFileIcon(filename: string): string { const ext = path.extname(filename).toLowerCase(); const basename = path.basename(filename).toLowerCase(); const iconMap: { [key: string]: string } = { // JavaScript/TypeScript Ecosystem '.js': '📄', '.ts': '📄', '.jsx': '📄', '.tsx': '📄', '.json': '📄', '.mjs': '📄', '.cjs': '📄', // Web Technologies '.html': '🌐', '.htm': '🌐', '.css': '🎨', '.scss': '🎨', '.sass': '🎨', '.less': '🎨', '.styl': '🎨', '.vue': '🟢', '.svelte': '🟠', // Python Ecosystem '.py': '🐍', '.pyw': '🐍', '.pyi': '🐍', '.ipynb': '📓', // Java Ecosystem '.java': '☕', '.class': '☕', '.jar': '☕', '.war': '☕', // C/C++ Ecosystem '.cpp': '⚙️', '.cc': '⚙️', '.cxx': '⚙️', '.c': '⚙️', '.h': '⚙️', '.hpp': '⚙️', '.hxx': '⚙️', '.obj': '⚙️', '.o': '⚙️', '.so': '⚙️', '.dll': '⚙️', '.exe': '⚙️', // Go '.go': '🐹', '.mod': '🐹', '.sum': '🐹', // Rust '.rs': '🦀', // PHP '.php': '🐘', '.phtml': '🐘', // Ruby '.rb': '💎', '.erb': '💎', '.rake': '💎', '.gemspec': '💎', // Swift '.swift': '🍎', // Kotlin '.kt': '🟦', '.kts': '🟦', // Scala '.scala': '🔴', '.sc': '🔴', // C# '.cs': '🟣', '.csproj': '🟣', '.sln': '🟣', // F# '.fs': '🟣', '.fsx': '🟣', '.fsproj': '🟣', // Dart '.dart': '🔵', // R '.r': '📊', '.R': '📊', '.Rmd': '📊', // MATLAB '.m': '📊', '.mat': '📊', // Julia '.jl': '🟣', // Perl '.pl': '🐪', '.pm': '🐪', '.t': '🐪', // Lua '.lua': '🔵', // Haskell '.hs': '🟣', '.lhs': '🟣', // Clojure '.clj': '🟢', '.cljs': '🟢', '.edn': '🟢', // Elixir '.ex': '🟣', '.exs': '🟣', '.eex': '🟣', // Erlang '.erl': '🔴', // OCaml '.ml': '🟠', '.mli': '🟠', // Nim '.nim': '🟡', // Zig '.zig': '🟡', // V '.v': '🔵', // Assembly '.asm': '⚙️', '.s': '⚙️', '.S': '⚙️', // Shell Scripts '.sh': '🐚', '.bash': '🐚', '.zsh': '🐚', '.fish': '🐚', '.bat': '🐚', '.cmd': '🐚', '.ps1': '🐚', // Configuration Files '.yml': '⚙️', '.yaml': '⚙️', '.xml': '📄', '.toml': '⚙️', '.ini': '⚙️', '.cfg': '⚙️', '.conf': '⚙️', // Documentation '.md': '📝', '.mdx': '📝', '.rst': '📝', '.txt': '📄', '.adoc': '📝', // Images '.svg': '🖼️', '.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️', '.ico': '🖼️', '.bmp': '🖼️', '.tiff': '🖼️', '.webp': '🖼️', // Documents '.pdf': '📕', '.doc': '📄', '.docx': '📄', '.xls': '📊', '.xlsx': '📊', '.ppt': '📊', '.pptx': '📊', // Archives '.zip': '📦', '.tar': '📦', '.gz': '📦', '.bz2': '📦', '.rar': '📦', '.7z': '📦', // Database '.sql': '🗄️', '.db': '🗄️', '.sqlite': '🗄️', // GraphQL '.graphql': '🟣', '.gql': '🟣', // Docker dockerfile: '🐳', // Makefiles makefile: '⚙️', // Special Files '.gitignore': '🚫', '.env': '🔒', '.lock': '🔒', '.log': '📋', '.tmp': '🗑️', '.cache': '🗑️', '.bak': '🗑️', '.old': '🗑️', }; // Check for special filenames (case-insensitive) if (basename === 'dockerfile') return '🐳'; if (basename === 'makefile') return '⚙️'; if (basename === 'readme' || basename.startsWith('readme.')) return '📖'; if (basename === 'license' || basename.startsWith('license.')) return '📜'; if (basename === 'changelog' || basename.startsWith('changelog.')) return '📝'; if (basename === '.gitignore') return '🚫'; if (basename === '.env') return '🔒'; return iconMap[ext] || '📄'; } export async function readGitignore(rootPath: string): Promise<string[]> { const gitignorePath = path.join(rootPath, '.gitignore'); try { const content = await vscode.workspace.fs.readFile(vscode.Uri.file(gitignorePath)); return content .toString() .split('\n') .filter(line => line.trim() && !line.startsWith('#')) .map(line => line.trim()); } catch { return []; } } export function shouldExclude(item: string): boolean { // Get user-defined exclusions from settings const config = vscode.workspace.getConfiguration('filetree-pro'); const userExclusions = config.get<string[]>('exclude', []); // Common folders and files to exclude const defaultExcludePatterns = [ // Build and dependency folders 'node_modules', 'dist', 'build', 'out', 'target', 'bin', 'obj', '.next', '.nuxt', '.output', 'coverage', 'coverage.lcov', '.nyc_output', 'lib', 'libs', 'vendor', 'bower_components', 'jspm_packages', // Version control '.git', '.svn', '.hg', '.bzr', // IDE and editor folders '.vscode', '.idea', '.vs', '.cursor', '.atom', '.sublime-project', '.sublime-workspace', // Environment and config '.env', '.env.local', '.env.production', '.env.development', '.env.test', 'venv', '.venv', 'env', '.python-version', '.ruby-version', '.node-version', // OS generated '.DS_Store', 'Thumbs.db', '.Trash', 'desktop.ini', '$RECYCLE.BIN', // Logs and temp files '*.log', '*.tmp', '*.cache', '*.pyc', '__pycache__', '*.swp', '*.swo', '*~', // Package managers 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'composer.lock', 'Gemfile.lock', 'Pipfile.lock', 'poetry.lock', 'Cargo.lock', 'go.sum', 'mix.lock', // Build artifacts '*.min.js', '*.min.css', '*.map', '*.bundle.js', '*.chunk.js', // Generated files '.eslintcache', '.prettierignore', '.gitignore', '.gitattributes', '.editorconfig', '.babelrc', '.babelrc.js', 'tsconfig.json', 'tsconfig.build.json', 'webpack.config.js', 'rollup.config.js', 'vite.config.js', 'jest.config.js', 'karma.conf.js', 'gulpfile.js', 'gruntfile.js', 'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'composer.json', 'composer.lock', 'Gemfile', 'Gemfile.lock', 'requirements.txt', 'Pipfile', 'poetry.lock', 'Cargo.toml', 'Cargo.lock', 'go.mod', 'go.sum', 'mix.exs', 'mix.lock', 'pom.xml', 'build.gradle', 'build.sbt', 'project.clj', 'deps.edn', 'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'composer.json', 'composer.lock', 'Gemfile', 'Gemfile.lock', 'requirements.txt', 'Pipfile', 'poetry.lock', 'Cargo.toml', 'Cargo.lock', 'go.mod', 'go.sum', 'mix.exs', 'mix.lock', 'pom.xml', 'build.gradle', 'build.sbt', 'project.clj', 'deps.edn', ]; // Combine default and user exclusions const excludePatterns = [...defaultExcludePatterns, ...userExclusions]; const itemLower = item.toLowerCase(); // Check exact matches (case-insensitive) if (excludePatterns.some(pattern => pattern.toLowerCase() === itemLower)) { return true; } // Check wildcard patterns (case-insensitive) for (const pattern of excludePatterns) { if (pattern.includes('*')) { const regex = new RegExp(pattern.replace('*', '.*'), 'i'); // 'i' flag for case-insensitive if (regex.test(item)) { return true; } } } // Check for common build/artifact patterns (only exact matches) if ( itemLower === 'build' || itemLower === 'dist' || itemLower === 'cache' || itemLower === 'temp' || itemLower === 'tmp' ) { return true; } return false; } export async function generateMarkdownTree( rootPath: string, maxDepth: number, showIcons: boolean, progressCallback?: (message: string) => void ): Promise<string> { const lines: string[] = []; // Add header lines.push(`# File Tree: ${path.basename(rootPath)}`); lines.push(''); lines.push(`Generated on: ${new Date().toLocaleString()}`); lines.push(`Root path: \`${rootPath}\``); lines.push(''); lines.push('```'); // Generate tree structure await generateTreeLines(rootPath, '', lines, 0, maxDepth, showIcons, progressCallback); lines.push('```'); lines.push(''); lines.push('---'); lines.push('*Generated by FileTree Pro Extension*'); return lines.join('\n'); } export async function generateJsonTree( rootPath: string, maxDepth: number, showIcons: boolean, progressCallback?: (message: string) => void ): Promise<string> { const treeData = await buildTreeData(rootPath, maxDepth, showIcons, 0, progressCallback); const jsonOutput = { name: path.basename(rootPath), path: rootPath, type: 'directory', children: treeData, generated: new Date().toISOString(), generator: 'FileTree Pro Extension', note: 'Common build folders, dependencies, and temporary files are automatically excluded.', showIcons: showIcons, }; return JSON.stringify(jsonOutput, null, 2); } export async function generateSvgTree( rootPath: string, maxDepth: number, showIcons: boolean, progressCallback?: (message: string) => void ): Promise<string> { const treeData = await buildTreeData(rootPath, maxDepth, showIcons, 0, progressCallback); // Calculate dimensions based on content const nodeCount = countNodes(treeData); const svgWidth = 1000; const svgHeight = Math.max(800, nodeCount * 25 + 100); let svgContent = `<svg width="${svgWidth}" height="${svgHeight}" xmlns="http://www.w3.org/2000/svg"> <defs> <style> .title { font-family: 'Arial', sans-serif; font-size: 18px; font-weight: bold; fill: #2c3e50; } .subtitle { font-family: 'Arial', sans-serif; font-size: 12px; fill: #7f8c8d; } .folder-text { font-family: 'Consolas', 'Courier New', monospace; font-size: 14px; fill: #27ae60; font-weight: bold; } .file-text { font-family: 'Consolas', 'Courier New', monospace; font-size: 14px; fill: #3498db; } .icon { font-family: 'Arial', sans-serif; font-size: 16px; } .line { stroke: #bdc3c7; stroke-width: 1.5; } .background { fill: #f8f9fa; } .header-bg { fill: #ecf0f1; } </style> </defs> <!-- Background --> <rect width="${svgWidth}" height="${svgHeight}" class="background"/> <!-- Header --> <rect x="0" y="0" width="${svgWidth}" height="60" class="header-bg"/> <text x="20" y="25" class="title">📁 File Tree: ${path.basename(rootPath)}</text> <text x="20" y="45" class="subtitle">Generated: ${new Date().toLocaleString()} | FileTree Pro Extension</text> <!-- Tree Structure -->`; let yOffset = 80; const renderNode = (node: any, x: number, level: number, isLast: boolean = false) => { const icon = showIcons ? (node.type === 'directory' ? '📁' : getFileIcon(node.name)) : ''; const displayName = node.name + (node.type === 'directory' ? '/' : ''); const textClass = node.type === 'directory' ? 'folder-text' : 'file-text'; // Draw connecting line if not root if (level > 0) { svgContent += ` <line x1="${x - 15}" y1="${yOffset - 10}" x2="${x - 15}" y2="${yOffset}" class="line"/>`; } // Draw horizontal line svgContent += ` <line x1="${x - 15}" y1="${yOffset}" x2="${x - 5}" y2="${yOffset}" class="line"/>`; // Draw icon and text if (showIcons && icon) { svgContent += ` <text x="${x}" y="${yOffset + 5}" class="icon">${icon}</text> <text x="${x + 25}" y="${yOffset + 5}" class="${textClass}">${displayName}</text>`; } else { svgContent += ` <text x="${x}" y="${yOffset + 5}" class="${textClass}">${displayName}</text>`; } if (node.children && node.children.length > 0) { yOffset += 30; for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const isLastChild = i === node.children.length - 1; renderNode(child, x + 30, level + 1, isLastChild); yOffset += 25; } } yOffset += 10; }; for (const node of treeData) { renderNode(node, 30, 0); } svgContent += ` </svg>`; return svgContent; } export async function generateAsciiTree( rootPath: string, maxDepth: number, showIcons: boolean, progressCallback?: (message: string) => void ): Promise<string> { const lines: string[] = []; // Add header lines.push(`File Tree: ${path.basename(rootPath)}`); lines.push(`Generated on: ${new Date().toLocaleString()}`); lines.push(`Root path: ${rootPath}`); lines.push(''); lines.push('─'.repeat(80)); // Generate tree structure await generateTreeLines(rootPath, '', lines, 0, maxDepth, showIcons, progressCallback); lines.push(''); lines.push('─'.repeat(80)); lines.push('Generated by FileTree Pro Extension'); return lines.join('\n'); } function countNodes(nodes: any[]): number { let count = nodes.length; for (const node of nodes) { if (node.children && node.children.length > 0) { count += countNodes(node.children); } } return count; } async function buildTreeData( currentPath: string, maxDepth: number, showIcons: boolean, depth: number = 0, progressCallback?: (message: string) => void ): Promise<any[]> { if (depth > maxDepth) { return []; } try { // Update progress for large directories if (progressCallback && depth === 0) { progressCallback(`Building tree data: ${path.basename(currentPath)}`); } const items = await vscode.workspace.fs.readDirectory(vscode.Uri.file(currentPath)); const result: any[] = []; // 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 isExcluded = shouldExclude(item); 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 batchResult of batchResults) { if (batchResult) { if (batchResult.type === 'folder') { folders.push(batchResult.item); } else { files.push(batchResult.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 folderPath = path.join(currentPath, folder); const children = await buildTreeData( folderPath, maxDepth, showIcons, depth + 1, progressCallback ); result.push({ name: folder, type: 'directory', path: folderPath, children: children, icon: showIcons ? '📁' : null, }); // Yield control periodically to prevent blocking 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]; result.push({ name: file, type: 'file', path: path.join(currentPath, file), icon: showIcons ? getFileIcon(file) : null, }); // Yield control periodically to prevent blocking if (i % 50 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); } } return result; } catch (error) { return [ { name: 'Error reading directory', type: 'error', error: String(error), }, ]; } }