From 6bd50154f0b40d73f1e3151f2a9f2e124606a69e Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Tue, 11 Mar 2025 18:18:32 +0000 Subject: [PATCH 01/11] add settings management and UI integration --- package.json | 11 ++- src/extension.ts | 185 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 162 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 6e74381..9df2150 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,11 @@ { "command": "prompter.generatePrompt", "title": "Copy" + }, + { + "command": "prompter.openSettings", + "title": "Settings", + "icon": "$(settings-gear)" } ], "viewsContainers": { @@ -44,7 +49,11 @@ "view/title": [ { "command": "prompter.generatePrompt", - "group": "navigation" + "group": "navigation@1" + }, + { + "command": "prompter.openSettings", + "group": "navigation@2" } ], "view/item/context": [ diff --git a/src/extension.ts b/src/extension.ts index 7319afe..8aa526f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ // The module 'vscode' contains the VS Code extensibility API import * as vscode from 'vscode'; import * as path from 'path'; +import * as fs from 'fs'; // Custom TreeItem for files and folders class FileTreeItem extends vscode.TreeItem { @@ -17,36 +18,82 @@ class FileTreeItem extends vscode.TreeItem { } } +// Settings interface +interface PrompterSettings { + xmlEditsEnabled: boolean; + includeLineNumbers: boolean; + includeComments: boolean; + tokenCalculationEnabled: boolean; +} + +// Custom TreeItem for settings +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'; + } +} + // TreeView provider for the sidebar -class PrompterTreeProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; +class PrompterTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; private selectedFiles: Set = new Set(); - private xmlEditsEnabled: boolean = false; + private settings: PrompterSettings = { + xmlEditsEnabled: false, + includeLineNumbers: false, + includeComments: true, + tokenCalculationEnabled: true + }; + private showingSettings: 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; + + // Toggle between file view and settings view + toggleSettingsView(): void { + this.showingSettings = !this.showingSettings; + this.refresh(); } - async getChildren(element?: FileTreeItem): Promise { + 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.selectedFiles.has(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) { - const dirPath = element.resourceUri.fsPath; - return this.getFilesInDirectory(dirPath); + // 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); } @@ -100,13 +147,42 @@ class PrompterTreeProvider implements vscode.TreeDataProvider { console.log('getSelectedFiles called, count:', this.selectedFiles.size); return this.selectedFiles; } + + // Get settings items for the tree view + private getSettingsItems(): SettingTreeItem[] { + return [ + new SettingTreeItem('XML Edits', 'xmlEditsEnabled', this.settings.xmlEditsEnabled), + new SettingTreeItem('Include Line Numbers', 'includeLineNumbers', this.settings.includeLineNumbers), + new SettingTreeItem('Include Comments', 'includeComments', this.settings.includeComments), + new SettingTreeItem('Token Calculation', 'tokenCalculationEnabled', this.settings.tokenCalculationEnabled) + ]; + } + + // Update a setting value + updateSetting(key: keyof PrompterSettings, value: boolean): void { + this.settings[key] = value; + this.refresh(); + } + + isShowingSettings(): boolean { + return this.showingSettings; + } toggleXmlEdits(): void { - this.xmlEditsEnabled = !this.xmlEditsEnabled; + this.settings.xmlEditsEnabled = !this.settings.xmlEditsEnabled; } isXmlEditsEnabled(): boolean { - return this.xmlEditsEnabled; + return this.settings.xmlEditsEnabled; + } + + getSettings(): PrompterSettings { + return this.settings; + } + + updateSettings(newSettings: PrompterSettings): void { + this.settings = { ...newSettings }; + this.refresh(); } refresh(): void { @@ -134,7 +210,7 @@ async function readFileContent(filePath: string): Promise { } // Function to generate XML prompt -function generateXMLPrompt(files: Map, xmlEdits: boolean): string { +function generateXMLPrompt(files: Map, settings: PrompterSettings): string { const xmlParts = ['', '']; // Add files section @@ -148,12 +224,18 @@ function generateXMLPrompt(files: Map'); - // Add XML edits flag if enabled - if (xmlEdits) { - xmlParts.push(' '); + // Add options based on settings + xmlParts.push(' '); + if (settings.xmlEditsEnabled) { xmlParts.push(' true'); - xmlParts.push(' '); } + if (settings.includeLineNumbers) { + xmlParts.push(' true'); + } + if (!settings.includeComments) { + xmlParts.push(' true'); + } + xmlParts.push(' '); xmlParts.push(''); return xmlParts.join('\n'); @@ -177,12 +259,26 @@ export function activate(context: vscode.ExtensionContext) { treeView.onDidChangeCheckboxState(e => { console.log('Checkbox state changed'); for (const [item, state] of e.items) { - const fileItem = item as FileTreeItem; - console.log(`Checkbox changed for ${fileItem.resourceUri.fsPath} to ${state}`); - if (state === vscode.TreeItemCheckboxState.Checked) { - prompterTreeProvider.addToSelection(fileItem); - } else { - prompterTreeProvider.removeFromSelection(fileItem); + if (item instanceof FileTreeItem) { + console.log(`Checkbox changed for ${item.resourceUri.fsPath} to ${state}`); + if (state === vscode.TreeItemCheckboxState.Checked) { + prompterTreeProvider.addToSelection(item); + } else { + 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 XML edits button text if that setting changed + if (item.settingKey === 'xmlEditsEnabled') { + xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ? + "$(check) XML Edits" : "$(diff-added) XML Edits"; + } } } }); @@ -213,6 +309,27 @@ export function activate(context: vscode.ExtensionContext) { xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ? "$(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(); + + // Settings panel state + let settingsPanel: vscode.WebviewPanel | undefined = undefined; + + // 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 let generatePromptCommand = vscode.commands.registerCommand('prompter.generatePrompt', async () => { @@ -245,7 +362,7 @@ export function activate(context: vscode.ExtensionContext) { ); // Generate XML prompt - const xmlPrompt = generateXMLPrompt(fileContents, prompterTreeProvider.isXmlEditsEnabled()); + const xmlPrompt = generateXMLPrompt(fileContents, prompterTreeProvider.getSettings()); // Copy to clipboard await vscode.env.clipboard.writeText(xmlPrompt); @@ -256,9 +373,11 @@ export function activate(context: vscode.ExtensionContext) { treeView, xmlEditsButton, copyButton, + settingsButton, toggleSelectionCommand, toggleXmlEditsCommand, - generatePromptCommand + generatePromptCommand, + openSettingsCommand ); } From 3676136692b257471224c80ec57d88d1c8972e9e Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Tue, 11 Mar 2025 18:27:57 +0000 Subject: [PATCH 02/11] modular code --- src/extension.ts | 311 +++----------------------- src/models/settings.ts | 15 ++ src/models/treeItems.ts | 32 +++ src/providers/prompterTreeProvider.ts | 171 ++++++++++++++ src/utils/fileReader.ts | 21 ++ src/utils/promptGenerator.ts | 135 +++++++++++ src/utils/tokenEstimator.ts | 16 ++ 7 files changed, 427 insertions(+), 274 deletions(-) create mode 100644 src/models/settings.ts create mode 100644 src/models/treeItems.ts create mode 100644 src/providers/prompterTreeProvider.ts create mode 100644 src/utils/fileReader.ts create mode 100644 src/utils/promptGenerator.ts create mode 100644 src/utils/tokenEstimator.ts diff --git a/src/extension.ts b/src/extension.ts index 8aa526f..4a40476 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,251 +1,22 @@ -// The module 'vscode' contains the VS Code extensibility API import * as vscode from 'vscode'; import * as path from 'path'; -import * as fs from 'fs'; -// Custom TreeItem for files and folders -class FileTreeItem extends vscode.TreeItem { - constructor( - public readonly resourceUri: vscode.Uri, - public readonly collapsibleState: vscode.TreeItemCollapsibleState, - 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; - } -} +// Import our modular components +import { PrompterSettings, DEFAULT_SETTINGS } from './models/settings'; +import { FileTreeItem, SettingTreeItem } from './models/treeItems'; +import { PrompterTreeProvider } from './providers/prompterTreeProvider'; +import { PromptGenerator } from './utils/promptGenerator'; -// Settings interface -interface PrompterSettings { - xmlEditsEnabled: boolean; - includeLineNumbers: boolean; - includeComments: boolean; - tokenCalculationEnabled: boolean; -} - -// Custom TreeItem for settings -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'; - } -} - -// TreeView provider for the sidebar -class PrompterTreeProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - private selectedFiles: Set = new Set(); - private settings: PrompterSettings = { - xmlEditsEnabled: false, - includeLineNumbers: false, - includeComments: true, - tokenCalculationEnabled: true - }; - private showingSettings: boolean = false; - - constructor(private workspaceRoot: string | undefined) {} - - // 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.selectedFiles.has(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 { - 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 { - console.log('getSelectedFiles called, count:', this.selectedFiles.size); - return this.selectedFiles; - } - - // Get settings items for the tree view - private getSettingsItems(): SettingTreeItem[] { - return [ - new SettingTreeItem('XML Edits', 'xmlEditsEnabled', this.settings.xmlEditsEnabled), - new SettingTreeItem('Include Line Numbers', 'includeLineNumbers', this.settings.includeLineNumbers), - new SettingTreeItem('Include Comments', 'includeComments', this.settings.includeComments), - new SettingTreeItem('Token Calculation', 'tokenCalculationEnabled', this.settings.tokenCalculationEnabled) - ]; - } - - // Update a setting value - updateSetting(key: keyof PrompterSettings, value: boolean): void { - this.settings[key] = value; - this.refresh(); - } - - isShowingSettings(): boolean { - return this.showingSettings; - } - - toggleXmlEdits(): void { - this.settings.xmlEditsEnabled = !this.settings.xmlEditsEnabled; - } - - isXmlEditsEnabled(): boolean { - return this.settings.xmlEditsEnabled; - } - - getSettings(): PrompterSettings { - return this.settings; - } - - updateSettings(newSettings: PrompterSettings): void { - this.settings = { ...newSettings }; - this.refresh(); - } - - 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 { - 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, settings: PrompterSettings): string { - const xmlParts = ['', '']; - - // Add files section - xmlParts.push(' '); - for (const [path, { content, tokens }] of files) { - xmlParts.push(' '); - xmlParts.push(` ${path}`); - xmlParts.push(` ${tokens}`); - xmlParts.push(` `); - xmlParts.push(' '); - } - xmlParts.push(' '); - - // Add options based on settings - xmlParts.push(' '); - if (settings.xmlEditsEnabled) { - xmlParts.push(' true'); - } - if (settings.includeLineNumbers) { - xmlParts.push(' true'); - } - if (!settings.includeComments) { - xmlParts.push(' true'); - } - xmlParts.push(' '); - - xmlParts.push(''); - return xmlParts.join('\n'); -} - -// This method is called when your extension is activated +/** + * This method is called when the extension is activated + */ 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; + + // Create the tree data provider const prompterTreeProvider = new PrompterTreeProvider(workspaceRoot); // Register the TreeView with checkbox support @@ -293,14 +64,16 @@ export function activate(context: vscode.ExtensionContext) { // Create copy button const copyButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); copyButton.text = "$(clippy) Copy"; - copyButton.tooltip = "Generate and copy XML prompt"; + copyButton.tooltip = "Generate and copy prompt"; copyButton.command = 'prompter.generatePrompt'; copyButton.show(); // Register command to toggle file selection let toggleSelectionCommand = vscode.commands.registerCommand('prompter.toggleSelection', (item: FileTreeItem) => { - console.log('Toggle selection command triggered for:', item.resourceUri.fsPath); - prompterTreeProvider.toggleSelection(item); + if (item.resourceUri) { + console.log('Toggle selection command triggered for:', item.resourceUri.fsPath); + prompterTreeProvider.toggleSelection(item); + } }); // Register command to toggle XML edits @@ -317,9 +90,6 @@ export function activate(context: vscode.ExtensionContext) { settingsButton.command = 'prompter.openSettings'; settingsButton.show(); - // Settings panel state - let settingsPanel: vscode.WebviewPanel | undefined = undefined; - // Register command to open settings let openSettingsCommand = vscode.commands.registerCommand('prompter.openSettings', () => { prompterTreeProvider.toggleSettingsView(); @@ -335,40 +105,29 @@ export function activate(context: vscode.ExtensionContext) { let generatePromptCommand = vscode.commands.registerCommand('prompter.generatePrompt', async () => { const selectedFiles = prompterTreeProvider.getSelectedFiles(); console.log('Generate prompt command triggered, selected files:', [...selectedFiles]); + if (selectedFiles.size === 0) { vscode.window.showInformationMessage('Please select files first'); return; } - let totalTokens = 0; - const fileContents = new Map(); - - // Process each selected file - for (const filePath of selectedFiles) { - 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}` + try { + // Generate the prompt using our utility class + const promptText = await PromptGenerator.generatePrompt( + selectedFiles, + prompterTreeProvider.getSettings() ); + + // 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.getSettings()); - - // 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( treeView, xmlEditsButton, @@ -381,5 +140,9 @@ export function activate(context: vscode.ExtensionContext) { ); } -// This method is called when your extension is deactivated -export function deactivate() {} \ No newline at end of file +/** + * This method is called when the extension is deactivated + */ +export function deactivate() { + // Clean up resources when the extension is deactivated +} \ No newline at end of file diff --git a/src/models/settings.ts b/src/models/settings.ts new file mode 100644 index 0000000..609cfef --- /dev/null +++ b/src/models/settings.ts @@ -0,0 +1,15 @@ +// Settings interface for the Prompter extension +export interface PrompterSettings { + xmlEditsEnabled: boolean; + includeLineNumbers: boolean; + includeComments: boolean; + tokenCalculationEnabled: boolean; +} + +// Default settings values +export const DEFAULT_SETTINGS: PrompterSettings = { + xmlEditsEnabled: false, + includeLineNumbers: false, + includeComments: true, + tokenCalculationEnabled: true +}; diff --git a/src/models/treeItems.ts b/src/models/treeItems.ts new file mode 100644 index 0000000..0522261 --- /dev/null +++ b/src/models/treeItems.ts @@ -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'; + } +} diff --git a/src/providers/prompterTreeProvider.ts b/src/providers/prompterTreeProvider.ts new file mode 100644 index 0000000..4c1c1a1 --- /dev/null +++ b/src/providers/prompterTreeProvider.ts @@ -0,0 +1,171 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { FileTreeItem, SettingTreeItem } from '../models/treeItems'; +import { PrompterSettings, DEFAULT_SETTINGS } from '../models/settings'; + +/** + * Tree provider for the Prompter extension + * Handles both file browsing and settings views + */ +export class PrompterTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter(); + + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; + + private selectedFiles: Set = new Set(); + private settings: PrompterSettings = { ...DEFAULT_SETTINGS }; + private showingSettings: boolean = false; + + constructor(private workspaceRoot: string | undefined) {} + + // 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.selectedFiles.has(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 { + 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) { + this.selectedFiles.add(item.resourceUri.fsPath); + console.log(`Added ${item.resourceUri.fsPath} to selection`); + } + } + + // Remove a file from the selection + removeFromSelection(item: FileTreeItem): void { + if (item.resourceUri) { + this.selectedFiles.delete(item.resourceUri.fsPath); + console.log(`Removed ${item.resourceUri.fsPath} from selection`); + } + } + + // Toggle a file's selection status + toggleSelection(item: FileTreeItem): void { + if (item.resourceUri) { + const filePath = item.resourceUri.fsPath; + if (this.selectedFiles.has(filePath)) { + this.removeFromSelection(item); + } else { + this.addToSelection(item); + } + this.refresh(); + } + } + + // Get the selected files + getSelectedFiles(): Set { + console.log('getSelectedFiles called, count:', this.selectedFiles.size); + return this.selectedFiles; + } + + // Get settings items for the tree view + private getSettingsItems(): SettingTreeItem[] { + return [ + new SettingTreeItem('XML Edits', 'xmlEditsEnabled', this.settings.xmlEditsEnabled), + new SettingTreeItem('Include Line Numbers', 'includeLineNumbers', this.settings.includeLineNumbers), + new SettingTreeItem('Include Comments', 'includeComments', this.settings.includeComments), + new SettingTreeItem('Token Calculation', 'tokenCalculationEnabled', this.settings.tokenCalculationEnabled) + ]; + } + + // Update a setting value + updateSetting(key: keyof PrompterSettings, value: boolean): void { + this.settings[key] = value; + this.refresh(); + } + + // Check if showing settings view + isShowingSettings(): boolean { + return this.showingSettings; + } + + // Toggle XML edits setting + toggleXmlEdits(): void { + this.settings.xmlEditsEnabled = !this.settings.xmlEditsEnabled; + this.refresh(); + } + + // Check if XML edits are enabled + isXmlEditsEnabled(): boolean { + return this.settings.xmlEditsEnabled; + } + + // Get all settings + getSettings(): PrompterSettings { + return { ...this.settings }; + } + + // Update all settings at once + updateSettings(newSettings: PrompterSettings): void { + this.settings = { ...newSettings }; + this.refresh(); + } +} diff --git a/src/utils/fileReader.ts b/src/utils/fileReader.ts new file mode 100644 index 0000000..8709194 --- /dev/null +++ b/src/utils/fileReader.ts @@ -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 { + 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 ''; + } + } +} diff --git a/src/utils/promptGenerator.ts b/src/utils/promptGenerator.ts new file mode 100644 index 0000000..3fa1e15 --- /dev/null +++ b/src/utils/promptGenerator.ts @@ -0,0 +1,135 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { PrompterSettings } from '../models/settings'; +import { FileReader } from './fileReader'; +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, settings: PrompterSettings): Promise { + if (selectedFiles.size === 0) { + throw new Error('No files selected'); + } + + let totalTokens = 0; + const fileContents = new Map(); + + // 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 }); + } + + // Generate the prompt based on settings + if (settings.xmlEditsEnabled) { + return this.generateXMLPrompt(fileContents, settings); + } else { + return this.generatePlainPrompt(fileContents, settings); + } + } + + /** + * Generate a plain text prompt + * @param files Map of file paths to content and token counts + * @param settings Settings to apply + * @returns Plain text prompt + */ + private static generatePlainPrompt(files: Map, settings: PrompterSettings): string { + let promptText = ''; + let totalTokenCount = 0; + + for (const [filePath, { content, tokens }] of files) { + const fileName = path.basename(filePath); + totalTokenCount += tokens; + + // Add the file to the prompt + promptText += `File: ${fileName}\n`; + + // Add line numbers if enabled + if (settings.includeLineNumbers) { + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + promptText += `${i + 1}: ${lines[i]}\n`; + } + } else { + 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 + * @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, settings: PrompterSettings): string { + const xmlParts = ['', '']; + + // Add files section + xmlParts.push(' '); + for (const [filePath, { content, tokens }] of files) { + const extension = path.extname(filePath); + let language = extension.substring(1); // Remove the dot + + // Handle special cases + if (extension === '.js' || extension === '.jsx') { + language = 'javascript'; + } else if (extension === '.ts' || extension === '.tsx') { + language = 'typescript'; + } + + xmlParts.push(' '); + xmlParts.push(` ${path.basename(filePath)}`); + xmlParts.push(` ${language}`); + xmlParts.push(` ${tokens}`); + + // Format content based on settings + let formattedContent = content; + if (settings.includeLineNumbers) { + const lines = content.split('\n'); + formattedContent = lines.map((line, i) => `${i + 1}: ${line}`).join('\n'); + } + + xmlParts.push(` `); + xmlParts.push(' '); + } + xmlParts.push(' '); + + // Add options based on settings + xmlParts.push(' '); + if (settings.includeLineNumbers) { + xmlParts.push(' true'); + } + if (!settings.includeComments) { + xmlParts.push(' true'); + } + if (settings.tokenCalculationEnabled) { + xmlParts.push(` ${Array.from(files.values()).reduce((sum, { tokens }) => sum + tokens, 0)}`); + } + xmlParts.push(' '); + + xmlParts.push(''); + return xmlParts.join('\n'); + } +} diff --git a/src/utils/tokenEstimator.ts b/src/utils/tokenEstimator.ts new file mode 100644 index 0000000..0fdacbf --- /dev/null +++ b/src/utils/tokenEstimator.ts @@ -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 + } +} From 0ec4e8e2d2348477adc1ec671f8870096305eb79 Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Tue, 11 Mar 2025 18:40:20 +0000 Subject: [PATCH 03/11] follow xml pattern --- src/extension.ts | 12 +- src/models/settings.ts | 12 +- src/providers/prompterTreeProvider.ts | 15 +-- src/utils/promptGenerator.ts | 179 +++++++++++++++++++------- 4 files changed, 150 insertions(+), 68 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 4a40476..5e630d7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -45,19 +45,19 @@ export function activate(context: vscode.ExtensionContext) { state === vscode.TreeItemCheckboxState.Checked ); - // Update XML edits button text if that setting changed - if (item.settingKey === 'xmlEditsEnabled') { + // Update formatting instructions button text if that setting changed + if (item.settingKey === 'includeFormattingInstructions') { xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ? - "$(check) XML Edits" : "$(diff-added) XML Edits"; + "$(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); - xmlEditsButton.text = "$(diff-added) XML Edits"; - xmlEditsButton.tooltip = "Toggle XML Edits mode"; + xmlEditsButton.text = "$(diff-added) Formatting Instructions"; + xmlEditsButton.tooltip = "Toggle formatting instructions mode"; xmlEditsButton.command = 'prompter.toggleXmlEdits'; xmlEditsButton.show(); diff --git a/src/models/settings.ts b/src/models/settings.ts index 609cfef..6c2805e 100644 --- a/src/models/settings.ts +++ b/src/models/settings.ts @@ -1,15 +1,13 @@ // Settings interface for the Prompter extension export interface PrompterSettings { - xmlEditsEnabled: boolean; - includeLineNumbers: boolean; - includeComments: boolean; + includeFormattingInstructions: boolean; tokenCalculationEnabled: boolean; + includeFileMap: boolean; } // Default settings values export const DEFAULT_SETTINGS: PrompterSettings = { - xmlEditsEnabled: false, - includeLineNumbers: false, - includeComments: true, - tokenCalculationEnabled: true + includeFormattingInstructions: false, + tokenCalculationEnabled: true, + includeFileMap: true }; diff --git a/src/providers/prompterTreeProvider.ts b/src/providers/prompterTreeProvider.ts index 4c1c1a1..e968856 100644 --- a/src/providers/prompterTreeProvider.ts +++ b/src/providers/prompterTreeProvider.ts @@ -129,10 +129,9 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider, settings: PrompterSettings): string { - const xmlParts = ['', '']; + const xmlParts: string[] = []; + + // Include XML formatting instructions if enabled + if (settings.includeFormattingInstructions) { + try { + // Get the workspace root path + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; + const formattingInstructionsPath = path.join(workspaceRoot, '..', 'project_management', 'xml_formatting_instructions.xml'); + + if (fs.existsSync(formattingInstructionsPath)) { + const instructions = fs.readFileSync(formattingInstructionsPath, 'utf8'); + xmlParts.push(instructions); + } + } catch (error) { + console.error('Error reading XML formatting instructions:', error); + } + } - // Add files section - xmlParts.push(' '); + // Generate file map section if enabled in settings + if (settings.includeFileMap) { + xmlParts.push(''); + + // Get the workspace root path + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; + xmlParts.push(workspaceRoot); + + // Create a tree representation of the files + const fileTree = this.generateFileTree(files, workspaceRoot); + xmlParts.push(fileTree); + xmlParts.push(''); + } + + // Generate file contents section + xmlParts.push(''); + + // Add each file with its content for (const [filePath, { content, tokens }] of files) { const extension = path.extname(filePath); let language = extension.substring(1); // Remove the dot - // Handle special cases + // 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'; } - xmlParts.push(' '); - xmlParts.push(` ${path.basename(filePath)}`); - xmlParts.push(` ${language}`); - xmlParts.push(` ${tokens}`); + // Use content as is + const formattedContent = content; - // Format content based on settings - let formattedContent = content; - if (settings.includeLineNumbers) { - const lines = content.split('\n'); - formattedContent = lines.map((line, i) => `${i + 1}: ${line}`).join('\n'); - } + // Get the workspace root path if not already defined + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; - xmlParts.push(` `); - xmlParts.push(' '); - } - xmlParts.push(' '); - - // Add options based on settings - xmlParts.push(' '); - if (settings.includeLineNumbers) { - xmlParts.push(' true'); - } - if (!settings.includeComments) { - xmlParts.push(' true'); + // Add file entry + xmlParts.push(`File: ${this.getRelativePath(filePath, workspaceRoot)}`); + xmlParts.push(`\`\`\`${language}`); + xmlParts.push(formattedContent); + xmlParts.push('\`\`\`'); } + + xmlParts.push(''); + + // Calculate tokens for toast notification but don't include in XML if (settings.tokenCalculationEnabled) { - xmlParts.push(` ${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}`); } - xmlParts.push(' '); - - xmlParts.push(''); + 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, rootPath: string): string { + // Create a simple tree representation + const treeLines: string[] = []; + + // Group files by directory + const dirMap = new Map(); + + for (const filePath of files.keys()) { + const relativePath = this.getRelativePath(filePath, rootPath); + const dir = path.dirname(relativePath); + + if (!dirMap.has(dir)) { + dirMap.set(dir, []); + } + + dirMap.get(dir)?.push(path.basename(filePath)); + } + + // Sort directories + const sortedDirs = Array.from(dirMap.keys()).sort(); + + // Build the tree + for (let i = 0; i < sortedDirs.length; i++) { + const dir = sortedDirs[i]; + const isLast = i === sortedDirs.length - 1; + const prefix = isLast ? '└── ' : '├── '; + + // Skip root directory + if (dir !== '.') { + treeLines.push(`${prefix}${dir}`); + } + + // Add files + const files = dirMap.get(dir)?.sort() || []; + for (let j = 0; j < files.length; j++) { + const file = files[j]; + const isLastFile = j === files.length - 1; + const filePrefix = dir === '.' ? (isLastFile ? '└── ' : '├── ') : ' ' + (isLastFile ? '└── ' : '├── '); + treeLines.push(`${filePrefix}${file}`); + } + } + + 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; + } } From 3adfd712c098aaa9f4f41f8c7489f9c98bc6acb1 Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Wed, 12 Mar 2025 06:42:53 +0000 Subject: [PATCH 04/11] add XML formatting instructions file and update prompt generator to include instructions --- resources/xml_formatting_instructions.xml | 294 ++++++++++++++++++++++ src/utils/promptGenerator.ts | 20 +- 2 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 resources/xml_formatting_instructions.xml diff --git a/resources/xml_formatting_instructions.xml b/resources/xml_formatting_instructions.xml new file mode 100644 index 0000000..ca76f9a --- /dev/null +++ b/resources/xml_formatting_instructions.xml @@ -0,0 +1,294 @@ + +### 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 doesn’t exist. +2. **rewrite** – Replace the entire content of an existing file. +3. **modify** (search/replace) – For partial edits with + . +4. **delete** – Remove a file entirely (empty ). + +### **Format to Follow for Repo Prompt's Diff Protocol** + + +Describe your approach or reasoning here. + + + + + Brief explanation of this specific change + +=== +// Exactly matching lines to find +=== + + +=== +// Provide the new or updated code here. Do not use placeholders +=== + + + + + +#### Tools Demonstration +1. `` – Full file in +2. `` – Empty +3. `` – Partial edit with `` + `` +4. `` – Entire file in +5. `` – Entire file in . No required. + +## Format Guidelines +1. **Plan**: Begin with a `` block explaining your approach. +2. ** Tag**: e.g. ``. Must match an available tool. +3. ** Tag**: Provide `` to clarify each change. Then `` for new/modified code. Additional rules depend on your capabilities. +4. **modify**: ** & **: Provide code blocks enclosed by ===. Respect indentation exactly, ensuring the block matches the original source down to braces, spacing, and any comments. The new will replace the 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 `` and put the entire file in ``. +7. **create**: For new files, put the full file in . +8. **delete**: Provide an empty . The file is removed. + +## Code Examples + +----- +### Example: Search and Replace (Add email property) + +Add an email property to `User` via search/replace. + + + + + Add email property to User struct + +=== +struct User { + let id: UUID + var name: String +} +=== + + +=== +struct User { + let id: UUID + var name: String + var email: String +} +=== + + + + +----- +### Example: Negative Example - Mismatched Search Block +// Example Input (not part of final output, just demonstration) + +File: path/service.swift +``` +import Foundation +class Example { + foo() { + Bar() + } +} +``` + + + +Demonstrate how a mismatched search block leads to failed merges. + + + + + This search block is missing or has mismatched indentation, braces, etc. + +=== + foo() { + Bar() + } +=== + + +=== + foo() { + Bar() + Bar2() + } +=== + + + + + + +----- +### Example: Negative Example - Mismatched Brace Balance +// This negative example shows how adding extra braces in the can break brace matching. + +Demonstrate that the new content block has one extra closing brace, causing mismatched braces. + + + + + Mismatched brace balance in the replacement content + +=== + foo() { + Bar() + } +=== + + +=== + foo() { + Bar() + } + + bar() { + foo2() + } +} +=== + + + + + + +----- +### Example: Negative Example - One-Line Search Block + +Demonstrate a one-line search block, which is too short to be reliable. + + + + + One-line search block is ambiguous + +=== +var email: String +=== + + +=== +var emailNew: String +=== + + + + + + +----- +### Example: Negative Example - Ambiguous Search Block + +Demonstrate an ambiguous search block that can match multiple blocks (e.g., multiple closing braces). + + + + + Ambiguous search block with multiple closing braces + +=== + } +} +=== + + +=== + foo() { + } + } +} +=== + + + + + + +----- +### Example: Full File Rewrite + +Rewrite the entire User file to include an email property. + + + + + Full file rewrite with new email field + +=== +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 + } +} +=== + + + + +----- +### Example: Create New File + +Create a new RoundedButton for a custom Swift UIButton subclass. + + + + + Create custom RoundedButton class + +=== +import UIKit +@IBDesignable +class RoundedButton: UIButton { + @IBInspectable var cornerRadius: CGFloat = 0 +} +=== + + + + +----- +### Example: Delete a File + +Remove an obsolete file. + + + + + Completely remove the file from the project + +=== +=== + + + + +## Final Notes +1. **modify** Always wrap the exact original lines in and your updated lines in , each enclosed by ===. +2. **modify** The 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 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 user’s relative or absolute paths. +7. Wrap your final output in ```XML ... ``` for clarity. +8. **Important:** Do not wrap any XML output in CDATA tags (i.e. ``). Repo Prompt expects raw XML exactly as shown in the examples. +9. **IMPORTANT** IF MAKING FILE CHANGES, YOU MUST USE THE AVAILABLE XML FORMATTING CAPABILITIES PROVIDED ABOVE - IT IS THE ONLY WAY FOR YOUR CHANGES TO BE APPLIED. +10. The final output must apply cleanly with no leftover syntax errors. + \ No newline at end of file diff --git a/src/utils/promptGenerator.ts b/src/utils/promptGenerator.ts index d21b17c..d902859 100644 --- a/src/utils/promptGenerator.ts +++ b/src/utils/promptGenerator.ts @@ -73,16 +73,19 @@ export class PromptGenerator { private static generateXMLPrompt(files: Map, settings: PrompterSettings): string { const xmlParts: string[] = []; - // Include XML formatting instructions if enabled + // Store formatting instructions to add at the end if enabled + let formattingInstructions = ''; if (settings.includeFormattingInstructions) { try { - // Get the workspace root path - const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || ''; - const formattingInstructionsPath = path.join(workspaceRoot, '..', 'project_management', 'xml_formatting_instructions.xml'); + // 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)) { - const instructions = fs.readFileSync(formattingInstructionsPath, 'utf8'); - xmlParts.push(instructions); + 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); @@ -150,6 +153,11 @@ export class PromptGenerator { 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'); } From 6e3fbd8f2bbb9929f44d64bb950cfbf11dcf83b7 Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Wed, 12 Mar 2025 06:44:37 +0000 Subject: [PATCH 05/11] add repository URL to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 9df2150..144d4aa 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Easy prompt generation and apply edits using prompter.", "version": "0.0.1", "publisher": "abhishekbhakat", + "repository": "https://github.com/abhishekbhakat/prompter", "engines": { "vscode": "^1.98.0" }, From 65bf168e1f9e29045e511c3430cb82f486c68c51 Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Wed, 12 Mar 2025 06:54:12 +0000 Subject: [PATCH 06/11] add conditional visibility for prompter commands and include 'ignore' dependency --- package.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 144d4aa..6cb2cf3 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,13 @@ "view/title": [ { "command": "prompter.generatePrompt", - "group": "navigation@1" + "group": "navigation@1", + "when": "view == prompterView" }, { "command": "prompter.openSettings", - "group": "navigation@2" + "group": "navigation@2", + "when": "view == prompterView" } ], "view/item/context": [ @@ -85,6 +87,7 @@ "typescript": "^5.7.3" }, "dependencies": { - "@vscode/vsce": "^3.2.2" + "@vscode/vsce": "^3.2.2", + "ignore": "^7.0.3" } } From 2d28f71e9b984e75895b927c9cbf7bfc2d361cef Mon Sep 17 00:00:00 2001 From: abhishekbhakat Date: Wed, 12 Mar 2025 07:25:26 +0000 Subject: [PATCH 07/11] add support for recursive selection and deselection of files in the tree view --- .vscode-test.mjs | 10 + src/extension.ts | 24 ++- src/providers/prompterTreeProvider.ts | 86 ++++++++- src/test/fileTreeTest.ts | 251 ++++++++++++++++++++++++++ src/utils/promptGenerator.ts | 141 ++++++++++++--- 5 files changed, 473 insertions(+), 39 deletions(-) create mode 100644 .vscode-test.mjs create mode 100644 src/test/fileTreeTest.ts diff --git a/.vscode-test.mjs b/.vscode-test.mjs new file mode 100644 index 0000000..2966dbc --- /dev/null +++ b/.vscode-test.mjs @@ -0,0 +1,10 @@ +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default { + version: 'stable', + extensionDevelopmentPath: __dirname, + extensionTestsPath: path.join(__dirname, 'out', 'test') +}; diff --git a/src/extension.ts b/src/extension.ts index 5e630d7..a0eba31 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,19 +15,31 @@ export function activate(context: vscode.ExtensionContext) { // Get the workspace folder const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; + console.log('Workspace root:', workspaceRoot); // Create the tree data provider + console.log('Creating tree data provider...'); const prompterTreeProvider = new PrompterTreeProvider(workspaceRoot); + console.log('Tree data provider created successfully'); // Register the TreeView with checkbox support - const treeView = vscode.window.createTreeView('prompterView', { - treeDataProvider: prompterTreeProvider, - showCollapseAll: true, - canSelectMany: true - }); + console.log('Registering tree view with ID: prompterView'); + let treeView: vscode.TreeView; + try { + treeView = vscode.window.createTreeView('prompterView', { + treeDataProvider: prompterTreeProvider, + showCollapseAll: true, + canSelectMany: true + }); + console.log('Tree view registered successfully'); + } catch (error) { + console.error('Error registering tree view:', error); + // Create a fallback empty tree view to prevent further errors + treeView = {} as vscode.TreeView; + } // Handle checkbox changes - treeView.onDidChangeCheckboxState(e => { + treeView.onDidChangeCheckboxState((e: vscode.TreeCheckboxChangeEvent) => { console.log('Checkbox state changed'); for (const [item, state] of e.items) { if (item instanceof FileTreeItem) { diff --git a/src/providers/prompterTreeProvider.ts b/src/providers/prompterTreeProvider.ts index e968856..955e2d1 100644 --- a/src/providers/prompterTreeProvider.ts +++ b/src/providers/prompterTreeProvider.ts @@ -94,16 +94,92 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider { + try { + // Add the directory itself + this.selectedFiles.add(dirPath); + console.log(`Added directory ${dirPath} to selection`); + + // Read directory contents + const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath)); + + // Process each item + for (const [name, type] of files) { + const filePath = path.join(dirPath, name); + + if (type === vscode.FileType.Directory) { + // Recursively process subdirectories + await this.addDirectoryToSelection(filePath); + } else { + // Add files + this.selectedFiles.add(filePath); + console.log(`Added ${filePath} to selection (from directory)`); + } + } + } catch (error) { + console.error(`Error adding directory to selection: ${dirPath}`, error); + } + } + // Remove a file from the selection removeFromSelection(item: FileTreeItem): void { if (item.resourceUri) { - this.selectedFiles.delete(item.resourceUri.fsPath); - console.log(`Removed ${item.resourceUri.fsPath} from selection`); + const filePath = item.resourceUri.fsPath; + + // Check if it's a directory + if (item.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed || + item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded) { + // It's a directory, recursively remove all files + this.removeDirectoryFromSelection(filePath); + } else { + // It's a file, just remove it + this.selectedFiles.delete(filePath); + console.log(`Removed ${filePath} from selection`); + } + } + } + + // Recursively remove all files in a directory from the selection + private async removeDirectoryFromSelection(dirPath: string): Promise { + try { + // Remove the directory itself + this.selectedFiles.delete(dirPath); + console.log(`Removed directory ${dirPath} from selection`); + + // Read directory contents + const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath)); + + // Process each item + for (const [name, type] of files) { + const filePath = path.join(dirPath, name); + + if (type === vscode.FileType.Directory) { + // Recursively process subdirectories + await this.removeDirectoryFromSelection(filePath); + } else { + // Remove files + this.selectedFiles.delete(filePath); + console.log(`Removed ${filePath} from selection (from directory)`); + } + } + } catch (error) { + console.error(`Error removing directory from selection: ${dirPath}`, error); } } diff --git a/src/test/fileTreeTest.ts b/src/test/fileTreeTest.ts new file mode 100644 index 0000000..1b4e831 --- /dev/null +++ b/src/test/fileTreeTest.ts @@ -0,0 +1,251 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +// Using require for the ignore package due to its module export style +const ignoreLib = require('ignore'); + +// A simplified version of our tree structure for testing +interface TreeNode { + name: string; + isDirectory: boolean; + children: Map; +} + +// Test implementation of the file tree generation logic +function generateFileTree(files: string[], rootPath: string): string { + // Create a tree representation + const treeLines: string[] = []; + + // Create root node + const root: TreeNode = { + name: path.basename(rootPath), + isDirectory: true, + children: new Map() + }; + + // Initialize the ignore instance + const ig = ignoreLib(); + + // Read .gitignore patterns if available + try { + const gitignorePath = path.join(rootPath, '.gitignore'); + if (fs.existsSync(gitignorePath)) { + const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); + console.log('Using .gitignore content:'); + console.log(gitignoreContent); + + // Add patterns from .gitignore + ig.add(gitignoreContent); + // Always include .gitignore itself + ig.add('!.gitignore'); + + // Debug what's being ignored + console.log('\nIgnore patterns loaded. Testing patterns:'); + ['file1.txt', 'file2.log', 'node_modules/package.json', 'src/index.ts', 'temp/temp.txt'].forEach(testPath => { + console.log(`${testPath}: ${ig.ignores(testPath) ? 'IGNORED' : 'included'}`); + }); + console.log(); + } + } catch (error) { + console.error('Error reading .gitignore:', error); + } + + // Build the tree structure + for (const filePath of files) { + const relativePath = getRelativePath(filePath, rootPath); + + // Skip ignored files using the ignore package + // Use forward slashes for paths to ensure consistent matching + const normalizedPath = relativePath.split(path.sep).join('/'); + if (ig.ignores(normalizedPath)) { + console.log(`Ignoring: ${normalizedPath}`); + continue; + } + + // Split the path into parts + const parts = relativePath.split('/'); + + // Start from the root + let currentNode = root; + + // Build the path in the tree + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (!part) { continue; } // Skip empty parts + + const isDirectory = i < parts.length - 1; + + if (!currentNode.children.has(part)) { + currentNode.children.set(part, { + name: part, + isDirectory, + children: new Map() + }); + } else if (isDirectory) { + // Ensure it's marked as a directory if we encounter it again + currentNode.children.get(part)!.isDirectory = true; + } + + currentNode = currentNode.children.get(part)!; + } + } + + // Function to recursively build the tree lines + const buildTreeLines = (node: TreeNode, prefix: string = '', isLast: boolean = true, parentPrefix: string = ''): void => { + // Skip the root node in the output + if (node !== root) { + const linePrefix = parentPrefix + (isLast ? '└── ' : '├── '); + treeLines.push(`${linePrefix}${node.name}${node.isDirectory ? '' : ''}`); + } + + // Sort children: directories first, then files, both alphabetically + const sortedChildren = Array.from(node.children.values()) + .sort((a, b) => { + if (a.isDirectory === b.isDirectory) { + return a.name.localeCompare(b.name); + } + return a.isDirectory ? -1 : 1; + }); + + // Process children + sortedChildren.forEach((child, index) => { + const isChildLast = index === sortedChildren.length - 1; + const childParentPrefix = node === root ? '' : parentPrefix + (isLast ? ' ' : '│ '); + + buildTreeLines(child, prefix, isChildLast, childParentPrefix); + }); + }; + + // Build the tree lines + buildTreeLines(root); + + return treeLines.join('\n'); +} + +// Helper function to get relative path +function getRelativePath(filePath: string, rootPath: string): string { + if (filePath.startsWith(rootPath)) { + const relativePath = filePath.substring(rootPath.length); + return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; + } + return filePath; +} + +// Run tests +function runTests() { + console.log('Running file tree generation tests...'); + + // Create a temporary directory for our tests + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prompter-test-')); + console.log(`Created temp directory: ${tempDir}`); + + try { + // Test 1: Basic file structure + console.log('\nTest 1: Basic file structure'); + const files = [ + path.join(tempDir, 'file1.txt'), + path.join(tempDir, 'folder1/file2.txt'), + path.join(tempDir, 'folder1/subfolder/file3.txt'), + path.join(tempDir, 'folder2/file4.txt') + ]; + + // Create the directories and files + files.forEach(file => { + const dir = path.dirname(file); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(file, 'test content'); + }); + + const result1 = generateFileTree(files, tempDir); + console.log(result1); + + // Test 2: With .gitignore + console.log('\nTest 2: With .gitignore'); + // Create a .gitignore file with more explicit patterns + const gitignorePath = path.join(tempDir, '.gitignore'); + fs.writeFileSync(gitignorePath, '# Ignore log files\n*.log\n\n# Ignore node_modules directory\nnode_modules/\n\n# Ignore temp directory\ntemp/\n'); + console.log('Created .gitignore with content:'); + console.log(fs.readFileSync(gitignorePath, 'utf8')); + + // Create test files for .gitignore testing + const files2 = [ + path.join(tempDir, '.gitignore'), + path.join(tempDir, 'file1.txt'), + path.join(tempDir, 'file2.log'), // Should be ignored + path.join(tempDir, 'node_modules/package.json'), // Should be ignored + path.join(tempDir, 'src/index.ts'), + path.join(tempDir, 'temp/temp.txt') // Should be ignored + ]; + + console.log('Test files created:'); + files2.forEach(f => console.log(` - ${getRelativePath(f, tempDir)}`)); + + // Create the directories and files + files2.forEach(file => { + const dir = path.dirname(file); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + // Don't overwrite the .gitignore file + if (path.basename(file) !== '.gitignore') { + fs.writeFileSync(file, 'test content'); + } + }); + + const result2 = generateFileTree(files2, tempDir); + console.log(result2); + + // Test 3: Empty directories + console.log('\nTest 3: Empty directories'); + const emptyDir = path.join(tempDir, 'emptyDir'); + if (!fs.existsSync(emptyDir)) { + fs.mkdirSync(emptyDir, { recursive: true }); + } + + const files3 = [ + path.join(tempDir, 'file1.txt'), + path.join(tempDir, 'emptyDir') // Empty directory + ]; + + const result3 = generateFileTree(files3, tempDir); + console.log(result3); + + // Test 4: Sorting + console.log('\nTest 4: Sorting (directories first, then files)'); + const files4 = [ + path.join(tempDir, 'z_file.txt'), + path.join(tempDir, 'a_file.txt'), + path.join(tempDir, 'z_folder/file.txt'), + path.join(tempDir, 'a_folder/file.txt') + ]; + + // Create the directories and files + files4.forEach(file => { + const dir = path.dirname(file); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(file, 'test content'); + }); + + const result4 = generateFileTree(files4, tempDir); + console.log(result4); + + } catch (error) { + console.error('Test error:', error); + } finally { + // Clean up + console.log('\nCleaning up...'); + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + console.log(`Removed temp directory: ${tempDir}`); + } catch (cleanupError) { + console.error('Error during cleanup:', cleanupError); + } + } +} + +// Run the tests +runTests(); diff --git a/src/utils/promptGenerator.ts b/src/utils/promptGenerator.ts index d902859..48e17ea 100644 --- a/src/utils/promptGenerator.ts +++ b/src/utils/promptGenerator.ts @@ -3,6 +3,21 @@ import * as fs from 'fs'; import * as path from 'path'; import { PrompterSettings } from '../models/settings'; import { FileReader } from './fileReader'; +// Using require for the ignore package due to its module export style +let ignore: any; +try { + ignore = require('ignore'); + console.log('Successfully loaded ignore package'); +} catch (error) { + console.error('Error loading ignore package:', error); + // Fallback implementation if the package fails to load + ignore = { + default: () => ({ + add: () => {}, + ignores: () => false + }) + }; +} import { TokenEstimator } from './tokenEstimator'; /** @@ -168,47 +183,117 @@ export class PromptGenerator { * @returns String representation of the file tree */ private static generateFileTree(files: Map, rootPath: string): string { - // Create a simple tree representation + // Create a tree representation const treeLines: string[] = []; - // Group files by directory - const dirMap = new Map(); + // Create a tree structure + interface TreeNode { + name: string; + isDirectory: boolean; + children: Map; + } + // Create root node + const root: TreeNode = { + name: path.basename(rootPath), + isDirectory: true, + children: new Map() + }; + + // Initialize the ignore instance + let ig; + try { + ig = typeof ignore === 'function' ? ignore() : ignore.default(); + console.log('Successfully initialized ignore instance'); + } catch (error) { + console.error('Error initializing ignore instance:', error); + // Fallback implementation + ig = { + add: () => {}, + ignores: () => false + }; + } + + // Read .gitignore patterns if available + try { + const gitignorePath = path.join(rootPath, '.gitignore'); + if (fs.existsSync(gitignorePath)) { + const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); + // Add patterns from .gitignore + ig.add(gitignoreContent); + // Always include .gitignore itself + ig.add('!.gitignore'); + } + } catch (error) { + console.error('Error reading .gitignore:', error); + } + + // Build the tree structure for (const filePath of files.keys()) { const relativePath = this.getRelativePath(filePath, rootPath); - const dir = path.dirname(relativePath); - if (!dirMap.has(dir)) { - dirMap.set(dir, []); + // Skip ignored files using the ignore package + // Use forward slashes for paths to ensure consistent matching across platforms + const normalizedPath = relativePath.split(path.sep).join('/'); + if (ig.ignores(normalizedPath)) { + continue; } - dirMap.get(dir)?.push(path.basename(filePath)); - } - - // Sort directories - const sortedDirs = Array.from(dirMap.keys()).sort(); - - // Build the tree - for (let i = 0; i < sortedDirs.length; i++) { - const dir = sortedDirs[i]; - const isLast = i === sortedDirs.length - 1; - const prefix = isLast ? '└── ' : '├── '; + // Split the path into parts + const parts = relativePath.split('/'); - // Skip root directory - if (dir !== '.') { - treeLines.push(`${prefix}${dir}`); - } + // Start from the root + let currentNode = root; - // Add files - const files = dirMap.get(dir)?.sort() || []; - for (let j = 0; j < files.length; j++) { - const file = files[j]; - const isLastFile = j === files.length - 1; - const filePrefix = dir === '.' ? (isLastFile ? '└── ' : '├── ') : ' ' + (isLastFile ? '└── ' : '├── '); - treeLines.push(`${filePrefix}${file}`); + // Build the path in the tree + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const isDirectory = i < parts.length - 1; + + if (!currentNode.children.has(part)) { + currentNode.children.set(part, { + name: part, + isDirectory, + children: new Map() + }); + } else if (isDirectory) { + // Ensure it's marked as a directory if we encounter it again + currentNode.children.get(part)!.isDirectory = true; + } + + currentNode = currentNode.children.get(part)!; } } + // Function to recursively build the tree lines + const buildTreeLines = (node: TreeNode, prefix: string = '', isLast: boolean = true, parentPrefix: string = ''): void => { + // Skip the root node in the output + if (node !== root) { + const linePrefix = parentPrefix + (isLast ? '└── ' : '├── '); + treeLines.push(`${linePrefix}${node.name}${node.isDirectory ? '' : ''}`); + } + + // Sort children: directories first, then files, both alphabetically + const sortedChildren = Array.from(node.children.values()) + .sort((a, b) => { + if (a.isDirectory === b.isDirectory) { + return a.name.localeCompare(b.name); + } + return a.isDirectory ? -1 : 1; + }); + + // Process children + sortedChildren.forEach((child, index) => { + const isChildLast = index === sortedChildren.length - 1; + const childParentPrefix = node === root ? '' : parentPrefix + (isLast ? ' ' : '│ '); + + buildTreeLines(child, prefix, isChildLast, childParentPrefix); + }); + }; + + // Build the tree lines + buildTreeLines(root); + return treeLines.join('\n'); } From 926c88780b7b9722cd9ca48ab8c7055cb8d2dc2f Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 12 Mar 2025 08:01:18 +0000 Subject: [PATCH 08/11] 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 --- src/providers/fileSelectionManager.ts | 103 ++++++++++++++++++ src/providers/prompterTreeProvider.ts | 146 ++++++++------------------ src/providers/settingsManager.ts | 46 ++++++++ 3 files changed, 192 insertions(+), 103 deletions(-) create mode 100644 src/providers/fileSelectionManager.ts create mode 100644 src/providers/settingsManager.ts diff --git a/src/providers/fileSelectionManager.ts b/src/providers/fileSelectionManager.ts new file mode 100644 index 0000000..16c3101 --- /dev/null +++ b/src/providers/fileSelectionManager.ts @@ -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 = 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 { + 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 { + 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 { + console.log('getSelectedFiles called, count:', this.selectedFiles.size); + return this.selectedFiles; + } +} diff --git a/src/providers/prompterTreeProvider.ts b/src/providers/prompterTreeProvider.ts index 955e2d1..a2472f7 100644 --- a/src/providers/prompterTreeProvider.ts +++ b/src/providers/prompterTreeProvider.ts @@ -2,6 +2,8 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { FileTreeItem, SettingTreeItem } from '../models/treeItems'; import { PrompterSettings, DEFAULT_SETTINGS } from '../models/settings'; +import { FileSelectionManager } from './fileSelectionManager'; +import { SettingsManager } from './settingsManager'; /** * Tree provider for the Prompter extension @@ -10,16 +12,19 @@ import { PrompterSettings, DEFAULT_SETTINGS } from '../models/settings'; export class PrompterTreeProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - - private selectedFiles: Set = new Set(); - private settings: PrompterSettings = { ...DEFAULT_SETTINGS }; + + private fileSelectionManager: FileSelectionManager; + private settingsManager: SettingsManager; private showingSettings: boolean = false; - constructor(private workspaceRoot: string | undefined) {} - + 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; @@ -31,10 +36,10 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider { 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 @@ -94,100 +99,35 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider { - try { - // Add the directory itself - this.selectedFiles.add(dirPath); - console.log(`Added directory ${dirPath} to selection`); - - // Read directory contents - const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath)); - - // Process each item - for (const [name, type] of files) { - const filePath = path.join(dirPath, name); - - if (type === vscode.FileType.Directory) { - // Recursively process subdirectories - await this.addDirectoryToSelection(filePath); - } else { - // Add files - this.selectedFiles.add(filePath); - console.log(`Added ${filePath} to selection (from directory)`); - } - } - } catch (error) { - console.error(`Error adding directory to selection: ${dirPath}`, error); - } - } - + // Remove a file from the selection removeFromSelection(item: FileTreeItem): void { if (item.resourceUri) { - const filePath = item.resourceUri.fsPath; - - // Check if it's a directory - if (item.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed || - item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded) { - // It's a directory, recursively remove all files - this.removeDirectoryFromSelection(filePath); + if (this.isDirectory(item)) { + this.fileSelectionManager.removeDirectory(item.resourceUri.fsPath); } else { - // It's a file, just remove it - this.selectedFiles.delete(filePath); - console.log(`Removed ${filePath} from selection`); + this.fileSelectionManager.removeFile(item.resourceUri.fsPath); } } } - - // Recursively remove all files in a directory from the selection - private async removeDirectoryFromSelection(dirPath: string): Promise { - try { - // Remove the directory itself - this.selectedFiles.delete(dirPath); - console.log(`Removed directory ${dirPath} from selection`); - - // Read directory contents - const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath)); - - // Process each item - for (const [name, type] of files) { - const filePath = path.join(dirPath, name); - - if (type === vscode.FileType.Directory) { - // Recursively process subdirectories - await this.removeDirectoryFromSelection(filePath); - } else { - // Remove files - this.selectedFiles.delete(filePath); - console.log(`Removed ${filePath} from selection (from directory)`); - } - } - } catch (error) { - console.error(`Error removing directory from selection: ${dirPath}`, error); - } + + 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.selectedFiles.has(filePath)) { + if (this.fileSelectionManager.isSelected(filePath)) { this.removeFromSelection(item); } else { this.addToSelection(item); @@ -198,25 +138,25 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider { - console.log('getSelectedFiles called, count:', this.selectedFiles.size); - return this.selectedFiles; + 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', this.settings.includeFormattingInstructions), - new SettingTreeItem('Token Calculation', 'tokenCalculationEnabled', this.settings.tokenCalculationEnabled), - new SettingTreeItem('Include File Map', 'includeFileMap', this.settings.includeFileMap) + 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.settings[key] = value; + this.settingsManager.updateSetting(key, value); this.refresh(); } - + // Check if showing settings view isShowingSettings(): boolean { return this.showingSettings; @@ -224,23 +164,23 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider Date: Wed, 12 Mar 2025 08:06:19 +0000 Subject: [PATCH 09/11] ignore config --- .vscodeignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscodeignore b/.vscodeignore index e0c6b8d..5d7beb3 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -25,4 +25,5 @@ generated-icon.png out/test/** package-lock.json # Assets not needed in the final package -attached_assets/** \ No newline at end of file +attached_assets/** +.config From 134bdf1e5e7d61f5d0349e9dfa177e87e6f56ef0 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 12 Mar 2025 08:39:44 +0000 Subject: [PATCH 10/11] 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 --- src/providers/prompterTreeProvider.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/providers/prompterTreeProvider.ts b/src/providers/prompterTreeProvider.ts index a2472f7..97b8551 100644 --- a/src/providers/prompterTreeProvider.ts +++ b/src/providers/prompterTreeProvider.ts @@ -1,7 +1,8 @@ + import * as vscode from 'vscode'; import * as path from 'path'; import { FileTreeItem, SettingTreeItem } from '../models/treeItems'; -import { PrompterSettings, DEFAULT_SETTINGS } from '../models/settings'; +import { PrompterSettings } from '../models/settings'; import { FileSelectionManager } from './fileSelectionManager'; import { SettingsManager } from './settingsManager'; @@ -183,4 +184,4 @@ export class PrompterTreeProvider implements vscode.TreeDataProvider Date: Wed, 12 Mar 2025 09:08:56 +0000 Subject: [PATCH 11/11] remove duplication --- README.md | 74 ------------------------------------------------------- 1 file changed, 74 deletions(-) diff --git a/README.md b/README.md index a265de2..92a57f9 100644 --- a/README.md +++ b/README.md @@ -77,77 +77,3 @@ See `vsc-extension-quickstart.md` for development setup and testing details. 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. - -## 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 - ``` -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.