From 2d28f71e9b984e75895b927c9cbf7bfc2d361cef Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Wed, 12 Mar 2025 07:25:26 +0000 Subject: [PATCH] add support for recursive selection and deselection of files in the tree view --- .vscode-test.mjs | 10 + src/extension.ts | 24 ++- src/providers/prompterTreeProvider.ts | 86 ++++++++- src/test/fileTreeTest.ts | 251 ++++++++++++++++++++++++++ src/utils/promptGenerator.ts | 141 ++++++++++++--- 5 files changed, 473 insertions(+), 39 deletions(-) create mode 100644 .vscode-test.mjs create mode 100644 src/test/fileTreeTest.ts diff --git a/.vscode-test.mjs b/.vscode-test.mjs new file mode 100644 index 0000000..2966dbc --- /dev/null +++ b/.vscode-test.mjs @@ -0,0 +1,10 @@ +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default { + version: 'stable', + extensionDevelopmentPath: __dirname, + extensionTestsPath: path.join(__dirname, 'out', 'test') +}; diff --git a/src/extension.ts b/src/extension.ts index 5e630d7..a0eba31 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,19 +15,31 @@ export function activate(context: vscode.ExtensionContext) { // Get the workspace folder const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; + console.log('Workspace root:', workspaceRoot); // Create the tree data provider + console.log('Creating tree data provider...'); const prompterTreeProvider = new PrompterTreeProvider(workspaceRoot); + console.log('Tree data provider created successfully'); // Register the TreeView with checkbox support - const treeView = vscode.window.createTreeView('prompterView', { - treeDataProvider: prompterTreeProvider, - showCollapseAll: true, - canSelectMany: true - }); + console.log('Registering tree view with ID: prompterView'); + let treeView: vscode.TreeView; + try { + treeView = vscode.window.createTreeView('prompterView', { + treeDataProvider: prompterTreeProvider, + showCollapseAll: true, + canSelectMany: true + }); + console.log('Tree view registered successfully'); + } catch (error) { + console.error('Error registering tree view:', error); + // Create a fallback empty tree view to prevent further errors + treeView = {} as vscode.TreeView; + } // Handle checkbox changes - treeView.onDidChangeCheckboxState(e => { + treeView.onDidChangeCheckboxState((e: vscode.TreeCheckboxChangeEvent) => { console.log('Checkbox state changed'); for (const [item, state] of e.items) { if (item instanceof FileTreeItem) { diff --git a/src/providers/prompterTreeProvider.ts b/src/providers/prompterTreeProvider.ts index e968856..955e2d1 100644 --- a/src/providers/prompterTreeProvider.ts +++ b/src/providers/prompterTreeProvider.ts @@ -94,16 +94,92 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider { + try { + // Add the directory itself + this.selectedFiles.add(dirPath); + console.log(`Added directory ${dirPath} to selection`); + + // Read directory contents + const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath)); + + // Process each item + for (const [name, type] of files) { + const filePath = path.join(dirPath, name); + + if (type === vscode.FileType.Directory) { + // Recursively process subdirectories + await this.addDirectoryToSelection(filePath); + } else { + // Add files + this.selectedFiles.add(filePath); + console.log(`Added ${filePath} to selection (from directory)`); + } + } + } catch (error) { + console.error(`Error adding directory to selection: ${dirPath}`, error); + } + } + // Remove a file from the selection removeFromSelection(item: FileTreeItem): void { if (item.resourceUri) { - this.selectedFiles.delete(item.resourceUri.fsPath); - console.log(`Removed ${item.resourceUri.fsPath} from selection`); + const filePath = item.resourceUri.fsPath; + + // Check if it's a directory + if (item.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed || + item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded) { + // It's a directory, recursively remove all files + this.removeDirectoryFromSelection(filePath); + } else { + // It's a file, just remove it + this.selectedFiles.delete(filePath); + console.log(`Removed ${filePath} from selection`); + } + } + } + + // Recursively remove all files in a directory from the selection + private async removeDirectoryFromSelection(dirPath: string): Promise { + try { + // Remove the directory itself + this.selectedFiles.delete(dirPath); + console.log(`Removed directory ${dirPath} from selection`); + + // Read directory contents + const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath)); + + // Process each item + for (const [name, type] of files) { + const filePath = path.join(dirPath, name); + + if (type === vscode.FileType.Directory) { + // Recursively process subdirectories + await this.removeDirectoryFromSelection(filePath); + } else { + // Remove files + this.selectedFiles.delete(filePath); + console.log(`Removed ${filePath} from selection (from directory)`); + } + } + } catch (error) { + console.error(`Error removing directory from selection: ${dirPath}`, error); } } diff --git a/src/test/fileTreeTest.ts b/src/test/fileTreeTest.ts new file mode 100644 index 0000000..1b4e831 --- /dev/null +++ b/src/test/fileTreeTest.ts @@ -0,0 +1,251 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +// Using require for the ignore package due to its module export style +const ignoreLib = require('ignore'); + +// A simplified version of our tree structure for testing +interface TreeNode { + name: string; + isDirectory: boolean; + children: Map; +} + +// Test implementation of the file tree generation logic +function generateFileTree(files: string[], rootPath: string): string { + // Create a tree representation + const treeLines: string[] = []; + + // Create root node + const root: TreeNode = { + name: path.basename(rootPath), + isDirectory: true, + children: new Map() + }; + + // Initialize the ignore instance + const ig = ignoreLib(); + + // Read .gitignore patterns if available + try { + const gitignorePath = path.join(rootPath, '.gitignore'); + if (fs.existsSync(gitignorePath)) { + const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); + console.log('Using .gitignore content:'); + console.log(gitignoreContent); + + // Add patterns from .gitignore + ig.add(gitignoreContent); + // Always include .gitignore itself + ig.add('!.gitignore'); + + // Debug what's being ignored + console.log('\nIgnore patterns loaded. Testing patterns:'); + ['file1.txt', 'file2.log', 'node_modules/package.json', 'src/index.ts', 'temp/temp.txt'].forEach(testPath => { + console.log(`${testPath}: ${ig.ignores(testPath) ? 'IGNORED' : 'included'}`); + }); + console.log(); + } + } catch (error) { + console.error('Error reading .gitignore:', error); + } + + // Build the tree structure + for (const filePath of files) { + const relativePath = getRelativePath(filePath, rootPath); + + // Skip ignored files using the ignore package + // Use forward slashes for paths to ensure consistent matching + const normalizedPath = relativePath.split(path.sep).join('/'); + if (ig.ignores(normalizedPath)) { + console.log(`Ignoring: ${normalizedPath}`); + continue; + } + + // Split the path into parts + const parts = relativePath.split('/'); + + // Start from the root + let currentNode = root; + + // Build the path in the tree + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (!part) { continue; } // Skip empty parts + + const isDirectory = i < parts.length - 1; + + if (!currentNode.children.has(part)) { + currentNode.children.set(part, { + name: part, + isDirectory, + children: new Map() + }); + } else if (isDirectory) { + // Ensure it's marked as a directory if we encounter it again + currentNode.children.get(part)!.isDirectory = true; + } + + currentNode = currentNode.children.get(part)!; + } + } + + // Function to recursively build the tree lines + const buildTreeLines = (node: TreeNode, prefix: string = '', isLast: boolean = true, parentPrefix: string = ''): void => { + // Skip the root node in the output + if (node !== root) { + const linePrefix = parentPrefix + (isLast ? '└── ' : '├── '); + treeLines.push(`${linePrefix}${node.name}${node.isDirectory ? '' : ''}`); + } + + // Sort children: directories first, then files, both alphabetically + const sortedChildren = Array.from(node.children.values()) + .sort((a, b) => { + if (a.isDirectory === b.isDirectory) { + return a.name.localeCompare(b.name); + } + return a.isDirectory ? -1 : 1; + }); + + // Process children + sortedChildren.forEach((child, index) => { + const isChildLast = index === sortedChildren.length - 1; + const childParentPrefix = node === root ? '' : parentPrefix + (isLast ? ' ' : '│ '); + + buildTreeLines(child, prefix, isChildLast, childParentPrefix); + }); + }; + + // Build the tree lines + buildTreeLines(root); + + return treeLines.join('\n'); +} + +// Helper function to get relative path +function getRelativePath(filePath: string, rootPath: string): string { + if (filePath.startsWith(rootPath)) { + const relativePath = filePath.substring(rootPath.length); + return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; + } + return filePath; +} + +// Run tests +function runTests() { + console.log('Running file tree generation tests...'); + + // Create a temporary directory for our tests + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prompter-test-')); + console.log(`Created temp directory: ${tempDir}`); + + try { + // Test 1: Basic file structure + console.log('\nTest 1: Basic file structure'); + const files = [ + path.join(tempDir, 'file1.txt'), + path.join(tempDir, 'folder1/file2.txt'), + path.join(tempDir, 'folder1/subfolder/file3.txt'), + path.join(tempDir, 'folder2/file4.txt') + ]; + + // Create the directories and files + files.forEach(file => { + const dir = path.dirname(file); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(file, 'test content'); + }); + + const result1 = generateFileTree(files, tempDir); + console.log(result1); + + // Test 2: With .gitignore + console.log('\nTest 2: With .gitignore'); + // Create a .gitignore file with more explicit patterns + const gitignorePath = path.join(tempDir, '.gitignore'); + fs.writeFileSync(gitignorePath, '# Ignore log files\n*.log\n\n# Ignore node_modules directory\nnode_modules/\n\n# Ignore temp directory\ntemp/\n'); + console.log('Created .gitignore with content:'); + console.log(fs.readFileSync(gitignorePath, 'utf8')); + + // Create test files for .gitignore testing + const files2 = [ + path.join(tempDir, '.gitignore'), + path.join(tempDir, 'file1.txt'), + path.join(tempDir, 'file2.log'), // Should be ignored + path.join(tempDir, 'node_modules/package.json'), // Should be ignored + path.join(tempDir, 'src/index.ts'), + path.join(tempDir, 'temp/temp.txt') // Should be ignored + ]; + + console.log('Test files created:'); + files2.forEach(f => console.log(` - ${getRelativePath(f, tempDir)}`)); + + // Create the directories and files + files2.forEach(file => { + const dir = path.dirname(file); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + // Don't overwrite the .gitignore file + if (path.basename(file) !== '.gitignore') { + fs.writeFileSync(file, 'test content'); + } + }); + + const result2 = generateFileTree(files2, tempDir); + console.log(result2); + + // Test 3: Empty directories + console.log('\nTest 3: Empty directories'); + const emptyDir = path.join(tempDir, 'emptyDir'); + if (!fs.existsSync(emptyDir)) { + fs.mkdirSync(emptyDir, { recursive: true }); + } + + const files3 = [ + path.join(tempDir, 'file1.txt'), + path.join(tempDir, 'emptyDir') // Empty directory + ]; + + const result3 = generateFileTree(files3, tempDir); + console.log(result3); + + // Test 4: Sorting + console.log('\nTest 4: Sorting (directories first, then files)'); + const files4 = [ + path.join(tempDir, 'z_file.txt'), + path.join(tempDir, 'a_file.txt'), + path.join(tempDir, 'z_folder/file.txt'), + path.join(tempDir, 'a_folder/file.txt') + ]; + + // Create the directories and files + files4.forEach(file => { + const dir = path.dirname(file); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(file, 'test content'); + }); + + const result4 = generateFileTree(files4, tempDir); + console.log(result4); + + } catch (error) { + console.error('Test error:', error); + } finally { + // Clean up + console.log('\nCleaning up...'); + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + console.log(`Removed temp directory: ${tempDir}`); + } catch (cleanupError) { + console.error('Error during cleanup:', cleanupError); + } + } +} + +// Run the tests +runTests(); diff --git a/src/utils/promptGenerator.ts b/src/utils/promptGenerator.ts index d902859..48e17ea 100644 --- a/src/utils/promptGenerator.ts +++ b/src/utils/promptGenerator.ts @@ -3,6 +3,21 @@ import * as fs from 'fs'; import * as path from 'path'; import { PrompterSettings } from '../models/settings'; import { FileReader } from './fileReader'; +// Using require for the ignore package due to its module export style +let ignore: any; +try { + ignore = require('ignore'); + console.log('Successfully loaded ignore package'); +} catch (error) { + console.error('Error loading ignore package:', error); + // Fallback implementation if the package fails to load + ignore = { + default: () => ({ + add: () => {}, + ignores: () => false + }) + }; +} import { TokenEstimator } from './tokenEstimator'; /** @@ -168,47 +183,117 @@ export class PromptGenerator { * @returns String representation of the file tree */ private static generateFileTree(files: Map, rootPath: string): string { - // Create a simple tree representation + // Create a tree representation const treeLines: string[] = []; - // Group files by directory - const dirMap = new Map(); + // Create a tree structure + interface TreeNode { + name: string; + isDirectory: boolean; + children: Map; + } + // Create root node + const root: TreeNode = { + name: path.basename(rootPath), + isDirectory: true, + children: new Map() + }; + + // Initialize the ignore instance + let ig; + try { + ig = typeof ignore === 'function' ? ignore() : ignore.default(); + console.log('Successfully initialized ignore instance'); + } catch (error) { + console.error('Error initializing ignore instance:', error); + // Fallback implementation + ig = { + add: () => {}, + ignores: () => false + }; + } + + // Read .gitignore patterns if available + try { + const gitignorePath = path.join(rootPath, '.gitignore'); + if (fs.existsSync(gitignorePath)) { + const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); + // Add patterns from .gitignore + ig.add(gitignoreContent); + // Always include .gitignore itself + ig.add('!.gitignore'); + } + } catch (error) { + console.error('Error reading .gitignore:', error); + } + + // Build the tree structure for (const filePath of files.keys()) { const relativePath = this.getRelativePath(filePath, rootPath); - const dir = path.dirname(relativePath); - if (!dirMap.has(dir)) { - dirMap.set(dir, []); + // Skip ignored files using the ignore package + // Use forward slashes for paths to ensure consistent matching across platforms + const normalizedPath = relativePath.split(path.sep).join('/'); + if (ig.ignores(normalizedPath)) { + continue; } - dirMap.get(dir)?.push(path.basename(filePath)); - } - - // Sort directories - const sortedDirs = Array.from(dirMap.keys()).sort(); - - // Build the tree - for (let i = 0; i < sortedDirs.length; i++) { - const dir = sortedDirs[i]; - const isLast = i === sortedDirs.length - 1; - const prefix = isLast ? '└── ' : '├── '; + // Split the path into parts + const parts = relativePath.split('/'); - // Skip root directory - if (dir !== '.') { - treeLines.push(`${prefix}${dir}`); - } + // Start from the root + let currentNode = root; - // Add files - const files = dirMap.get(dir)?.sort() || []; - for (let j = 0; j < files.length; j++) { - const file = files[j]; - const isLastFile = j === files.length - 1; - const filePrefix = dir === '.' ? (isLastFile ? '└── ' : '├── ') : ' ' + (isLastFile ? '└── ' : '├── '); - treeLines.push(`${filePrefix}${file}`); + // Build the path in the tree + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const isDirectory = i < parts.length - 1; + + if (!currentNode.children.has(part)) { + currentNode.children.set(part, { + name: part, + isDirectory, + children: new Map() + }); + } else if (isDirectory) { + // Ensure it's marked as a directory if we encounter it again + currentNode.children.get(part)!.isDirectory = true; + } + + currentNode = currentNode.children.get(part)!; } } + // Function to recursively build the tree lines + const buildTreeLines = (node: TreeNode, prefix: string = '', isLast: boolean = true, parentPrefix: string = ''): void => { + // Skip the root node in the output + if (node !== root) { + const linePrefix = parentPrefix + (isLast ? '└── ' : '├── '); + treeLines.push(`${linePrefix}${node.name}${node.isDirectory ? '' : ''}`); + } + + // Sort children: directories first, then files, both alphabetically + const sortedChildren = Array.from(node.children.values()) + .sort((a, b) => { + if (a.isDirectory === b.isDirectory) { + return a.name.localeCompare(b.name); + } + return a.isDirectory ? -1 : 1; + }); + + // Process children + sortedChildren.forEach((child, index) => { + const isChildLast = index === sortedChildren.length - 1; + const childParentPrefix = node === root ? '' : parentPrefix + (isLast ? ' ' : '│ '); + + buildTreeLines(child, prefix, isChildLast, childParentPrefix); + }); + }; + + // Build the tree lines + buildTreeLines(root); + return treeLines.join('\n'); }