Compare commits

...

13 Commits

Author SHA1 Message Date
c18e4dac10 Merge pull request #1 from abhishekbhakat/feat-settings
Feat settings
2025-03-12 09:09:19 +00:00
5a6e1a5e90 remove duplication 2025-03-12 09:08:56 +00:00
134bdf1e5e Assistant checkpoint: Fixed tree provider implementation and recompiled
Assistant generated file changes:
- src/providers/prompterTreeProvider.ts: Fix imports and improve modularity

---

User prompt:

This broke the import of ignore and caused `There is no data provider registered that can provide view data.`

Replit-Commit-Author: Assistant
Replit-Commit-Session-Id: e2f69f78-99c3-447b-a97a-9a2c4347a1d6
2025-03-12 08:39:44 +00:00
5a4560758c ignore config 2025-03-12 08:06:19 +00:00
926c88780b Assistant checkpoint: Refactored prompterTreeProvider.ts into modular components
Assistant generated file changes:
- src/providers/prompterTreeProvider.ts: Refactor to a more modular structure
- src/providers/fileSelectionManager.ts: Create file selection manager class
- src/providers/settingsManager.ts: Create settings manager class

---

User prompt:

Can you help me make this file more modular?
@src/providers/prompterTreeProvider.ts

Replit-Commit-Author: Assistant
Replit-Commit-Session-Id: e2f69f78-99c3-447b-a97a-9a2c4347a1d6
2025-03-12 08:01:18 +00:00
2d28f71e9b add support for recursive selection and deselection of files in the tree view 2025-03-12 07:25:26 +00:00
65bf168e1f add conditional visibility for prompter commands and include 'ignore' dependency 2025-03-12 06:54:12 +00:00
6e3fbd8f2b add repository URL to package.json 2025-03-12 06:44:37 +00:00
3adfd712c0 add XML formatting instructions file and update prompt generator to include instructions 2025-03-12 06:42:53 +00:00
759cfad76e ignore assets 2025-03-12 05:23:36 +00:00
0ec4e8e2d2 follow xml pattern 2025-03-11 18:40:20 +00:00
3676136692 modular code 2025-03-11 18:27:57 +00:00
6bd50154f0 add settings management and UI integration 2025-03-11 18:18:32 +00:00
16 changed files with 1404 additions and 282 deletions

2
.gitignore vendored
View File

@@ -137,3 +137,5 @@ dist
package-lock.json package-lock.json
*.vsix *.vsix
attached_assets

10
.vscode-test.mjs Normal file
View File

@@ -0,0 +1,10 @@
import * as path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default {
version: 'stable',
extensionDevelopmentPath: __dirname,
extensionTestsPath: path.join(__dirname, 'out', 'test')
};

View File

