• Uncategorised
  • 0

Creating cursor Plugin to interact with your RAG server

This is a VS Code extension (written in TypeScript) that provides RAG‑powered ColdFusion code completions. Here’s how it’s structured:


1. Output Channel

  • Creates a dedicated “ColdFusion RAG” output channel to log every step (initialization, errors, retrievals, completions).

2. SharedContext Singleton

  • Stores the last RAG context (retrieved examples) and user query so that the completion provider can access them across calls.
  • Methods: setContext(), getContext(), clearContext().

3. ColdFusionCompletionProvider

Implements VS Code’s CompletionItemProvider to generate AI‑driven snippets:

  • Constructor & initializeClients()
    • Reads openaiKey and pineconeKey from workspace settings.
    • Instantiates OpenAI and Pinecone clients, selects Pinecone index/namespace.
  • provideCompletionItems()
    • Logs the current line prefix and checks if we already have RAG context.
    • If the user types <cf without any context, it kicks off generateCode() to populate context.
    • Once context + query are available:
      1. Builds a prompt combining the “similar examples” (RAG context) and the user’s request.
      2. Calls openai.chat.completions.create() (gpt‑3.5‑turbo) to generate a tag.
      3. Cleans up and converts the response into a snippet, replacing common attributes with placeholders (${1:name}, etc.).
      4. Returns the AI‑generated snippet as a top‑ranked completion item.
    • Falls back to a simple ColdFusion/HTML tag snippet if something goes wrong.

4. generateCode() Helper

Runs on-demand (command or first <cf trigger) to:

  1. Gather Context
    • Ensures the file is .cfm/.cfc (ColdFusion).
    • Grabs the current line (and optional user input via a QuickPick).
  2. RAG Retrieval
    • Uses OpenAI embeddings (ada‑002) on the line / query, queries Pinecone for the top 3 nearest matches.
    • Concatenates the retrieved examples’ fullContent into a single ragContext string.
  3. Store Context
    • Saves ragContext + user query into SharedContext so that the next completion request can use it.
  4. Trigger Completion
    • Inserts a blank line at the cursor, then fires editor.action.triggerSuggest() to open the suggestion list.

5. Command Handlers

  • coldfusion-rag.cmdKCompletion (⌘+K) and
    coldfusion-rag.getCompletion
    —both call generateCode() when invoked, after verifying the active file is ColdFusion.

6. Extension Activation / Deactivation

  • activate()
    • Shows and logs to the output channel.
    • Registers the two commands and the </space‑triggered completion provider for CFML files.
  • deactivate()
    • Logs that the assistant has been shut down.

In short, this extension uses Pinecone to fetch similar ColdFusion examples, stitches them into a prompt, and then calls OpenAI to generate context‑aware CFML/HTML tag snippets directly inside the editor.

import * as vscode from 'vscode';
import { OpenAI } from 'openai';
import { Pinecone } from '@pinecone-database/pinecone';

// Create output channel at the module level
const outputChannel = vscode.window.createOutputChannel('ColdFusion RAG');

interface PineconeMatch {
    id?: string;
    score?: number;
    values?: number[];
    metadata?: {
        fullContent?: string;
        [key: string]: any;
    };
}

// Add a class to store the shared context
class SharedContext {
    private static instance: SharedContext;
    private _ragContext: string = '';
    private _userQuery: string = '';

    private constructor() {}

    static getInstance(): SharedContext {
        if (!SharedContext.instance) {
            SharedContext.instance = new SharedContext();
        }
        return SharedContext.instance;
    }

    setContext(ragContext: string, userQuery: string) {
        this._ragContext = ragContext;
        this._userQuery = userQuery;
        outputChannel.appendLine('📝 Updated shared context');
    }

    getContext(): { ragContext: string; userQuery: string } {
        return {
            ragContext: this._ragContext,
            userQuery: this._userQuery
        };
    }

    clearContext() {
        this._ragContext = '';
        this._userQuery = '';
    }
}

