diff --git a/src/activate/handleUri.ts b/src/activate/handleUri.ts new file mode 100644 index 0000000..05440c7 --- /dev/null +++ b/src/activate/handleUri.ts @@ -0,0 +1,32 @@ +import * as vscode from "vscode" + +import { ClineProvider } from "../core/webview/ClineProvider" + +export const handleUri = async (uri: vscode.Uri) => { + const path = uri.path + const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B")) + const visibleProvider = ClineProvider.getVisibleInstance() + + if (!visibleProvider) { + return + } + + switch (path) { + case "/glama": { + const code = query.get("code") + if (code) { + await visibleProvider.handleGlamaCallback(code) + } + break + } + case "/openrouter": { + const code = query.get("code") + if (code) { + await visibleProvider.handleOpenRouterCallback(code) + } + break + } + default: + break + } +} diff --git a/src/activate/index.ts b/src/activate/index.ts new file mode 100644 index 0000000..76eebd1 --- /dev/null +++ b/src/activate/index.ts @@ -0,0 +1,3 @@ +export { handleUri } from "./handleUri" +export { registerCommands } from "./registerCommands" +export { registerCodeActions } from "./registerCodeActions" diff --git a/src/activate/registerCodeActions.ts b/src/activate/registerCodeActions.ts new file mode 100644 index 0000000..35e0766 --- /dev/null +++ b/src/activate/registerCodeActions.ts @@ -0,0 +1,91 @@ +import * as vscode from "vscode" + +import { ACTION_NAMES, COMMAND_IDS } from "../core/CodeActionProvider" +import { EditorUtils } from "../core/EditorUtils" +import { ClineProvider } from "../core/webview/ClineProvider" + +export const registerCodeActions = (context: vscode.ExtensionContext) => { + registerCodeActionPair( + context, + COMMAND_IDS.EXPLAIN, + "EXPLAIN", + "What would you like Roo to explain?", + "E.g. How does the error handling work?", + ) + + registerCodeActionPair( + context, + COMMAND_IDS.FIX, + "FIX", + "What would you like Roo to fix?", + "E.g. Maintain backward compatibility", + ) + + registerCodeActionPair( + context, + COMMAND_IDS.IMPROVE, + "IMPROVE", + "What would you like Roo to improve?", + "E.g. Focus on performance optimization", + ) + + registerCodeAction(context, COMMAND_IDS.ADD_TO_CONTEXT, "ADD_TO_CONTEXT") +} + +const registerCodeAction = ( + context: vscode.ExtensionContext, + command: string, + promptType: keyof typeof ACTION_NAMES, + inputPrompt?: string, + inputPlaceholder?: string, +) => { + let userInput: string | undefined + + context.subscriptions.push( + vscode.commands.registerCommand(command, async (...args: any[]) => { + if (inputPrompt) { + userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder, + }) + } + + // Handle both code action and direct command cases. + let filePath: string + let selectedText: string + let diagnostics: any[] | undefined + + if (args.length > 1) { + // Called from code action. + ;[filePath, selectedText, diagnostics] = args + } else { + // Called directly from command palette. + const context = EditorUtils.getEditorContext() + if (!context) return + ;({ filePath, selectedText, diagnostics } = context) + } + + const params = { + ...{ filePath, selectedText }, + ...(diagnostics ? { diagnostics } : {}), + ...(userInput ? { userInput } : {}), + } + + await ClineProvider.handleCodeAction(command, promptType, params) + }), + ) +} + +const registerCodeActionPair = ( + context: vscode.ExtensionContext, + baseCommand: string, + promptType: keyof typeof ACTION_NAMES, + inputPrompt?: string, + inputPlaceholder?: string, +) => { + // Register new task version. + registerCodeAction(context, baseCommand, promptType, inputPrompt, inputPlaceholder) + + // Register current task version. + registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, inputPrompt, inputPlaceholder) +} diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts new file mode 100644 index 0000000..fa650c6 --- /dev/null +++ b/src/activate/registerCommands.ts @@ -0,0 +1,83 @@ +import * as vscode from "vscode" +import delay from "delay" + +import { ClineProvider } from "../core/webview/ClineProvider" + +export type RegisterCommandOptions = { + context: vscode.ExtensionContext + outputChannel: vscode.OutputChannel + provider: ClineProvider +} + +export const registerCommands = (options: RegisterCommandOptions) => { + const { context, outputChannel } = options + + for (const [command, callback] of Object.entries(getCommandsMap(options))) { + context.subscriptions.push(vscode.commands.registerCommand(command, callback)) + } +} + +const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => { + return { + "roo-cline.plusButtonClicked": async () => { + await provider.clearTask() + await provider.postStateToWebview() + await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) + }, + "roo-cline.mcpButtonClicked": () => { + provider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" }) + }, + "roo-cline.promptsButtonClicked": () => { + provider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" }) + }, + "roo-cline.popoutButtonClicked": () => openClineInNewTab({ context, outputChannel }), + "roo-cline.openInNewTab": () => openClineInNewTab({ context, outputChannel }), + "roo-cline.settingsButtonClicked": () => { + provider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) + }, + "roo-cline.historyButtonClicked": () => { + provider.postMessageToWebview({ type: "action", action: "historyButtonClicked" }) + }, + } +} + +const openClineInNewTab = async ({ context, outputChannel }: Omit) => { + outputChannel.appendLine("Opening Roo Code in new tab") + + // (This example uses webviewProvider activation event which is necessary to + // deserialize cached webview, but since we use retainContextWhenHidden, we + // don't need to use that event). + // https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts + const tabProvider = new ClineProvider(context, outputChannel) + // const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined + const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0)) + + // Check if there are any visible text editors, otherwise open a new group + // to the right. + const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0 + + if (!hasVisibleEditors) { + await vscode.commands.executeCommand("workbench.action.newGroupRight") + } + + const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two + + const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [context.extensionUri], + }) + + // TODO: use better svg icon with light and dark variants (see + // https://stackoverflow.com/questions/58365687/vscode-extension-iconpath). + panel.iconPath = { + light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"), + dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"), + } + + tabProvider.resolveWebviewView(panel) + + // Lock the editor group so clicking on files doesn't open them over the panel + await delay(100) + await vscode.commands.executeCommand("workbench.action.lockEditorGroup") +} diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index 0754c1a..285e6da 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -9,7 +9,7 @@ export const ACTION_NAMES = { ADD_TO_CONTEXT: "Roo Code: Add to Context", } as const -const COMMAND_IDS = { +export const COMMAND_IDS = { EXPLAIN: "roo-cline.explainCode", FIX: "roo-cline.fixCode", IMPROVE: "roo-cline.improveCode", diff --git a/src/extension.ts b/src/extension.ts index 719f38d..39f3d90 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,37 +1,33 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import delay from "delay" import * as vscode from "vscode" + import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" -import "./utils/path" // necessary to have access to String.prototype.toPosix -import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider" -import { EditorUtils } from "./core/EditorUtils" +import "./utils/path" // Necessary to have access to String.prototype.toPosix. +import { CodeActionProvider } from "./core/CodeActionProvider" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" +import { handleUri, registerCommands, registerCodeActions } from "./activate" -/* -Built using https://github.com/microsoft/vscode-webview-ui-toolkit - -Inspired by -https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview -https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra - -*/ +/** + * Built using https://github.com/microsoft/vscode-webview-ui-toolkit + * + * Inspired by: + * - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview + * - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra + */ let outputChannel: vscode.OutputChannel -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed +// This method is called when your extension is activated. +// Your extension is activated the very first time the command is executed. export function activate(context: vscode.ExtensionContext) { outputChannel = vscode.window.createOutputChannel("Roo-Code") context.subscriptions.push(outputChannel) - outputChannel.appendLine("Roo-Code extension activated") - // Get default commands from configuration + // Get default commands from configuration. const defaultCommands = vscode.workspace.getConfiguration("roo-cline").get("allowedCommands") || [] - // Initialize global state if not already set + // Initialize global state if not already set. if (!context.globalState.get("allowedCommands")) { context.globalState.update("allowedCommands", defaultCommands) } @@ -44,220 +40,49 @@ export function activate(context: vscode.ExtensionContext) { }), ) - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.plusButtonClicked", async () => { - outputChannel.appendLine("Plus button Clicked") - await sidebarProvider.clearTask() - await sidebarProvider.postStateToWebview() - await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) - }), - ) + registerCommands({ context, outputChannel, provider: sidebarProvider }) - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.mcpButtonClicked", () => { - sidebarProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" }) - }), - ) - - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.promptsButtonClicked", () => { - sidebarProvider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" }) - }), - ) - - const openClineInNewTab = async () => { - outputChannel.appendLine("Opening Roo Code in new tab") - // (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event) - // https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts - const tabProvider = new ClineProvider(context, outputChannel) - //const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined - const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0)) - - // Check if there are any visible text editors, otherwise open a new group to the right - const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0 - if (!hasVisibleEditors) { - await vscode.commands.executeCommand("workbench.action.newGroupRight") - } - const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two - - const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, { - enableScripts: true, - retainContextWhenHidden: true, - localResourceRoots: [context.extensionUri], - }) - // TODO: use better svg icon with light and dark variants (see https://stackoverflow.com/questions/58365687/vscode-extension-iconpath) - - panel.iconPath = { - light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"), - dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"), - } - tabProvider.resolveWebviewView(panel) - - // Lock the editor group so clicking on files doesn't open them over the panel - await delay(100) - await vscode.commands.executeCommand("workbench.action.lockEditorGroup") - } - - context.subscriptions.push(vscode.commands.registerCommand("roo-cline.popoutButtonClicked", openClineInNewTab)) - context.subscriptions.push(vscode.commands.registerCommand("roo-cline.openInNewTab", openClineInNewTab)) - - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.settingsButtonClicked", () => { - //vscode.window.showInformationMessage(message) - sidebarProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) - }), - ) - - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.historyButtonClicked", () => { - sidebarProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" }) - }), - ) - - /* - We use the text document content provider API to show the left side for diff view by creating a virtual document for the original content. This makes it readonly so users know to edit the right side if they want to keep their changes. - - - This API allows you to create readonly documents in VSCode from arbitrary sources, and works by claiming an uri-scheme for which your provider then returns text contents. The scheme must be provided when registering a provider and cannot change afterwards. - - Note how the provider doesn't create uris for virtual documents - its role is to provide contents given such an uri. In return, content providers are wired into the open document logic so that providers are always considered. - https://code.visualstudio.com/api/extension-guides/virtual-documents - */ + /** + * We use the text document content provider API to show the left side for diff + * view by creating a virtual document for the original content. This makes it + * readonly so users know to edit the right side if they want to keep their changes. + * + * This API allows you to create readonly documents in VSCode from arbitrary + * sources, and works by claiming an uri-scheme for which your provider then + * returns text contents. The scheme must be provided when registering a + * provider and cannot change afterwards. + * + * Note how the provider doesn't create uris for virtual documents - its role + * is to provide contents given such an uri. In return, content providers are + * wired into the open document logic so that providers are always considered. + * + * https://code.visualstudio.com/api/extension-guides/virtual-documents + */ const diffContentProvider = new (class implements vscode.TextDocumentContentProvider { provideTextDocumentContent(uri: vscode.Uri): string { return Buffer.from(uri.query, "base64").toString("utf-8") } })() + context.subscriptions.push( vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider), ) - // URI Handler - const handleUri = async (uri: vscode.Uri) => { - const path = uri.path - const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B")) - const visibleProvider = ClineProvider.getVisibleInstance() - if (!visibleProvider) { - return - } - switch (path) { - case "/glama": { - const code = query.get("code") - if (code) { - await visibleProvider.handleGlamaCallback(code) - } - break - } - - case "/openrouter": { - const code = query.get("code") - if (code) { - await visibleProvider.handleOpenRouterCallback(code) - } - break - } - default: - break - } - } context.subscriptions.push(vscode.window.registerUriHandler({ handleUri })) - // Register code actions provider + // Register code actions provider. context.subscriptions.push( vscode.languages.registerCodeActionsProvider({ pattern: "**/*" }, new CodeActionProvider(), { providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds, }), ) - // Helper function to handle code actions - const registerCodeAction = ( - context: vscode.ExtensionContext, - command: string, - promptType: keyof typeof ACTION_NAMES, - inputPrompt?: string, - inputPlaceholder?: string, - ) => { - let userInput: string | undefined - - context.subscriptions.push( - vscode.commands.registerCommand(command, async (...args: any[]) => { - if (inputPrompt) { - userInput = await vscode.window.showInputBox({ - prompt: inputPrompt, - placeHolder: inputPlaceholder, - }) - } - - // Handle both code action and direct command cases - let filePath: string - let selectedText: string - let diagnostics: any[] | undefined - - if (args.length > 1) { - // Called from code action - ;[filePath, selectedText, diagnostics] = args - } else { - // Called directly from command palette - const context = EditorUtils.getEditorContext() - if (!context) return - ;({ filePath, selectedText, diagnostics } = context) - } - - const params = { - ...{ filePath, selectedText }, - ...(diagnostics ? { diagnostics } : {}), - ...(userInput ? { userInput } : {}), - } - - await ClineProvider.handleCodeAction(command, promptType, params) - }), - ) - } - - // Helper function to register both versions of a code action - const registerCodeActionPair = ( - context: vscode.ExtensionContext, - baseCommand: string, - promptType: keyof typeof ACTION_NAMES, - inputPrompt?: string, - inputPlaceholder?: string, - ) => { - // Register new task version - registerCodeAction(context, baseCommand, promptType, inputPrompt, inputPlaceholder) - - // Register current task version - registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, inputPrompt, inputPlaceholder) - } - - // Register code action commands - registerCodeActionPair( - context, - "roo-cline.explainCode", - "EXPLAIN", - "What would you like Roo to explain?", - "E.g. How does the error handling work?", - ) - - registerCodeActionPair( - context, - "roo-cline.fixCode", - "FIX", - "What would you like Roo to fix?", - "E.g. Maintain backward compatibility", - ) - - registerCodeActionPair( - context, - "roo-cline.improveCode", - "IMPROVE", - "What would you like Roo to improve?", - "E.g. Focus on performance optimization", - ) - - registerCodeAction(context, "roo-cline.addToContext", "ADD_TO_CONTEXT") + registerCodeActions(context) return createClineAPI(outputChannel, sidebarProvider) } -// This method is called when your extension is deactivated +// This method is called when your extension is deactivated. export function deactivate() { outputChannel.appendLine("Roo-Code extension deactivated") }