@herb-tools/language-server 0.8.10 → 0.9.1

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 +0 -233
  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 +152936 -41156
  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 +1299 -333
  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 +27 -6
  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 +39 -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 +1 -35
  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 +4 -8
  60. package/dist/utils.js +10 -15
  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 +1 -305
  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 +35 -9
  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 +54 -2
  81. package/src/service.ts +15 -0
  82. package/src/utils.ts +12 -21
@@ -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
  }
@@ -0,0 +1,287 @@
1
+ import { FoldingRange, FoldingRangeKind } 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 } from "@herb-tools/core"
8
+ import { lspLine } from "./range_utils"
9
+
10
+ import type {
11
+ Node,
12
+ ERBNode,
13
+ ERBContentNode,
14
+ HTMLElementNode,
15
+ HTMLOpenTagNode,
16
+ HTMLAttributeValueNode,
17
+ HTMLCommentNode,
18
+ HTMLConditionalElementNode,
19
+ CDATANode,
20
+ ERBIfNode,
21
+ ERBUnlessNode,
22
+ ERBCaseNode,
23
+ ERBCaseMatchNode,
24
+ ERBBeginNode,
25
+ ERBRescueNode,
26
+ ERBEnsureNode,
27
+ ERBElseNode,
28
+ ERBWhenNode,
29
+ ERBInNode,
30
+ SerializedPosition,
31
+ } from "@herb-tools/core"
32
+
33
+ export class FoldingRangeService {
34
+ private parserService: ParserService
35
+
36
+ constructor(parserService: ParserService) {
37
+ this.parserService = parserService
38
+ }
39
+
40
+ getFoldingRanges(textDocument: TextDocument): FoldingRange[] {
41
+ const parseResult = this.parserService.parseDocument(textDocument)
42
+ const collector = new FoldingRangeCollector()
43
+
44
+ collector.visit(parseResult.document)
45
+
46
+ return collector.ranges
47
+ }
48
+ }
49
+
50
+ export class FoldingRangeCollector extends Visitor {
51
+ public ranges: FoldingRange[] = []
52
+ private processedIfNodes: Set<ERBIfNode> = new Set()
53
+
54
+ visitHTMLElementNode(node: HTMLElementNode): void {
55
+ if (node.body.length > 0 && node.open_tag && node.close_tag) {
56
+ this.addRange(node.open_tag.location.end, node.close_tag.location.start)
57
+ }
58
+
59
+ this.visitChildNodes(node)
60
+ }
61
+
62
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
63
+ if (node.children.length > 0 && node.tag_opening && node.tag_closing) {
64
+ this.addRange(node.tag_opening.location.end, node.tag_closing.location.start)
65
+ }
66
+
67
+ this.visitChildNodes(node)
68
+ }
69
+
70
+ visitHTMLCommentNode(node: HTMLCommentNode): void {
71
+ if (node.comment_start && node.comment_end) {
72
+ this.addRange(node.comment_start.location.end, node.comment_end.location.start, FoldingRangeKind.Comment)
73
+ }
74
+
75
+ this.visitChildNodes(node)
76
+ }
77
+
78
+ visitHTMLAttributeValueNode(node: HTMLAttributeValueNode): void {
79
+ if (node.children.length > 0) {
80
+ const first = node.children[0]
81
+ const last = node.children[node.children.length - 1]
82
+
83
+ this.addRange(first.location.start, last.location.end)
84
+ }
85
+
86
+ this.visitChildNodes(node)
87
+ }
88
+
89
+ visitCDATANode(node: CDATANode): void {
90
+ this.addRange(node.location.start, node.location.end)
91
+ this.visitChildNodes(node)
92
+ }
93
+
94
+ visitHTMLConditionalElementNode(node: HTMLConditionalElementNode): void {
95
+ this.addRange(node.location.start, node.location.end)
96
+ this.visitChildNodes(node)
97
+ }
98
+
99
+ visitERBNode(node: ERBNode): void {
100
+ if (node.tag_closing && 'end_node' in node && node.end_node?.tag_opening) {
101
+ this.addRange(node.tag_closing.location.end, node.end_node.tag_opening.location.start)
102
+ } else {
103
+ this.addRange(node.location.start, node.location.end)
104
+ }
105
+ }
106
+
107
+ visitERBContentNode(node: ERBContentNode): void {
108
+ if (node.tag_opening && node.tag_closing) {
109
+ this.addRange(node.tag_opening.location.end, node.tag_closing.location.start)
110
+ }
111
+
112
+ this.visitChildNodes(node)
113
+ }
114
+
115
+ visitERBIfNode(node: ERBIfNode): void {
116
+ if (this.processedIfNodes.has(node)) {
117
+ this.visitChildNodes(node)
118
+ return
119
+ }
120
+
121
+ this.markIfChainAsProcessed(node)
122
+
123
+ const nextAfterIf = node.subsequent ?? node.end_node
124
+
125
+ if (node.tag_closing && nextAfterIf?.tag_opening) {
126
+ this.addRange(node.tag_closing.location.end, nextAfterIf.tag_opening.location.start)
127
+ }
128
+
129
+ let current: ERBIfNode | ERBElseNode | null = node.subsequent
130
+
131
+ while (current) {
132
+ if (isERBIfNode(current)) {
133
+ const nextAfterElsif = current.subsequent ?? node.end_node
134
+
135
+ if (current.tag_closing && nextAfterElsif?.tag_opening) {
136
+ this.addRange(current.tag_closing.location.end, nextAfterElsif.tag_opening.location.start)
137
+ }
138
+
139
+ current = current.subsequent
140
+ } else {
141
+ break
142
+ }
143
+ }
144
+
145
+ this.visitChildNodes(node)
146
+ }
147
+
148
+ visitERBUnlessNode(node: ERBUnlessNode): void {
149
+ const nextAfterUnless = node.else_clause ?? node.end_node
150
+
151
+ if (node.tag_closing && nextAfterUnless?.tag_opening) {
152
+ this.addRange(node.tag_closing.location.end, nextAfterUnless.tag_opening.location.start)
153
+ }
154
+
155
+ if (node.else_clause) {
156
+ if (node.else_clause.tag_closing && node.end_node?.tag_opening) {
157
+ this.addRange(node.else_clause.tag_closing.location.end, node.end_node.tag_opening.location.start)
158
+ }
159
+ }
160
+
161
+ this.visitChildNodes(node)
162
+ }
163
+
164
+ visitERBCaseNode(node: ERBCaseNode): void {
165
+ this.addCaseFoldingRanges(node)
166
+ this.visitChildNodes(node)
167
+ }
168
+
169
+ visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
170
+ this.addCaseFoldingRanges(node)
171
+ this.visitChildNodes(node)
172
+ }
173
+
174
+ visitERBWhenNode(node: ERBWhenNode): void {
175
+ this.visitChildNodes(node)
176
+ }
177
+
178
+ visitERBInNode(node: ERBInNode): void {
179
+ this.visitChildNodes(node)
180
+ }
181
+
182
+ visitERBBeginNode(node: ERBBeginNode): void {
183
+ const nextAfterBegin = node.rescue_clause ?? node.else_clause ?? node.ensure_clause ?? node.end_node
184
+
185
+ if (node.tag_closing && nextAfterBegin?.tag_opening) {
186
+ this.addRange(node.tag_closing.location.end, nextAfterBegin.tag_opening.location.start)
187
+ }
188
+
189
+ let rescue: ERBRescueNode | null = node.rescue_clause
190
+
191
+ while (rescue) {
192
+ const nextAfterRescue = rescue.subsequent ?? node.else_clause ?? node.ensure_clause ?? node.end_node
193
+
194
+ if (rescue.tag_closing && nextAfterRescue?.tag_opening) {
195
+ this.addRange(rescue.tag_closing.location.end, nextAfterRescue.tag_opening.location.start)
196
+ }
197
+
198
+ rescue = rescue.subsequent
199
+ }
200
+
201
+ if (node.else_clause) {
202
+ const nextAfterElse = node.ensure_clause ?? node.end_node
203
+
204
+ if (node.else_clause.tag_closing && nextAfterElse?.tag_opening) {
205
+ this.addRange(node.else_clause.tag_closing.location.end, nextAfterElse.tag_opening.location.start)
206
+ }
207
+ }
208
+
209
+ if (node.ensure_clause) {
210
+ if (node.ensure_clause.tag_closing && node.end_node?.tag_opening) {
211
+ this.addRange(node.ensure_clause.tag_closing.location.end, node.end_node.tag_opening.location.start)
212
+ }
213
+ }
214
+
215
+ this.visitChildNodes(node)
216
+ }
217
+
218
+ visitERBRescueNode(node: ERBRescueNode): void {
219
+ this.visitChildNodes(node)
220
+ }
221
+
222
+ visitERBElseNode(node: ERBElseNode): void {
223
+ this.addRange(node.location.start, node.location.end)
224
+ this.visitChildNodes(node)
225
+ }
226
+
227
+ visitERBEnsureNode(node: ERBEnsureNode): void {
228
+ this.visitChildNodes(node)
229
+ }
230
+
231
+ private markIfChainAsProcessed(node: ERBIfNode): void {
232
+ this.processedIfNodes.add(node)
233
+
234
+ let current: Node | null = node.subsequent
235
+
236
+ while (current) {
237
+ if (isERBIfNode(current)) {
238
+ this.processedIfNodes.add(current)
239
+ current = current.subsequent
240
+ } else {
241
+ break
242
+ }
243
+ }
244
+ }
245
+
246
+ private addCaseFoldingRanges(node: ERBCaseNode | ERBCaseMatchNode): void {
247
+ type ConditionNode = ERBWhenNode | ERBInNode
248
+ const conditions = node.conditions as ConditionNode[]
249
+
250
+ const firstCondition = conditions[0]
251
+ const nextAfterCase = firstCondition ?? node.else_clause ?? node.end_node
252
+
253
+ if (node.tag_closing && nextAfterCase?.tag_opening) {
254
+ this.addRange(node.tag_closing.location.end, nextAfterCase.tag_opening.location.start)
255
+ }
256
+
257
+ for (let i = 0; i < conditions.length; i++) {
258
+ const condition = conditions[i]
259
+ const nextCondition = conditions[i + 1] ?? node.else_clause ?? node.end_node
260
+
261
+ if (condition.tag_closing && nextCondition?.tag_opening) {
262
+ this.addRange(condition.tag_closing.location.end, nextCondition.tag_opening.location.start)
263
+ }
264
+ }
265
+
266
+ if (node.else_clause) {
267
+ if (node.else_clause.tag_closing && node.end_node?.tag_opening) {
268
+ this.addRange(node.else_clause.tag_closing.location.end, node.end_node.tag_opening.location.start)
269
+ }
270
+ }
271
+ }
272
+
273
+ private addRange(start: SerializedPosition, end: SerializedPosition, kind?: FoldingRangeKind): void {
274
+ const startLine = lspLine(start)
275
+ const endLine = lspLine(end) - 1
276
+
277
+ if (endLine > startLine) {
278
+ this.ranges.push({
279
+ startLine,
280
+ startCharacter: start.column,
281
+ endLine,
282
+ endCharacter: end.column,
283
+ kind,
284
+ })
285
+ }
286
+ }
287
+ }
@@ -186,7 +186,7 @@ export class FormattingService {
186
186
  }
