diff --git a/codegen-vscode-extension/README.md b/codegen-vscode-extension/README.md new file mode 100644 index 000000000..f213f58c4 --- /dev/null +++ b/codegen-vscode-extension/README.md @@ -0,0 +1,78 @@ +# Codegen VSCode Extension + +A VSCode extension for Codegen - AI-powered code generation and assistance. + +## Features + +- **Ask Questions**: Get answers to your programming questions directly in VSCode +- **Generate Code**: Generate code snippets based on your descriptions +- **Explain Code**: Get explanations for selected code +- **Improve Code**: Get suggestions to improve your code +- **Fix Code**: Get help fixing bugs in your code + +## Requirements + +- VSCode 1.78.0 or higher +- A Codegen API key (get one at [codegen.sh](https://codegen.sh)) + +## Installation + +1. Install the extension from the VSCode Marketplace +1. Set your Codegen API key in the extension settings +1. Start using Codegen in your development workflow! + +## Extension Settings + +This extension contributes the following settings: + +- `codegen.apiKey`: Your Codegen API key +- `codegen.endpoint`: The endpoint for the Codegen API (defaults to https://api.codegen.sh) + +## Commands + +The extension provides the following commands: + +- `codegen.askQuestion`: Ask Codegen a question +- `codegen.generateCode`: Generate code with Codegen +- `codegen.explainCode`: Explain selected code +- `codegen.improveCode`: Improve selected code +- `codegen.fixCode`: Fix selected code + +## Usage + +### Ask a Question + +1. Open the command palette (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS) +1. Type "Ask Codegen a Question" and press Enter +1. Enter your question and press Enter +1. View the response in the Codegen Chat view + +### Generate Code + +1. Open the command palette +1. Type "Generate Code with Codegen" and press Enter +1. Describe the code you want to generate +1. The generated code will be inserted at your cursor position + +### Explain Code + +1. Select the code you want to explain +1. Right-click and select "Explain Selected Code" from the context menu +1. View the explanation in the Codegen Chat view + +### Improve Code + +1. Select the code you want to improve +1. Right-click and select "Improve Selected Code" from the context menu +1. Review the suggested improvements and apply them if desired + +### Fix Code + +1. Select the code you want to fix +1. Right-click and select "Fix Selected Code" from the context menu +1. Optionally provide the error message you're getting +1. Review the suggested fixes and apply them if desired + +## License + +MIT diff --git a/codegen-vscode-extension/media/codegen-icon.svg b/codegen-vscode-extension/media/codegen-icon.svg new file mode 100644 index 000000000..53aa599b8 --- /dev/null +++ b/codegen-vscode-extension/media/codegen-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/codegen-vscode-extension/media/main.css b/codegen-vscode-extension/media/main.css new file mode 100644 index 000000000..86a842023 --- /dev/null +++ b/codegen-vscode-extension/media/main.css @@ -0,0 +1,289 @@ +:root { + --container-padding: 10px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 8px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; + --message-max-height: 150px; /* Added for collapsed messages */ +} + +body { + padding: 0; + margin: 0; + color: var(--vscode-foreground); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + font-family: var(--vscode-font-family); + background-color: var(--vscode-editor-background); +} + +#chat-container { + display: flex; + flex-direction: column; + height: 100vh; + padding: var(--container-padding); + box-sizing: border-box; +} + +#messages { + flex: 1; + overflow-y: auto; + margin-bottom: 10px; + padding-right: 8px; +} + +.message { + margin-bottom: 10px; + padding: 8px 12px; + border-radius: 6px; + max-width: 90%; + word-wrap: break-word; + cursor: pointer; /* Added to indicate clickable */ + transition: max-height 0.3s ease; /* Added for smooth transition */ +} + +.user-message { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + align-self: flex-end; + margin-left: auto; +} + +.assistant-message { + background-color: var(--vscode-editor-inactiveSelectionBackground); + color: var(--vscode-editor-foreground); + align-self: flex-start; +} + +.message-content { + margin-bottom: 5px; + overflow: hidden; /* Added for collapsed state */ +} + +.code-block { + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-editor-lineHighlightBorder); + border-radius: 3px; + padding: 8px; + margin-top: 5px; + font-family: var(--vscode-editor-font-family); + font-size: var(--vscode-editor-font-size); + white-space: pre-wrap; + overflow-x: auto; +} + +.code-actions { + display: flex; + justify-content: flex-end; + margin-top: 5px; +} + +.code-action-button { + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border: none; + padding: 4px 8px; + border-radius: 2px; + cursor: pointer; + font-size: 12px; + margin-left: 5px; +} + +.code-action-button:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + +.timestamp { + font-size: 10px; + color: var(--vscode-descriptionForeground); + text-align: right; + margin-top: 2px; +} + +#input-container { + display: flex; + padding: 8px 0; +} + +#message-input { + flex: 1; + height: 60px; + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + border: 1px solid var(--vscode-input-border); + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + resize: none; + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + border-radius: 2px; +} + +#message-input:focus { + outline: 1px solid var(--vscode-focusBorder); +} + +#send-button { + margin-left: 8px; + padding: 0 14px; + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + cursor: pointer; + border-radius: 2px; +} + +#send-button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +#loading { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.3); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.hidden { + display: none !important; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: var(--vscode-button-background); + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Markdown styling */ +.markdown h1, +.markdown h2, +.markdown h3, +.markdown h4, +.markdown h5, +.markdown h6 { + margin-top: 16px; + margin-bottom: 8px; + font-weight: 600; +} + +.markdown p { + margin-top: 0; + margin-bottom: 8px; +} + +.markdown ul, +.markdown ol { + padding-left: 20px; + margin-top: 0; + margin-bottom: 8px; +} + +.markdown code { + font-family: var(--vscode-editor-font-family); + background-color: var(--vscode-textCodeBlock-background); + padding: 2px 4px; + border-radius: 3px; +} + +.markdown pre { + background-color: var(--vscode-textCodeBlock-background); + padding: 8px; + border-radius: 3px; + overflow-x: auto; + margin-top: 8px; + margin-bottom: 8px; +} + +.markdown pre code { + background-color: transparent; + padding: 0; +} + +.markdown a { + color: var(--vscode-textLink-foreground); + text-decoration: none; +} + +.markdown a:hover { + text-decoration: underline; +} + +.markdown blockquote { + border-left: 3px solid var(--vscode-textBlockQuote-border); + padding-left: 8px; + margin-left: 0; + margin-right: 0; + color: var(--vscode-textBlockQuote-foreground); +} + +.markdown table { + border-collapse: collapse; + width: 100%; + margin-bottom: 16px; +} + +.markdown th, +.markdown td { + border: 1px solid var(--vscode-editor-lineHighlightBorder); + padding: 6px 13px; +} + +.markdown th { + background-color: var(--vscode-editor-inactiveSelectionBackground); + font-weight: 600; +} + +/* Added for collapsed messages */ +.message-content.collapsed { + max-height: var(--message-max-height); + overflow: hidden; + position: relative; +} + +/* Added gradient fade for collapsed messages */ +.message-content.collapsed::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 40px; + background: linear-gradient( + to bottom, + transparent, + var(--vscode-editor-inactiveSelectionBackground) + ); + pointer-events: none; +} + +/* Adjust gradient color for user messages */ +.user-message .message-content.collapsed::after { + background: linear-gradient( + to bottom, + transparent, + var(--vscode-button-background) + ); +} + +/* Added expand indicator */ +.expand-indicator { + text-align: center; + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-top: 4px; + user-select: none; +} diff --git a/codegen-vscode-extension/media/main.js b/codegen-vscode-extension/media/main.js new file mode 100644 index 000000000..fa098d2c7 --- /dev/null +++ b/codegen-vscode-extension/media/main.js @@ -0,0 +1,227 @@ +(() => { + // Get VS Code API + const vscode = acquireVsCodeApi(); + + // DOM elements + const messagesContainer = document.getElementById("messages"); + const messageInput = document.getElementById("message-input"); + const sendButton = document.getElementById("send-button"); + const loadingElement = document.getElementById("loading"); + + // Store messages + let messages = []; + + // Initialize + window.addEventListener("message", (event) => { + const message = event.data; + + switch (message.command) { + case "updateMessages": + messages = message.messages; + renderMessages(); + break; + case "showLoading": + toggleLoading(message.value); + break; + } + }); + + // Send message when button is clicked + sendButton.addEventListener("click", () => { + sendMessage(); + }); + + // Send message when Enter is pressed (without Shift) + messageInput.addEventListener("keydown", (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }); + + // Send message to extension + function sendMessage() { + const text = messageInput.value.trim(); + if (text) { + vscode.postMessage({ + command: "sendMessage", + text: text, + }); + messageInput.value = ""; + } + } + + // Check if content needs to be collapsed + function shouldCollapseContent(contentElement) { + // Get the computed max-height value from CSS variable + const maxHeight = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue( + "--message-max-height", + ), + ); + + // If the content is taller than the max height, it should be collapsed + return contentElement.scrollHeight > maxHeight; + } + + // Toggle message expansion + function toggleMessageExpansion(contentElement, indicatorElement) { + if (contentElement.classList.contains("collapsed")) { + // Expand + contentElement.classList.remove("collapsed"); + indicatorElement.textContent = "Click to collapse"; + } else { + // Collapse + contentElement.classList.add("collapsed"); + indicatorElement.textContent = "Click to expand"; + } + } + + // Render all messages + function renderMessages() { + messagesContainer.innerHTML = ""; + + messages.forEach((message) => { + const messageElement = document.createElement("div"); + messageElement.className = `message ${message.role}-message`; + + // Message content + const contentElement = document.createElement("div"); + contentElement.className = "message-content markdown"; + contentElement.innerHTML = formatMarkdown(message.content); + messageElement.appendChild(contentElement); + + // Add expand indicator element + const expandIndicator = document.createElement("div"); + expandIndicator.className = "expand-indicator"; + messageElement.appendChild(expandIndicator); + + // Check if content should be collapsed (after adding to DOM to get accurate height) + setTimeout(() => { + if (shouldCollapseContent(contentElement)) { + contentElement.classList.add("collapsed"); + expandIndicator.textContent = "Click to expand"; + } else { + expandIndicator.style.display = "none"; // Hide indicator if not needed + } + }, 0); + + // Add click event to toggle expansion + messageElement.addEventListener("click", (e) => { + // Don't toggle if clicking on code action buttons + if (e.target.closest(".code-action-button")) { + return; + } + + // Toggle expansion + if (shouldCollapseContent(contentElement)) { + toggleMessageExpansion(contentElement, expandIndicator); + } + }); + + // Code block (if any) + if (message.code) { + const codeElement = document.createElement("div"); + codeElement.className = "code-block"; + codeElement.textContent = message.code; + messageElement.appendChild(codeElement); + + // Code actions + const actionsElement = document.createElement("div"); + actionsElement.className = "code-actions"; + + // Insert code button + const insertButton = document.createElement("button"); + insertButton.className = "code-action-button"; + insertButton.textContent = "Insert Code"; + insertButton.addEventListener("click", () => { + vscode.postMessage({ + command: "insertCode", + code: message.code, + }); + }); + actionsElement.appendChild(insertButton); + + // Copy code button + const copyButton = document.createElement("button"); + copyButton.className = "code-action-button"; + copyButton.textContent = "Copy"; + copyButton.addEventListener("click", () => { + navigator.clipboard.writeText(message.code); + copyButton.textContent = "Copied!"; + setTimeout(() => { + copyButton.textContent = "Copy"; + }, 2000); + }); + actionsElement.appendChild(copyButton); + + messageElement.appendChild(actionsElement); + } + + // Timestamp + const timestampElement = document.createElement("div"); + timestampElement.className = "timestamp"; + timestampElement.textContent = formatTimestamp(message.timestamp); + messageElement.appendChild(timestampElement); + + messagesContainer.appendChild(messageElement); + }); + + // Scroll to bottom + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + + // Format timestamp + function formatTimestamp(timestamp) { + const date = new Date(timestamp); + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + } + + // Toggle loading indicator + function toggleLoading(show) { + if (show) { + loadingElement.classList.remove("hidden"); + } else { + loadingElement.classList.add("hidden"); + } + } + + // Format markdown to HTML + function formatMarkdown(text) { + // This is a very simple markdown parser + // In a real extension, you might want to use a library like marked.js + + // Code blocks + text = text.replace( + /```(\w*)\n([\s\S]*?)\n```/g, + "
$2
", + ); + + // Inline code + text = text.replace(/`([^`]+)`/g, "$1"); + + // Headers + text = text.replace(/^### (.*$)/gm, "