@@ -26,3 +26,4 @@ out/test/**
package-lock.json package-lock.json
# Assets not needed in the final package # Assets not needed in the final package
attached_assets/** attached_assets/**
.config

View File

@@ -77,77 +77,3 @@ 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. Code smarter with AI—no more messy copy-pasting. Prompter structures your prompts and applies AI changes seamlessly, streamlining your coding workflow.
## Why 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.
Prompter empowers you to work efficiently with AI, reducing token waste and improving clarity.
## Features
- **Advanced File Selection & Token Estimation**
Precisely filter files and estimate token usage instantly for optimized, cost-effective prompts.
- **Optimized XML Prompt**
Structured file trees, CodeMaps, content, and instructions in XML for maximum LLM clarity.
- **Structured XML Diffs**
Converts LLM-generated XML edits into precise, reviewable diffs—works at any file size.
- **Codemap Extraction**
Scans files locally to extract classes, functions, and references, minimizing tokens and hallucinations. Auto-detects referenced types.
- **Mac-Native Performance**
Built for macOS with native speed and responsiveness—because performance matters.
- **Clipboard Integration**
Copy structured prompts into any AI chat app—your data stays local, no external API needed.
- **Works with Any Model**
Compatible with OpenAI, Anthropic, DeepSeek, Gemini, Azure, OpenRouter, and local models—private and offline when you need it.
- **Privacy First**
Local models, offline scanning, and direct clipboard use—no intermediaries required.
## 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 <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
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.
---
Built with ❤️ by the Prompter team.

View File

@@ -4,6 +4,7 @@
"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.1",
"publisher": "abhishekbhakat", "publisher": "abhishekbhakat",
"repository": "https://github.com/abhishekbhakat/prompter",
"engines": { "engines": {
"vscode": "^1.98.0" "vscode": "^1.98.0"
}, },
@@ -21,6 +22,11 @@
{ {
"command": "prompter.generatePrompt", "command": "prompter.generatePrompt",
"title": "Copy" "title": "Copy"
},
{
"command": "prompter.openSettings",
"title": "Settings",
"icon": "$(settings-gear)"
} }
], ],
"viewsContainers": { "viewsContainers": {
@@ -44,7 +50,13 @@
"view/title": [ "view/title": [
{ {
"command": "prompter.generatePrompt", "command": "prompter.generatePrompt",
"group": "navigation" "group": "navigation@1",
"when": "view == prompterView"
},
{
"command": "prompter.openSettings",
"group": "navigation@2",
"when": "view == prompterView"
} }
], ],
"view/item/context": [ "view/item/context": [
@@ -75,6 +87,7 @@
"typescript": "^5.7.3" "typescript": "^5.7.3"
}, },
"dependencies": { "dependencies": {
"@vscode/vsce": "^3.2.2" "@vscode/vsce": "^3.2.2",
"ignore": "^7.0.3"
} }
} }

View File

@@ -0,0 +1,294 @@
<xml_formatting_instructions>
### Role
- You are a **code editing assistant**: You can fulfill edit requests and chat with the user about code or other questions. Provide complete instructions or code lines when replying with xml formatting.
### Capabilities
- Can create new files.
- Can rewrite entire files.
- Can perform partial search/replace modifications.
- Can delete existing files.
Avoid placeholders like `...` or `// existing code here`. Provide complete lines or code.
## Tools & Actions
1. **create** Create a new file if it doesnt exist.
2. **rewrite** Replace the entire content of an existing file.
3. **modify** (search/replace) For partial edits with <search> + <content>.
4. **delete** Remove a file entirely (empty <content>).
### **Format to Follow for Repo Prompt's Diff Protocol**
<Plan>
Describe your approach or reasoning here.
</Plan>
<file path="path/to/example.swift" action="one_of_the_tools">
<change>
<description>Brief explanation of this specific change</description>
<search>
===
// Exactly matching lines to find
===
</search>
<content>
===
// Provide the new or updated code here. Do not use placeholders
===
</content>
</change>
<!-- Add more <change> blocks if you have multiple edits for the same file -->
</file>
#### Tools Demonstration
1. `<file path="NewFile.swift" action="create">` Full file in <content>
2. `<file path="DeleteMe.swift" action="delete">` Empty <content>
3. `<file path="ModifyMe.swift" action="modify">` Partial edit with `<search>` + `<content>`
4. `<file path="RewriteMe.swift" action="rewrite">` Entire file in <content>
5. `<file path="RewriteMe.swift" action="rewrite">` Entire file in <content>. No <search> required.
## Format Guidelines
1. **Plan**: Begin with a `<Plan>` block explaining your approach.
2. **<file> Tag**: e.g. `<file path="Models/User.swift" action="...">`. Must match an available tool.
3. **<change> Tag**: Provide `<description>` to clarify each change. Then `<content>` for new/modified code. Additional rules depend on your capabilities.
4. **modify**: **<search> & <content>**: Provide code blocks enclosed by ===. Respect indentation exactly, ensuring the <search> block matches the original source down to braces, spacing, and any comments. The new <content> will replace the <search> block, and should should fit perfectly in the space left by it's removal.
5. **modify**: For changes to the same file, ensure that you use multiple change blocks, rather than separate file blocks.
6. **rewrite**: For large overhauls; omit `<search>` and put the entire file in `<content>`.
7. **create**: For new files, put the full file in <content>.
8. **delete**: Provide an empty <content>. The file is removed.
## Code Examples
-----
### Example: Search and Replace (Add email property)
<Plan>
Add an email property to `User` via search/replace.
</Plan>
<file path="Models/User.swift" action="modify">
<change>
<description>Add email property to User struct</description>
<search>
===
struct User {
let id: UUID
var name: String
}
===
</search>
<content>
===
struct User {
let id: UUID
var name: String
var email: String
}
===
</content>
</change>
</file>
-----
### Example: Negative Example - Mismatched Search Block
// Example Input (not part of final output, just demonstration)
<file_contents>
File: path/service.swift
```
import Foundation
class Example {
foo() {
Bar()
}
}
```
</file_contents>
<Plan>
Demonstrate how a mismatched search block leads to failed merges.
</Plan>
<file path="path/service.swift" action="modify">
<change>
<description>This search block is missing or has mismatched indentation, braces, etc.</description>
<search>
===
foo() {
Bar()
}
===
</search>
<content>
===
foo() {
Bar()
Bar2()
}
===
</content>
</change>
</file>
<!-- This example fails because the <search> block doesn't exactly match the original file contents. -->
-----
### Example: Negative Example - Mismatched Brace Balance
// This negative example shows how adding extra braces in the <content> can break brace matching.
<Plan>
Demonstrate that the new content block has one extra closing brace, causing mismatched braces.
</Plan>
<file path="Functions/MismatchedBracesExample.swift" action="modify">
<change>
<description>Mismatched brace balance in the replacement content</description>
<search>
===
foo() {
Bar()
}
===
</search>
<content>
===
foo() {
Bar()
}
bar() {
foo2()
}
}
===
</content>
</change>
</file>
<!-- Because the <search> block was only a small brace segment, adding extra braces in <content> breaks the balance. -->
-----
### Example: Negative Example - One-Line Search Block
<Plan>
Demonstrate a one-line search block, which is too short to be reliable.
</Plan>
<file path="path/service.swift" action="modify">
<change>
<description>One-line search block is ambiguous</description>
<search>
===
var email: String
===
</search>
<content>
===
var emailNew: String
===
</content>
</change>
</file>
<!-- This example fails because the <search> block is only one line and ambiguous. -->
-----
### Example: Negative Example - Ambiguous Search Block
<Plan>
Demonstrate an ambiguous search block that can match multiple blocks (e.g., multiple closing braces).
</Plan>
<file path="path/service.swift" action="modify">
<change>
<description>Ambiguous search block with multiple closing braces</description>
<search>
===
}
}
===
</search>
<content>
===
foo() {
}
}
}
===
</content>
</change>
</file>
<!-- This example fails because the <search> block is ambiguous due to multiple matching closing braces. -->
-----
### Example: Full File Rewrite
<Plan>
Rewrite the entire User file to include an email property.
</Plan>
<file path="Models/User.swift" action="rewrite">
<change>
<description>Full file rewrite with new email field</description>
<content>
===
import Foundation
struct User {
let id: UUID
var name: String
var email: String
init(name: String, email: String) {
self.id = UUID()
self.name = name
self.email = email
}
}
===
</content>
</change>
</file>
-----
### Example: Create New File
<Plan>
Create a new RoundedButton for a custom Swift UIButton subclass.
</Plan>
<file path="Views/RoundedButton.swift" action="create">
<change>
<description>Create custom RoundedButton class</description>
<content>
===
import UIKit
@IBDesignable
class RoundedButton: UIButton {
@IBInspectable var cornerRadius: CGFloat = 0
}
===
</content>
</change>
</file>
-----
### Example: Delete a File
<Plan>
Remove an obsolete file.
</Plan>
<file path="Obsolete/File.swift" action="delete">
<change>
<description>Completely remove the file from the project</description>
<content>
===
===
</content>
</change>
</file>
## Final Notes
1. **modify** Always wrap the exact original lines in <search> and your updated lines in <content>, each enclosed by ===.
2. **modify** The <search> block must match the source code exactly—down to indentation, braces, spacing, and any comments. Even a minor mismatch causes failed merges.
3. **modify** Only replace exactly what you need. Avoid including entire functions or files if only a small snippet changes, and ensure the <search> content is unique and easy to identify.
4. **rewrite** Use `rewrite` for major overhauls, and `modify` for smaller, localized edits. Rewrite requires the entire code to be replaced, so use it sparingly.
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.
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.
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.
</xml_formatting_instructions>

View File

@@ -1,210 +1,91 @@
// The module 'vscode' contains the VS Code extensibility API
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as path from 'path'; import * as path from 'path';
// Custom TreeItem for files and folders // Import our modular components
class FileTreeItem extends vscode.TreeItem { import { PrompterSettings, DEFAULT_SETTINGS } from './models/settings';
constructor( import { FileTreeItem, SettingTreeItem } from './models/treeItems';
public readonly resourceUri: vscode.Uri, import { PrompterTreeProvider } from './providers/prompterTreeProvider';
public readonly collapsibleState: vscode.TreeItemCollapsibleState, import { PromptGenerator } from './utils/promptGenerator';
public readonly isDirectory: boolean
) {
super(resourceUri, collapsibleState);
this.contextValue = isDirectory ? 'directory' : 'file';
this.tooltip = resourceUri.fsPath;
this.description = isDirectory ? 'folder' : path.extname(resourceUri.fsPath);
this.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
}
}
// TreeView provider for the sidebar /**
class PrompterTreeProvider implements vscode.TreeDataProvider<FileTreeItem> { * This method is called when the extension is activated
private _onDidChangeTreeData: vscode.EventEmitter<FileTreeItem | undefined | null | void> = new vscode.EventEmitter<FileTreeItem | undefined | null | void>(); */
readonly onDidChangeTreeData: vscode.Event<FileTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
private selectedFiles: Set<string> = new Set();
private xmlEditsEnabled: boolean = false;
constructor(private workspaceRoot: string | undefined) {}
getTreeItem(element: FileTreeItem): vscode.TreeItem {
const item = element;
if (this.selectedFiles.has(element.resourceUri.fsPath)) {
item.checkboxState = vscode.TreeItemCheckboxState.Checked;
item.label = path.basename(element.resourceUri.fsPath);
} else {
item.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
item.label = path.basename(element.resourceUri.fsPath);
}
return item;
}
async getChildren(element?: FileTreeItem): Promise<FileTreeItem[]> {
if (!this.workspaceRoot) {
vscode.window.showInformationMessage('No workspace folder is opened');
return Promise.resolve([]);
}
if (element) {
const dirPath = element.resourceUri.fsPath;
return this.getFilesInDirectory(dirPath);
} else {
return this.getFilesInDirectory(this.workspaceRoot);
}
}
private async getFilesInDirectory(dirPath: string): Promise<FileTreeItem[]> {
try {
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
return Promise.all(files.map(async ([name, type]) => {
const filePath = path.join(dirPath, name);
const uri = vscode.Uri.file(filePath);
const isDirectory = (type & vscode.FileType.Directory) !== 0;
return new FileTreeItem(
uri,
isDirectory ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None,
isDirectory
);
}));
} catch (error) {
console.error(`Error reading directory ${dirPath}:`, error);
return [];
}
}
toggleSelection(item: FileTreeItem): void {
const filePath = item.resourceUri.fsPath;
console.log('Toggle selection called for:', filePath);
if (this.selectedFiles.has(filePath)) {
this.removeFromSelection(item);
} else {
this.addToSelection(item);
}
}
addToSelection(item: FileTreeItem): void {
const filePath = item.resourceUri.fsPath;
this.selectedFiles.add(filePath);
console.log('Added file to selection, count:', this.selectedFiles.size);
this._onDidChangeTreeData.fire();
}
removeFromSelection(item: FileTreeItem): void {
const filePath = item.resourceUri.fsPath;
this.selectedFiles.delete(filePath);
console.log('Removed file from selection, count:', this.selectedFiles.size);
this._onDidChangeTreeData.fire();
}
getSelectedFiles(): Set<string> {
console.log('getSelectedFiles called, count:', this.selectedFiles.size);
return this.selectedFiles;
}
toggleXmlEdits(): void {
this.xmlEditsEnabled = !this.xmlEditsEnabled;
}
isXmlEditsEnabled(): boolean {
return this.xmlEditsEnabled;
}
refresh(): void {
this._onDidChangeTreeData.fire();
}
}
// Utility function to estimate tokens in text
function estimateTokens(text: string): number {
// Rough estimation: Split by whitespace and punctuation
// This is a simple approximation, actual token count may vary by model
const words = text.split(/[\s\p{P}]+/u).filter(Boolean);
return Math.ceil(words.length * 1.3); // Add 30% overhead for special tokens
}
// Function to read file content
async function readFileContent(filePath: string): Promise<string> {
try {
const readData = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath));
return Buffer.from(readData).toString('utf8');
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
return '';
}
}
// Function to generate XML prompt
function generateXMLPrompt(files: Map<string, { content: string; tokens: number }>, xmlEdits: boolean): string {
const xmlParts = ['<?xml version="1.0" encoding="UTF-8"?>', '<prompt>'];
// Add files section
xmlParts.push(' <files>');
for (const [path, { content, tokens }] of files) {
xmlParts.push(' <file>');
xmlParts.push(` <path>${path}</path>`);
xmlParts.push(` <tokens>${tokens}</tokens>`);
xmlParts.push(` <content><![CDATA[${content}]]></content>`);
xmlParts.push(' </file>');
}
xmlParts.push(' </files>');
// Add XML edits flag if enabled
if (xmlEdits) {
xmlParts.push(' <options>');
xmlParts.push(' <xml_edits>true</xml_edits>');
xmlParts.push(' </options>');
}
xmlParts.push('</prompt>');
return xmlParts.join('\n');
}
// This method is called when your extension is activated
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "prompter" is now active!'); console.log('Prompter extension is now active!');
// Get the workspace folder
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
console.log('Workspace root:', workspaceRoot);
// Create the tree data provider
console.log('Creating tree data provider...');
const prompterTreeProvider = new PrompterTreeProvider(workspaceRoot); const prompterTreeProvider = new PrompterTreeProvider(workspaceRoot);
console.log('Tree data provider created successfully');
// Register the TreeView with checkbox support // Register the TreeView with checkbox support
const treeView = vscode.window.createTreeView('prompterView', { console.log('Registering tree view with ID: prompterView');
treeDataProvider: prompterTreeProvider, let treeView: vscode.TreeView<FileTreeItem | SettingTreeItem>;
showCollapseAll: true, try {
canSelectMany: true treeView = vscode.window.createTreeView('prompterView', {
}); treeDataProvider: prompterTreeProvider,
showCollapseAll: true,
canSelectMany: true
});
console.log('Tree view registered successfully');
} catch (error) {
console.error('Error registering tree view:', error);
// Create a fallback empty tree view to prevent further errors
treeView = {} as vscode.TreeView<FileTreeItem | SettingTreeItem>;
}
// Handle checkbox changes // Handle checkbox changes
treeView.onDidChangeCheckboxState(e => { treeView.onDidChangeCheckboxState((e: vscode.TreeCheckboxChangeEvent<FileTreeItem | SettingTreeItem>) => {
console.log('Checkbox state changed'); console.log('Checkbox state changed');
for (const [item, state] of e.items) { for (const [item, state] of e.items) {
const fileItem = item as FileTreeItem; if (item instanceof FileTreeItem) {
console.log(`Checkbox changed for ${fileItem.resourceUri.fsPath} to ${state}`); console.log(`Checkbox changed for ${item.resourceUri.fsPath} to ${state}`);
if (state === vscode.TreeItemCheckboxState.Checked) { if (state === vscode.TreeItemCheckboxState.Checked) {
prompterTreeProvider.addToSelection(fileItem); prompterTreeProvider.addToSelection(item);
} else { } else {
prompterTreeProvider.removeFromSelection(fileItem); prompterTreeProvider.removeFromSelection(item);
}
} else if (item instanceof SettingTreeItem) {
// Handle settings checkbox changes
console.log(`Setting changed: ${item.settingKey} to ${state === vscode.TreeItemCheckboxState.Checked}`);
prompterTreeProvider.updateSetting(
item.settingKey,
state === vscode.TreeItemCheckboxState.Checked
);
// Update formatting instructions button text if that setting changed
if (item.settingKey === 'includeFormattingInstructions') {
xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ?
"$(check) Formatting Instructions" : "$(diff-added) Formatting Instructions";
}
} }
} }
}); });
// Create XML edits toggle button // Create formatting instructions toggle button
const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
xmlEditsButton.text = "$(diff-added) XML Edits"; xmlEditsButton.text = "$(diff-added) Formatting Instructions";
xmlEditsButton.tooltip = "Toggle XML Edits 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.Right);
copyButton.text = "$(clippy) Copy"; copyButton.text = "$(clippy) Copy";
copyButton.tooltip = "Generate and copy XML prompt"; copyButton.tooltip = "Generate and copy prompt";
copyButton.command = 'prompter.generatePrompt'; copyButton.command = 'prompter.generatePrompt';
copyButton.show(); copyButton.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) => {
console.log('Toggle selection command triggered for:', item.resourceUri.fsPath); if (item.resourceUri) {
prompterTreeProvider.toggleSelection(item); console.log('Toggle selection command triggered for:', item.resourceUri.fsPath);
prompterTreeProvider.toggleSelection(item);
}
}); });
// Register command to toggle XML edits // Register command to toggle XML edits
@@ -214,53 +95,66 @@ 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
let openSettingsCommand = vscode.commands.registerCommand('prompter.openSettings', () => {
prompterTreeProvider.toggleSettingsView();
// Update the settings button icon based on current view
settingsButton.text = prompterTreeProvider.isShowingSettings() ?
"$(list-tree)" : "$(settings-gear)";
settingsButton.tooltip = prompterTreeProvider.isShowingSettings() ?
"Show Files" : "Prompter Settings";
});
// 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();
console.log('Generate prompt command triggered, selected files:', [...selectedFiles]); console.log('Generate prompt command triggered, selected files:', [...selectedFiles]);
if (selectedFiles.size === 0) { if (selectedFiles.size === 0) {
vscode.window.showInformationMessage('Please select files first'); vscode.window.showInformationMessage('Please select files first');
return; return;
} }
let totalTokens = 0; try {
const fileContents = new Map<string, { content: string; tokens: number }>(); // Generate the prompt using our utility class
const promptText = await PromptGenerator.generatePrompt(
// Process each selected file selectedFiles,
for (const filePath of selectedFiles) { prompterTreeProvider.getSettings()
const content = await readFileContent(filePath);
const tokens = estimateTokens(content);
totalTokens += tokens;
fileContents.set(filePath, { content, tokens });
// Show individual file token count
vscode.window.showInformationMessage(
`File: ${filePath}\nEstimated tokens: ${tokens}`
); );
// Copy to clipboard
await vscode.env.clipboard.writeText(promptText);
vscode.window.showInformationMessage('Prompt copied to clipboard!');
} catch (error) {
console.error('Error generating prompt:', error);
vscode.window.showErrorMessage('Error generating prompt');
} }
// Show total token count
vscode.window.showInformationMessage(
`Total estimated tokens for ${selectedFiles.size} files: ${totalTokens}`
);
// Generate XML prompt
const xmlPrompt = generateXMLPrompt(fileContents, prompterTreeProvider.isXmlEditsEnabled());
// Copy to clipboard
await vscode.env.clipboard.writeText(xmlPrompt);
vscode.window.showInformationMessage('XML prompt copied to clipboard!');
}); });
// Add all disposables to context subscriptions
context.subscriptions.push( context.subscriptions.push(
treeView, treeView,
xmlEditsButton, xmlEditsButton,
copyButton, copyButton,
settingsButton,
toggleSelectionCommand, toggleSelectionCommand,
toggleXmlEditsCommand, toggleXmlEditsCommand,
generatePromptCommand generatePromptCommand,
openSettingsCommand
); );
} }
// This method is called when your extension is deactivated /**
export function deactivate() {} * This method is called when the extension is deactivated
*/
export function deactivate() {
// Clean up resources when the extension is deactivated
}

