diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 2966dbc..e0b8dcd 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -6,5 +6,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default { version: 'stable', extensionDevelopmentPath: __dirname, - extensionTestsPath: path.join(__dirname, 'out', 'test') + extensionTestsPath: path.join(__dirname, 'out', 'test'), + testFiles: ['**/**.test.js'], + workspaceFolder: __dirname }; diff --git a/.vscodeignore b/.vscodeignore index 5d7beb3..42ad94e 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -8,6 +8,8 @@ src/** **/.eslintrc.json **/*.map **/*.ts +out/** +webpack.config.js .replit .breakpoints .local/** diff --git a/CHANGELOG.md b/CHANGELOG.md index d25d351..a6c76f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,4 +6,10 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] -- Initial release \ No newline at end of file +- Initial release + +## [0.0.1] - 2025-03-13 + +### Added + +- PromptGeneration extension with basic file selection and xml edits option. diff --git a/README.md b/README.md index 92a57f9..3bc73ff 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,48 @@ # Prompter -Code smarter with AI—no more messy copy-pasting. Prompter structures your prompts and applies AI changes seamlessly, streamlining your coding workflow. +Enhance your coding with AI—Prompter organizes your prompts and implements AI changes effortlessly, improving your development workflow. -## Why Prompter? +## Why Use Prompter? -- **Too much bloat in your repo?** Stop zipping everything—send only the files that matter. -- **LLM underperforming?** Cut the noise for sharper, more accurate responses. -- **Better AI coding?** Select just the right context to optimize results. +- **Reduce repository clutter** - Share only essential files instead of everything +- **Improve AI responses** - Eliminate irrelevant context for better results +- **Optimize coding assistance** - Target exactly what you need for precise AI help -Prompter empowers you to work efficiently with AI, reducing token waste and improving clarity. +Prompter helps you collaborate efficiently with AI, minimizing token usage and enhancing clarity. -## Features +## Key Features -- **Advanced File Selection & Token Estimation** - Precisely filter files and estimate token usage instantly for optimized, cost-effective prompts. +- **Smart File Selection & Token Counting** + Filter files and track token usage for efficient, economical prompts -- **Optimized XML Prompt** - Structured file trees, CodeMaps, content, and instructions in XML for maximum LLM clarity. +- **XML-Based Prompt Structure** + Organize file trees, CodeMaps, and instructions in XML for better AI comprehension -- **Structured XML Diffs** - Converts LLM-generated XML edits into precise, reviewable diffs—works at any file size. +- **Clean XML Diff Application** + Transform AI-generated XML changes into clear, reviewable diffs at any scale -- **Codemap Extraction** - Scans files locally to extract classes, functions, and references, minimizing tokens and hallucinations. Auto-detects referenced types. +- **Intelligent Code Scanning** + Extract code structure locally to reduce tokens and prevent hallucinations -- **Mac-Native Performance** - Built for macOS with native speed and responsiveness—because performance matters. +- **Cross-Platform Support** + Designed for any VSCode platform with native performance and responsiveness -- **Clipboard Integration** - Copy structured prompts into any AI chat app—your data stays local, no external API needed. +- **Direct Clipboard Support** + Copy structured prompts to any AI platform with your data remaining local -- **Works with Any Model** - Compatible with OpenAI, Anthropic, DeepSeek, Gemini, Azure, OpenRouter, and local models—private and offline when you need it. +- **Privacy-Focused Design** + Process everything locally without sending data to third parties -- **Privacy First** - Local models, offline scanning, and direct clipboard use—no intermediaries required. +## Quick Start -## Installation - -*(Note: Installation steps are assumed based on the VS Code context from other files. Adjust as needed.)* -1. Clone the repository: - ```bash - git clone - ``` -2. Open the project in VS Code. -3. Install dependencies: - ```bash - npm install - ``` -4. Build the extension: - ```bash - npm run compile - ``` -5. Press `F5` in VS Code to launch the extension in a development window. - -## Usage - -1. Open your project in VS Code. -2. Use the Prompter interface to select files and estimate tokens. -3. Generate a structured XML prompt via the clipboard. -4. Paste into your preferred AI model (e.g., ChatGPT, Claude, or a local LLM). -5. Apply the returned XML diffs directly through Prompter for seamless integration. +1. Install the extension in VS Code +2. Select relevant files through the Prompter sidebar +3. Generate and copy a structured XML prompt +4. Paste into your AI tool of choice ## Contributing -We welcome contributions! To get started: -1. Fork the repository. -2. Create a feature branch: `git checkout -b my-feature`. -3. Commit your changes: `git commit -m "Add my feature"`. -4. Push to your branch: `git push origin my-feature`. -5. Open a pull request. - -See `vsc-extension-quickstart.md` for development setup and testing details. +Contributions welcome! --- -Built with ❤️ by the Prompter team. - -Code smarter with AI—no more messy copy-pasting. Prompter structures your prompts and applies AI changes seamlessly, streamlining your coding workflow. +Built with ❤️ by the Prompter team. \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..bdebed9 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,19 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\.tsx?$': ['ts-jest', {}], + }, + testMatch: ['**/test/**/*.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.json', + }, + }, + // Mock the vscode module since it's not available during testing + moduleNameMapper: { + '^vscode$': '/src/test/vscode-mock.ts' + }, +}; \ No newline at end of file diff --git a/package.json b/package.json index 6cb2cf3..68af5c7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "Other" ], "activationEvents": [], - "main": "./out/extension.js", + "main": "./dist/extension.js", "contributes": { "commands": [ { @@ -21,7 +21,13 @@ }, { "command": "prompter.generatePrompt", - "title": "Copy" + "title": "Copy", + "icon": "$(clippy)" + }, + { + "command": "prompter.openTreeView", + "title": "Show Tree View", + "icon": "$(list-tree)" }, { "command": "prompter.openSettings", @@ -42,7 +48,8 @@ "prompter-sidebar": [ { "id": "prompterView", - "name": "Prompter" + "name": "Prompter", + "icon": "resources/icon.svg" } ] }, @@ -54,9 +61,14 @@ "when": "view == prompterView" }, { - "command": "prompter.openSettings", + "command": "prompter.openTreeView", "group": "navigation@2", "when": "view == prompterView" + }, + { + "command": "prompter.openSettings", + "group": "navigation@3", + "when": "view == prompterView" } ], "view/item/context": [ @@ -68,12 +80,14 @@ } }, "scripts": { - "vscode:prepublish": "npm run compile", + "vscode:prepublish": "npm run webpack:prod", + "webpack:dev": "webpack --mode development", + "webpack:prod": "webpack --mode production", "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", + "watch": "webpack --watch --mode development", "pretest": "npm run compile && npm run lint", "lint": "eslint src", - "test": "vscode-test" + "test": "node ./src/test/runTest.mjs" }, "devDependencies": { "@types/mocha": "^10.0.10", @@ -84,10 +98,13 @@ "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.4.1", "eslint": "^9.21.0", - "typescript": "^5.7.3" + "ts-loader": "^9.5.2", + "typescript": "^5.7.3", + "webpack": "^5.98.0", + "webpack-cli": "^6.0.1" }, "dependencies": { "@vscode/vsce": "^3.2.2", "ignore": "^7.0.3" } -} +} \ No newline at end of file diff --git a/resources/xml_formatting_instructions.xml b/resources/xml_formatting_instructions.xml index ca76f9a..0b928a2 100644 --- a/resources/xml_formatting_instructions.xml +++ b/resources/xml_formatting_instructions.xml @@ -16,7 +16,7 @@ Avoid placeholders like `...` or `// existing code here`. Provide complete lines 3. **modify** (search/replace) – For partial edits with + . 4. **delete** – Remove a file entirely (empty ). -### **Format to Follow for Repo Prompt's Diff Protocol** +### **Format to Follow for Diff Protocol** Describe your approach or reasoning here. @@ -288,7 +288,7 @@ Remove an obsolete file. 5. You can always **create** new files and **delete** existing files. Provide full code for create, and empty content for delete. Avoid creating files you know exist already. 6. If a file tree is provided, place your files logically within that structure. Respect the user’s relative or absolute paths. 7. Wrap your final output in ```XML ... ``` for clarity. -8. **Important:** Do not wrap any XML output in CDATA tags (i.e. ``). Repo Prompt expects raw XML exactly as shown in the examples. +8. **Important:** Do not wrap any XML output in CDATA tags (i.e. ``). We xpect raw XML exactly as shown in the examples. 9. **IMPORTANT** IF MAKING FILE CHANGES, YOU MUST USE THE AVAILABLE XML FORMATTING CAPABILITIES PROVIDED ABOVE - IT IS THE ONLY WAY FOR YOUR CHANGES TO BE APPLIED. 10. The final output must apply cleanly with no leftover syntax errors. \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index a0eba31..4b38705 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -60,26 +60,33 @@ export function activate(context: vscode.ExtensionContext) { // Update formatting instructions button text if that setting changed if (item.settingKey === 'includeFormattingInstructions') { xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ? - "$(check) Formatting Instructions" : "$(diff-added) Formatting Instructions"; + "$(check) Formatting Instructions" : "$(diff-added) XML Edits"; } } } }); // Create formatting instructions toggle button - const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - xmlEditsButton.text = "$(diff-added) Formatting Instructions"; + const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1); + xmlEditsButton.text = "$(diff-added) XML Edits"; xmlEditsButton.tooltip = "Toggle formatting instructions mode"; xmlEditsButton.command = 'prompter.toggleXmlEdits'; xmlEditsButton.show(); // Create copy button - const copyButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); + const copyButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 3); copyButton.text = "$(clippy) Copy"; copyButton.tooltip = "Generate and copy prompt"; copyButton.command = 'prompter.generatePrompt'; copyButton.show(); + // Create settings button + const settingsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 2); + settingsButton.text = "$(settings-gear)"; + settingsButton.tooltip = "Prompter Settings"; + settingsButton.command = 'prompter.openSettings'; + settingsButton.show(); + // Register command to toggle file selection let toggleSelectionCommand = vscode.commands.registerCommand('prompter.toggleSelection', (item: FileTreeItem) => { if (item.resourceUri) { @@ -95,13 +102,6 @@ export function activate(context: vscode.ExtensionContext) { "$(check) XML Edits" : "$(diff-added) XML Edits"; }); - // Create settings button - const settingsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - settingsButton.text = "$(settings-gear)"; - settingsButton.tooltip = "Prompter Settings"; - settingsButton.command = 'prompter.openSettings'; - settingsButton.show(); - // Register command to open settings let openSettingsCommand = vscode.commands.registerCommand('prompter.openSettings', () => { prompterTreeProvider.toggleSettingsView(); @@ -113,6 +113,11 @@ export function activate(context: vscode.ExtensionContext) { "Show Files" : "Prompter Settings"; }); + // Register command to show the tree view + let openTreeViewCommand = vscode.commands.registerCommand('prompter.openTreeView', () => { + prompterTreeProvider.showFilesView(); + }); + // Register command to generate prompt from selected files let generatePromptCommand = vscode.commands.registerCommand('prompter.generatePrompt', async () => { const selectedFiles = prompterTreeProvider.getSelectedFiles(); @@ -130,7 +135,14 @@ export function activate(context: vscode.ExtensionContext) { prompterTreeProvider.getSettings() ); - // Copy to clipboard + // Check if we got a valid prompt text + if (promptText === null) { + // Show warning if all files were filtered out + vscode.window.showWarningMessage('All selected files were filtered out by ignore patterns'); + return; + } + + // Copy to clipboard only if we have valid content await vscode.env.clipboard.writeText(promptText); vscode.window.showInformationMessage('Prompt copied to clipboard!'); } catch (error) { @@ -148,7 +160,8 @@ export function activate(context: vscode.ExtensionContext) { toggleSelectionCommand, toggleXmlEditsCommand, generatePromptCommand, - openSettingsCommand + openSettingsCommand, + openTreeViewCommand ); } diff --git a/src/providers/fileSelectionManager.ts b/src/providers/fileSelectionManager.ts index 16c3101..bad80bd 100644 --- a/src/providers/fileSelectionManager.ts +++ b/src/providers/fileSelectionManager.ts @@ -1,6 +1,26 @@ - import * as vscode from 'vscode'; import * as path from 'path'; +import * as fs from 'fs'; + +// Use require for ignore package with proper fallback +let ignoreFunc: () => any; +try { + const ignoreModule = require('ignore'); + if (typeof ignoreModule === 'function') { + ignoreFunc = ignoreModule; + } else if (ignoreModule && typeof ignoreModule.default === 'function') { + ignoreFunc = ignoreModule.default; + } else { + throw new Error('Ignore module is neither a function nor has a default function'); + } + console.log('Successfully loaded ignore function in FileSelectionManager'); +} catch (error) { + console.error('Error loading ignore package in FileSelectionManager:', error); + ignoreFunc = () => ({ + add: () => {}, + ignores: () => false + }); +} /** * Manages file selection state for the Prompter extension @@ -11,40 +31,134 @@ export class FileSelectionManager { constructor() {} /** - * Add a file to the selection + * Get the path relative to the workspace root + * @param filePath Absolute file path + * @param rootPath Workspace root path + * @returns Relative path + */ + private 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; + } + + /** + * Check if a path or any of its parent directories are ignored + * @param pathToCheck The path to check + * @param ig The ignore instance + * @param workspaceRoot The workspace root path + * @returns True if the path or any parent is ignored + */ + private isPathIgnored(pathToCheck: string, ig: any, workspaceRoot: string): boolean { + let currentPath = pathToCheck; + while (currentPath !== workspaceRoot) { + const relativePath = this.getRelativePath(currentPath, workspaceRoot); + const normalizedPath = relativePath.split(path.sep).join('/'); + const isIgnored = ig.ignores(normalizedPath); + console.log(`Checking ${normalizedPath}: ignored = ${isIgnored}`); + if (isIgnored) { + console.log(`Path ${pathToCheck} ignored because parent ${normalizedPath} is ignored`); + return true; + } + currentPath = path.dirname(currentPath); + if (currentPath === workspaceRoot) { + break; + } + } + return false; + } + + /** + * Load ignore patterns from .gitignore files in the given directory and its parent directories + * @param directoryPath The directory path to start searching from + * @param workspaceRoot The workspace root path + * @returns An ignore instance with loaded patterns + */ + private loadIgnorePatternsFromDirectory(directoryPath: string, workspaceRoot: string): any { + const ig = ignoreFunc(); + let currentDir = directoryPath; + + // Check for .gitignore in the current directory and all parent directories up to workspace root + while (currentDir.startsWith(workspaceRoot)) { + try { + const gitignorePath = path.join(currentDir, '.gitignore'); + if (fs.existsSync(gitignorePath)) { + const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); + ig.add(gitignoreContent); + console.log(`Loaded .gitignore patterns from ${gitignorePath}`); + } + } catch (error) { + console.error(`Error loading .gitignore from ${currentDir}:`, error); + } + + // Stop if we've reached the workspace root + if (currentDir === workspaceRoot) { + break; + } + + // Move up to parent directory + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { // Avoid infinite loop if we've reached the root + break; + } + currentDir = parentDir; + } + + return ig; + } + + /** + * Add a file to the selection if it's not ignored */ addFile(filePath: string): void { + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; + const dirPath = path.dirname(filePath); + const ig = this.loadIgnorePatternsFromDirectory(dirPath, workspaceRoot); + + if (this.isPathIgnored(filePath, ig, workspaceRoot)) { + console.log(`Ignoring file ${filePath} because it or a parent directory is ignored`); + return; + } this.selectedFiles.add(filePath); console.log(`Added ${filePath} to selection`); } /** - * Add a directory and all its contents to the selection + * Add a directory and all its non-ignored contents to the selection */ async addDirectory(dirPath: string): Promise { + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; + const ig = this.loadIgnorePatternsFromDirectory(dirPath, workspaceRoot); + + const relativeDirPath = this.getRelativePath(dirPath, workspaceRoot); + const normalizedDirPath = relativeDirPath.split(path.sep).join('/'); + if (ig.ignores(normalizedDirPath)) { + console.log(`Directory ${dirPath} is ignored, skipping its contents`); + return; + } + + this.selectedFiles.add(dirPath); + console.log(`Added directory ${dirPath} to selection`); + 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); - + const relativeFilePath = this.getRelativePath(filePath, workspaceRoot); + const normalizedFilePath = relativeFilePath.split(path.sep).join('/'); if (type === vscode.FileType.Directory) { - // Recursively process subdirectories await this.addDirectory(filePath); - } else { - // Add files + } else if (!ig.ignores(normalizedFilePath)) { this.selectedFiles.add(filePath); - console.log(`Added ${filePath} to selection (from directory)`); + console.log(`Added ${filePath} to selection`); + } else { + console.log(`Ignoring ${filePath} due to ignore patterns`); } } } catch (error) { - console.error(`Error adding directory to selection: ${dirPath}`, error); + console.error(`Error adding directory ${dirPath}:`, error); } } @@ -61,22 +175,14 @@ export class FileSelectionManager { */ async removeDirectory(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.removeDirectory(filePath); } else { - // Remove files this.selectedFiles.delete(filePath); console.log(`Removed ${filePath} from selection (from directory)`); } diff --git a/src/providers/prompterTreeProvider.ts b/src/providers/prompterTreeProvider.ts index 97b8551..5883e1a 100644 --- a/src/providers/prompterTreeProvider.ts +++ b/src/providers/prompterTreeProvider.ts @@ -32,6 +32,16 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider; -} - -// 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/test/index.ts b/src/test/index.ts new file mode 100644 index 0000000..e954d97 --- /dev/null +++ b/src/test/index.ts @@ -0,0 +1,33 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import Mocha from 'mocha'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true + }); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((resolve, reject) => { + try { + // Simple approach - add extension.test.js directly + // This will find our basic test file + mocha.addFile(path.resolve(testsRoot, 'test/extension.test.js')); + + // Run the mocha test + mocha.run((failures: number) => { + if (failures > 0) { + reject(new Error(`${failures} tests failed.`)); + } else { + resolve(); + } + }); + } catch (err) { + console.error(err); + reject(err); + } + }); +} diff --git a/src/test/runTest.mjs b/src/test/runTest.mjs new file mode 100644 index 0000000..cf153de --- /dev/null +++ b/src/test/runTest.mjs @@ -0,0 +1,59 @@ +import { runTests } from '@vscode/test-electron'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; +import * as os from 'os'; +import * as fs from 'fs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function main() { + try { + // Create a temporary test workspace + // Using the system temp directory instead of external volume + const tmpDir = os.tmpdir(); + const testWorkspaceDir = path.join(tmpDir, `vscode-test-workspace-${Math.random().toString(36).substring(2)}`); + const userDataDir = path.join(tmpDir, `vscode-test-user-data-${Math.random().toString(36).substring(2)}`); + + // Ensure the test workspace directory exists + if (!fs.existsSync(testWorkspaceDir)) { + fs.mkdirSync(testWorkspaceDir, { recursive: true }); + } + + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to the extension test script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, '../../out/test'); + + console.log('Running tests with the following configuration:'); + console.log(`Extension Development Path: ${extensionDevelopmentPath}`); + console.log(`Extension Tests Path: ${extensionTestsPath}`); + console.log(`Workspace Dir: ${testWorkspaceDir}`); + console.log(`User Data Dir: ${userDataDir}`); + + // Download VS Code, unzip it and run the integration test + await runTests({ + version: '1.98.0', // Specify the exact version your extension is built for + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [ + testWorkspaceDir, + '--disable-extensions', + `--user-data-dir=${userDataDir}`, + '--skip-getting-started', + '--skip-release-notes', + '--disable-telemetry', + '--disable-updates', + '--disable-crash-reporter', + '--disable-workspace-trust' + ] + }); + } catch (err) { + console.error('Failed to run tests', err); + process.exit(1); + } +} + +main(); diff --git a/src/utils/promptGenerator.ts b/src/utils/promptGenerator.ts index 48e17ea..acbaee0 100644 --- a/src/utils/promptGenerator.ts +++ b/src/utils/promptGenerator.ts @@ -3,26 +3,28 @@ 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'; -/** - * Utility class for generating prompts from selected files - */ +// Use require for ignore package with proper fallback +let ignoreFunc: () => any; +try { + const ignoreModule = require('ignore'); + if (typeof ignoreModule === 'function') { + ignoreFunc = ignoreModule; + } else if (ignoreModule && typeof ignoreModule.default === 'function') { + ignoreFunc = ignoreModule.default; + } else { + throw new Error('Ignore module is neither a function nor has a default function'); + } + console.log('Successfully loaded ignore function'); +} catch (error) { + console.error('Error loading ignore package:', error); + ignoreFunc = () => ({ + add: () => {}, + ignores: () => false + }); +} + export class PromptGenerator { /** * Generate a prompt from the selected files @@ -30,55 +32,82 @@ export class PromptGenerator { * @param settings Settings to apply when generating the prompt * @returns The generated prompt text */ - static async generatePrompt(selectedFiles: Set, settings: PrompterSettings): Promise { + static async generatePrompt(selectedFiles: Set, settings: PrompterSettings): Promise { if (selectedFiles.size === 0) { throw new Error('No files selected'); } - let totalTokens = 0; - const fileContents = new Map(); + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; + + // Create ignore instance using the resolved function + const ig = ignoreFunc(); + console.log('Created ignore instance'); + + // Load .gitignore patterns + try { + const gitignorePath = path.join(workspaceRoot, '.gitignore'); + if (fs.existsSync(gitignorePath)) { + const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); + ig.add(gitignoreContent); + console.log('Successfully loaded .gitignore patterns:', gitignoreContent.split('\n').filter(Boolean)); + } else { + console.log('No .gitignore file found at:', gitignorePath); + } + } catch (error) { + console.error('Error loading .gitignore:', error); + } - // Process each selected file + const fileContents = new Map(); + const filteredFiles = new Set(); + let totalTokens = 0; + + // Process and filter files for (const filePath of selectedFiles) { + try { + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + console.log(`Skipping directory: ${filePath}`); + continue; + } + } catch (error) { + console.error(`Error checking file stats for ${filePath}:`, error); + continue; + } + + const relativePath = this.getRelativePath(filePath, workspaceRoot); + const normalizedPath = relativePath.split(path.sep).join('/'); + + console.log(`Checking path: ${normalizedPath}`); + if (ig.ignores(normalizedPath)) { + console.log(`Ignoring file based on patterns: ${normalizedPath}`); + continue; + } + + console.log(`Processing file: ${normalizedPath}`); + filteredFiles.add(filePath); const content = await FileReader.readFileContent(filePath); const tokens = TokenEstimator.estimateTokens(content); totalTokens += tokens; fileContents.set(filePath, { content, tokens }); } - // Always generate XML prompt - return this.generateXMLPrompt(fileContents, settings); - } - - /** - * Generate a plain text prompt - * @param files Map of file paths to content and token counts - * @param settings Settings to apply - * @returns Plain text prompt - */ - private static generatePlainPrompt(files: Map, settings: PrompterSettings): string { - let promptText = ''; - let totalTokenCount = 0; - - for (const [filePath, { content, tokens }] of files) { - const fileName = path.basename(filePath); - totalTokenCount += tokens; - - // Add the file to the prompt - promptText += `File: ${fileName}\n`; - promptText += `${content}\n`; - - promptText += '\n'; + if (filteredFiles.size === 0) { + // Return null to signal that no files were available after filtering + return null; } - - // Add token count if enabled - if (settings.tokenCalculationEnabled) { - promptText += `\nEstimated token count: ${totalTokenCount}`; + + // Create filtered contents map + const filteredContents = new Map(); + for (const filePath of filteredFiles) { + if (fileContents.has(filePath)) { + filteredContents.set(filePath, fileContents.get(filePath)!); + } } - - return promptText; + + return this.generateXMLPrompt(filteredContents, settings); } - + + /** * Generate an XML formatted prompt following the new schema format * @param files Map of file paths to content and token counts @@ -88,13 +117,11 @@ export class PromptGenerator { private static generateXMLPrompt(files: Map, settings: PrompterSettings): string { const xmlParts: string[] = []; - // Store formatting instructions to add at the end if enabled let formattingInstructions = ''; if (settings.includeFormattingInstructions) { try { - // Get the extension path const extensionPath = vscode.extensions.getExtension('prompter')?.extensionPath || - path.join(__dirname, '..', '..'); + path.join(__dirname, '..', '..'); const formattingInstructionsPath = path.join(extensionPath, 'resources', 'xml_formatting_instructions.xml'); if (fs.existsSync(formattingInstructionsPath)) { @@ -107,29 +134,21 @@ export class PromptGenerator { } } - // Generate file map section if enabled in settings if (settings.includeFileMap) { xmlParts.push(''); - - // Get the workspace root path const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; xmlParts.push(workspaceRoot); - - // Create a tree representation of the files const fileTree = this.generateFileTree(files, workspaceRoot); xmlParts.push(fileTree); xmlParts.push(''); } - // Generate file contents section xmlParts.push(''); - // Add each file with its content for (const [filePath, { content, tokens }] of files) { const extension = path.extname(filePath); - let language = extension.substring(1); // Remove the dot + let language = extension.substring(1); - // Handle special cases for language detection if (extension === '.js' || extension === '.jsx') { language = 'javascript'; } else if (extension === '.ts' || extension === '.tsx') { @@ -146,13 +165,9 @@ export class PromptGenerator { language = 'json'; } - // Use content as is const formattedContent = content; - - // Get the workspace root path if not already defined const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; - // Add file entry xmlParts.push(`File: ${this.getRelativePath(filePath, workspaceRoot)}`); xmlParts.push(`\`\`\`${language}`); xmlParts.push(formattedContent); @@ -161,14 +176,11 @@ export class PromptGenerator { xmlParts.push(''); - // Calculate tokens for toast notification but don't include in XML if (settings.tokenCalculationEnabled) { const totalTokens = Array.from(files.values()).reduce((sum, { tokens }) => sum + tokens, 0); - // We'll show this in a toast notification but not include it in the XML vscode.window.showInformationMessage(`Total tokens: ${totalTokens}`); } - // Add formatting instructions at the end if enabled if (settings.includeFormattingInstructions && formattingInstructions) { xmlParts.push(formattingInstructions); } @@ -183,69 +195,43 @@ export class PromptGenerator { * @returns String representation of the file tree */ private static generateFileTree(files: Map, rootPath: string): string { - // Create a tree representation const treeLines: string[] = []; - // 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 + const ig = ignoreFunc(); 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); - - // 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; } - // 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]; const isDirectory = i < parts.length - 1; @@ -257,7 +243,6 @@ export class PromptGenerator { children: new Map() }); } else if (isDirectory) { - // Ensure it's marked as a directory if we encounter it again currentNode.children.get(part)!.isDirectory = true; } @@ -265,15 +250,12 @@ export class PromptGenerator { } } - // 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) { @@ -282,18 +264,14 @@ export class PromptGenerator { 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'); } diff --git a/tsconfig.webpack.json b/tsconfig.webpack.json new file mode 100644 index 0000000..7549416 --- /dev/null +++ b/tsconfig.webpack.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "dist", + "sourceMap": true, + "esModuleInterop": true + }, + "exclude": [ + "node_modules", + "src/test/**", + "**/*.test.ts" + ], + "include": [ + "src/**/*.ts" + ] +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..3267e96 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,46 @@ +const path = require('path'); +const webpack = require('webpack'); + +/** + * @type {import('webpack').Configuration} + */ +const config = { + target: 'node', + mode: 'none', // Set to 'production' for minified output + + entry: { + main: './src/extension.ts' + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../[resource-path]' + }, + devtool: 'source-map', + externals: { + vscode: 'commonjs vscode' // The vscode module is created on-the-fly and must be excluded + }, + resolve: { + extensions: ['.ts', '.js'] + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules|src\/test/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + configFile: path.resolve(__dirname, './tsconfig.webpack.json') + } + } + ] + } + ] + } +}; + +module.exports = config;