2
0

EditorUtils.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import * as vscode from "vscode"
  2. import * as path from "path"
  3. /**
  4. * Represents an effective range in a document along with the corresponding text.
  5. */
  6. export interface EffectiveRange {
  7. /** The range within the document. */
  8. range: vscode.Range
  9. /** The text within the specified range. */
  10. text: string
  11. }
  12. /**
  13. * Represents diagnostic information extracted from a VSCode diagnostic.
  14. */
  15. export interface DiagnosticData {
  16. /** The diagnostic message. */
  17. message: string
  18. /** The severity level of the diagnostic. */
  19. severity: vscode.DiagnosticSeverity
  20. /**
  21. * Optional diagnostic code.
  22. * Can be a string, number, or an object with value and target.
  23. */
  24. code?: string | number | { value: string | number; target: vscode.Uri }
  25. /** Optional source identifier for the diagnostic (e.g., the extension name). */
  26. source?: string
  27. /** The range within the document where the diagnostic applies. */
  28. range: vscode.Range
  29. }
  30. /**
  31. * Contextual information for a VSCode text editor.
  32. */
  33. export interface EditorContext {
  34. /** The file path of the current document. */
  35. filePath: string
  36. /** The effective text selected or derived from the document. */
  37. selectedText: string
  38. /** Optional list of diagnostics associated with the effective range. */
  39. diagnostics?: DiagnosticData[]
  40. }
  41. /**
  42. * Utility class providing helper methods for working with VSCode editors and documents.
  43. */
  44. export class EditorUtils {
  45. /** Cache mapping text documents to their computed file paths. */
  46. private static readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
  47. /**
  48. * Computes the effective range of text from the given document based on the user's selection.
  49. * If the selection is non-empty, returns that directly.
  50. * Otherwise, if the current line is non-empty, expands the range to include the adjacent lines.
  51. *
  52. * @param document - The text document to extract text from.
  53. * @param range - The user selected range or selection.
  54. * @returns An EffectiveRange object containing the effective range and its text, or null if no valid text is found.
  55. */
  56. static getEffectiveRange(
  57. document: vscode.TextDocument,
  58. range: vscode.Range | vscode.Selection,
  59. ): EffectiveRange | null {
  60. try {
  61. const selectedText = document.getText(range)
  62. if (selectedText) {
  63. return { range, text: selectedText }
  64. }
  65. const currentLine = document.lineAt(range.start.line)
  66. if (!currentLine.text.trim()) {
  67. return null
  68. }
  69. const startLineIndex = Math.max(0, currentLine.lineNumber - 1)
  70. const endLineIndex = Math.min(document.lineCount - 1, currentLine.lineNumber + 1)
  71. const effectiveRange = new vscode.Range(
  72. new vscode.Position(startLineIndex, 0),
  73. new vscode.Position(endLineIndex, document.lineAt(endLineIndex).text.length),
  74. )
  75. return {
  76. range: effectiveRange,
  77. text: document.getText(effectiveRange),
  78. }
  79. } catch (error) {
  80. console.error("Error getting effective range:", error)
  81. return null
  82. }
  83. }
  84. /**
  85. * Retrieves the file path of a given text document.
  86. * Utilizes an internal cache to avoid redundant computations.
  87. * If the document belongs to a workspace, attempts to compute a relative path; otherwise, returns the absolute fsPath.
  88. *
  89. * @param document - The text document for which to retrieve the file path.
  90. * @returns The file path as a string.
  91. */
  92. static getFilePath(document: vscode.TextDocument): string {
  93. let filePath = this.filePathCache.get(document)
  94. if (filePath) {
  95. return filePath
  96. }
  97. try {
  98. const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
  99. if (!workspaceFolder) {
  100. filePath = document.uri.fsPath
  101. } else {
  102. const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath)
  103. filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath
  104. }
  105. this.filePathCache.set(document, filePath)
  106. return filePath
  107. } catch (error) {
  108. console.error("Error getting file path:", error)
  109. return document.uri.fsPath
  110. }
  111. }
  112. /**
  113. * Converts a VSCode Diagnostic object to a local DiagnosticData instance.
  114. *
  115. * @param diagnostic - The VSCode diagnostic to convert.
  116. * @returns The corresponding DiagnosticData object.
  117. */
  118. static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData {
  119. return {
  120. message: diagnostic.message,
  121. severity: diagnostic.severity,
  122. code: diagnostic.code,
  123. source: diagnostic.source,
  124. range: diagnostic.range,
  125. }
  126. }
  127. /**
  128. * Determines whether two VSCode ranges intersect.
  129. *
  130. * @param range1 - The first range.
  131. * @param range2 - The second range.
  132. * @returns True if the ranges intersect; otherwise, false.
  133. */
  134. static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean {
  135. if (
  136. range1.end.line < range2.start.line ||
  137. (range1.end.line === range2.start.line && range1.end.character <= range2.start.character)
  138. ) {
  139. return false
  140. }
  141. if (
  142. range2.end.line < range1.start.line ||
  143. (range2.end.line === range1.start.line && range2.end.character <= range1.start.character)
  144. ) {
  145. return false
  146. }
  147. return true
  148. }
  149. /**
  150. * Builds the editor context from the provided text editor or from the active text editor.
  151. * The context includes file path, effective selected text, and any diagnostics that intersect with the effective range.
  152. *
  153. * @param editor - (Optional) A specific text editor instance. If not provided, the active text editor is used.
  154. * @returns An EditorContext object if successful; otherwise, null.
  155. */
  156. static getEditorContext(editor?: vscode.TextEditor): EditorContext | null {
  157. try {
  158. if (!editor) {
  159. editor = vscode.window.activeTextEditor
  160. }
  161. if (!editor) {
  162. return null
  163. }
  164. const document = editor.document
  165. const selection = editor.selection
  166. const effectiveRange = this.getEffectiveRange(document, selection)
  167. if (!effectiveRange) {
  168. return null
  169. }
  170. const filePath = this.getFilePath(document)
  171. const diagnostics = vscode.languages
  172. .getDiagnostics(document.uri)
  173. .filter((d) => this.hasIntersectingRange(effectiveRange.range, d.range))
  174. .map(this.createDiagnosticData)
  175. return {
  176. filePath,
  177. selectedText: effectiveRange.text,
  178. ...(diagnostics.length > 0 ? { diagnostics } : {}),
  179. }
  180. } catch (error) {
  181. console.error("Error getting editor context:", error)
  182. return null
  183. }
  184. }
  185. }