$1

"); + text = text.replace(/^## (.*$)/gm, "

$1

"); + text = text.replace(/^# (.*$)/gm, "

$1

"); + + // Bold + text = text.replace(/\*\*(.*?)\*\*/g, "$1"); + + // Italic + text = text.replace(/\*(.*?)\*/g, "$1"); + + // Lists + text = text.replace(/^\s*\*\s(.*$)/gm, "
  • $1
  • "); + text = text.replace(/(
  • .*<\/li>)/g, ""); + + // Links + text = text.replace(/\[(.*?)\]\((.*?)\)/g, '$1'); + + // Paragraphs + text = text.replace(/\n\n/g, "

    "); + text = "

    " + text + "

    "; + + return text; + } +})(); diff --git a/codegen-vscode-extension/package.json b/codegen-vscode-extension/package.json new file mode 100644 index 000000000..18475fd69 --- /dev/null +++ b/codegen-vscode-extension/package.json @@ -0,0 +1,126 @@ +{ + "name": "codegen-vscode-extension", + "displayName": "Codegen", + "description": "VSCode extension for Codegen - AI-powered code generation and assistance", + "version": "0.1.0", + "engines": { + "vscode": "^1.78.0" + }, + "categories": ["Programming Languages", "Machine Learning", "Other"], + "activationEvents": ["onStartupFinished"], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "codegen.askQuestion", + "title": "Ask Codegen a Question", + "category": "Codegen" + }, + { + "command": "codegen.generateCode", + "title": "Generate Code with Codegen", + "category": "Codegen" + }, + { + "command": "codegen.explainCode", + "title": "Explain Selected Code", + "category": "Codegen" + }, + { + "command": "codegen.improveCode", + "title": "Improve Selected Code", + "category": "Codegen" + }, + { + "command": "codegen.fixCode", + "title": "Fix Selected Code", + "category": "Codegen" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "codegen-sidebar", + "title": "Codegen", + "icon": "media/codegen-icon.svg" + } + ] + }, + "views": { + "codegen-sidebar": [ + { + "type": "webview", + "id": "codegen.chatView", + "name": "Codegen Chat" + } + ] + }, + "configuration": { + "title": "Codegen", + "properties": { + "codegen.apiKey": { + "type": "string", + "default": "", + "description": "API key for Codegen services" + }, + "codegen.endpoint": { + "type": "string", + "default": "https://api.codegen.sh", + "description": "Endpoint for Codegen API" + } + } + }, + "menus": { + "editor/context": [ + { + "command": "codegen.explainCode", + "when": "editorHasSelection", + "group": "codegen" + }, + { + "command": "codegen.improveCode", + "when": "editorHasSelection", + "group": "codegen" + }, + { + "command": "codegen.fixCode", + "when": "editorHasSelection", + "group": "codegen" + }, + { + "command": "codegen.generateCode", + "group": "codegen" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "npm run package", + "compile": "webpack", + "watch": "webpack --watch", + "package": "webpack --mode production --devtool source-map --config ./webpack.config.js", + "compile-tests": "tsc -p . --outDir out", + "watch-tests": "tsc -p . -w --outDir out", + "lint": "eslint src --ext ts", + "test": "node ./out/test/runTest.js" + }, + "devDependencies": { + "@types/glob": "^8.1.0", + "@types/mocha": "^10.0.1", + "@types/node": "16.x", + "@types/vscode": "^1.78.0", + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", + "@vscode/test-electron": "^2.3.0", + "eslint": "^8.39.0", + "glob": "^8.1.0", + "mocha": "^10.2.0", + "ts-loader": "^9.4.2", + "typescript": "^5.0.4", + "webpack": "^5.81.0", + "webpack-cli": "^5.0.2" + }, + "dependencies": { + "axios": "^1.4.0" + } +} diff --git a/codegen-vscode-extension/src/api/codegenAPI.ts b/codegen-vscode-extension/src/api/codegenAPI.ts new file mode 100644 index 000000000..f201c20e0 --- /dev/null +++ b/codegen-vscode-extension/src/api/codegenAPI.ts @@ -0,0 +1,155 @@ +import axios, { type AxiosInstance } from "axios"; +import * as vscode from "vscode"; + +export interface CodegenResponse { + text: string; + code?: string; + language?: string; +} + +export class CodegenAPI { + private client: AxiosInstance; + private apiKey = ""; + private endpoint = ""; + + constructor() { + this.loadConfiguration(); + + this.client = axios.create({ + baseURL: this.endpoint, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + }); + + // Listen for configuration changes + vscode.workspace.onDidChangeConfiguration((e) => { + if ( + e.affectsConfiguration("codegen.apiKey") || + e.affectsConfiguration("codegen.endpoint") + ) { + this.loadConfiguration(); + this.updateClient(); + } + }); + } + + private loadConfiguration() { + const config = vscode.workspace.getConfiguration("codegen"); + this.apiKey = config.get("apiKey") || ""; + this.endpoint = config.get("endpoint") || "https://api.codegen.sh"; + } + + private updateClient() { + this.client = axios.create({ + baseURL: this.endpoint, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + }); + } + + public async validateApiKey(): Promise { + if (!this.apiKey) { + return false; + } + + try { + // Make a simple request to validate the API key + await this.client.get("/api/validate"); + return true; + } catch (error) { + console.error("API key validation failed:", error); + return false; + } + } + + public async askQuestion( + question: string, + context?: string, + ): Promise { + try { + const response = await this.client.post("/api/ask", { + question, + context, + }); + + return response.data; + } catch (error) { + console.error("Error asking question:", error); + throw new Error("Failed to get response from Codegen API"); + } + } + + public async generateCode( + prompt: string, + language?: string, + ): Promise { + try { + const response = await this.client.post("/api/generate", { + prompt, + language, + }); + + return response.data; + } catch (error) { + console.error("Error generating code:", error); + throw new Error("Failed to generate code from Codegen API"); + } + } + + public async explainCode( + code: string, + language?: string, + ): Promise { + try { + const response = await this.client.post("/api/explain", { + code, + language, + }); + + return response.data; + } catch (error) { + console.error("Error explaining code:", error); + throw new Error("Failed to explain code from Codegen API"); + } + } + + public async improveCode( + code: string, + language?: string, + ): Promise { + try { + const response = await this.client.post("/api/improve", { + code, + language, + }); + + return response.data; + } catch (error) { + console.error("Error improving code:", error); + throw new Error("Failed to improve code from Codegen API"); + } + } + + public async fixCode( + code: string, + error?: string, + language?: string, + ): Promise { + try { + const response = await this.client.post("/api/fix", { + code, + error, + language, + }); + + return response.data; + } catch (error) { + console.error("Error fixing code:", error); + throw new Error("Failed to fix code from Codegen API"); + } + } +} diff --git a/codegen-vscode-extension/src/commands.ts b/codegen-vscode-extension/src/commands.ts new file mode 100644 index 000000000..e605d4e0e --- /dev/null +++ b/codegen-vscode-extension/src/commands.ts @@ -0,0 +1,328 @@ +import * as vscode from "vscode"; +import type { CodegenAPI } from "./api/codegenAPI"; +import type { ChatViewProvider } from "./providers/chatViewProvider"; +import { getDocumentLanguage, getSelectedText, insertText } from "./utils"; + +export function registerCommands( + context: vscode.ExtensionContext, + api: CodegenAPI, + chatViewProvider: ChatViewProvider, +) { + // Ask a question + context.subscriptions.push( + vscode.commands.registerCommand("codegen.askQuestion", async () => { + const question = await vscode.window.showInputBox({ + prompt: "What would you like to ask Codegen?", + placeHolder: "Ask a programming question...", + }); + + if (!question) { + return; + } + + try { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Asking Codegen...", + cancellable: false, + }, + async (progress) => { + progress.report({ increment: 0 }); + + const response = await api.askQuestion(question); + + progress.report({ increment: 100 }); + + // Send to chat view + chatViewProvider.addMessage("user", question); + chatViewProvider.addMessage("assistant", response.text); + + // Show the chat view + vscode.commands.executeCommand("codegen.chatView.focus"); + + return response; + }, + ); + } catch (error) { + vscode.window.showErrorMessage(`Error: ${error.message}`); + } + }), + ); + + // Generate code + context.subscriptions.push( + vscode.commands.registerCommand("codegen.generateCode", async () => { + const prompt = await vscode.window.showInputBox({ + prompt: "Describe the code you want to generate", + placeHolder: "Generate a function that...", + }); + + if (!prompt) { + return; + } + + const editor = vscode.window.activeTextEditor; + const language = editor + ? getDocumentLanguage(editor.document) + : undefined; + + try { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Generating code...", + cancellable: false, + }, + async (progress) => { + progress.report({ increment: 0 }); + + const response = await api.generateCode(prompt, language); + + progress.report({ increment: 100 }); + + if (editor && response.code) { + // Insert the generated code at the cursor position + insertText(editor, response.code); + } + + // Send to chat view + chatViewProvider.addMessage("user", `Generate code: ${prompt}`); + chatViewProvider.addMessage( + "assistant", + response.text, + response.code, + ); + + return response; + }, + ); + } catch (error) { + vscode.window.showErrorMessage(`Error: ${error.message}`); + } + }), + ); + + // Explain code + context.subscriptions.push( + vscode.commands.registerCommand("codegen.explainCode", async () => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showErrorMessage("No active editor"); + return; + } + + const selectedText = getSelectedText(editor); + if (!selectedText) { + vscode.window.showErrorMessage("No code selected"); + return; + } + + const language = getDocumentLanguage(editor.document); + + try { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Explaining code...", + cancellable: false, + }, + async (progress) => { + progress.report({ increment: 0 }); + + const response = await api.explainCode(selectedText, language); + + progress.report({ increment: 100 }); + + // Send to chat view + chatViewProvider.addMessage( + "user", + `Explain this code:\n\`\`\`${language}\n${selectedText}\n\`\`\``, + ); + chatViewProvider.addMessage("assistant", response.text); + + // Show the chat view + vscode.commands.executeCommand("codegen.chatView.focus"); + + return response; + }, + ); + } catch (error) { + vscode.window.showErrorMessage(`Error: ${error.message}`); + } + }), + ); + + // Improve code + context.subscriptions.push( + vscode.commands.registerCommand("codegen.improveCode", async () => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showErrorMessage("No active editor"); + return; + } + + const selectedText = getSelectedText(editor); + if (!selectedText) { + vscode.window.showErrorMessage("No code selected"); + return; + } + + const language = getDocumentLanguage(editor.document); + + try { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Improving code...", + cancellable: false, + }, + async (progress) => { + progress.report({ increment: 0 }); + + const response = await api.improveCode(selectedText, language); + + progress.report({ increment: 100 }); + + if (response.code) { + // Show diff and ask if user wants to apply changes + const document = editor.document; + const selection = editor.selection; + + const diffEditor = + await vscode.diff.createTextDocumentAndEditorEdit( + document, + selection, + response.code, + ); + + // Add buttons to apply or discard changes + const applyChanges = "Apply Changes"; + const discardChanges = "Discard"; + + const choice = await vscode.window.showInformationMessage( + "Review the suggested improvements", + applyChanges, + discardChanges, + ); + + if (choice === applyChanges) { + // Replace the selected text with the improved code + editor.edit((editBuilder) => { + editBuilder.replace(selection, response.code || ""); + }); + } + } + + // Send to chat view + chatViewProvider.addMessage( + "user", + `Improve this code:\n\`\`\`${language}\n${selectedText}\n\`\`\``, + ); + chatViewProvider.addMessage( + "assistant", + response.text, + response.code, + ); + + return response; + }, + ); + } catch (error) { + vscode.window.showErrorMessage(`Error: ${error.message}`); + } + }), + ); + + // Fix code + context.subscriptions.push( + vscode.commands.registerCommand("codegen.fixCode", async () => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showErrorMessage("No active editor"); + return; + } + + const selectedText = getSelectedText(editor); + if (!selectedText) { + vscode.window.showErrorMessage("No code selected"); + return; + } + + const language = getDocumentLanguage(editor.document); + + // Ask for error message + const errorMessage = await vscode.window.showInputBox({ + prompt: "What error are you getting? (optional)", + placeHolder: "Error message or description of the issue", + }); + + try { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Fixing code...", + cancellable: false, + }, + async (progress) => { + progress.report({ increment: 0 }); + + const response = await api.fixCode( + selectedText, + errorMessage, + language, + ); + + progress.report({ increment: 100 }); + + if (response.code) { + // Show diff and ask if user wants to apply changes + const document = editor.document; + const selection = editor.selection; + + const diffEditor = + await vscode.diff.createTextDocumentAndEditorEdit( + document, + selection, + response.code, + ); + + // Add buttons to apply or discard changes + const applyChanges = "Apply Changes"; + const discardChanges = "Discard"; + + const choice = await vscode.window.showInformationMessage( + "Review the suggested fixes", + applyChanges, + discardChanges, + ); + + if (choice === applyChanges) { + // Replace the selected text with the fixed code + editor.edit((editBuilder) => { + editBuilder.replace(selection, response.code || ""); + }); + } + } + + // Send to chat view + const userMessage = errorMessage + ? `Fix this code (error: ${errorMessage}):\n\`\`\`${language}\n${selectedText}\n\`\`\`` + : `Fix this code:\n\`\`\`${language}\n${selectedText}\n\`\`\``; + + chatViewProvider.addMessage("user", userMessage); + chatViewProvider.addMessage( + "assistant", + response.text, + response.code, + ); + + return response; + }, + ); + } catch (error) { + vscode.window.showErrorMessage(`Error: ${error.message}`); + } + }), + ); +} diff --git a/codegen-vscode-extension/src/extension.ts b/codegen-vscode-extension/src/extension.ts new file mode 100644 index 000000000..fa01b5d29 --- /dev/null +++ b/codegen-vscode-extension/src/extension.ts @@ -0,0 +1,39 @@ +import * as vscode from "vscode"; +import { CodegenAPI } from "./api/codegenAPI"; +import { registerCommands } from "./commands"; +import { ChatViewProvider } from "./providers/chatViewProvider"; + +export function activate(context: vscode.ExtensionContext) { + console.log("Codegen extension is now active!"); + + // Initialize the API client + const api = new CodegenAPI(); + + // Register the chat view provider + const chatViewProvider = new ChatViewProvider(context.extensionUri, api); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider( + "codegen.chatView", + chatViewProvider, + { webviewOptions: { retainContextWhenHidden: true } }, + ), + ); + + // Register commands + registerCommands(context, api, chatViewProvider); + + // Status bar item + const statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100, + ); + statusBarItem.text = "$(sparkle) Codegen"; + statusBarItem.tooltip = "Codegen AI Assistant"; + statusBarItem.command = "codegen.askQuestion"; + statusBarItem.show(); + context.subscriptions.push(statusBarItem); +} + +export function deactivate() { + // Clean up resources +} diff --git a/codegen-vscode-extension/src/providers/chatViewProvider.ts b/codegen-vscode-extension/src/providers/chatViewProvider.ts new file mode 100644 index 000000000..d10774f39 --- /dev/null +++ b/codegen-vscode-extension/src/providers/chatViewProvider.ts @@ -0,0 +1,159 @@ +import * as vscode from "vscode"; +import type { CodegenAPI } from "../api/codegenAPI"; + +interface ChatMessage { + role: "user" | "assistant"; + content: string; + code?: string; + timestamp: number; +} + +export class ChatViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = "codegen.chatView"; + private _view?: vscode.WebviewView; + private _messages: ChatMessage[] = []; + + constructor( + private readonly _extensionUri: vscode.Uri, + private readonly _api: CodegenAPI, + ) {} + + public resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ) { + this._view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionUri], + }; + + webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + + // Handle messages from the webview + webviewView.webview.onDidReceiveMessage(async (message) => { + switch (message.command) { + case "sendMessage": + await this._handleUserMessage(message.text); + break; + case "clearChat": + this._messages = []; + this._updateWebview(); + break; + case "insertCode": + this._insertCodeToEditor(message.code); + break; + } + }); + + // Update the webview with existing messages + this._updateWebview(); + } + + public addMessage( + role: "user" | "assistant", + content: string, + code?: string, + ) { + this._messages.push({ + role, + content, + code, + timestamp: Date.now(), + }); + + this._updateWebview(); + } + + private async _handleUserMessage(text: string) { + // Add user message to chat + this.addMessage("user", text); + + try { + // Show loading indicator + this._view?.webview.postMessage({ command: "showLoading", value: true }); + + // Get response from API + const response = await this._api.askQuestion(text); + + // Add assistant message to chat + this.addMessage("assistant", response.text, response.code); + + // Hide loading indicator + this._view?.webview.postMessage({ command: "showLoading", value: false }); + } catch (error) { + // Hide loading indicator + this._view?.webview.postMessage({ command: "showLoading", value: false }); + + // Show error message + this.addMessage("assistant", `Error: ${error.message}`); + } + } + + private _updateWebview() { + if (this._view) { + this._view.webview.postMessage({ + command: "updateMessages", + messages: this._messages, + }); + } + } + + private _insertCodeToEditor(code: string) { + const editor = vscode.window.activeTextEditor; + if (editor) { + editor.edit((editBuilder) => { + editBuilder.insert(editor.selection.active, code); + }); + } + } + + private _getHtmlForWebview(webview: vscode.Webview) { + // Create URIs for scripts and styles + const scriptUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionUri, "media", "main.js"), + ); + const styleUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionUri, "media", "main.css"), + ); + + // Use a nonce to allow only specific scripts to be run + const nonce = this._getNonce(); + + return ` + + + + + + + Codegen Chat + + +
    +
    +
    + + +
    + +
    + + + `; + } + + private _getNonce() { + let text = ""; + const possible = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + } +} diff --git a/codegen-vscode-extension/src/utils.ts b/codegen-vscode-extension/src/utils.ts new file mode 100644 index 000000000..f0478eba0 --- /dev/null +++ b/codegen-vscode-extension/src/utils.ts @@ -0,0 +1,110 @@ +import * as vscode from "vscode"; + +/** + * Get the selected text from the editor + */ +export function getSelectedText(editor: vscode.TextEditor): string { + const selection = editor.selection; + if (selection.isEmpty) { + return ""; + } + return editor.document.getText(selection); +} + +/** + * Get the language of the document + */ +export function getDocumentLanguage(document: vscode.TextDocument): string { + return document.languageId; +} + +/** + * Insert text at the current cursor position + */ +export function insertText(editor: vscode.TextEditor, text: string): void { + const selection = editor.selection; + editor.edit((editBuilder) => { + editBuilder.insert(selection.active, text); + }); +} + +/** + * Get the current file path + */ +export function getCurrentFilePath(): string | undefined { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return undefined; + } + return editor.document.uri.fsPath; +} + +/** + * Get the current workspace folder + */ +export function getCurrentWorkspaceFolder(): + | vscode.WorkspaceFolder + | undefined { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return undefined; + } + return vscode.workspace.getWorkspaceFolder(editor.document.uri); +} + +/** + * Get the current project name + */ +export function getCurrentProjectName(): string | undefined { + const workspaceFolder = getCurrentWorkspaceFolder(); + if (!workspaceFolder) { + return undefined; + } + return workspaceFolder.name; +} + +/** + * Get the current file name + */ +export function getCurrentFileName(): string | undefined { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return undefined; + } + return editor.document.fileName.split(/[\\/]/).pop(); +} + +/** + * Get the current line of code + */ +export function getCurrentLine(editor: vscode.TextEditor): string { + const position = editor.selection.active; + const line = editor.document.lineAt(position.line); + return line.text; +} + +/** + * Get the current function or class + * This is a simple implementation and might not work for all languages + */ +export function getCurrentFunction( + editor: vscode.TextEditor, +): string | undefined { + const document = editor.document; + const position = editor.selection.active; + + // Simple implementation - search backwards for function or class definition + for (let i = position.line; i >= 0; i--) { + const line = document.lineAt(i).text.trim(); + if ( + line.startsWith("function ") || + line.startsWith("class ") || + line.startsWith("def ") || + line.match(/^[a-zA-Z0-9_]+\s*\([^)]*\)\s*{/) + ) { + return line; + } + } + + return undefined; +} diff --git a/codegen-vscode-extension/tsconfig.json b/codegen-vscode-extension/tsconfig.json new file mode 100644 index 000000000..0749ae80f --- /dev/null +++ b/codegen-vscode-extension/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020", "DOM"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", ".vscode-test"] +} diff --git a/codegen-vscode-extension/webpack.config.js b/codegen-vscode-extension/webpack.config.js new file mode 100644 index 000000000..8fbf295ab --- /dev/null +++ b/codegen-vscode-extension/webpack.config.js @@ -0,0 +1,34 @@ +const path = require("path"); + +const config = { + target: "node", + entry: "./src/extension.ts", + output: { + path: path.resolve(__dirname, "dist"), + filename: "extension.js", + libraryTarget: "commonjs2", + devtoolModuleFilenameTemplate: "../[resource-path]", + }, + devtool: "source-map", + externals: { + vscode: "commonjs vscode", + }, + resolve: { + extensions: [".ts", ".js"], + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: "ts-loader", + }, + ], + }, + ], + }, +}; + +module.exports = config;