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 + } +}