mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Merge pull request #656 from samhvw8/feat/add-to-context-code-action
Feat add to context code action & Fix some code action error
This commit is contained in:
10
package.json
10
package.json
@@ -118,6 +118,11 @@
|
|||||||
"command": "roo-cline.improveCode",
|
"command": "roo-cline.improveCode",
|
||||||
"title": "Roo Code: Improve Code",
|
"title": "Roo Code: Improve Code",
|
||||||
"category": "Roo Code"
|
"category": "Roo Code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "roo-cline.addToContext",
|
||||||
|
"title": "Roo Code: Add To Context",
|
||||||
|
"category": "Roo Code"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
@@ -136,6 +141,11 @@
|
|||||||
"command": "roo-cline.improveCode",
|
"command": "roo-cline.improveCode",
|
||||||
"when": "editorHasSelection",
|
"when": "editorHasSelection",
|
||||||
"group": "Roo Code@3"
|
"group": "Roo Code@3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "roo-cline.addToContext",
|
||||||
|
"when": "editorHasSelection",
|
||||||
|
"group": "Roo Code@4"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view/title": [
|
"view/title": [
|
||||||
|
|||||||
@@ -1,113 +1,27 @@
|
|||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import * as path from "path"
|
import { EditorUtils } from "./EditorUtils"
|
||||||
import { ClineProvider } from "./webview/ClineProvider"
|
|
||||||
|
|
||||||
export const ACTION_NAMES = {
|
export const ACTION_NAMES = {
|
||||||
EXPLAIN: "Roo Code: Explain Code",
|
EXPLAIN: "Roo Code: Explain Code",
|
||||||
FIX: "Roo Code: Fix Code",
|
FIX: "Roo Code: Fix Code",
|
||||||
|
FIX_LOGIC: "Roo Code: Fix Logic",
|
||||||
IMPROVE: "Roo Code: Improve Code",
|
IMPROVE: "Roo Code: Improve Code",
|
||||||
|
ADD_TO_CONTEXT: "Roo Code: Add to Context",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const COMMAND_IDS = {
|
const COMMAND_IDS = {
|
||||||
EXPLAIN: "roo-cline.explainCode",
|
EXPLAIN: "roo-cline.explainCode",
|
||||||
FIX: "roo-cline.fixCode",
|
FIX: "roo-cline.fixCode",
|
||||||
IMPROVE: "roo-cline.improveCode",
|
IMPROVE: "roo-cline.improveCode",
|
||||||
|
ADD_TO_CONTEXT: "roo-cline.addToContext",
|
||||||
} as const
|
} 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 {
|
export class CodeActionProvider implements vscode.CodeActionProvider {
|
||||||
public static readonly providedCodeActionKinds = [
|
public static readonly providedCodeActionKinds = [
|
||||||
vscode.CodeActionKind.QuickFix,
|
vscode.CodeActionKind.QuickFix,
|
||||||
vscode.CodeActionKind.RefactorRewrite,
|
vscode.CodeActionKind.RefactorRewrite,
|
||||||
]
|
]
|
||||||
|
|
||||||
// Cache file paths for performance
|
|
||||||
private readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
|
|
||||||
|
|
||||||
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 {
|
private createAction(title: string, kind: vscode.CodeActionKind, command: string, args: any[]): vscode.CodeAction {
|
||||||
const action = new vscode.CodeAction(title, kind)
|
const action = new vscode.CodeAction(title, kind)
|
||||||
action.command = { command, title, arguments: args }
|
action.command = { command, title, arguments: args }
|
||||||
@@ -126,32 +40,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(
|
public provideCodeActions(
|
||||||
document: vscode.TextDocument,
|
document: vscode.TextDocument,
|
||||||
range: vscode.Range | vscode.Selection,
|
range: vscode.Range | vscode.Selection,
|
||||||
context: vscode.CodeActionContext,
|
context: vscode.CodeActionContext,
|
||||||
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
|
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
|
||||||
try {
|
try {
|
||||||
const effectiveRange = this.getEffectiveRange(document, range)
|
const effectiveRange = EditorUtils.getEffectiveRange(document, range)
|
||||||
if (!effectiveRange) {
|
if (!effectiveRange) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = this.getFilePath(document)
|
const filePath = EditorUtils.getFilePath(document)
|
||||||
const actions: vscode.CodeAction[] = []
|
const actions: vscode.CodeAction[] = []
|
||||||
|
|
||||||
// Create actions using helper method
|
|
||||||
// Add explain actions
|
|
||||||
actions.push(
|
actions.push(
|
||||||
...this.createActionPair(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [
|
...this.createActionPair(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [
|
||||||
filePath,
|
filePath,
|
||||||
@@ -159,14 +61,13 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Only process diagnostics if they exist
|
|
||||||
if (context.diagnostics.length > 0) {
|
if (context.diagnostics.length > 0) {
|
||||||
const relevantDiagnostics = context.diagnostics.filter((d) =>
|
const relevantDiagnostics = context.diagnostics.filter((d) =>
|
||||||
this.hasIntersectingRange(effectiveRange.range, d.range),
|
EditorUtils.hasIntersectingRange(effectiveRange.range, d.range),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (relevantDiagnostics.length > 0) {
|
if (relevantDiagnostics.length > 0) {
|
||||||
const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData)
|
const diagnosticMessages = relevantDiagnostics.map(EditorUtils.createDiagnosticData)
|
||||||
actions.push(
|
actions.push(
|
||||||
...this.createActionPair(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
|
...this.createActionPair(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
|
||||||
filePath,
|
filePath,
|
||||||
@@ -175,9 +76,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
actions.push(
|
||||||
|
...this.createActionPair(ACTION_NAMES.FIX_LOGIC, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
|
||||||
|
filePath,
|
||||||
|
effectiveRange.text,
|
||||||
|
]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add improve actions
|
|
||||||
actions.push(
|
actions.push(
|
||||||
...this.createActionPair(
|
...this.createActionPair(
|
||||||
ACTION_NAMES.IMPROVE,
|
ACTION_NAMES.IMPROVE,
|
||||||
@@ -187,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
|
return actions
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error providing code actions:", error)
|
console.error("Error providing code actions:", error)
|
||||||
|
|||||||
141
src/core/EditorUtils.ts
Normal file
141
src/core/EditorUtils.ts
Normal file
@@ -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<vscode.TextDocument, string>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { CodeActionProvider, ACTION_NAMES } from "../CodeActionProvider"
|
import { CodeActionProvider, ACTION_NAMES } from "../CodeActionProvider"
|
||||||
|
import { EditorUtils } from "../EditorUtils"
|
||||||
|
|
||||||
// Mock VSCode API
|
// Mock VSCode API
|
||||||
jest.mock("vscode", () => ({
|
jest.mock("vscode", () => ({
|
||||||
@@ -16,13 +17,6 @@ jest.mock("vscode", () => ({
|
|||||||
start: { line: startLine, character: startChar },
|
start: { line: startLine, character: startChar },
|
||||||
end: { line: endLine, character: endChar },
|
end: { line: endLine, character: endChar },
|
||||||
})),
|
})),
|
||||||
Position: jest.fn().mockImplementation((line, character) => ({
|
|
||||||
line,
|
|
||||||
character,
|
|
||||||
})),
|
|
||||||
workspace: {
|
|
||||||
getWorkspaceFolder: jest.fn(),
|
|
||||||
},
|
|
||||||
DiagnosticSeverity: {
|
DiagnosticSeverity: {
|
||||||
Error: 0,
|
Error: 0,
|
||||||
Warning: 1,
|
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", () => {
|
describe("CodeActionProvider", () => {
|
||||||
let provider: CodeActionProvider
|
let provider: CodeActionProvider
|
||||||
let mockDocument: any
|
let mockDocument: any
|
||||||
@@ -55,68 +59,32 @@ describe("CodeActionProvider", () => {
|
|||||||
mockContext = {
|
mockContext = {
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
describe("getEffectiveRange", () => {
|
// Setup default EditorUtils mocks
|
||||||
it("should return selected text when available", () => {
|
;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue({
|
||||||
mockDocument.getText.mockReturnValue("selected text")
|
range: mockRange,
|
||||||
|
text: "test code",
|
||||||
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")
|
|
||||||
})
|
})
|
||||||
|
;(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", () => {
|
describe("provideCodeActions", () => {
|
||||||
beforeEach(() => {
|
it("should provide explain, improve, fix logic, and add to context actions by default", () => {
|
||||||
mockDocument.getText.mockReturnValue("test code")
|
|
||||||
mockDocument.lineAt.mockReturnValue({ text: "test code", lineNumber: 0 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should provide explain and improve actions by default", () => {
|
|
||||||
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
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)[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)[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)[2].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in New Task`)
|
||||||
expect((actions as any)[3].title).toBe(`${ACTION_NAMES.IMPROVE} in Current 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 = [
|
mockContext.diagnostics = [
|
||||||
{
|
{
|
||||||
message: "test error",
|
message: "test error",
|
||||||
@@ -127,22 +95,33 @@ describe("CodeActionProvider", () => {
|
|||||||
|
|
||||||
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
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 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} 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 handle errors gracefully", () => {
|
it("should return empty array when no effective range", () => {
|
||||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
|
;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue(null)
|
||||||
mockDocument.getText.mockImplementation(() => {
|
|
||||||
throw new Error("Test error")
|
|
||||||
})
|
|
||||||
mockDocument.lineAt.mockReturnValue({ text: "test", lineNumber: 0 })
|
|
||||||
|
|
||||||
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
||||||
|
|
||||||
expect(actions).toEqual([])
|
expect(actions).toEqual([])
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting effective range:", expect.any(Error))
|
})
|
||||||
|
|
||||||
|
it("should handle errors gracefully", () => {
|
||||||
|
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
|
||||||
|
;(EditorUtils.getEffectiveRange as jest.Mock).mockImplementation(() => {
|
||||||
|
throw new Error("Test error")
|
||||||
|
})
|
||||||
|
|
||||||
|
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
||||||
|
|
||||||
|
expect(actions).toEqual([])
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Error providing code actions:", expect.any(Error))
|
||||||
|
|
||||||
consoleErrorSpy.mockRestore()
|
consoleErrorSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|||||||
75
src/core/__tests__/EditorUtils.test.ts
Normal file
75
src/core/__tests__/EditorUtils.test.ts
Normal file
@@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -238,6 +238,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
|
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")) {
|
if (visibleProvider.cline && command.endsWith("InCurrentTask")) {
|
||||||
await visibleProvider.postMessageToWebview({
|
await visibleProvider.postMessageToWebview({
|
||||||
type: "invoke",
|
type: "invoke",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ClineProvider } from "./core/webview/ClineProvider"
|
|||||||
import { createClineAPI } from "./exports"
|
import { createClineAPI } from "./exports"
|
||||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||||
import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider"
|
import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider"
|
||||||
|
import { EditorUtils } from "./core/EditorUtils"
|
||||||
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -171,33 +172,43 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
command: string,
|
command: string,
|
||||||
promptType: keyof typeof ACTION_NAMES,
|
promptType: keyof typeof ACTION_NAMES,
|
||||||
inNewTask: boolean,
|
|
||||||
inputPrompt?: string,
|
inputPrompt?: string,
|
||||||
inputPlaceholder?: string,
|
inputPlaceholder?: string,
|
||||||
) => {
|
) => {
|
||||||
let userInput: string | undefined
|
let userInput: string | undefined
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand(
|
vscode.commands.registerCommand(command, async (...args: any[]) => {
|
||||||
command,
|
if (inputPrompt) {
|
||||||
async (filePath: string, selectedText: string, diagnostics?: any[]) => {
|
userInput = await vscode.window.showInputBox({
|
||||||
if (inputPrompt) {
|
prompt: inputPrompt,
|
||||||
userInput = await vscode.window.showInputBox({
|
placeHolder: inputPlaceholder,
|
||||||
prompt: inputPrompt,
|
})
|
||||||
placeHolder: inputPlaceholder,
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
// Handle both code action and direct command cases
|
||||||
filePath,
|
let filePath: string
|
||||||
selectedText,
|
let selectedText: string
|
||||||
...(diagnostics ? { diagnostics } : {}),
|
let diagnostics: any[] | undefined
|
||||||
...(userInput ? { userInput } : {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
await ClineProvider.handleCodeAction(command, promptType, params)
|
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)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,10 +221,10 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
inputPlaceholder?: string,
|
inputPlaceholder?: string,
|
||||||
) => {
|
) => {
|
||||||
// Register new task version
|
// Register new task version
|
||||||
registerCodeAction(context, baseCommand, promptType, true, inputPrompt, inputPlaceholder)
|
registerCodeAction(context, baseCommand, promptType, inputPrompt, inputPlaceholder)
|
||||||
|
|
||||||
// Register current task version
|
// Register current task version
|
||||||
registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, false, inputPrompt, inputPlaceholder)
|
registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, inputPrompt, inputPlaceholder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register code action commands
|
// Register code action commands
|
||||||
@@ -241,6 +252,8 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
"E.g. Focus on performance optimization",
|
"E.g. Focus on performance optimization",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
registerCodeAction(context, "roo-cline.addToContext", "ADD_TO_CONTEXT")
|
||||||
|
|
||||||
return createClineAPI(outputChannel, sidebarProvider)
|
return createClineAPI(outputChannel, sidebarProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export interface ExtensionMessage {
|
|||||||
| "historyButtonClicked"
|
| "historyButtonClicked"
|
||||||
| "promptsButtonClicked"
|
| "promptsButtonClicked"
|
||||||
| "didBecomeVisible"
|
| "didBecomeVisible"
|
||||||
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
|
||||||
state?: ExtensionState
|
state?: ExtensionState
|
||||||
images?: string[]
|
images?: string[]
|
||||||
ollamaModels?: string[]
|
ollamaModels?: string[]
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const createPrompt = (template: string, params: PromptParams): string =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace any remaining user_input placeholders with empty string
|
// Replace any remaining placeholders with empty strings
|
||||||
result = result.replaceAll("${userInput}", "")
|
result = result.replaceAll(/\${[^}]*}/g, "")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ const supportPromptConfigs: Record<string, SupportPromptConfig> = {
|
|||||||
EXPLAIN: {
|
EXPLAIN: {
|
||||||
label: "Explain Code",
|
label: "Explain Code",
|
||||||
description:
|
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}:
|
template: `Explain the following code from file path @/\${filePath}:
|
||||||
\${userInput}
|
\${userInput}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ Please provide a clear and concise explanation of what this code does, including
|
|||||||
FIX: {
|
FIX: {
|
||||||
label: "Fix Issues",
|
label: "Fix Issues",
|
||||||
description:
|
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}
|
template: `Fix any issues in the following code from file path @/\${filePath}
|
||||||
\${diagnosticText}
|
\${diagnosticText}
|
||||||
\${userInput}
|
\${userInput}
|
||||||
@@ -76,7 +76,7 @@ Please:
|
|||||||
IMPROVE: {
|
IMPROVE: {
|
||||||
label: "Improve Code",
|
label: "Improve Code",
|
||||||
description:
|
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}:
|
template: `Improve the following code from file path @/\${filePath}:
|
||||||
\${userInput}
|
\${userInput}
|
||||||
|
|
||||||
@@ -92,6 +92,15 @@ Please suggest improvements for:
|
|||||||
|
|
||||||
Provide the improved code along with explanations for each enhancement.`,
|
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
|
} as const
|
||||||
|
|
||||||
type SupportPromptType = keyof typeof supportPromptConfigs
|
type SupportPromptType = keyof typeof supportPromptConfigs
|
||||||
|
|||||||
@@ -330,6 +330,20 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
[messages.length, clineAsk],
|
[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(() => {
|
const startNewTask = useCallback(() => {
|
||||||
vscode.postMessage({ type: "clearTask" })
|
vscode.postMessage({ type: "clearTask" })
|
||||||
}, [])
|
}, [])
|
||||||
@@ -469,6 +483,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
case "sendMessage":
|
case "sendMessage":
|
||||||
handleSendMessage(message.text ?? "", message.images ?? [])
|
handleSendMessage(message.text ?? "", message.images ?? [])
|
||||||
break
|
break
|
||||||
|
case "setChatBoxMessage":
|
||||||
|
handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
|
||||||
|
break
|
||||||
case "primaryButtonClick":
|
case "primaryButtonClick":
|
||||||
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
|
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
|
||||||
break
|
break
|
||||||
@@ -484,6 +501,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
textAreaDisabled,
|
textAreaDisabled,
|
||||||
enableButtons,
|
enableButtons,
|
||||||
handleSendMessage,
|
handleSendMessage,
|
||||||
|
handleSetChatBoxMessage,
|
||||||
handlePrimaryButtonClick,
|
handlePrimaryButtonClick,
|
||||||
handleSecondaryButtonClick,
|
handleSecondaryButtonClick,
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user