From 35a7e433f22f064379278980be559ef4f8d43aab Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 30 Jan 2025 16:26:44 +0700 Subject: [PATCH 1/6] refactor: centralize editor utilities and unify command handling - Create EditorUtils class to centralize shared editor functionality - Remove duplicated code between CodeActionProvider and command handlers - Improve command handling to work consistently for both code actions and direct commands - Add better type safety and error handling for editor operations --- src/core/CodeActionProvider.ts | 112 ++------------------------ src/core/EditorUtils.ts | 141 +++++++++++++++++++++++++++++++++ src/extension.ts | 52 +++++++----- 3 files changed, 178 insertions(+), 127 deletions(-) create mode 100644 src/core/EditorUtils.ts diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index 1f977a4c022..24dc72c5065 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode" -import * as path from "path" import { ClineProvider } from "./webview/ClineProvider" +import { EditorUtils } from "./EditorUtils" export const ACTION_NAMES = { EXPLAIN: "Roo Code: Explain Code", @@ -14,100 +14,12 @@ const COMMAND_IDS = { IMPROVE: "roo-cline.improveCode", } as const -interface DiagnosticData { - message: string - severity: vscode.DiagnosticSeverity - code?: string | number | { value: string | number; target: vscode.Uri } - source?: string - range: vscode.Range -} - -interface EffectiveRange { - range: vscode.Range - text: string -} - export class CodeActionProvider implements vscode.CodeActionProvider { public static readonly providedCodeActionKinds = [ vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.RefactorRewrite, ] - // Cache file paths for performance - private readonly filePathCache = new WeakMap() - - private getEffectiveRange( - document: vscode.TextDocument, - range: vscode.Range | vscode.Selection, - ): EffectiveRange | null { - try { - const selectedText = document.getText(range) - if (selectedText) { - return { range, text: selectedText } - } - - const currentLine = document.lineAt(range.start.line) - if (!currentLine.text.trim()) { - return null - } - - // Optimize range creation by checking bounds first - const startLine = Math.max(0, currentLine.lineNumber - 1) - const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1) - - // Only create new positions if needed - const effectiveRange = new vscode.Range( - startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), - endLine === currentLine.lineNumber - ? range.end - : new vscode.Position(endLine, document.lineAt(endLine).text.length), - ) - - return { - range: effectiveRange, - text: document.getText(effectiveRange), - } - } catch (error) { - console.error("Error getting effective range:", error) - return null - } - } - - private getFilePath(document: vscode.TextDocument): string { - // Check cache first - let filePath = this.filePathCache.get(document) - if (filePath) { - return filePath - } - - try { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri) - if (!workspaceFolder) { - filePath = document.uri.fsPath - } else { - const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath) - filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath - } - - // Cache the result - this.filePathCache.set(document, filePath) - return filePath - } catch (error) { - console.error("Error getting file path:", error) - return document.uri.fsPath - } - } - - private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { - return { - message: diagnostic.message, - severity: diagnostic.severity, - code: diagnostic.code, - source: diagnostic.source, - range: diagnostic.range, // Reuse the range object - } - } - private createAction(title: string, kind: vscode.CodeActionKind, command: string, args: any[]): vscode.CodeAction { const action = new vscode.CodeAction(title, kind) action.command = { command, title, arguments: args } @@ -126,32 +38,20 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ] } - private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { - // Optimize range intersection check - return !( - range2.end.line < range1.start.line || - range2.start.line > range1.end.line || - (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || - (range2.start.line === range1.end.line && range2.start.character > range1.end.character) - ) - } - public provideCodeActions( document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { try { - const effectiveRange = this.getEffectiveRange(document, range) + const effectiveRange = EditorUtils.getEffectiveRange(document, range) if (!effectiveRange) { return [] } - const filePath = this.getFilePath(document) + const filePath = EditorUtils.getFilePath(document) const actions: vscode.CodeAction[] = [] - // Create actions using helper method - // Add explain actions actions.push( ...this.createActionPair(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [ filePath, @@ -159,14 +59,13 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ]), ) - // Only process diagnostics if they exist if (context.diagnostics.length > 0) { const relevantDiagnostics = context.diagnostics.filter((d) => - this.hasIntersectingRange(effectiveRange.range, d.range), + EditorUtils.hasIntersectingRange(effectiveRange.range, d.range), ) if (relevantDiagnostics.length > 0) { - const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData) + const diagnosticMessages = relevantDiagnostics.map(EditorUtils.createDiagnosticData) actions.push( ...this.createActionPair(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [ filePath, @@ -177,7 +76,6 @@ export class CodeActionProvider implements vscode.CodeActionProvider { } } - // Add improve actions actions.push( ...this.createActionPair( ACTION_NAMES.IMPROVE, diff --git a/src/core/EditorUtils.ts b/src/core/EditorUtils.ts new file mode 100644 index 00000000000..e58a82f18f2 --- /dev/null +++ b/src/core/EditorUtils.ts @@ -0,0 +1,141 @@ +import * as vscode from "vscode" +import * as path from "path" + +export interface EffectiveRange { + range: vscode.Range + text: string +} + +export interface DiagnosticData { + message: string + severity: vscode.DiagnosticSeverity + code?: string | number | { value: string | number; target: vscode.Uri } + source?: string + range: vscode.Range +} + +export interface EditorContext { + filePath: string + selectedText: string + diagnostics?: DiagnosticData[] +} + +export class EditorUtils { + // Cache file paths for performance + private static readonly filePathCache = new WeakMap() + + static getEffectiveRange( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + ): EffectiveRange | null { + try { + const selectedText = document.getText(range) + if (selectedText) { + return { range, text: selectedText } + } + + const currentLine = document.lineAt(range.start.line) + if (!currentLine.text.trim()) { + return null + } + + // Optimize range creation by checking bounds first + const startLine = Math.max(0, currentLine.lineNumber - 1) + const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1) + + // Only create new positions if needed + const effectiveRange = new vscode.Range( + startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), + endLine === currentLine.lineNumber + ? range.end + : new vscode.Position(endLine, document.lineAt(endLine).text.length), + ) + + return { + range: effectiveRange, + text: document.getText(effectiveRange), + } + } catch (error) { + console.error("Error getting effective range:", error) + return null + } + } + + static getFilePath(document: vscode.TextDocument): string { + // Check cache first + let filePath = this.filePathCache.get(document) + if (filePath) { + return filePath + } + + try { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri) + if (!workspaceFolder) { + filePath = document.uri.fsPath + } else { + const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath) + filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath + } + + // Cache the result + this.filePathCache.set(document, filePath) + return filePath + } catch (error) { + console.error("Error getting file path:", error) + return document.uri.fsPath + } + } + + static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { + return { + message: diagnostic.message, + severity: diagnostic.severity, + code: diagnostic.code, + source: diagnostic.source, + range: diagnostic.range, + } + } + + static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { + return !( + range2.end.line < range1.start.line || + range2.start.line > range1.end.line || + (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || + (range2.start.line === range1.end.line && range2.start.character > range1.end.character) + ) + } + + static getEditorContext(editor?: vscode.TextEditor): EditorContext | null { + try { + if (!editor) { + editor = vscode.window.activeTextEditor + } + if (!editor) { + return null + } + + const document = editor.document + const selection = editor.selection + const effectiveRange = this.getEffectiveRange(document, selection) + + if (!effectiveRange) { + return null + } + + const filePath = this.getFilePath(document) + const diagnostics = vscode.languages + .getDiagnostics(document.uri) + .filter((d) => this.hasIntersectingRange(effectiveRange.range, d.range)) + .map(this.createDiagnosticData) + + return { + filePath, + selectedText: effectiveRange.text, + ...(diagnostics.length > 0 ? { diagnostics } : {}), + } + } catch (error) { + console.error("Error getting editor context:", error) + return null + } + } +} diff --git a/src/extension.ts b/src/extension.ts index 332759a3811..37cc8c06eb8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ 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 { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" /* @@ -178,26 +179,37 @@ export function activate(context: vscode.ExtensionContext) { let userInput: string | undefined context.subscriptions.push( - vscode.commands.registerCommand( - command, - async (filePath: string, selectedText: string, diagnostics?: any[]) => { - if (inputPrompt) { - userInput = await vscode.window.showInputBox({ - prompt: inputPrompt, - placeHolder: inputPlaceholder, - }) - } - - const params = { - filePath, - selectedText, - ...(diagnostics ? { diagnostics } : {}), - ...(userInput ? { userInput } : {}), - } - - await ClineProvider.handleCodeAction(command, promptType, params) - }, - ), + 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) + }), ) } From 2e561496203b78591412f7b4b9604a3f5e4250cc Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 30 Jan 2025 17:23:12 +0700 Subject: [PATCH 2/6] feat: add 'Add To Context' code action - Add new command registration and menu item - Add new code action type and command ID - Add support prompt config for adding code to context - Add message handling in webview for setting chat box content - Add logic to append selected code to existing chat input --- package.json | 10 ++++++++++ src/core/CodeActionProvider.ts | 20 +++++++++++++++++++- src/core/webview/ClineProvider.ts | 10 ++++++++++ src/extension.ts | 7 ++++--- src/shared/ExtensionMessage.ts | 2 +- src/shared/support-prompt.ts | 19 ++++++++++++++----- webview-ui/src/components/chat/ChatView.tsx | 18 ++++++++++++++++++ 7 files changed, 76 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index fa3b84ff82d..5df7d0323dc 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,11 @@ "command": "roo-cline.improveCode", "title": "Roo Code: Improve Code", "category": "Roo Code" + }, + { + "command": "roo-cline.addToContext", + "title": "Roo Code: Add To Context", + "category": "Roo Code" } ], "menus": { @@ -136,6 +141,11 @@ "command": "roo-cline.improveCode", "when": "editorHasSelection", "group": "Roo Code@3" + }, + { + "command": "roo-cline.addToContext", + "when": "editorHasSelection", + "group": "Roo Code@4" } ], "view/title": [ diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index 24dc72c5065..0754c1a7e24 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -1,17 +1,19 @@ import * as vscode from "vscode" -import { ClineProvider } from "./webview/ClineProvider" import { EditorUtils } from "./EditorUtils" export const ACTION_NAMES = { EXPLAIN: "Roo Code: Explain Code", FIX: "Roo Code: Fix Code", + FIX_LOGIC: "Roo Code: Fix Logic", IMPROVE: "Roo Code: Improve Code", + ADD_TO_CONTEXT: "Roo Code: Add to Context", } as const const COMMAND_IDS = { EXPLAIN: "roo-cline.explainCode", FIX: "roo-cline.fixCode", IMPROVE: "roo-cline.improveCode", + ADD_TO_CONTEXT: "roo-cline.addToContext", } as const export class CodeActionProvider implements vscode.CodeActionProvider { @@ -74,6 +76,13 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ]), ) } + } else { + actions.push( + ...this.createActionPair(ACTION_NAMES.FIX_LOGIC, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [ + filePath, + effectiveRange.text, + ]), + ) } actions.push( @@ -85,6 +94,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ), ) + actions.push( + this.createAction( + ACTION_NAMES.ADD_TO_CONTEXT, + vscode.CodeActionKind.QuickFix, + COMMAND_IDS.ADD_TO_CONTEXT, + [filePath, effectiveRange.text], + ), + ) + return actions } catch (error) { console.error("Error providing code actions:", error) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 58ec513529e..08a95ae44b0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -238,6 +238,16 @@ export class ClineProvider implements vscode.WebviewViewProvider { const prompt = supportPrompt.create(promptType, params, customSupportPrompts) + if (command.endsWith("addToContext")) { + await visibleProvider.postMessageToWebview({ + type: "invoke", + invoke: "setChatBoxMessage", + text: prompt, + }) + + return + } + if (visibleProvider.cline && command.endsWith("InCurrentTask")) { await visibleProvider.postMessageToWebview({ type: "invoke", diff --git a/src/extension.ts b/src/extension.ts index 37cc8c06eb8..719f38d5e8c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -172,7 +172,6 @@ export function activate(context: vscode.ExtensionContext) { context: vscode.ExtensionContext, command: string, promptType: keyof typeof ACTION_NAMES, - inNewTask: boolean, inputPrompt?: string, inputPlaceholder?: string, ) => { @@ -222,10 +221,10 @@ export function activate(context: vscode.ExtensionContext) { inputPlaceholder?: string, ) => { // Register new task version - registerCodeAction(context, baseCommand, promptType, true, inputPrompt, inputPlaceholder) + registerCodeAction(context, baseCommand, promptType, inputPrompt, inputPlaceholder) // Register current task version - registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, false, inputPrompt, inputPlaceholder) + registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, inputPrompt, inputPlaceholder) } // Register code action commands @@ -253,6 +252,8 @@ export function activate(context: vscode.ExtensionContext) { "E.g. Focus on performance optimization", ) + registerCodeAction(context, "roo-cline.addToContext", "ADD_TO_CONTEXT") + return createClineAPI(outputChannel, sidebarProvider) } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 69cc5187f87..a226beb802c 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -50,7 +50,7 @@ export interface ExtensionMessage { | "historyButtonClicked" | "promptsButtonClicked" | "didBecomeVisible" - invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" + invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage" state?: ExtensionState images?: string[] ollamaModels?: string[] diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 881c0600572..d80e9e40142 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -18,8 +18,8 @@ export const createPrompt = (template: string, params: PromptParams): string => } } - // Replace any remaining user_input placeholders with empty string - result = result.replaceAll("${userInput}", "") + // Replace any remaining placeholders with empty strings + result = result.replaceAll(/\${[^}]*}/g, "") return result } @@ -42,7 +42,7 @@ const supportPromptConfigs: Record = { EXPLAIN: { label: "Explain Code", description: - "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in the editor context menu (right-click on selected code).", + "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", template: `Explain the following code from file path @/\${filePath}: \${userInput} @@ -58,7 +58,7 @@ Please provide a clear and concise explanation of what this code does, including FIX: { label: "Fix Issues", description: - "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in the editor context menu (right-click on selected code).", + "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", template: `Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} \${userInput} @@ -76,7 +76,7 @@ Please: IMPROVE: { label: "Improve Code", description: - "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in the editor context menu (right-click on selected code).", + "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", template: `Improve the following code from file path @/\${filePath}: \${userInput} @@ -92,6 +92,15 @@ Please suggest improvements for: Provide the improved code along with explanations for each enhancement.`, }, + ADD_TO_CONTEXT: { + label: "Add to Context", + description: + "Add context to your current task or conversation. Useful for providing additional information or clarifications. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", + template: `@/\${filePath}: +\`\`\` +\${selectedText} +\`\`\``, + }, } as const type SupportPromptType = keyof typeof supportPromptConfigs diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 0a32eef77a2..76543f3b614 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -330,6 +330,20 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie [messages.length, clineAsk], ) + const handleSetChatBoxMessage = useCallback( + (text: string, images: string[]) => { + // Avoid nested template literals by breaking down the logic + let newValue = text + if (inputValue !== "") { + newValue = inputValue + " " + text + } + + setInputValue(newValue) + setSelectedImages([...selectedImages, ...images]) + }, + [inputValue, selectedImages], + ) + const startNewTask = useCallback(() => { vscode.postMessage({ type: "clearTask" }) }, []) @@ -429,6 +443,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie case "sendMessage": handleSendMessage(message.text ?? "", message.images ?? []) break + case "setChatBoxMessage": + handleSetChatBoxMessage(message.text ?? "", message.images ?? []) + break case "primaryButtonClick": handlePrimaryButtonClick() break @@ -444,6 +461,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie textAreaDisabled, enableButtons, handleSendMessage, + handleSetChatBoxMessage, handlePrimaryButtonClick, handleSecondaryButtonClick, ], From bb5d506679e36bf809b58e9f4420504186841bcd Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 30 Jan 2025 17:42:11 +0700 Subject: [PATCH 3/6] refactor: extract editor utilities into EditorUtils module and add tests --- src/core/__tests__/CodeActionProvider.test.ts | 103 +++++++----------- src/core/__tests__/EditorUtils.test.ts | 75 +++++++++++++ 2 files changed, 116 insertions(+), 62 deletions(-) create mode 100644 src/core/__tests__/EditorUtils.test.ts diff --git a/src/core/__tests__/CodeActionProvider.test.ts b/src/core/__tests__/CodeActionProvider.test.ts index 0fb9ad1aa10..6042f41b2b2 100644 --- a/src/core/__tests__/CodeActionProvider.test.ts +++ b/src/core/__tests__/CodeActionProvider.test.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode" import { CodeActionProvider, ACTION_NAMES } from "../CodeActionProvider" +import { EditorUtils } from "../EditorUtils" // Mock VSCode API jest.mock("vscode", () => ({ @@ -16,13 +17,6 @@ jest.mock("vscode", () => ({ start: { line: startLine, character: startChar }, end: { line: endLine, character: endChar }, })), - Position: jest.fn().mockImplementation((line, character) => ({ - line, - character, - })), - workspace: { - getWorkspaceFolder: jest.fn(), - }, DiagnosticSeverity: { Error: 0, Warning: 1, @@ -31,6 +25,16 @@ jest.mock("vscode", () => ({ }, })) +// Mock EditorUtils +jest.mock("../EditorUtils", () => ({ + EditorUtils: { + getEffectiveRange: jest.fn(), + getFilePath: jest.fn(), + hasIntersectingRange: jest.fn(), + createDiagnosticData: jest.fn(), + }, +})) + describe("CodeActionProvider", () => { let provider: CodeActionProvider let mockDocument: any @@ -55,68 +59,32 @@ describe("CodeActionProvider", () => { mockContext = { diagnostics: [], } - }) - - describe("getEffectiveRange", () => { - it("should return selected text when available", () => { - mockDocument.getText.mockReturnValue("selected text") - - const result = (provider as any).getEffectiveRange(mockDocument, mockRange) - - expect(result).toEqual({ - range: mockRange, - text: "selected text", - }) - }) - - it("should return null for empty line", () => { - mockDocument.getText.mockReturnValue("") - mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 }) - - const result = (provider as any).getEffectiveRange(mockDocument, mockRange) - - expect(result).toBeNull() - }) - }) - - describe("getFilePath", () => { - it("should return relative path when in workspace", () => { - const mockWorkspaceFolder = { - uri: { fsPath: "/test" }, - } - ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder) - - const result = (provider as any).getFilePath(mockDocument) - - expect(result).toBe("file.ts") - }) - - it("should return absolute path when not in workspace", () => { - ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null) - const result = (provider as any).getFilePath(mockDocument) - - expect(result).toBe("/test/file.ts") + // Setup default EditorUtils mocks + ;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue({ + range: mockRange, + text: "test code", }) + ;(EditorUtils.getFilePath as jest.Mock).mockReturnValue("/test/file.ts") + ;(EditorUtils.hasIntersectingRange as jest.Mock).mockReturnValue(true) + ;(EditorUtils.createDiagnosticData as jest.Mock).mockImplementation((d) => d) }) describe("provideCodeActions", () => { - beforeEach(() => { - mockDocument.getText.mockReturnValue("test code") - mockDocument.lineAt.mockReturnValue({ text: "test code", lineNumber: 0 }) - }) - - it("should provide explain and improve actions by default", () => { + it("should provide explain, improve, fix logic, and add to context actions by default", () => { const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) - expect(actions).toHaveLength(4) + expect(actions).toHaveLength(7) // 2 explain + 2 fix logic + 2 improve + 1 add to context expect((actions as any)[0].title).toBe(`${ACTION_NAMES.EXPLAIN} in New Task`) expect((actions as any)[1].title).toBe(`${ACTION_NAMES.EXPLAIN} in Current Task`) - expect((actions as any)[2].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`) - expect((actions as any)[3].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`) + expect((actions as any)[2].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in New Task`) + expect((actions as any)[3].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in Current Task`) + expect((actions as any)[4].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`) + expect((actions as any)[5].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`) + expect((actions as any)[6].title).toBe(ACTION_NAMES.ADD_TO_CONTEXT) }) - it("should provide fix action when diagnostics exist", () => { + it("should provide fix action instead of fix logic when diagnostics exist", () => { mockContext.diagnostics = [ { message: "test error", @@ -127,22 +95,33 @@ describe("CodeActionProvider", () => { const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) - expect(actions).toHaveLength(6) + expect(actions).toHaveLength(7) // 2 explain + 2 fix + 2 improve + 1 add to context expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in New Task`)).toBe(true) expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in Current Task`)).toBe(true) + expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in New Task`)).toBe(false) + expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in Current Task`)).toBe( + false, + ) + }) + + it("should return empty array when no effective range", () => { + ;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue(null) + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toEqual([]) }) it("should handle errors gracefully", () => { const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}) - mockDocument.getText.mockImplementation(() => { + ;(EditorUtils.getEffectiveRange as jest.Mock).mockImplementation(() => { throw new Error("Test error") }) - mockDocument.lineAt.mockReturnValue({ text: "test", lineNumber: 0 }) const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) expect(actions).toEqual([]) - expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting effective range:", expect.any(Error)) + expect(consoleErrorSpy).toHaveBeenCalledWith("Error providing code actions:", expect.any(Error)) consoleErrorSpy.mockRestore() }) diff --git a/src/core/__tests__/EditorUtils.test.ts b/src/core/__tests__/EditorUtils.test.ts new file mode 100644 index 00000000000..88d64e6da82 --- /dev/null +++ b/src/core/__tests__/EditorUtils.test.ts @@ -0,0 +1,75 @@ +import * as vscode from "vscode" +import { EditorUtils } from "../EditorUtils" + +// Mock VSCode API +jest.mock("vscode", () => ({ + Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ + start: { line: startLine, character: startChar }, + end: { line: endLine, character: endChar }, + })), + Position: jest.fn().mockImplementation((line, character) => ({ + line, + character, + })), + workspace: { + getWorkspaceFolder: jest.fn(), + }, +})) + +describe("EditorUtils", () => { + let mockDocument: any + + beforeEach(() => { + mockDocument = { + getText: jest.fn(), + lineAt: jest.fn(), + lineCount: 10, + uri: { fsPath: "/test/file.ts" }, + } + }) + + describe("getEffectiveRange", () => { + it("should return selected text when available", () => { + const mockRange = new vscode.Range(0, 0, 0, 10) + mockDocument.getText.mockReturnValue("selected text") + + const result = EditorUtils.getEffectiveRange(mockDocument, mockRange) + + expect(result).toEqual({ + range: mockRange, + text: "selected text", + }) + }) + + it("should return null for empty line", () => { + const mockRange = new vscode.Range(0, 0, 0, 10) + mockDocument.getText.mockReturnValue("") + mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 }) + + const result = EditorUtils.getEffectiveRange(mockDocument, mockRange) + + expect(result).toBeNull() + }) + }) + + describe("getFilePath", () => { + it("should return relative path when in workspace", () => { + const mockWorkspaceFolder = { + uri: { fsPath: "/test" }, + } + ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder) + + const result = EditorUtils.getFilePath(mockDocument) + + expect(result).toBe("file.ts") + }) + + it("should return absolute path when not in workspace", () => { + ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null) + + const result = EditorUtils.getFilePath(mockDocument) + + expect(result).toBe("/test/file.ts") + }) + }) +}) From 308eab05635fa90c88de6abb291652a8f5682817 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 31 Jan 2025 00:22:15 -0500 Subject: [PATCH 4/6] Update src/shared/support-prompt.ts --- src/shared/support-prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index d80e9e40142..7146e477e58 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -42,7 +42,7 @@ const supportPromptConfigs: Record = { EXPLAIN: { label: "Explain Code", description: - "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", + "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).", template: `Explain the following code from file path @/\${filePath}: \${userInput} From 099447d06bc8dc5f48fa327a139cbaa9d843e04e Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 31 Jan 2025 00:22:20 -0500 Subject: [PATCH 5/6] Update src/shared/support-prompt.ts --- src/shared/support-prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 7146e477e58..6086f3a9fd8 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -58,7 +58,7 @@ Please provide a clear and concise explanation of what this code does, including FIX: { label: "Fix Issues", description: - "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", + "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).", template: `Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} \${userInput} From 7614048ed46d160b2785147c5dcaafe2961226cb Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 31 Jan 2025 00:22:26 -0500 Subject: [PATCH 6/6] Update src/shared/support-prompt.ts --- src/shared/support-prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 6086f3a9fd8..d097162a4ef 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -76,7 +76,7 @@ Please: IMPROVE: { label: "Improve Code", description: - "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", + "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).", template: `Improve the following code from file path @/\${filePath}: \${userInput}