Skip to content

Commit 6da0559

Browse files
authored
Merge pull request #730 from nissa-seru/fix-editorutils
Update EditorUtils
2 parents 3e303ea + 5bec629 commit 6da0559

File tree

2 files changed

+162
-32
lines changed

2 files changed

+162
-32
lines changed

src/core/EditorUtils.ts

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,63 @@
11
import * as vscode from "vscode"
22
import * as path from "path"
33

4+
/**
5+
* Represents an effective range in a document along with the corresponding text.
6+
*/
47
export interface EffectiveRange {
8+
/** The range within the document. */
59
range: vscode.Range
10+
/** The text within the specified range. */
611
text: string
712
}
813

14+
/**
15+
* Represents diagnostic information extracted from a VSCode diagnostic.
16+
*/
917
export interface DiagnosticData {
18+
/** The diagnostic message. */
1019
message: string
20+
/** The severity level of the diagnostic. */
1121
severity: vscode.DiagnosticSeverity
22+
/**
23+
* Optional diagnostic code.
24+
* Can be a string, number, or an object with value and target.
25+
*/
1226
code?: string | number | { value: string | number; target: vscode.Uri }
27+
/** Optional source identifier for the diagnostic (e.g., the extension name). */
1328
source?: string
29+
/** The range within the document where the diagnostic applies. */
1430
range: vscode.Range
1531
}
1632

33+
/**
34+
* Contextual information for a VSCode text editor.
35+
*/
1736
export interface EditorContext {
37+
/** The file path of the current document. */
1838
filePath: string
39+
/** The effective text selected or derived from the document. */
1940
selectedText: string
41+
/** Optional list of diagnostics associated with the effective range. */
2042
diagnostics?: DiagnosticData[]
2143
}
2244

45+
/**
46+
* Utility class providing helper methods for working with VSCode editors and documents.
47+
*/
2348
export class EditorUtils {
24-
// Cache file paths for performance
49+
/** Cache mapping text documents to their computed file paths. */
2550
private static readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
2651

52+
/**
53+
* Computes the effective range of text from the given document based on the user's selection.
54+
* If the selection is non-empty, returns that directly.
55+
* Otherwise, if the current line is non-empty, expands the range to include the adjacent lines.
56+
*
57+
* @param document - The text document to extract text from.
58+
* @param range - The user selected range or selection.
59+
* @returns An EffectiveRange object containing the effective range and its text, or null if no valid text is found.
60+
*/
2761
static getEffectiveRange(
2862
document: vscode.TextDocument,
2963
range: vscode.Range | vscode.Selection,
@@ -39,16 +73,12 @@ export class EditorUtils {
3973
return null
4074
}
4175

42-
// Optimize range creation by checking bounds first
43-
const startLine = Math.max(0, currentLine.lineNumber - 1)
44-
const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1)
76+
const startLineIndex = Math.max(0, currentLine.lineNumber - 1)
77+
const endLineIndex = Math.min(document.lineCount - 1, currentLine.lineNumber + 1)
4578

46-
// Only create new positions if needed
4779
const effectiveRange = new vscode.Range(
48-
startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0),
49-
endLine === currentLine.lineNumber
50-
? range.end
51-
: new vscode.Position(endLine, document.lineAt(endLine).text.length),
80+
new vscode.Position(startLineIndex, 0),
81+
new vscode.Position(endLineIndex, document.lineAt(endLineIndex).text.length),
5282
)
5383

5484
return {
@@ -61,8 +91,15 @@ export class EditorUtils {
6191
}
6292
}
6393