187
187
  }
188
188
 
189
- async formatOnSave(document: TextDocument, reason: TextDocumentSaveReason): Promise<TextEdit[]> {
189
+ async formatOnSave(document: TextDocument, reason: TextDocumentSaveReason, textOverride?: string): Promise<TextEdit[]> {
190
190
  this.connection.console.log(`[Formatting] formatOnSave called for ${document.uri}`)
191
191
 
192
192
  if (reason !== TextDocumentSaveReason.Manual) {
@@ -203,7 +203,7 @@ export class FormattingService {
203
203
  return []
204
204
  }
205
205
 
206
- return this.performFormatting({ textDocument: { uri: document.uri }, options: { tabSize: 2, insertSpaces: true } })
206
+ return this.performFormatting({ textDocument: { uri: document.uri }, options: { tabSize: 2, insertSpaces: true } }, textOverride)
207
207
  }
208
208
 
209
209
  private shouldFormatFile(filePath: string): boolean {
@@ -235,7 +235,7 @@ export class FormattingService {
235
235
  } as Config
236
236
  }
237
237
 
238
- private async performFormatting(params: DocumentFormattingParams): Promise<TextEdit[]> {
238
+ private async performFormatting(params: DocumentFormattingParams, textOverride?: string): Promise<TextEdit[]> {
239
239
  const document = this.documents.get(params.textDocument.uri)
240
240
 
241
241
  if (!document) {
@@ -243,7 +243,7 @@ export class FormattingService {
243
243
  }
244
244
 
245
245
  try {
246
- const text = document.getText()
246
+ const text = textOverride ?? document.getText()
247
247
  const config = await this.getConfigWithSettings(params.textDocument.uri)
248
248
 
249
249
  this.connection.console.log(`[Formatting] Creating formatter with ${this.preRewriters.length} pre-rewriters, ${this.postRewriters.length} post-rewriters`)