export class ColdFusionCompletionProvider implements vscode.CompletionItemProvider {
    private openai: OpenAI | null = null;
    private pinecone: Pinecone | null = null;
    private pineconeIndex: any;
    private pineconeNamespace: any;
    private outputChannel: vscode.OutputChannel;

    constructor(channel: vscode.OutputChannel) {
        this.outputChannel = channel;
        this.outputChannel.appendLine('🔄 Initializing ColdFusion RAG completion provider...');
        this.initializeClients().catch(error => {
            this.outputChannel.appendLine(`❌ Error during initialization: ${error.message}`);
            vscode.window.showErrorMessage(`Failed to initialize ColdFusion RAG: ${error.message}`);
        });
    }

    async initializeClients(): Promise<void> {
        try {
            const config = vscode.workspace.getConfiguration('coldfusion-rag');
            this.outputChannel.appendLine('📝 Configuration object retrieved');
            
            // Log all available configuration keys
            const configKeys = Object.keys(config);
            this.outputChannel.appendLine(`🔑 Available config keys: ${configKeys.join(', ')}`);
            
            const openaiKey = config.get<string>('openaiKey');
            const pineconeKey = config.get<string>('pineconeKey');
            
            // Safely log key presence and partial values
            this.outputChannel.appendLine(`🔐 OpenAI Key: ${openaiKey ? `present (ends with ...${openaiKey.slice(-4)})` : 'missing'}`);
            this.outputChannel.appendLine(`🔐 Pinecone Key: ${pineconeKey ? `present (ends with ...${pineconeKey.slice(-4)})` : 'missing'}`);
            
            if (!openaiKey || !pineconeKey) {
                outputChannel.appendLine('❌ Missing configuration - Please set OpenAI and Pinecone API keys');
                return;
            }

            this.openai = new OpenAI({ apiKey: openaiKey });
            this.pinecone = new Pinecone({ apiKey: pineconeKey });
            
            // Initialize Pinecone index and namespace
            this.pineconeIndex = this.pinecone.index('ogra');
            this.pineconeNamespace = this.pineconeIndex.namespace('ns1');
            
            this.outputChannel.appendLine('✅ OpenAI and Pinecone clients initialized successfully');
            this.outputChannel.appendLine(`📊 Using Pinecone index: ogra, namespace: ns1`);
        } catch (error) {
            const errorMsg = `❌ Error initializing clients: ${error instanceof Error ? error.message : 'Unknown error'}`;
            this.outputChannel.appendLine(errorMsg);
            vscode.window.showErrorMessage(errorMsg);
            throw error;
        }
    }