94+
/**
95+
* Retrieves the file path of a given text document.
96+
* Utilizes an internal cache to avoid redundant computations.
97+
* If the document belongs to a workspace, attempts to compute a relative path; otherwise, returns the absolute fsPath.
98+
*
99+
* @param document - The text document for which to retrieve the file path.
100+
* @returns The file path as a string.
101+
*/
64102
static getFilePath(document: vscode.TextDocument): string {
65-
// Check cache first
66103
let filePath = this.filePathCache.get(document)
67104
if (filePath) {
68105
return filePath
@@ -77,7 +114,6 @@ export class EditorUtils {
77114
filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath
78115
}
79116

80-
// Cache the result
81117
this.filePathCache.set(document, filePath)
82118
return filePath
83119
} catch (error) {
@@ -86,6 +122,12 @@ export class EditorUtils {
86122
}
87123
}
88124

125+
/**
126+
* Converts a VSCode Diagnostic object to a local DiagnosticData instance.
127+
*
128+
* @param diagnostic - The VSCode diagnostic to convert.
129+
* @returns The corresponding DiagnosticData object.
130+
*/
89131
static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData {
90132
return {
91133
message: diagnostic.message,
@@ -96,15 +138,36 @@ export class EditorUtils {
96138
}
97139
}
98140

141+
/**
142+
* Determines whether two VSCode ranges intersect.
143+
*
144+
* @param range1 - The first range.
145+
* @param range2 - The second range.
146+
* @returns True if the ranges intersect; otherwise, false.
147+
*/
99148
static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean {
100-
return !(
149+
if (
150+
range1.end.line < range2.start.line ||
151+
(range1.end.line === range2.start.line && range1.end.character <= range2.start.character)
152+
) {
153+
return false
154+
}
155+
if (
101156
range2.end.line < range1.start.line ||
102-
range2.start.line > range1.end.line ||
103-
(range2.end.line === range1.start.line && range2.end.character < range1.start.character) ||
104-
(range2.start.line === range1.end.line && range2.start.character > range1.end.character)
105-
)
157+
(range2.end.line === range1.start.line && range2.end.character <= range1.start.character)
158+
) {
159+
return false
160+
}
161+
return true
106162
}
107163

164+
/**
165+
* Builds the editor context from the provided text editor or from the active text editor.
166+
* The context includes file path, effective selected text, and any diagnostics that intersect with the effective range.
167+
*
168+
* @param editor - (Optional) A specific text editor instance. If not provided, the active text editor is used.
169+
* @returns An EditorContext object if successful; otherwise, null.
170+
*/
108171
static getEditorContext(editor?: vscode.TextEditor): EditorContext | null {
109172
try {
110173
if (!editor) {

src/core/__tests__/EditorUtils.test.ts

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
11
import * as vscode from "vscode"
22
import { EditorUtils } from "../EditorUtils"
33

4-
// Mock VSCode API
5-
jest.mock("vscode", () => ({
6-
Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({
7-
start: { line: startLine, character: startChar },
8-
end: { line: endLine, character: endChar },
9-
})),
10-
Position: jest.fn().mockImplementation((line, character) => ({
11-
line,
12-
character,
13-
})),
14-
workspace: {
15-
getWorkspaceFolder: jest.fn(),
16-
},
17-
}))
4+
// Use simple classes to simulate VSCode's Range and Position behavior.
5+
jest.mock("vscode", () => {
6+
class MockPosition {
7+
constructor(
8+
public line: number,
9+
public character: number,
10+
) {}
11+
}
12+
class MockRange {
13+
start: MockPosition
14+
end: MockPosition
15+
constructor(start: MockPosition, end: MockPosition) {
16+
this.start = start
17+
this.end = end
18+
}
19+
}
20+
21+
return {
22+
Range: MockRange,
23+
Position: MockPosition,
24+
workspace: {
25+
getWorkspaceFolder: jest.fn(),
26+
},
27+
window: { activeTextEditor: undefined },
28+
languages: {
29+
getDiagnostics: jest.fn(() => []),
30+
},
31+
}
32+
})
1833

1934
describe("EditorUtils", () => {
2035
let mockDocument: any
@@ -30,7 +45,7 @@ describe("EditorUtils", () => {
3045

3146
describe("getEffectiveRange", () => {
3247
it("should return selected text when available", () => {
33-
const mockRange = new vscode.Range(0, 0, 0, 10)
48+
const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
3449
mockDocument.getText.mockReturnValue("selected text")
3550

3651
const result = EditorUtils.getEffectiveRange(mockDocument, mockRange)
@@ -42,14 +57,66 @@ describe("EditorUtils", () => {
4257
})
4358

4459
it("should return null for empty line", () => {
45-
const mockRange = new vscode.Range(0, 0, 0, 10)
60+
const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
4661
mockDocument.getText.mockReturnValue("")
4762
mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 })
4863

4964
const result = EditorUtils.getEffectiveRange(mockDocument, mockRange)
5065

5166
expect(result).toBeNull()
5267
})
68+
69+
it("should expand empty selection to full lines", () => {
70+
// Simulate a caret (empty selection) on line 2 at character 5.
71+
const initialRange = new vscode.Range(new vscode.Position(2, 5), new vscode.Position(2, 5))
72+
// Return non-empty text for any line with text (lines 1, 2, and 3).
73+
mockDocument.lineAt.mockImplementation((line: number) => {
74+
return { text: `Line ${line} text`, lineNumber: line }
75+
})
76+
mockDocument.getText.mockImplementation((range: any) => {
77+
// If the range is exactly the empty initial selection, return an empty string.
78+
if (
79+
range.start.line === initialRange.start.line &&
80+
range.start.character === initialRange.start.character &&
81+
range.end.line === initialRange.end.line &&
82+
range.end.character === initialRange.end.character
83+
) {
84+
return ""
85+
}
86+
return "expanded text"
87+
})
88+
89+
const result = EditorUtils.getEffectiveRange(mockDocument, initialRange)
90+
91+
expect(result).not.toBeNull()
92+
// Expected effective range: from the beginning of line 1 to the end of line 3.
93+
expect(result?.range.start).toEqual({ line: 1, character: 0 })
94+
expect(result?.range.end).toEqual({ line: 3, character: 11 })
95+
expect(result?.text).toBe("expanded text")
96+
})
97+
})
98+
99+
describe("hasIntersectingRange", () => {
100+
it("should return false for ranges that only touch boundaries", () => {
101+
// Range1: [0, 0) - [0, 10) and Range2: [0, 10) - [0, 20)
102+
const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
103+
const range2 = new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 20))
104+
expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(false)
105+
})
106+
107+
it("should return true for overlapping ranges", () => {
108+
// Range1: [0, 0) - [0, 15) and Range2: [0, 10) - [0, 20)
109+
const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 15))
110+
const range2 = new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 20))
111+
expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(true)
112+
})
113+
114+
it("should return false for non-overlapping ranges", () => {
115+
// Range1: [0, 0) - [0, 10) and Range2: [1, 0) - [1, 5)
116+
const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
117+
const range2 = new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 5))
118+
expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(false)
119+
})
53120
})
54121

55122
describe("getFilePath", () => {

0 commit comments

Comments
 (0)