13
src/models/settings.ts Normal file
View File

@@ -0,0 +1,13 @@
// Settings interface for the Prompter extension
export interface PrompterSettings {
includeFormattingInstructions: boolean;
tokenCalculationEnabled: boolean;
includeFileMap: boolean;
}
// Default settings values
export const DEFAULT_SETTINGS: PrompterSettings = {
includeFormattingInstructions: false,
tokenCalculationEnabled: true,
includeFileMap: true
};

32
src/models/treeItems.ts Normal file
View File

@@ -0,0 +1,32 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { PrompterSettings } from './settings';
// Tree item for files in the explorer
export class FileTreeItem extends vscode.TreeItem {
constructor(
public readonly resourceUri: vscode.Uri,
public readonly collapsibleState: vscode.TreeItemCollapsibleState
) {
super(resourceUri, collapsibleState);
this.tooltip = resourceUri.fsPath;
this.description = path.basename(resourceUri.fsPath);
this.contextValue = 'file';
// Set the checkbox state to unchecked by default
this.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
}
}
// Tree item for settings in the settings view
export class SettingTreeItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly settingKey: keyof PrompterSettings,
public readonly checked: boolean
) {
super(label, vscode.TreeItemCollapsibleState.None);
this.checkboxState = checked ? vscode.TreeItemCheckboxState.Checked : vscode.TreeItemCheckboxState.Unchecked;
this.contextValue = 'setting';
}
}

