diff --git a/src/core/EditorUtils.ts b/src/core/EditorUtils.ts index e58a82f18f..ee81353b7b 100644 --- a/src/core/EditorUtils.ts +++ b/src/core/EditorUtils.ts @@ -1,29 +1,63 @@ import * as vscode from "vscode" import * as path from "path" +/** + * Represents an effective range in a document along with the corresponding text. + */ export interface EffectiveRange { + /** The range within the document. */ range: vscode.Range + /** The text within the specified range. */ text: string } +/** + * Represents diagnostic information extracted from a VSCode diagnostic. + */ export interface DiagnosticData { + /** The diagnostic message. */ message: string + /** The severity level of the diagnostic. */ severity: vscode.DiagnosticSeverity + /** + * Optional diagnostic code. + * Can be a string, number, or an object with value and target. + */ code?: string | number | { value: string | number; target: vscode.Uri } + /** Optional source identifier for the diagnostic (e.g., the extension name). */ source?: string + /** The range within the document where the diagnostic applies. */ range: vscode.Range } +/** + * Contextual information for a VSCode text editor. + */ export interface EditorContext { + /** The file path of the current document. */ filePath: string + /** The effective text selected or derived from the document. */ selectedText: string + /** Optional list of diagnostics associated with the effective range. */ diagnostics?: DiagnosticData[] } +/** + * Utility class providing helper methods for working with VSCode editors and documents. + */ export class EditorUtils { - // Cache file paths for performance + /** Cache mapping text documents to their computed file paths. */ private static readonly filePathCache = new WeakMap() + /** + * Computes the effective range of text from the given document based on the user's selection. + * If the selection is non-empty, returns that directly. + * Otherwise, if the current line is non-empty, expands the range to include the adjacent lines. + * + * @param document - The text document to extract text from. + * @param range - The user selected range or selection. + * @returns An EffectiveRange object containing the effective range and its text, or null if no valid text is found. + */ static getEffectiveRange( document: vscode.TextDocument, range: vscode.Range | vscode.Selection, @@ -39,16 +73,12 @@ export class EditorUtils { 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) + const startLineIndex = Math.max(0, currentLine.lineNumber - 1) + const endLineIndex = 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), + new vscode.Position(startLineIndex, 0), + new vscode.Position(endLineIndex, document.lineAt(endLineIndex).text.length), ) return { @@ -61,8 +91,15 @@ export class EditorUtils { } } + /** + * Retrieves the file path of a given text document. + * Utilizes an internal cache to avoid redundant computations. + * If the document belongs to a workspace, attempts to compute a relative path; otherwise, returns the absolute fsPath. + * + * @param document - The text document for which to retrieve the file path. + * @returns The file path as a string. + */ static getFilePath(document: vscode.TextDocument): string { - // Check cache first let filePath = this.filePathCache.get(document) if (filePath) { return filePath @@ -77,7 +114,6 @@ export class EditorUtils { filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath } - // Cache the result this.filePathCache.set(document, filePath) return filePath } catch (error) { @@ -86,6 +122,12 @@ export class EditorUtils { } } + /** + * Converts a VSCode Diagnostic object to a local DiagnosticData instance. + * + * @param diagnostic - The VSCode diagnostic to convert. + * @returns The corresponding DiagnosticData object. + */ static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { return { message: diagnostic.message, @@ -96,15 +138,36 @@ export class EditorUtils { } } + /** + * Determines whether two VSCode ranges intersect. + * + * @param range1 - The first range. + * @param range2 - The second range. + * @returns True if the ranges intersect; otherwise, false. + */ static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { - return !( + if ( + range1.end.line < range2.start.line || + (range1.end.line === range2.start.line && range1.end.character <= range2.start.character) + ) { + return false + } + if ( 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) - ) + (range2.end.line === range1.start.line && range2.end.character <= range1.start.character) + ) { + return false + } + return true } + /** + * Builds the editor context from the provided text editor or from the active text editor. + * The context includes file path, effective selected text, and any diagnostics that intersect with the effective range. + * + * @param editor - (Optional) A specific text editor instance. If not provided, the active text editor is used. + * @returns An EditorContext object if successful; otherwise, null. + */ static getEditorContext(editor?: vscode.TextEditor): EditorContext | null { try { if (!editor) { diff --git a/src/core/__tests__/EditorUtils.test.ts b/src/core/__tests__/EditorUtils.test.ts index 88d64e6da8..1a01838693 100644 --- a/src/core/__tests__/EditorUtils.test.ts +++ b/src/core/__tests__/EditorUtils.test.ts @@ -1,20 +1,35 @@ 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(), - }, -})) +// Use simple classes to simulate VSCode's Range and Position behavior. +jest.mock("vscode", () => { + class MockPosition { + constructor( + public line: number, + public character: number, + ) {} + } + class MockRange { + start: MockPosition + end: MockPosition + constructor(start: MockPosition, end: MockPosition) { + this.start = start + this.end = end + } + } + + return { + Range: MockRange, + Position: MockPosition, + workspace: { + getWorkspaceFolder: jest.fn(), + }, + window: { activeTextEditor: undefined }, + languages: { + getDiagnostics: jest.fn(() => []), + }, + } +}) describe("EditorUtils", () => { let mockDocument: any @@ -30,7 +45,7 @@ describe("EditorUtils", () => { describe("getEffectiveRange", () => { it("should return selected text when available", () => { - const mockRange = new vscode.Range(0, 0, 0, 10) + const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) mockDocument.getText.mockReturnValue("selected text") const result = EditorUtils.getEffectiveRange(mockDocument, mockRange) @@ -42,7 +57,7 @@ describe("EditorUtils", () => { }) it("should return null for empty line", () => { - const mockRange = new vscode.Range(0, 0, 0, 10) + const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) mockDocument.getText.mockReturnValue("") mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 }) @@ -50,6 +65,58 @@ describe("EditorUtils", () => { expect(result).toBeNull() }) + + it("should expand empty selection to full lines", () => { + // Simulate a caret (empty selection) on line 2 at character 5. + const initialRange = new vscode.Range(new vscode.Position(2, 5), new vscode.Position(2, 5)) + // Return non-empty text for any line with text (lines 1, 2, and 3). + mockDocument.lineAt.mockImplementation((line: number) => { + return { text: `Line ${line} text`, lineNumber: line } + }) + mockDocument.getText.mockImplementation((range: any) => { + // If the range is exactly the empty initial selection, return an empty string. + if ( + range.start.line === initialRange.start.line && + range.start.character === initialRange.start.character && + range.end.line === initialRange.end.line && + range.end.character === initialRange.end.character + ) { + return "" + } + return "expanded text" + }) + + const result = EditorUtils.getEffectiveRange(mockDocument, initialRange) + + expect(result).not.toBeNull() + // Expected effective range: from the beginning of line 1 to the end of line 3. + expect(result?.range.start).toEqual({ line: 1, character: 0 }) + expect(result?.range.end).toEqual({ line: 3, character: 11 }) + expect(result?.text).toBe("expanded text") + }) + }) + + describe("hasIntersectingRange", () => { + it("should return false for ranges that only touch boundaries", () => { + // Range1: [0, 0) - [0, 10) and Range2: [0, 10) - [0, 20) + const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) + const range2 = new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 20)) + expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(false) + }) + + it("should return true for overlapping ranges", () => { + // Range1: [0, 0) - [0, 15) and Range2: [0, 10) - [0, 20) + const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 15)) + const range2 = new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 20)) + expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(true) + }) + + it("should return false for non-overlapping ranges", () => { + // Range1: [0, 0) - [0, 10) and Range2: [1, 0) - [1, 5) + const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) + const range2 = new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 5)) + expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(false) + }) }) describe("getFilePath", () => {