@herb-tools/language-server 0.8.10 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/action_view_helpers.js +19 -0
  2. package/dist/action_view_helpers.js.map +1 -0
  3. package/dist/autofix_service.js +1 -1
  4. package/dist/autofix_service.js.map +1 -1
  5. package/dist/code_action_service.js +3 -6
  6. package/dist/code_action_service.js.map +1 -1
  7. package/dist/comment_ast_utils.js +206 -0
  8. package/dist/comment_ast_utils.js.map +1 -0
  9. package/dist/comment_service.js +175 -0
  10. package/dist/comment_service.js.map +1 -0
  11. package/dist/diagnostics.js +15 -27
  12. package/dist/diagnostics.js.map +1 -1
  13. package/dist/document_highlight_service.js +196 -0
  14. package/dist/document_highlight_service.js.map +1 -0
  15. package/dist/document_save_service.js +16 -6
  16. package/dist/document_save_service.js.map +1 -1
  17. package/dist/folding_range_service.js +209 -0
  18. package/dist/folding_range_service.js.map +1 -0
  19. package/dist/formatting_service.js +4 -4
  20. package/dist/formatting_service.js.map +1 -1
  21. package/dist/herb-language-server.js +150189 -41260
  22. package/dist/herb-language-server.js.map +1 -1
  23. package/dist/hover_service.js +70 -0
  24. package/dist/hover_service.js.map +1 -0
  25. package/dist/index.cjs +1227 -63
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/line_context_collector.js +73 -0
  30. package/dist/line_context_collector.js.map +1 -0
  31. package/dist/linter_service.js +20 -4
  32. package/dist/linter_service.js.map +1 -1
  33. package/dist/parser_service.js +6 -5
  34. package/dist/parser_service.js.map +1 -1
  35. package/dist/range_utils.js +65 -0
  36. package/dist/range_utils.js.map +1 -0
  37. package/dist/rewrite_code_action_service.js +135 -0
  38. package/dist/rewrite_code_action_service.js.map +1 -0
  39. package/dist/server.js +36 -2
  40. package/dist/server.js.map +1 -1
  41. package/dist/service.js +10 -0
  42. package/dist/service.js.map +1 -1
  43. package/dist/types/action_view_helpers.d.ts +5 -0
  44. package/dist/types/comment_ast_utils.d.ts +20 -0
  45. package/dist/types/comment_service.d.ts +14 -0
  46. package/dist/types/diagnostics.d.ts +0 -1
  47. package/dist/types/document_highlight_service.d.ts +28 -0
  48. package/dist/types/document_save_service.d.ts +8 -0
  49. package/dist/types/folding_range_service.d.ts +35 -0
  50. package/dist/types/formatting_service.d.ts +1 -1
  51. package/dist/types/hover_service.d.ts +8 -0
  52. package/dist/types/index.d.ts +4 -0
  53. package/dist/types/line_context_collector.d.ts +19 -0
  54. package/dist/types/linter_service.d.ts +1 -0
  55. package/dist/types/parser_service.d.ts +2 -1
  56. package/dist/types/range_utils.d.ts +16 -0
  57. package/dist/types/rewrite_code_action_service.d.ts +11 -0
  58. package/dist/types/service.d.ts +10 -0
  59. package/dist/types/utils.d.ts +0 -6
  60. package/dist/utils.js +0 -16
  61. package/dist/utils.js.map +1 -1
  62. package/package.json +10 -5
  63. package/src/action_view_helpers.ts +23 -0
  64. package/src/autofix_service.ts +1 -1
  65. package/src/code_action_service.ts +3 -6
  66. package/src/comment_ast_utils.ts +282 -0
  67. package/src/comment_service.ts +228 -0
  68. package/src/diagnostics.ts +24 -38
  69. package/src/document_highlight_service.ts +267 -0
  70. package/src/document_save_service.ts +19 -7
  71. package/src/folding_range_service.ts +287 -0
  72. package/src/formatting_service.ts +4 -4
  73. package/src/hover_service.ts +90 -0
  74. package/src/index.ts +4 -0
  75. package/src/line_context_collector.ts +97 -0
  76. package/src/linter_service.ts +25 -7
  77. package/src/parser_service.ts +9 -10
  78. package/src/range_utils.ts +90 -0
  79. package/src/rewrite_code_action_service.ts +165 -0
  80. package/src/server.ts +51 -2
  81. package/src/service.ts +15 -0
  82. package/src/utils.ts +0 -22