View File

@@ -0,0 +1,103 @@
import * as vscode from 'vscode';
import * as path from 'path';
/**
* Manages file selection state for the Prompter extension
*/
export class FileSelectionManager {
private selectedFiles: Set<string> = new Set();
constructor() {}
/**
* Add a file to the selection
*/
addFile(filePath: string): void {
this.selectedFiles.add(filePath);
console.log(`Added ${filePath} to selection`);
}
/**
* Add a directory and all its contents to the selection
*/
async addDirectory(dirPath: string): Promise<void> {
try {
// Add the directory itself
this.selectedFiles.add(dirPath);
console.log(`Added directory ${dirPath} to selection`);
// Read directory contents
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
// Process each item
for (const [name, type] of files) {
const filePath = path.join(dirPath, name);
if (type === vscode.FileType.Directory) {
// Recursively process subdirectories
await this.addDirectory(filePath);
} else {
// Add files
this.selectedFiles.add(filePath);
console.log(`Added ${filePath} to selection (from directory)`);
}
}
} catch (error) {
console.error(`Error adding directory to selection: ${dirPath}`, error);
}
}
/**
* Remove a file from the selection
*/
removeFile(filePath: string): void {
this.selectedFiles.delete(filePath);
console.log(`Removed ${filePath} from selection`);
}
/**
* Remove a directory and all its contents from the selection
*/
async removeDirectory(dirPath: string): Promise<void> {
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)`);
}
}
} catch (error) {
console.error(`Error removing directory from selection: ${dirPath}`, error);
}
}
/**
* Check if a file is selected
*/
isSelected(filePath: string): boolean {
return this.selectedFiles.has(filePath);
}
/**
* Get all selected files
*/
getSelectedFiles(): Set<string> {
console.log('getSelectedFiles called, count:', this.selectedFiles.size);
return this.selectedFiles;
}
}

View File

@@ -0,0 +1,187 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { FileTreeItem, SettingTreeItem } from '../models/treeItems';
import { PrompterSettings } from '../models/settings';
import { FileSelectionManager } from './fileSelectionManager';
import { SettingsManager } from './settingsManager';
/**
* Tree provider for the Prompter extension
* Handles both file browsing and settings views
*/
export class PrompterTreeProvider implements vscode.TreeDataProvider<FileTreeItem | SettingTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<FileTreeItem | SettingTreeItem | undefined | null | void> =
new vscode.EventEmitter<FileTreeItem | SettingTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<FileTreeItem | SettingTreeItem | undefined | null | void> =
this._onDidChangeTreeData.event;
private fileSelectionManager: FileSelectionManager;
private settingsManager: SettingsManager;
private showingSettings: boolean = false;
constructor(private workspaceRoot: string | undefined) {
this.fileSelectionManager = new FileSelectionManager();
this.settingsManager = new SettingsManager();
}
// Toggle between file view and settings view
toggleSettingsView(): void {
this.showingSettings = !this.showingSettings;
this.refresh();
}
getTreeItem(element: FileTreeItem | SettingTreeItem): vscode.TreeItem {
// Return the element as is if it's a SettingTreeItem
if (element instanceof SettingTreeItem) {
return element;
}
// Handle FileTreeItem
const fileItem = element as FileTreeItem;
if (fileItem.resourceUri && this.fileSelectionManager.isSelected(fileItem.resourceUri.fsPath)) {
fileItem.checkboxState = vscode.TreeItemCheckboxState.Checked;
} else {
fileItem.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
}
return fileItem;
}
async getChildren(element?: FileTreeItem | SettingTreeItem): Promise<(FileTreeItem | SettingTreeItem)[]> {
// If we're showing settings, return settings items
if (this.showingSettings && !element) {
return this.getSettingsItems();
}
// Otherwise show files
if (!this.workspaceRoot) {
vscode.window.showInformationMessage('No workspace folder is opened');
return Promise.resolve([]);
}
if (element) {
// Make sure we're dealing with a FileTreeItem that has a resourceUri
if (!(element instanceof SettingTreeItem) && element.resourceUri) {
return this.getFilesInDirectory(element.resourceUri.fsPath);
}
return Promise.resolve([]);
} else {
return this.getFilesInDirectory(this.workspaceRoot);
}
}
private async getFilesInDirectory(dirPath: string): Promise<FileTreeItem[]> {
try {
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
return files.map(([name, type]) => {
const filePath = path.join(dirPath, name);
const uri = vscode.Uri.file(filePath);
return new FileTreeItem(
uri,
type === vscode.FileType.Directory
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None
);
});
} catch (error) {
console.error(`Error reading directory: ${dirPath}`, error);
return [];
}
}
// Refresh the tree view
refresh(): void {
this._onDidChangeTreeData.fire();
}
// Add a file to the selection
addToSelection(item: FileTreeItem): void {
if (item.resourceUri) {
if (this.isDirectory(item)) {
this.fileSelectionManager.addDirectory(item.resourceUri.fsPath);
} else {
this.fileSelectionManager.addFile(item.resourceUri.fsPath);
}
}
}
// Remove a file from the selection
removeFromSelection(item: FileTreeItem): void {
if (item.resourceUri) {
if (this.isDirectory(item)) {
this.fileSelectionManager.removeDirectory(item.resourceUri.fsPath);
} else {
this.fileSelectionManager.removeFile(item.resourceUri.fsPath);
}
}
}
private isDirectory(item: FileTreeItem): boolean {
return item.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed ||
item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded;
}
// Toggle a file's selection status
toggleSelection(item: FileTreeItem): void {
if (item.resourceUri) {
const filePath = item.resourceUri.fsPath;
if (this.fileSelectionManager.isSelected(filePath)) {
this.removeFromSelection(item);
} else {
this.addToSelection(item);
}
this.refresh();
}
}
// Get the selected files
getSelectedFiles(): Set<string> {
return this.fileSelectionManager.getSelectedFiles();
}
// Get settings items for the tree view
private getSettingsItems(): SettingTreeItem[] {
const settings = this.settingsManager.getSettings();
return [
new SettingTreeItem('Include Formatting Instructions', 'includeFormattingInstructions', settings.includeFormattingInstructions),
new SettingTreeItem('Token Calculation', 'tokenCalculationEnabled', settings.tokenCalculationEnabled),
new SettingTreeItem('Include File Map', 'includeFileMap', settings.includeFileMap)
];
}
// Update a setting value
updateSetting(key: keyof PrompterSettings, value: boolean): void {
this.settingsManager.updateSetting(key, value);
this.refresh();
}
// Check if showing settings view
isShowingSettings(): boolean {
return this.showingSettings;
}
// Toggle formatting instructions setting
toggleXmlEdits(): void {
this.settingsManager.toggleFormattingInstructions();
this.refresh();
}
// Check if formatting instructions are enabled
isXmlEditsEnabled(): boolean {
return this.settingsManager.isFormattingInstructionsEnabled();
}
// Get all settings
getSettings(): PrompterSettings {
return this.settingsManager.getSettings();
}
// Update all settings at once
updateSettings(newSettings: PrompterSettings): void {
this.settingsManager.updateAllSettings(newSettings);
this.refresh();
}
}

View File

@@ -0,0 +1,46 @@
import { PrompterSettings, DEFAULT_SETTINGS } from '../models/settings';
/**
* Manages settings for the Prompter extension
*/
export class SettingsManager {
private settings: PrompterSettings = { ...DEFAULT_SETTINGS };
constructor() {}
/**
* Update a specific setting
*/
updateSetting(key: keyof PrompterSettings, value: boolean): void {
this.settings[key] = value;
}
/**
* Toggle formatting instructions setting
*/
toggleFormattingInstructions(): void {
this.settings.includeFormattingInstructions = !this.settings.includeFormattingInstructions;
}
/**
* Check if formatting instructions are enabled
*/
isFormattingInstructionsEnabled(): boolean {
return this.settings.includeFormattingInstructions;
}
/**
* Get all settings
*/
getSettings(): PrompterSettings {
return { ...this.settings };
}
/**
* Update all settings at once
*/
updateAllSettings(newSettings: PrompterSettings): void {
this.settings = { ...newSettings };
}
}

251
src/test/fileTreeTest.ts Normal file
View File

@@ -0,0 +1,251 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
// Using require for the ignore package due to its module export style
const ignoreLib = require('ignore');
// A simplified version of our tree structure for testing
interface TreeNode {
name: string;
isDirectory: boolean;
children: Map<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();

21
src/utils/fileReader.ts Normal file
View File

@@ -0,0 +1,21 @@
import * as vscode from 'vscode';
/**
* Utility for reading file contents
*/
export class FileReader {
/**
* Read the content of a file
* @param filePath Path to the file to read
* @returns The file content as a string
*/
static async readFileContent(filePath: string): Promise<string> {
try {
const readData = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath));
return Buffer.from(readData).toString('utf8');
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
return '';
}
}
}

View File

@@ -0,0 +1,313 @@
import * as vscode from 'vscode';
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
*/
export class PromptGenerator {
/**
* Generate a prompt from the selected files
* @param selectedFiles Set of file paths to include in the prompt
* @param settings Settings to apply when generating the prompt
* @returns The generated prompt text
*/
static async generatePrompt(selectedFiles: Set<string>, settings: PrompterSettings): Promise<string> {
if (selectedFiles.size === 0) {
throw new Error('No files selected');
}
let totalTokens = 0;
const fileContents = new Map<string, { content: string; tokens: number }>();
// Process each selected file
for (const filePath of selectedFiles) {
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<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
if (settings.tokenCalculationEnabled) {
promptText += `\nEstimated token count: ${totalTokenCount}`;
}
return promptText;
}
/**
* Generate an XML formatted prompt following the new schema format
* @param files Map of file paths to content and token counts
* @param settings Settings to apply
* @returns XML formatted prompt
*/
private static generateXMLPrompt(files: Map<string, { content: string; tokens: number }>, 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, '..', '..');
const formattingInstructionsPath = path.join(extensionPath, 'resources', 'xml_formatting_instructions.xml');
if (fs.existsSync(formattingInstructionsPath)) {
formattingInstructions = fs.readFileSync(formattingInstructionsPath, 'utf8');
} else {
console.warn('XML formatting instructions file not found at:', formattingInstructionsPath);
}
} catch (error) {
console.error('Error reading XML formatting instructions:', error);
}
}
// Generate file map section if enabled in settings
if (settings.includeFileMap) {
xmlParts.push('<file_map>');
// 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('</file_map>');
}
// Generate file contents section
xmlParts.push('<file_contents>');
// 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
// Handle special cases for language detection
if (extension === '.js' || extension === '.jsx') {
language = 'javascript';
} else if (extension === '.ts' || extension === '.tsx') {
language = 'typescript';
} else if (extension === '.md') {
language = 'md';
} else if (extension === '.py') {
language = 'python';
} else if (extension === '.html') {
language = 'html';
} else if (extension === '.css') {
language = 'css';
} else if (extension === '.json') {
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);
xmlParts.push('\`\`\`');
}
xmlParts.push('</file_contents>');
// 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);
}
return xmlParts.join('\n');
}
/**
* Generate a tree representation of files
* @param files Map of file paths
* @param rootPath The workspace root path
* @returns String representation of the file tree
*/
private static generateFileTree(files: Map<string, any>, rootPath: string): string {
// Create a tree representation
const treeLines: string[] = [];
// Create a tree structure
interface TreeNode {
name: string;
isDirectory: boolean;
children: Map<string, TreeNode>;
}
// Create root node
const root: TreeNode = {
name: path.basename(rootPath),
isDirectory: true,
children: new Map<string, TreeNode>()
};
// Initialize the ignore instance
let ig;
try {
ig = typeof ignore === 'function' ? ignore() : ignore.default();
console.log('Successfully initialized ignore instance');
} catch (error) {
console.error('Error initializing ignore instance:', error);
// Fallback implementation
ig = {
add: () => {},
ignores: () => false
};
}
// Read .gitignore patterns if available
try {
const gitignorePath = path.join(rootPath, '.gitignore');
if (fs.existsSync(gitignorePath)) {
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
// Add patterns from .gitignore
ig.add(gitignoreContent);
// Always include .gitignore itself
ig.add('!.gitignore');
}
} catch (error) {
console.error('Error reading .gitignore:', error);
}
// Build the tree structure
for (const filePath of files.keys()) {
const relativePath = this.getRelativePath(filePath, rootPath);
// 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;
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');
}
/**
* Get the path relative to the workspace root
* @param filePath Absolute file path
* @param rootPath Workspace root path
* @returns Relative path
*/
private static 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;
}
}

View File

@@ -0,0 +1,16 @@
/**
* Utility for estimating token counts in text
*/
export class TokenEstimator {
/**
* Estimate the number of tokens in a text string
* @param text The text to estimate tokens for
* @returns Estimated token count
*/
static estimateTokens(text: string): number {
// Rough estimation: Split by whitespace and punctuation
// This is a simple approximation, actual token count may vary by model
const words = text.split(/[\s\p{P}]+/u).filter(Boolean);
return Math.ceil(words.length * 1.3); // Add 30% overhead for special tokens
}
}