    async provideCompletionItems(
        document: vscode.TextDocument,
        position: vscode.Position
    ): Promise<vscode.CompletionItem[]> {
        try {
            this.outputChannel.appendLine("\n🔍 Triggered provideCompletionItems");
            
            // Get the line prefix and check if we should trigger
            const linePrefix = document.lineAt(position).text.substr(0, position.character);
            this.outputChannel.appendLine(`📝 Line prefix: "${linePrefix}"`);

            // Get the shared context
            const { ragContext, userQuery } = SharedContext.getInstance().getContext();
            this.outputChannel.appendLine(`📝 RAG Context available: ${Boolean(ragContext)}`);
            this.outputChannel.appendLine(`📝 User Query available: ${Boolean(userQuery)}`);
            
            // If typing <cf and no context, trigger generateCode
            if (linePrefix.trim().toLowerCase().startsWith('<cf') && (!ragContext || !userQuery)) {
                this.outputChannel.appendLine('🔄 Triggering generateCode for <cf completion');
                const editor = vscode.window.activeTextEditor;
                if (editor) {
                    await generateCode(editor, 'completion');
                    // Get the updated context
                    const updatedContext = SharedContext.getInstance().getContext();
                    if (updatedContext.ragContext && updatedContext.userQuery) {
                        this.outputChannel.appendLine('✅ Context updated from generateCode');
                        return this.provideCompletionItems(document, position); // Retry with new context
                    }
                }
            }
            
            if (!ragContext || !userQuery) {
                this.outputChannel.appendLine('❌ No RAG context or user query available');
                // Return a basic tag completion when no context is available
                const basicItem = new vscode.CompletionItem('tag (Basic)', vscode.CompletionItemKind.Snippet);
                basicItem.insertText = new vscode.SnippetString('${1:tagname} ${2:attributes}>${0}</${1:tagname}>');
                basicItem.detail = "Basic ColdFusion/HTML Tag";
                return [basicItem];
            }

            this.outputChannel.appendLine('📝 Creating completion items with shared RAG context');

            // Create completion items with the RAG context
            const completionItems: vscode.CompletionItem[] = [];
            
            // Add a completion item that includes both RAG and current context
            const item = new vscode.CompletionItem('Generate Tag', vscode.CompletionItemKind.Snippet);
            item.detail = "AI Generated Tag based on your code";
            item.documentation = new vscode.MarkdownString(
                "**Similar examples from your codebase:**\n```coldfusion\n" +
                ragContext +
                "\n```\n\n**Your request:**\n" + userQuery
            );

            try {
                // Generate code using OpenAI based on RAG context and user query
                if (!this.openai) {
                    throw new Error('OpenAI client not initialized');
                }

                const prompt = `Generate a ColdFusion tag based on this request: "${userQuery}"

Here are similar examples from the codebase for reference:
${ragContext}

 Make it concise and follow the patterns shown in the examples. The tag can be any valid ColdFusion or HTML tag (not limited to cf-prefixed tags).`;

                const completion = await this.openai.chat.completions.create({
                    model: "gpt-3.5-turbo",
                    messages: [
                        {
                            role: "system",
                            content: "You are a ColdFusion expert. Generate only the requested tag without < or > symbols, nothing else. The tag can be any valid ColdFusion or HTML tag."
                        },
                        {
                            role: "user",
                            content: prompt
                        }
                    ],
                    temperature: 0.2,
                    max_tokens: 150
                });

                const generatedCode = completion.choices[0].message.content?.trim() || '';
                this.outputChannel.appendLine('✨ Generated code: ' + generatedCode);
                
                // Clean up the generated code
                const cleanedCode = generatedCode
                    .replace(/^</, '')  // Remove leading <
                    .replace(/>$/, ''); // Remove trailing >
                
                // Convert the generated code into a snippet
                let snippetText = cleanedCode;
                
                // Handle different attribute patterns
                snippetText = snippetText
                    .replace(/name="[^"]*"/g, 'name="${1:name}"')
                    .replace(/value="[^"]*"/g, 'value="${2:value}"')
                    .replace(/datasource="[^"]*"/g, 'datasource="${3:datasource}"')
                    .replace(/class="[^"]*"/g, 'class="${4:className}"')
                    .replace(/id="[^"]*"/g, 'id="${5:id}"')
                    .replace(/style="[^"]*"/g, 'style="${6:style}"');

                // Update the completion item to show the actual code
                item.label = `<${generatedCode}>`; // Show the actual tag in suggestions
                item.detail = "Generated tag based on your request";
                item.filterText = generatedCode; // For filtering in the suggestion list
                
                // Always provide both self-closing and regular tag options
                item.insertText = new vscode.SnippetString(`${snippetText}\${1: /}\${0}`);
                
                // Ensure this appears at the top of suggestions
                item.sortText = '!';
                item.preselect = true;  // Preselect this item in the completion list
                
                this.outputChannel.appendLine('📝 Snippet prepared: ' + snippetText);

            } catch (error) {
                this.outputChannel.appendLine(`❌ Error generating code: ${error}. Using fallback snippet.`);
                // Fallback to basic snippet if generation fails
                item.insertText = new vscode.SnippetString('${1:tagname} ${2:attributes}>${0}</${1:tagname}>');
            }
            
            // Add the item to our completion list
            completionItems.push(item);
            
            // Clear the context after use
            SharedContext.getInstance().clearContext();
            
            this.outputChannel.appendLine(`✅ Returning ${completionItems.length} completion items`);
            return completionItems;

        } catch (error) {
            this.outputChannel.appendLine(`❌ Error in completion provider: ${error}`);
            return [];
        }
    }
}