@@ -4,6 +4,7 @@ import { Visitor } from "@herb-tools/core"
4
4
 
5
5
  import type {
6
6
  Node,
7
+ SerializedLocation,
7
8
  ERBCaseNode,
8
9
  ERBCaseMatchNode,
9
10
  ERBIfNode,
@@ -20,12 +21,13 @@ import type {
20
21
  ERBInNode,
21
22
  } from "@herb-tools/core"
22
23
 
23
- import { isHTMLTextNode } from "@herb-tools/core"
24
+ import { isHTMLTextNode, isERBIfNode, isERBElseNode } from "@herb-tools/core"
24
25
 
25
26
  import { ParserService } from "./parser_service"
26
27
  import { LinterService } from "./linter_service"
27
28
  import { DocumentService } from "./document_service"
28
29
  import { ConfigService } from "./config_service"
30
+ import { lspRangeFromLocation } from "./range_utils"
29
31
 
30
32
  export class Diagnostics {
31
33
  private readonly connection: Connection
@@ -253,18 +255,9 @@ export class UnreachableCodeCollector extends Visitor {
253
255
  )
254
256
  }
255
257
 
256
- private addDiagnostic(location: { start: { line: number; column: number }, end: { line: number; column: number } }, message: string): void {
258
+ private addDiagnostic(location: SerializedLocation, message: string): void {
257
259
  const diagnostic: Diagnostic = {
258
- range: {
259
- start: {
260
- line: this.toZeroBased(location.start.line),
261
- character: location.start.column
262
- },
263
- end: {
264
- line: this.toZeroBased(location.end.line),
265
- character: location.end.column
266
- }
267
- },
260
+ range: lspRangeFromLocation(location),
268
261
  message,
269
262
  severity: DiagnosticSeverity.Hint,
270
263
  tags: [DiagnosticTag.Unnecessary],
@@ -298,27 +291,29 @@ export class UnreachableCodeCollector extends Visitor {
298
291
  private markIfChainAsProcessed(node: ERBIfNode): void {
299
292
  this.processedIfNodes.add(node)
300
293
  this.traverseSubsequentNodes(node.subsequent, (current) => {
301
- if (current.type === 'AST_ERB_IF_NODE') {
302
- this.processedIfNodes.add(current as ERBIfNode)
294
+ if (isERBIfNode(current)) {
295
+ this.processedIfNodes.add(current)
303
296
  }
304
297
  })
305
298
  }
306
299
 
307
300
  private markElseNodesInIfChain(node: ERBIfNode): void {
308
301
  this.traverseSubsequentNodes(node.subsequent, (current) => {
309
- if (current.type === 'AST_ERB_ELSE_NODE') {
310
- this.processedElseNodes.add(current as ERBElseNode)
302
+ if (isERBElseNode(current)) {
303
+ this.processedElseNodes.add(current)
311
304
  }
312
305
  })
313
306
  }
314
307
 
315
- private traverseSubsequentNodes(startNode: Node | null, callback: (node: Node) => void): void {
316
- let current = startNode
308
+ private traverseSubsequentNodes(startNode: Node | null, callback: (node: ERBIfNode | ERBElseNode) => void): void {
309
+ let current: Node | null = startNode
317
310
  while (current) {
318
- callback(current)
319
-
320
- if ('subsequent' in current) {
321
- current = (current as any).subsequent
311
+ if (isERBIfNode(current)) {
312
+ callback(current)
313
+ current = current.subsequent
314
+ } else if (isERBElseNode(current)) {
315
+ callback(current)
316
+ break
322
317
  } else {
323
318
  break
324
319
  }
@@ -331,21 +326,17 @@ export class UnreachableCodeCollector extends Visitor {
331
326
  }
332
327
 
333
328
  this.traverseSubsequentNodes(node.subsequent, (current) => {
334
- if (!('statements' in current) || !Array.isArray((current as any).statements)) {
335
- return
336
- }
337
-
338
- if (this.statementsHaveContent((current as any).statements)) {
329
+ if (this.statementsHaveContent(current.statements)) {
339
330
  return
340
331
  }
341
332
 
342
- const blockType = current.type === 'AST_ERB_IF_NODE' ? 'elsif' : 'else'
343
- const nextSubsequent = 'subsequent' in current ? (current as any).subsequent : null
333
+ const blockType = isERBIfNode(current) ? 'elsif' : 'else'
334
+ const nextSubsequent = isERBIfNode(current) ? current.subsequent : null
344
335
 
345
336
  if (nextSubsequent) {
346
- this.checkEmptyStatementsWithEndLocation(current, (current as any).statements, blockType, nextSubsequent)
337
+ this.checkEmptyStatementsWithEndLocation(current, current.statements, blockType, nextSubsequent)
347
338
  } else {
348
- this.checkEmptyStatements(current, (current as any).statements, blockType)
339
+ this.checkEmptyStatements(current, current.statements, blockType)
349
340
  }
350
341
  })
351
342
  }
@@ -357,17 +348,12 @@ export class UnreachableCodeCollector extends Visitor {
357
348
 
358
349
  let hasContent = false
359
350
  this.traverseSubsequentNodes(node.subsequent, (current) => {
360
- if ('statements' in current && Array.isArray((current as any).statements)) {
361
- if (this.statementsHaveContent((current as any).statements)) {
362
- hasContent = true
363
- }
351
+ if (this.statementsHaveContent(current.statements)) {
352
+ hasContent = true
364
353
  }
365
354
  })
366
355
 
367
356
  return !hasContent
368
357
  }
369
358
 
370
- private toZeroBased(line: number): number {
371
- return line - 1
372
- }
373
359
  }
@@ -0,0 +1,267 @@
1
+ import { DocumentHighlight, DocumentHighlightKind, Range, Position } from "vscode-languageserver/node"
2
+ import { TextDocument } from "vscode-languageserver-textdocument"
3
+
4
+ import { Visitor } from "@herb-tools/core"
5
+ import { ParserService } from "./parser_service"
6
+
7
+ import { isERBIfNode, isERBElseNode, isHTMLOpenTagNode } from "@herb-tools/core"
8
+ import { erbTagToRange, tokenToRange, nodeToRange, openTagRanges, isPositionInRange, rangeSize } from "./range_utils"
9
+
10
+ import type {
11
+ Node,
12
+ ERBNode,
13
+ ERBContentNode,
14
+ HTMLElementNode,
15
+ HTMLConditionalElementNode,
16
+ HTMLAttributeNode,
17
+ HTMLCommentNode,
18
+ ERBIfNode,
19
+ ERBUnlessNode,
20
+ ERBCaseNode,
21
+ ERBCaseMatchNode,
22
+ ERBBeginNode,
23
+ ERBRescueNode,
24
+ } from "@herb-tools/core"
25
+
26
+ export class DocumentHighlightCollector extends Visitor {
27
+ public groups: Range[][] = []
28
+ private processedIfNodes: Set<ERBIfNode> = new Set()
29
+
30
+ visitERBNode(node: ERBNode): void {
31
+ if ('end_node' in node && node.end_node) {
32
+ this.addGroup([erbTagToRange(node), erbTagToRange(node.end_node)])
33
+ }
34
+ }
35
+
36
+ visitERBContentNode(node: ERBContentNode): void {
37
+ this.addGroup([tokenToRange(node.tag_opening), tokenToRange(node.tag_closing)])
38
+ this.visitChildNodes(node)
39
+ }
40
+
41
+ visitHTMLCommentNode(node: HTMLCommentNode): void {
42
+ this.addGroup([tokenToRange(node.comment_start), tokenToRange(node.comment_end)])
43
+ this.visitChildNodes(node)
44
+ }
45
+
46
+ visitHTMLElementNode(node: HTMLElementNode): void {
47
+ const ranges: (Range | null)[] = []
48
+
49
+ if (node.open_tag && isHTMLOpenTagNode(node.open_tag)) {
50
+ ranges.push(...openTagRanges(node.open_tag))
51
+ } else if (node.open_tag) {
52
+ ranges.push(nodeToRange(node.open_tag))
53
+ }
54
+
55
+ if (node.close_tag) {
56
+ ranges.push(nodeToRange(node.close_tag))
57
+ }
58
+
59
+ this.addGroup(ranges)
60
+ this.visitChildNodes(node)
61
+ }
62
+
63
+ visitHTMLAttributeNode(node: HTMLAttributeNode): void {
64
+ const ranges: (Range | null)[] = []
65
+
66
+ if (node.name) {
67
+ ranges.push(nodeToRange(node.name))
68
+ }
69
+
70
+ if (node.equals) {
71
+ ranges.push(tokenToRange(node.equals))
72
+ }
73
+
74
+ if (node.value) {
75
+ ranges.push(nodeToRange(node.value))
76
+ }
77
+
78
+ this.addGroup(ranges)
79
+ this.visitChildNodes(node)
80
+ }
81
+
82
+ visitHTMLConditionalElementNode(node: HTMLConditionalElementNode): void {
83
+ const ranges: (Range | null)[] = []
84
+
85
+ if (node.open_conditional) {
86
+ ranges.push(erbTagToRange(node.open_conditional))
87
+ }
88
+
89
+ if (node.open_tag) {
90
+ ranges.push(...openTagRanges(node.open_tag))
91
+ }
92
+
93
+ if (node.close_tag) {
94
+ ranges.push(nodeToRange(node.close_tag))
95
+ }
96
+
97
+ if (node.close_conditional) {
98
+ ranges.push(erbTagToRange(node.close_conditional))
99
+ }
100
+
101
+ this.addGroup(ranges)
102
+ this.visitChildNodes(node)
103
+ }
104
+
105
+ visitERBIfNode(node: ERBIfNode): void {
106
+ if (this.processedIfNodes.has(node)) {
107
+ this.visitChildNodes(node)
108
+ return
109
+ }
110
+
111
+ this.markIfChainAsProcessed(node)
112
+
113
+ const ranges: (Range | null)[] = []
114
+ ranges.push(erbTagToRange(node))
115
+
116
+ let current: Node | null = node.subsequent
117
+
118
+ while (current) {
119
+ if (isERBIfNode(current)) {
120
+ ranges.push(erbTagToRange(current))
121
+ current = current.subsequent
122
+ } else if (isERBElseNode(current)) {
123
+ ranges.push(erbTagToRange(current))
124
+ break
125
+ } else {
126
+ break
127
+ }
128
+ }
129
+
130
+ if (node.end_node) {
131
+ ranges.push(erbTagToRange(node.end_node))
132
+ }
133
+
134
+ this.addGroup(ranges)
135
+ this.visitChildNodes(node)
136
+ }
137
+
138
+ visitERBUnlessNode(node: ERBUnlessNode): void {
139
+ const ranges: (Range | null)[] = []
140
+ ranges.push(erbTagToRange(node))
141
+
142
+ if (node.else_clause) {
143
+ ranges.push(erbTagToRange(node.else_clause))
144
+ }
145
+
146
+ if (node.end_node) {
147
+ ranges.push(erbTagToRange(node.end_node))
148
+ }
149
+
150
+ this.addGroup(ranges)
151
+ this.visitChildNodes(node)
152
+ }
153
+
154
+ visitERBCaseNode(node: ERBCaseNode): void {
155
+ this.visitERBAnyCaseNode(node)
156
+ }
157
+
158
+ visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
159
+ this.visitERBAnyCaseNode(node)
160
+ }
161
+
162
+ visitERBAnyCaseNode(node: ERBCaseNode | ERBCaseMatchNode): void {
163
+ const ranges: (Range | null)[] = []
164
+ ranges.push(erbTagToRange(node))
165
+
166
+ for (const condition of node.conditions) {
167
+ ranges.push(erbTagToRange(condition as ERBNode))
168
+ }
169
+
170
+ if (node.else_clause) {
171
+ ranges.push(erbTagToRange(node.else_clause))
172
+ }
173
+
174
+ if (node.end_node) {
175
+ ranges.push(erbTagToRange(node.end_node))
176
+ }
177
+
178
+ this.addGroup(ranges)
179
+ this.visitChildNodes(node)
180
+ }
181
+
182
+ visitERBBeginNode(node: ERBBeginNode): void {
183
+ const ranges: (Range | null)[] = []
184
+ ranges.push(erbTagToRange(node))
185
+
186
+ let rescue: ERBRescueNode | null = node.rescue_clause
187
+
188
+ while (rescue) {
189
+ ranges.push(erbTagToRange(rescue))
190
+ rescue = rescue.subsequent
191
+ }
192
+
193
+ if (node.else_clause) {
194
+ ranges.push(erbTagToRange(node.else_clause))
195
+ }
196
+
197
+ if (node.ensure_clause) {
198
+ ranges.push(erbTagToRange(node.ensure_clause))
199
+ }
200
+
201
+ if (node.end_node) {
202
+ ranges.push(erbTagToRange(node.end_node))
203
+ }
204
+
205
+ this.addGroup(ranges)
206
+ this.visitChildNodes(node)
207
+ }
208
+
209
+ private markIfChainAsProcessed(node: ERBIfNode): void {
210
+ this.processedIfNodes.add(node)
211
+
212
+ let current: Node | null = node.subsequent
213
+
214
+ while (current) {
215
+ if (isERBIfNode(current)) {
216
+ this.processedIfNodes.add(current)
217
+ current = current.subsequent
218
+ } else {
219
+ break
220
+ }
221
+ }
222
+ }
223
+
224
+ private addGroup(ranges: (Range | null)[]): void {
225
+ const filtered = ranges.filter((r): r is Range => r !== null)
226
+ if (filtered.length >= 2) {
227
+ this.groups.push(filtered)
228
+ }
229
+ }
230
+ }
231
+
232
+ export class DocumentHighlightService {
233
+ private parserService: ParserService
234
+
235
+ constructor(parserService: ParserService) {
236
+ this.parserService = parserService
237
+ }
238
+
239
+ getDocumentHighlights(textDocument: TextDocument, position: Position): DocumentHighlight[] {
240
+ const parseResult = this.parserService.parseDocument(textDocument)
241
+ const collector = new DocumentHighlightCollector()
242
+ collector.visit(parseResult.document)
243
+
244
+ let bestGroup: Range[] | null = null
245
+ let bestSize = Infinity
246
+
247
+ for (const group of collector.groups) {
248
+ const matchingRange = group.find(range => isPositionInRange(position, range))
249
+
250
+ if (matchingRange) {
251
+ const size = rangeSize(matchingRange)
252
+
253
+ if (size < bestSize) {
254
+ bestSize = size
255
+ bestGroup = group
256
+ }
257
+ }
258
+ }
259
+
260
+ if (bestGroup) {
261
+ return bestGroup.map(range => DocumentHighlight.create(range, DocumentHighlightKind.Text))
262
+ }
263
+
264
+ return []
265
+ }
266
+
267
+ }
@@ -11,6 +11,15 @@ export class DocumentSaveService {
11
11
  private autofixService: AutofixService
12
12
  private formattingService: FormattingService
13
13
 
14
+ /**
15
+ * Tracks documents that were recently autofixed via applyFixesAndFormatting
16
+ * (triggered by onDocumentFormatting). When editor.formatOnSave is enabled,
17
+ * onDocumentFormatting fires BEFORE willSaveWaitUntil. If applyFixesAndFormatting
18
+ * already applied autofix, applyFixes must skip to avoid conflicting edits
19
+ * (since this.documents hasn't been updated between the two events).
20
+ */
21
+ private recentlyAutofixedViaFormatting = new Set<string>()
22
+
14
23
  constructor(connection: Connection, settings: Settings, autofixService: AutofixService, formattingService: FormattingService) {
15
24
  this.connection = connection
16
25
  this.settings = settings
@@ -30,6 +39,11 @@ export class DocumentSaveService {
30
39
 
31
40
  if (!fixOnSave) return []
32
41
 
42
+ if (this.recentlyAutofixedViaFormatting.delete(document.uri)) {
43
+ this.connection.console.log(`[DocumentSave] applyFixes skipping: already autofixed via formatting`)
44
+ return []
45
+ }
46
+
33
47
  return this.autofixService.autofix(document)
34
48
  }
35
49
 
@@ -48,6 +62,10 @@ export class DocumentSaveService {
48
62
 
49
63
  if (fixOnSave) {
50
64
  autofixEdits = await this.autofixService.autofix(document)
65
+
66
+ if (autofixEdits.length > 0) {
67
+ this.recentlyAutofixedViaFormatting.add(document.uri)
68
+ }
51
69
  }
52
70
 
53
71
  if (!formatterEnabled) return autofixEdits
@@ -56,12 +74,6 @@ export class DocumentSaveService {
56
74
  return this.formattingService.formatOnSave(document, reason)
57
75
  }
58
76
 
59
- const autofixedDocument: TextDocument = {
60
- ...document,
61
- uri: document.uri,
62
- getText: () => autofixEdits[0].newText,
63
- }
64
-
65
- return this.formattingService.formatOnSave(autofixedDocument, reason)
77
+ return this.formattingService.formatOnSave(document, reason, autofixEdits[0].newText)
66
78
  }
67
79
  }