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
andpineconeKey
from workspace settings. - Instantiates OpenAI and Pinecone clients, selects Pinecone index/namespace.
- Reads
provideCompletionItems()
- Logs the current line prefix and checks if we already have RAG context.
- If the user types
<cf
without any context, it kicks offgenerateCode()
to populate context. - Once context + query are available:
- Builds a prompt combining the “similar examples” (RAG context) and the user’s request.
- Calls
openai.chat.completions.create()
(gpt‑3.5‑turbo) to generate a tag. - Cleans up and converts the response into a snippet, replacing common attributes with placeholders (
${1:name}
, etc.). - 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:
- Gather Context
- Ensures the file is
.cfm
/.cfc
(ColdFusion). - Grabs the current line (and optional user input via a QuickPick).
- Ensures the file is
- 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 singleragContext
string.
- Store Context
- Saves
ragContext
+ user query intoSharedContext
so that the next completion request can use it.
- Saves
- Trigger Completion
- Inserts a blank line at the cursor, then fires
editor.action.triggerSuggest()
to open the suggestion list.
- Inserts a blank line at the cursor, then fires
5. Command Handlers
coldfusion-rag.cmdKCompletion
(⌘+K) and
coldfusion-rag.getCompletion
—both callgenerateCode()
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');
}