// Shared code generation function
async function generateCode(editor: vscode.TextEditor, source: string) {
    try {
        outputChannel.show(true);
        outputChannel.appendLine('\n' + '='.repeat(50));
        outputChannel.appendLine(`🚀 Generating code from: ${source}`);

        if (!isColdFusionFile(editor.document)) {
            outputChannel.appendLine('❌ Not a ColdFusion file - Generation cancelled');
            return;
        }

        const position = editor.selection.active;
        const document = editor.document;

        // Get user input only if source is cmdK or command
        let userInput = '';
        if (source === 'cmdK' || source === 'command') {
            const quickPick = vscode.window.createQuickPick();
            quickPick.placeholder = 'Enter your ColdFusion code generation query';
            quickPick.title = 'ColdFusion Code Generation';

            userInput = await new Promise<string>((resolve) => {
                quickPick.onDidAccept(() => {
                    resolve(quickPick.value);
                    quickPick.hide();
                });

                quickPick.onDidHide(() => {
                    resolve('');
                    quickPick.dispose();
                });

                quickPick.show();
            });

            if (!userInput) {
                outputChannel.appendLine('❌ No input provided - Generation cancelled');
                return;
            }
        }

        // Get configurations
        const config = vscode.workspace.getConfiguration('coldfusion-rag');
        const openaiKey = config.get<string>('openaiKey');
        const pineconeKey = config.get<string>('pineconeKey');

        if (!openaiKey || !pineconeKey) {
            outputChannel.appendLine('❌ Missing configuration - Please set OpenAI and Pinecone API keys');
            return;
        }

        // Initialize clients
        const openai = new OpenAI({ apiKey: openaiKey });
        const pinecone = new Pinecone({ apiKey: pineconeKey });
        const index = pinecone.index('ogra');
        const pineconeNamespace = index.namespace('ns1');

        // Get current code context
        const currentLine = document.lineAt(position.line).text;
        
        // Get embedding for RAG - use either userInput+currentLine or just currentLine
        const embeddingInput = userInput ? `${currentLine} ${userInput}` : currentLine;
        
        const embeddingResponse = await openai.embeddings.create({
            model: "text-embedding-ada-002",
            input: embeddingInput
        });
        const queryEmbedding = embeddingResponse.data[0].embedding;

        // Query Pinecone for relevant examples
        const queryResponse = await pineconeNamespace.query({
            vector: queryEmbedding,
            topK: 3,
            includeMetadata: true
        });

        // Process Pinecone results
        const ragContext = queryResponse.matches
            .map((match: PineconeMatch) => match.metadata?.fullContent || '')
            .join('\n\n');

        // Store the context for the completion provider to use
        SharedContext.getInstance().setContext(ragContext, userInput || currentLine);
        outputChannel.appendLine('✅ Stored RAG context for completion provider');

        // Get the current line's indentation
        const indentation = currentLine.match(/^\s*/)?.[0] || '';

        outputChannel.appendLine('✅ Context prepared and completion triggered');
        outputChannel.appendLine('🤖 Cursor AI is generating code...');
        
        // Insert the code at the current position
        await editor.edit(editBuilder => {
            editBuilder.insert(position, '\n' + indentation);
        });

        // Small delay to ensure the edit is complete
        await new Promise(resolve => setTimeout(resolve, 100));
        
        // Trigger suggestions
        await vscode.commands.executeCommand('editor.action.triggerSuggest');
        
        outputChannel.appendLine('✅ Code generation process completed');
    } catch (error) {
        outputChannel.appendLine(`❌ Error in code generation: ${error}`);
    }
}

