Compare commits

22 Commits

Author SHA1 Message Date
a89b6d757e Update .vscodeignore to exclude additional asset directories and files 2025-03-28 04:27:09 +00:00
1924971d22 Add avatar image asset 2025-03-28 04:27:05 +00:00
a31a1bd3e2 fix upload path
All checks were successful
Release VSCode Extension / release (push) Successful in 33s
2025-03-27 10:04:39 +00:00
d6450fd597 Add ajv
Some checks failed
Release VSCode Extension / release (push) Failing after 43s
2025-03-27 10:02:27 +00:00
c4f97e252d Bump node 2025-03-27 10:01:46 +00:00
77e4df420e Deps sorting and codegen for build
Some checks failed
Release VSCode Extension / release (push) Failing after 40s
2025-03-27 07:23:40 +00:00
f6467a82c2 refactor comments 2025-03-27 07:17:22 +00:00
d895f58963 no global vsce and using new name
Some checks failed
Release VSCode Extension / release (push) Failing after 26s
2025-03-27 07:14:53 +00:00
d9e44b78ce Merge pull request 'sorting-files' (#2) from sorting-files into main
Some checks failed
Release VSCode Extension / release (push) Failing after 1m22s
Reviewed-on: #2
2025-03-27 07:10:17 +00:00
4b38333d23 Release workflow 2025-03-27 07:08:29 +00:00
bea2ea2a0f Sort folders first then files 2025-03-27 06:28:16 +00:00
8246879bea Fix missing newline at end of file in extension.ts 2025-03-17 07:24:19 +00:00
72f21325dd Merge pull request 'feat-filter-ignore' (#1) from feat-filter-ignore into main
Reviewed-on: #1
2025-03-17 07:23:37 +00:00
7aece43a6b Enhance status bar with new buttons for tree view and settings; refactor existing button setup 2025-03-17 07:22:40 +00:00
d2b09eefbe Revise README.md for clarity and conciseness, updating sections on usage, features, and installation instructions 2025-03-13 08:52:22 +00:00
19a5dc658e Fix formatting instructions in XML documentation for clarity and correct spelling 2025-03-13 08:49:46 +00:00
f0f5b315e7 Update CHANGELOG.md to include initial release details and added features 2025-03-13 08:41:46 +00:00
5bb5236d3a Remove unused plain text prompt generation method from PromptGenerator 2025-03-13 08:29:43 +00:00
7eb0d49d07 Refactor test setup and improve prompt generation error handling 2025-03-13 08:28:00 +00:00
69475782eb Enhance file selection by loading ignore patterns from .gitignore in parent directories 2025-03-13 08:12:06 +00:00
385e35a8ad Use webpack bundler 2025-03-13 08:00:45 +00:00
2df0dc666b Intermediate still contains errors, but an attempt to solve filtering as per .gitignore 2025-03-12 15:05:52 +00:00
18 changed files with 600 additions and 471 deletions

View File

@@ -0,0 +1,84 @@
name: Release VSCode Extension
run-name: ${{ gitea.actor }} is releasing a new version 🚀
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
jobs:
release:
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository code
- name: Checkout code
uses: actions/checkout@v4
# Step 2: Set up Node.js environment
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '23.9.0'
# Step 3: Install jq for JSON parsing
- name: Install jq
run: sudo apt-get install -y jq
# Step 4: Install project dependencies inside prompter directory
- name: Install dependencies
run: npm install
# Step 5: Install vsce
- name: Install vsce
run: npm install @vscode/vsce
# Step 6: Install additional dev dependencies for building inside prompter directory
- name: Install build tools
run: npm install --save-dev webpack webpack-cli ts-loader codegen
# Step 7: Build the extension inside prompter directory
- name: Build the extension
run: npm run webpack:prod
# Step 8: Package the extension into a VSIX file inside prompter directory
- name: Package the extension
run: npx vsce package
# Step 9: Extract the tag name from gitea.ref
- name: Extract tag name
run: |
TAG_NAME=$(echo "${{ gitea.ref }}" | sed 's/refs\/tags\///')
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
# Step 10: Determine the VSIX file name from package.json inside prompter directory
- name: Get VSIX file name
run: |
NAME=$(jq -r .name package.json)
VERSION=$(jq -r .version package.json)
VSIX_FILE="${NAME}-${VERSION}.vsix"
echo "VSIX_FILE=$VSIX_FILE" >> $GITHUB_ENV
# Step 11: Create a new release in Gitea
- name: Create release
run: |
curl -X POST \
-H "Authorization: token ${{ gitea.token }}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG_NAME\", \"name\": \"Release $TAG_NAME\", \"body\": \"Automated release for version $TAG_NAME\"}" \
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases
# Step 12: Retrieve the release ID
- name: Get release ID
run: |
RELEASE_ID=$(curl -s -H "Authorization: token ${{ gitea.token }}" \
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases/tags/$TAG_NAME | jq .id)
echo "RELEASE_ID=$RELEASE_ID" >> $GITHUB_ENV
# Step 13: Upload the VSIX file as a release asset (from prompter directory)
- name: Upload VSIX to release
run: |
curl -X POST \
-H "Authorization: token ${{ gitea.token }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$VSIX_FILE" \
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases/$RELEASE_ID/assets?name=$VSIX_FILE

View File

@@ -6,5 +6,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default { export default {
version: 'stable', version: 'stable',
extensionDevelopmentPath: __dirname, extensionDevelopmentPath: __dirname,
extensionTestsPath: path.join(__dirname, 'out', 'test') extensionTestsPath: path.join(__dirname, 'out', 'test'),
testFiles: ['**/**.test.js'],
workspaceFolder: __dirname
}; };

View File

@@ -8,6 +8,8 @@ src/**
**/.eslintrc.json **/.eslintrc.json
**/*.map **/*.map
**/*.ts **/*.ts
out/**
webpack.config.js
.replit .replit
.breakpoints .breakpoints
.local/** .local/**
@@ -27,3 +29,7 @@ package-lock.json
# Assets not needed in the final package # Assets not needed in the final package
attached_assets/** attached_assets/**
.config .config
assets/
.gitea/**
*.md
.vscode-test.mjs

View File

@@ -6,4 +6,10 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
## [Unreleased] ## [Unreleased]
- Initial release - Initial release
## [0.0.1] - 2025-03-13
### Added
- PromptGeneration extension with basic file selection and xml edits option.

View File

@@ -1,79 +1,48 @@
# Prompter # 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. - **Reduce repository clutter** - Share only essential files instead of everything
- **LLM underperforming?** Cut the noise for sharper, more accurate responses. - **Improve AI responses** - Eliminate irrelevant context for better results
- **Better AI coding?** Select just the right context to optimize 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** - **Smart File Selection & Token Counting**
Precisely filter files and estimate token usage instantly for optimized, cost-effective prompts. Filter files and track token usage for efficient, economical prompts
- **Optimized XML Prompt** - **XML-Based Prompt Structure**
Structured file trees, CodeMaps, content, and instructions in XML for maximum LLM clarity. Organize file trees, CodeMaps, and instructions in XML for better AI comprehension
- **Structured XML Diffs** - **Clean XML Diff Application**
Converts LLM-generated XML edits into precise, reviewable diffs—works at any file size. Transform AI-generated XML changes into clear, reviewable diffs at any scale
- **Codemap Extraction** - **Intelligent Code Scanning**
Scans files locally to extract classes, functions, and references, minimizing tokens and hallucinations. Auto-detects referenced types. Extract code structure locally to reduce tokens and prevent hallucinations
- **Mac-Native Performance** - **Cross-Platform Support**
Built for macOS with native speed and responsiveness—because performance matters. Designed for any VSCode platform with native performance and responsiveness
- **Clipboard Integration** - **Direct Clipboard Support**
Copy structured prompts into any AI chat app—your data stays local, no external API needed. Copy structured prompts to any AI platform with your data remaining local
- **Works with Any Model** - **Privacy-Focused Design**
Compatible with OpenAI, Anthropic, DeepSeek, Gemini, Azure, OpenRouter, and local models—private and offline when you need it. Process everything locally without sending data to third parties
- **Privacy First** ## Quick Start
Local models, offline scanning, and direct clipboard use—no intermediaries required.
## Installation 1. Install the extension in VS Code
2. Select relevant files through the Prompter sidebar
*(Note: Installation steps are assumed based on the VS Code context from other files. Adjust as needed.)* 3. Generate and copy a structured XML prompt
1. Clone the repository: 4. Paste into your AI tool of choice
```bash
git clone <repository-url>
```
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.
## Contributing ## Contributing
We welcome contributions! To get started: Contributions welcome!
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.
--- ---
Built with ❤️ by the Prompter team. 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.

BIN
assets/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

19
jest.config.js Normal file
View File

@@ -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$': '<rootDir>/src/test/vscode-mock.ts'
},
};

View File

@@ -2,9 +2,9 @@
"name": "prompter", "name": "prompter",
"displayName": "Prompter", "displayName": "Prompter",
"description": "Easy prompt generation and apply edits using prompter.", "description": "Easy prompt generation and apply edits using prompter.",
"version": "0.0.1", "version": "0.0.2",
"publisher": "abhishekbhakat", "publisher": "abhishekbhakat",
"repository": "https://github.com/abhishekbhakat/prompter", "repository": "https://git.bhakat.dev/abhishekbhakat/Prompter",
"engines": { "engines": {
"vscode": "^1.98.0" "vscode": "^1.98.0"
}, },
@@ -12,7 +12,7 @@
"Other" "Other"
], ],
"activationEvents": [], "activationEvents": [],
"main": "./out/extension.js", "main": "./dist/extension.js",
"contributes": { "contributes": {
"commands": [ "commands": [
{ {
@@ -21,7 +21,13 @@
}, },
{ {
"command": "prompter.generatePrompt", "command": "prompter.generatePrompt",
"title": "Copy" "title": "Copy",
"icon": "$(clippy)"
},
{
"command": "prompter.openTreeView",
"title": "Show Tree View",
"icon": "$(list-tree)"
}, },
{ {
"command": "prompter.openSettings", "command": "prompter.openSettings",
@@ -42,7 +48,8 @@
"prompter-sidebar": [ "prompter-sidebar": [
{ {
"id": "prompterView", "id": "prompterView",
"name": "Prompter" "name": "Prompter",
"icon": "resources/icon.svg"
} }
] ]
}, },
@@ -54,9 +61,14 @@
"when": "view == prompterView" "when": "view == prompterView"
}, },
{ {
"command": "prompter.openSettings", "command": "prompter.openTreeView",
"group": "navigation@2", "group": "navigation@2",
"when": "view == prompterView" "when": "view == prompterView"
},
{
"command": "prompter.openSettings",
"group": "navigation@3",
"when": "view == prompterView"
} }
], ],
"view/item/context": [ "view/item/context": [
@@ -68,12 +80,14 @@
} }
}, },
"scripts": { "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 ./", "compile": "tsc -p ./",
"watch": "tsc -watch -p ./", "watch": "webpack --watch --mode development",
"pretest": "npm run compile && npm run lint", "pretest": "npm run compile && npm run lint",
"lint": "eslint src", "lint": "eslint src",
"test": "vscode-test" "test": "node ./src/test/runTest.mjs"
}, },
"devDependencies": { "devDependencies": {
"@types/mocha": "^10.0.10", "@types/mocha": "^10.0.10",
@@ -83,11 +97,14 @@
"@typescript-eslint/parser": "^8.25.0", "@typescript-eslint/parser": "^8.25.0",
"@vscode/test-cli": "^0.0.10", "@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1", "@vscode/test-electron": "^2.4.1",
"ajv": "^8.17.0",
"eslint": "^9.21.0", "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": { "dependencies": {
"@vscode/vsce": "^3.2.2",
"ignore": "^7.0.3" "ignore": "^7.0.3"
} }
} }

View File

@@ -16,7 +16,7 @@ Avoid placeholders like `...` or `// existing code here`. Provide complete lines
3. **modify** (search/replace) For partial edits with <search> + <content>. 3. **modify** (search/replace) For partial edits with <search> + <content>.
4. **delete** Remove a file entirely (empty <content>). 4. **delete** Remove a file entirely (empty <content>).
### **Format to Follow for Repo Prompt's Diff Protocol** ### **Format to Follow for Diff Protocol**
<Plan> <Plan>
Describe your approach or reasoning here. 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. 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 users relative or absolute paths. 6. If a file tree is provided, place your files logically within that structure. Respect the users relative or absolute paths.
7. Wrap your final output in ```XML ... ``` for clarity. 7. Wrap your final output in ```XML ... ``` for clarity.
8. **Important:** Do not wrap any XML output in CDATA tags (i.e. `<![CDATA[ ... ]]>`). Repo Prompt expects raw XML exactly as shown in the examples. 8. **Important:** Do not wrap any XML output in CDATA tags (i.e. `<![CDATA[ ... ]]>`). 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. 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. 10. The final output must apply cleanly with no leftover syntax errors.
</xml_formatting_instructions> </xml_formatting_instructions>

View File

@@ -60,26 +60,33 @@ export function activate(context: vscode.ExtensionContext) {
// Update formatting instructions button text if that setting changed // Update formatting instructions button text if that setting changed
if (item.settingKey === 'includeFormattingInstructions') { if (item.settingKey === 'includeFormattingInstructions') {
xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ? xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ?
"$(check) Formatting Instructions" : "$(diff-added) Formatting Instructions"; "$(check) Formatting Instructions" : "$(diff-added) XML Edits";
} }
} }
} }
}); });
// Create formatting instructions toggle button // Create formatting instructions toggle button
const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1);
xmlEditsButton.text = "$(diff-added) Formatting Instructions"; xmlEditsButton.text = "$(diff-added) XML Edits";
xmlEditsButton.tooltip = "Toggle formatting instructions mode"; xmlEditsButton.tooltip = "Toggle formatting instructions mode";
xmlEditsButton.command = 'prompter.toggleXmlEdits'; xmlEditsButton.command = 'prompter.toggleXmlEdits';
xmlEditsButton.show(); xmlEditsButton.show();
// Create copy button // 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.text = "$(clippy) Copy";
copyButton.tooltip = "Generate and copy prompt"; copyButton.tooltip = "Generate and copy prompt";
copyButton.command = 'prompter.generatePrompt'; copyButton.command = 'prompter.generatePrompt';
copyButton.show(); 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 // Register command to toggle file selection
let toggleSelectionCommand = vscode.commands.registerCommand('prompter.toggleSelection', (item: FileTreeItem) => { let toggleSelectionCommand = vscode.commands.registerCommand('prompter.toggleSelection', (item: FileTreeItem) => {
if (item.resourceUri) { if (item.resourceUri) {
@@ -95,13 +102,6 @@ export function activate(context: vscode.ExtensionContext) {
"$(check) XML Edits" : "$(diff-added) XML Edits"; "$(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 // Register command to open settings
let openSettingsCommand = vscode.commands.registerCommand('prompter.openSettings', () => { let openSettingsCommand = vscode.commands.registerCommand('prompter.openSettings', () => {
prompterTreeProvider.toggleSettingsView(); prompterTreeProvider.toggleSettingsView();
@@ -113,6 +113,11 @@ export function activate(context: vscode.ExtensionContext) {
"Show Files" : "Prompter Settings"; "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 // Register command to generate prompt from selected files
let generatePromptCommand = vscode.commands.registerCommand('prompter.generatePrompt', async () => { let generatePromptCommand = vscode.commands.registerCommand('prompter.generatePrompt', async () => {
const selectedFiles = prompterTreeProvider.getSelectedFiles(); const selectedFiles = prompterTreeProvider.getSelectedFiles();
@@ -130,7 +135,14 @@ export function activate(context: vscode.ExtensionContext) {
prompterTreeProvider.getSettings() 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); await vscode.env.clipboard.writeText(promptText);
vscode.window.showInformationMessage('Prompt copied to clipboard!'); vscode.window.showInformationMessage('Prompt copied to clipboard!');
} catch (error) { } catch (error) {
@@ -148,7 +160,8 @@ export function activate(context: vscode.ExtensionContext) {
toggleSelectionCommand, toggleSelectionCommand,
toggleXmlEditsCommand, toggleXmlEditsCommand,
generatePromptCommand, generatePromptCommand,
openSettingsCommand openSettingsCommand,
openTreeViewCommand
); );
} }
@@ -157,4 +170,4 @@ export function activate(context: vscode.ExtensionContext) {
*/ */
export function deactivate() { export function deactivate() {
// Clean up resources when the extension is deactivated // Clean up resources when the extension is deactivated
} }

View File

@@ -1,6 +1,26 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as path from 'path'; 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 * Manages file selection state for the Prompter extension
@@ -11,40 +31,134 @@ export class FileSelectionManager {
constructor() {} 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 { 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); this.selectedFiles.add(filePath);
console.log(`Added ${filePath} to selection`); 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<void> { async addDirectory(dirPath: string): Promise<void> {
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 { 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)); const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
// Process each item
for (const [name, type] of files) { for (const [name, type] of files) {
const filePath = path.join(dirPath, name); const filePath = path.join(dirPath, name);
const relativeFilePath = this.getRelativePath(filePath, workspaceRoot);
const normalizedFilePath = relativeFilePath.split(path.sep).join('/');
if (type === vscode.FileType.Directory) { if (type === vscode.FileType.Directory) {
// Recursively process subdirectories
await this.addDirectory(filePath); await this.addDirectory(filePath);
} else { } else if (!ig.ignores(normalizedFilePath)) {
// Add files
this.selectedFiles.add(filePath); 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) { } 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<void> { async removeDirectory(dirPath: string): Promise<void> {
try { try {
// Remove the directory itself
this.selectedFiles.delete(dirPath); this.selectedFiles.delete(dirPath);
console.log(`Removed directory ${dirPath} from selection`); console.log(`Removed directory ${dirPath} from selection`);
// Read directory contents
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath)); const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
// Process each item
for (const [name, type] of files) { for (const [name, type] of files) {
const filePath = path.join(dirPath, name); const filePath = path.join(dirPath, name);
if (type === vscode.FileType.Directory) { if (type === vscode.FileType.Directory) {
// Recursively process subdirectories
await this.removeDirectory(filePath); await this.removeDirectory(filePath);
} else { } else {
// Remove files
this.selectedFiles.delete(filePath); this.selectedFiles.delete(filePath);
console.log(`Removed ${filePath} from selection (from directory)`); console.log(`Removed ${filePath} from selection (from directory)`);
} }

View File

@@ -32,6 +32,16 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider<FileTreeIte
this.refresh(); this.refresh();
} }
showSettingsView(): void {
this.showingSettings = true;
this.refresh();
}
showFilesView(): void {
this.showingSettings = false;
this.refresh();
}
getTreeItem(element: FileTreeItem | SettingTreeItem): vscode.TreeItem { getTreeItem(element: FileTreeItem | SettingTreeItem): vscode.TreeItem {
// Return the element as is if it's a SettingTreeItem // Return the element as is if it's a SettingTreeItem
if (element instanceof SettingTreeItem) { if (element instanceof SettingTreeItem) {
@@ -75,7 +85,21 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider<FileTreeIte
try { try {
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath)); const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
return files.map(([name, type]) => { // Sort by type first (directories first), then alphabetically by name
const sortedFiles = [...files].sort((a, b) => {
const [nameA, typeA] = a;
const [nameB, typeB] = b;
// If types are different, directories (type 2) come before files (type 1)
if (typeA !== typeB) {
return typeB - typeA; // Descending order by type puts directories first
}
// If types are the same, sort alphabetically by name
return nameA.localeCompare(nameB, undefined, { sensitivity: 'base' });
});
return sortedFiles.map(([name, type]) => {
const filePath = path.join(dirPath, name); const filePath = path.join(dirPath, name);
const uri = vscode.Uri.file(filePath); const uri = vscode.Uri.file(filePath);

View File

@@ -1,251 +0,0 @@
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<string, TreeNode>;
}
// 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<string, TreeNode>()
};
// 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<string, TreeNode>()
});
} 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();

33
src/test/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import * as path from 'path';
import * as fs from 'fs';
import Mocha from 'mocha';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
color: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise<void>((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);
}
});
}

59
src/test/runTest.mjs Normal file
View File

@@ -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();

View File

@@ -3,26 +3,28 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { PrompterSettings } from '../models/settings'; import { PrompterSettings } from '../models/settings';
import { FileReader } from './fileReader'; 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'; import { TokenEstimator } from './tokenEstimator';
/** // Use require for ignore package with proper fallback
* Utility class for generating prompts from selected files 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 { export class PromptGenerator {
/** /**
* Generate a prompt from the selected files * Generate a prompt from the selected files
@@ -30,55 +32,82 @@ export class PromptGenerator {
* @param settings Settings to apply when generating the prompt * @param settings Settings to apply when generating the prompt
* @returns The generated prompt text * @returns The generated prompt text
*/ */
static async generatePrompt(selectedFiles: Set<string>, settings: PrompterSettings): Promise<string> { static async generatePrompt(selectedFiles: Set<string>, settings: PrompterSettings): Promise<string | null> {
if (selectedFiles.size === 0) { if (selectedFiles.size === 0) {
throw new Error('No files selected'); throw new Error('No files selected');
} }
let totalTokens = 0; const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
const fileContents = new Map<string, { content: string; tokens: number }>();
// 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<string, { content: string; tokens: number }>();
const filteredFiles = new Set<string>();
let totalTokens = 0;
// Process and filter files
for (const filePath of selectedFiles) { 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 content = await FileReader.readFileContent(filePath);
const tokens = TokenEstimator.estimateTokens(content); const tokens = TokenEstimator.estimateTokens(content);
totalTokens += tokens; totalTokens += tokens;
fileContents.set(filePath, { content, tokens }); fileContents.set(filePath, { content, tokens });
} }
// Always generate XML prompt if (filteredFiles.size === 0) {
return this.generateXMLPrompt(fileContents, settings); // Return null to signal that no files were available after filtering
} return null;
/**
* 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<string, { content: string; tokens: number }>, 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';
} }
// Add token count if enabled // Create filtered contents map
if (settings.tokenCalculationEnabled) { const filteredContents = new Map<string, { content: string; tokens: number }>();
promptText += `\nEstimated token count: ${totalTokenCount}`; 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 * Generate an XML formatted prompt following the new schema format
* @param files Map of file paths to content and token counts * @param files Map of file paths to content and token counts
@@ -88,13 +117,11 @@ export class PromptGenerator {
private static generateXMLPrompt(files: Map<string, { content: string; tokens: number }>, settings: PrompterSettings): string { private static generateXMLPrompt(files: Map<string, { content: string; tokens: number }>, settings: PrompterSettings): string {
const xmlParts: string[] = []; const xmlParts: string[] = [];
// Store formatting instructions to add at the end if enabled
let formattingInstructions = ''; let formattingInstructions = '';
if (settings.includeFormattingInstructions) { if (settings.includeFormattingInstructions) {
try { try {
// Get the extension path
const extensionPath = vscode.extensions.getExtension('prompter')?.extensionPath || const extensionPath = vscode.extensions.getExtension('prompter')?.extensionPath ||
path.join(__dirname, '..', '..'); path.join(__dirname, '..', '..');
const formattingInstructionsPath = path.join(extensionPath, 'resources', 'xml_formatting_instructions.xml'); const formattingInstructionsPath = path.join(extensionPath, 'resources', 'xml_formatting_instructions.xml');
if (fs.existsSync(formattingInstructionsPath)) { if (fs.existsSync(formattingInstructionsPath)) {
@@ -107,29 +134,21 @@ export class PromptGenerator {
} }
} }
// Generate file map section if enabled in settings
if (settings.includeFileMap) { if (settings.includeFileMap) {
xmlParts.push('<file_map>'); xmlParts.push('<file_map>');
// Get the workspace root path
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
xmlParts.push(workspaceRoot); xmlParts.push(workspaceRoot);
// Create a tree representation of the files
const fileTree = this.generateFileTree(files, workspaceRoot); const fileTree = this.generateFileTree(files, workspaceRoot);
xmlParts.push(fileTree); xmlParts.push(fileTree);
xmlParts.push('</file_map>'); xmlParts.push('</file_map>');
} }
// Generate file contents section
xmlParts.push('<file_contents>'); xmlParts.push('<file_contents>');
// Add each file with its content
for (const [filePath, { content, tokens }] of files) { for (const [filePath, { content, tokens }] of files) {
const extension = path.extname(filePath); 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') { if (extension === '.js' || extension === '.jsx') {
language = 'javascript'; language = 'javascript';
} else if (extension === '.ts' || extension === '.tsx') { } else if (extension === '.ts' || extension === '.tsx') {
@@ -146,13 +165,9 @@ export class PromptGenerator {
language = 'json'; language = 'json';
} }
// Use content as is
const formattedContent = content; const formattedContent = content;
// Get the workspace root path if not already defined
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
// Add file entry
xmlParts.push(`File: ${this.getRelativePath(filePath, workspaceRoot)}`); xmlParts.push(`File: ${this.getRelativePath(filePath, workspaceRoot)}`);
xmlParts.push(`\`\`\`${language}`); xmlParts.push(`\`\`\`${language}`);
xmlParts.push(formattedContent); xmlParts.push(formattedContent);
@@ -161,14 +176,11 @@ export class PromptGenerator {
xmlParts.push('</file_contents>'); xmlParts.push('</file_contents>');
// Calculate tokens for toast notification but don't include in XML
if (settings.tokenCalculationEnabled) { if (settings.tokenCalculationEnabled) {
const totalTokens = Array.from(files.values()).reduce((sum, { tokens }) => sum + tokens, 0); 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}`); vscode.window.showInformationMessage(`Total tokens: ${totalTokens}`);
} }
// Add formatting instructions at the end if enabled
if (settings.includeFormattingInstructions && formattingInstructions) { if (settings.includeFormattingInstructions && formattingInstructions) {
xmlParts.push(formattingInstructions); xmlParts.push(formattingInstructions);
} }
@@ -183,69 +195,43 @@ export class PromptGenerator {
* @returns String representation of the file tree * @returns String representation of the file tree
*/ */
private static generateFileTree(files: Map<string, any>, rootPath: string): string { private static generateFileTree(files: Map<string, any>, rootPath: string): string {
// Create a tree representation
const treeLines: string[] = []; const treeLines: string[] = [];
// Create a tree structure
interface TreeNode { interface TreeNode {
name: string; name: string;
isDirectory: boolean; isDirectory: boolean;
children: Map<string, TreeNode>; children: Map<string, TreeNode>;
} }
// Create root node
const root: TreeNode = { const root: TreeNode = {
name: path.basename(rootPath), name: path.basename(rootPath),
isDirectory: true, isDirectory: true,
children: new Map<string, TreeNode>() children: new Map<string, TreeNode>()
}; };
// Initialize the ignore instance const ig = ignoreFunc();
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 { try {
const gitignorePath = path.join(rootPath, '.gitignore'); const gitignorePath = path.join(rootPath, '.gitignore');
if (fs.existsSync(gitignorePath)) { if (fs.existsSync(gitignorePath)) {
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
// Add patterns from .gitignore
ig.add(gitignoreContent); ig.add(gitignoreContent);
// Always include .gitignore itself
ig.add('!.gitignore'); ig.add('!.gitignore');
} }
} catch (error) { } catch (error) {
console.error('Error reading .gitignore:', error); console.error('Error reading .gitignore:', error);
} }
// Build the tree structure
for (const filePath of files.keys()) { for (const filePath of files.keys()) {
const relativePath = this.getRelativePath(filePath, rootPath); 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('/'); const normalizedPath = relativePath.split(path.sep).join('/');
if (ig.ignores(normalizedPath)) { if (ig.ignores(normalizedPath)) {
continue; continue;
} }
// Split the path into parts
const parts = relativePath.split('/'); const parts = relativePath.split('/');
// Start from the root
let currentNode = root; let currentNode = root;
// Build the path in the tree
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
const part = parts[i]; const part = parts[i];
const isDirectory = i < parts.length - 1; const isDirectory = i < parts.length - 1;
@@ -257,7 +243,6 @@ export class PromptGenerator {
children: new Map<string, TreeNode>() children: new Map<string, TreeNode>()
}); });
} else if (isDirectory) { } else if (isDirectory) {
// Ensure it's marked as a directory if we encounter it again
currentNode.children.get(part)!.isDirectory = true; 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 => { const buildTreeLines = (node: TreeNode, prefix: string = '', isLast: boolean = true, parentPrefix: string = ''): void => {
// Skip the root node in the output
if (node !== root) { if (node !== root) {
const linePrefix = parentPrefix + (isLast ? '└── ' : '├── '); const linePrefix = parentPrefix + (isLast ? '└── ' : '├── ');
treeLines.push(`${linePrefix}${node.name}${node.isDirectory ? '' : ''}`); treeLines.push(`${linePrefix}${node.name}${node.isDirectory ? '' : ''}`);
} }
// Sort children: directories first, then files, both alphabetically
const sortedChildren = Array.from(node.children.values()) const sortedChildren = Array.from(node.children.values())
.sort((a, b) => { .sort((a, b) => {
if (a.isDirectory === b.isDirectory) { if (a.isDirectory === b.isDirectory) {
@@ -282,18 +264,14 @@ export class PromptGenerator {
return a.isDirectory ? -1 : 1; return a.isDirectory ? -1 : 1;
}); });
// Process children
sortedChildren.forEach((child, index) => { sortedChildren.forEach((child, index) => {
const isChildLast = index === sortedChildren.length - 1; const isChildLast = index === sortedChildren.length - 1;
const childParentPrefix = node === root ? '' : parentPrefix + (isLast ? ' ' : '│ '); const childParentPrefix = node === root ? '' : parentPrefix + (isLast ? ' ' : '│ ');
buildTreeLines(child, prefix, isChildLast, childParentPrefix); buildTreeLines(child, prefix, isChildLast, childParentPrefix);
}); });
}; };
// Build the tree lines
buildTreeLines(root); buildTreeLines(root);
return treeLines.join('\n'); return treeLines.join('\n');
} }

18
tsconfig.webpack.json Normal file
View File

@@ -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"
]
}

46
webpack.config.js Normal file
View File

@@ -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;