فهرست منبع

Merge pull request #730 from nissa-seru/fix-editorutils

Update EditorUtils
Matt Rubens 11 ماه پیش
والد
کامیت
6da05598bf
2فایلهای تغییر یافته به همراه162 افزوده شده و 32 حذف شده
  1. 79 16
      src/core/EditorUtils.ts
  2. 83 16
      src/core/__tests__/EditorUtils.test.ts

+ 79 - 16
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<vscode.TextDocument, string>()
 
+	/**
+	 * 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) {

+ 83 - 16
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", () => {