function isColdFusionFile(document: vscode.TextDocument): boolean {
    const fileName = document.fileName.toLowerCase();
    const languageId = document.languageId.toLowerCase();
    return fileName.endsWith('.cfm') || 
           fileName.endsWith('.cfc') || 
           languageId === 'cfml' || 
           languageId === 'coldfusion';
}

// Command handlers
async function handleCmdKCompletion(editor: vscode.TextEditor) {
    try {
        outputChannel.appendLine('\n🔑 Command+K command triggered');
        outputChannel.appendLine(`📄 Active file: ${editor.document.fileName}`);
        outputChannel.appendLine(`🔤 Language ID: ${editor.document.languageId}`);
        
        if (!isColdFusionFile(editor.document)) {
            outputChannel.appendLine('❌ Not a ColdFusion file, command cancelled');
            vscode.window.showInformationMessage('Command+K completion only works in ColdFusion files (.cfm, .cfc)');
            return;
        }

        await generateCode(editor, 'cmdK');
    } catch (error) {
        outputChannel.appendLine(`❌ Error in Cmd+K handler: ${error}`);
    }
}

async function handleGetCompletion(editor: vscode.TextEditor) {
    try {
        outputChannel.appendLine('\n🎯 Get Completion command triggered');
        outputChannel.appendLine(`📄 Active file: ${editor.document.fileName}`);
        outputChannel.appendLine(`🔤 Language ID: ${editor.document.languageId}`);
        
        if (!isColdFusionFile(editor.document)) {
            outputChannel.appendLine('❌ Not a ColdFusion file, command cancelled');
            vscode.window.showInformationMessage('Completion only works in ColdFusion files (.cfm, .cfc)');
            return;
        }

        await generateCode(editor, 'command');
    } catch (error) {
        outputChannel.appendLine(`❌ Error in Get Completion handler: ${error}`);
    }
}

// Command identifiers
const COMMANDS = {
    CMD_K_COMPLETION: 'coldfusion-rag.cmdKCompletion',
    GET_COMPLETION: 'coldfusion-rag.getCompletion'
} as const;

export function activate(context: vscode.ExtensionContext) {
    // Force show output channel and log activation
    outputChannel.show(true);
    outputChannel.appendLine('\n\n' + '='.repeat(80));
    outputChannel.appendLine('🚀 COLDFUSION RAG ASSISTANT ACTIVATION STARTED 🚀');
    outputChannel.appendLine('='.repeat(80));
    
    // Log workspace information
    const workspaceFolders = vscode.workspace.workspaceFolders;
    outputChannel.appendLine(`📂 Workspace Folders: ${workspaceFolders ? workspaceFolders.map(f => f.uri.fsPath).join(', ') : 'None'}`);
    
    // Register commands
    outputChannel.appendLine('\n📝 Registering commands...');
    const registeredCommands = [
        // Command+K command
        vscode.commands.registerTextEditorCommand(
            COMMANDS.CMD_K_COMPLETION,
            handleCmdKCompletion
        ),
        
        // Regular completion command
        vscode.commands.registerTextEditorCommand(
            COMMANDS.GET_COMPLETION,
            handleGetCompletion
        )
    ];
    
    // Register completion provider
    outputChannel.appendLine('\n🔄 Registering completion provider...');
    const provider = new ColdFusionCompletionProvider(outputChannel);
    
    const completionDisposable = vscode.languages.registerCompletionItemProvider(
        [
            { scheme: 'file', language: 'cfml' },
            { scheme: 'file', language: 'coldfusion' },
            { scheme: 'file', pattern: '**/*.cfm' },
            { scheme: 'file', pattern: '**/*.cfc' }
        ],
        provider,
        '<', ' ' // Trigger completion on < and space
    );
    
    // Add all disposables to subscriptions
    context.subscriptions.push(
        outputChannel,
        completionDisposable,
        ...registeredCommands
    );
    
    outputChannel.appendLine('✅ Extension activation completed successfully\n');
    outputChannel.appendLine('='.repeat(80));
}

export function deactivate(): void {
    outputChannel.appendLine('👋 ColdFusion RAG Assistant is now deactivated');
} 

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *