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
This commit is contained in:
sam hoang
2025-01-30 16:26:44 +07:00
parent bc5b00ea0b
commit 35a7e433f2
3 changed files with 176 additions and 125 deletions

View File

@@ -1,6 +1,6 @@
import * as vscode from "vscode" import * as vscode from "vscode"
import * as path from "path"
import { ClineProvider } from "./webview/ClineProvider" import { ClineProvider } from "./webview/ClineProvider"
import { EditorUtils } from "./EditorUtils"
export const ACTION_NAMES = { export const ACTION_NAMES = {
EXPLAIN: "Roo Code: Explain Code", EXPLAIN: "Roo Code: Explain Code",
@@ -14,100 +14,12 @@ const COMMAND_IDS = {
IMPROVE: "roo-cline.improveCode", IMPROVE: "roo-cline.improveCode",
} 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 +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( 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 +59,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,
@@ -177,7 +76,6 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
} }
} }
// Add improve actions
actions.push( actions.push(
...this.createActionPair( ...this.createActionPair(
ACTION_NAMES.IMPROVE, ACTION_NAMES.IMPROVE,

141
src/core/EditorUtils.ts Normal file
View 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
}
}
}

View File

@@ -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"
/* /*
@@ -178,9 +179,7 @@ export function activate(context: vscode.ExtensionContext) {
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,
async (filePath: string, selectedText: string, diagnostics?: any[]) => {
if (inputPrompt) { if (inputPrompt) {
userInput = await vscode.window.showInputBox({ userInput = await vscode.window.showInputBox({
prompt: inputPrompt, prompt: inputPrompt,
@@ -188,16 +187,29 @@ export function activate(context: vscode.ExtensionContext) {
}) })
} }
// 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 = { const params = {
filePath, ...{ filePath, selectedText },
selectedText,
...(diagnostics ? { diagnostics } : {}), ...(diagnostics ? { diagnostics } : {}),
...(userInput ? { userInput } : {}), ...(userInput ? { userInput } : {}),
} }
await ClineProvider.handleCodeAction(command, promptType, params) await ClineProvider.handleCodeAction(command, promptType, params)
}, }),
),
) )
} }