@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
package/dist/index.cjs CHANGED
@@ -4,13 +4,15 @@ var node = require('vscode-languageserver/node');
4
4
  var config = require('@herb-tools/config');
5
5
  var formatter = require('@herb-tools/formatter');
6
6
  var vscodeLanguageserverTextdocument = require('vscode-languageserver-textdocument');
7
- var core = require('@herb-tools/core');
8
7
  var nodeWasm = require('@herb-tools/node-wasm');
9
8
  var linter = require('@herb-tools/linter');
10
9
  var loader = require('@herb-tools/linter/loader');
11
10
  var loader$1 = require('@herb-tools/rewriter/loader');
11
+ var core = require('@herb-tools/core');
12
+ var printer = require('@herb-tools/printer');
13
+ var rewriter = require('@herb-tools/rewriter');
12
14
 
13
- var version = "0.8.10";
15
+ var version = "0.9.1";
14
16
 
15
17
  class Settings {
16
18
  constructor(params, connection) {
@@ -154,21 +156,14 @@ class Diagnostics {
154
156
  else {
155
157
  const parseResult = this.parserService.parseDocument(textDocument);
156
158
  const lintResult = await this.linterService.lintDocument(textDocument);
157
- const unreachableCodeDiagnostics = this.getUnreachableCodeDiagnostics(parseResult.document);
158
159
  allDiagnostics = [
159
160
  ...parseResult.diagnostics,
160
161
  ...lintResult.diagnostics,
161
- ...unreachableCodeDiagnostics,
162
162
  ];
163
163
  }
164
164
  this.diagnostics.set(textDocument, allDiagnostics);
165
165
  this.sendDiagnosticsFor(textDocument);
166
166
  }
167
- getUnreachableCodeDiagnostics(document) {
168
- const collector = new UnreachableCodeCollector();
169
- collector.visit(document);
170
- return collector.diagnostics;
171
- }
172
167
  async refreshDocument(document) {
173
168
  await this.validate(document);
174
169
  }
@@ -185,228 +180,69 @@ class Diagnostics {
185
180
  this.diagnostics.delete(textDocument);
186
181
  }
187
182
  }
188
- class UnreachableCodeCollector extends core.Visitor {
189
- constructor() {
190
- super(...arguments);
191
- this.diagnostics = [];
192
- this.processedIfNodes = new Set();
193
- this.processedElseNodes = new Set();
194
- }
195
- visitERBCaseNode(node) {
196
- this.checkUnreachableChildren(node.children);
197
- this.checkAndMarkElseClause(node.else_clause);
198
- this.visitChildNodes(node);
199
- }
200
- visitERBCaseMatchNode(node) {
201
- this.checkUnreachableChildren(node.children);
202
- this.checkAndMarkElseClause(node.else_clause);
203
- this.visitChildNodes(node);
204
- }
205
- visitERBIfNode(node) {
206
- if (this.processedIfNodes.has(node)) {
207
- return;
208
- }
209
- this.markIfChainAsProcessed(node);
210
- this.markElseNodesInIfChain(node);
211
- const entireChainEmpty = this.isEntireIfChainEmpty(node);
212
- if (entireChainEmpty) {
213
- this.checkEmptyStatements(node, node.statements, "if");
214
- }
215
- else {
216
- this.checkIfChainParts(node);
217
- }
218
- this.visitChildNodes(node);
219
- }
220
- visitERBElseNode(node) {
221
- if (this.processedElseNodes.has(node)) {
222
- this.visitChildNodes(node);
223
- return;
224
- }
225
- this.checkEmptyStatements(node, node.statements, "else");
226
- this.visitChildNodes(node);
227
- }
228
- visitERBUnlessNode(node) {
229
- const unlessHasContent = this.statementsHaveContent(node.statements);
230
- const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
231
- if (node.else_clause) {
232
- this.processedElseNodes.add(node.else_clause);
233
- }
234
- const entireBlockEmpty = !unlessHasContent && !elseHasContent;
235
- if (entireBlockEmpty) {
236
- this.checkEmptyStatements(node, node.statements, "unless");
237
- }
238
- else {
239
- if (!unlessHasContent) {
240
- this.checkEmptyStatementsWithEndLocation(node, node.statements, "unless", node.else_clause);
241
- }
242
- if (node.else_clause && !elseHasContent) {
243
- this.checkEmptyStatements(node.else_clause, node.else_clause.statements, "else");
244
- }
245
- }
246
- this.visitChildNodes(node);
247
- }
248
- visitERBForNode(node) {
249
- this.checkEmptyStatements(node, node.statements, "for");
250
- this.visitChildNodes(node);
251
- }
252
- visitERBWhileNode(node) {
253
- this.checkEmptyStatements(node, node.statements, "while");
254
- this.visitChildNodes(node);
255
- }
256
- visitERBUntilNode(node) {
257
- this.checkEmptyStatements(node, node.statements, "until");
258
- this.visitChildNodes(node);
259
- }
260
- visitERBWhenNode(node) {
261
- if (!node.then_keyword) {
262
- this.checkEmptyStatements(node, node.statements, "when");
263
- }
264
- this.visitChildNodes(node);
265
- }
266
- visitERBBeginNode(node) {
267
- this.checkEmptyStatements(node, node.statements, "begin");
268
- this.visitChildNodes(node);
269
- }
270
- visitERBRescueNode(node) {
271
- this.checkEmptyStatements(node, node.statements, "rescue");
272
- this.visitChildNodes(node);
273
- }
274
- visitERBEnsureNode(node) {
275
- this.checkEmptyStatements(node, node.statements, "ensure");
276
- this.visitChildNodes(node);
277
- }
278
- visitERBBlockNode(node) {
279
- this.checkEmptyStatements(node, node.body, "block");
280
- this.visitChildNodes(node);
281
- }
282
- visitERBInNode(node) {
283
- if (!node.then_keyword) {
284
- this.checkEmptyStatements(node, node.statements, "in");
285
- }
286
- this.visitChildNodes(node);
287
- }
288
- checkUnreachableChildren(children) {
289
- for (const child of children) {
290
- if (core.isHTMLTextNode(child) && child.content.trim() === "") {
291
- continue;
292
- }
293
- this.addDiagnostic(child.location, "Unreachable code: content between case and when/in is never executed");
294
- }
295
- }
296
- checkEmptyStatements(node, statements, blockType) {
297
- this.checkEmptyStatementsWithEndLocation(node, statements, blockType, null);
298
- }
299
- checkEmptyStatementsWithEndLocation(node, statements, blockType, subsequentNode) {
300
- if (this.statementsHaveContent(statements)) {
301
- return;
302
- }
303
- const startLocation = node.location.start;
304
- const endLocation = subsequentNode
305
- ? subsequentNode.location.start
306
- : node.location.end;
307
- this.addDiagnostic({ start: startLocation, end: endLocation }, `Empty ${blockType} block: this control flow statement has no content`);
308
- }
309
- addDiagnostic(location, message) {
310
- const diagnostic = {
311
- range: {
312
- start: {
313
- line: this.toZeroBased(location.start.line),
314
- character: location.start.column
315
- },
316
- end: {
317
- line: this.toZeroBased(location.end.line),
318
- character: location.end.column
319
- }
320
- },
321
- message,
322
- severity: node.DiagnosticSeverity.Hint,
323
- tags: [node.DiagnosticTag.Unnecessary],
324
- source: "Herb Language Server"
325
- };
326
- this.diagnostics.push(diagnostic);
327
- }
328
- statementsHaveContent(statements) {
329
- return statements.some(statement => {
330
- if (core.isHTMLTextNode(statement)) {
331
- return statement.content.trim() !== "";
332
- }
333
- return true;
334
- });
335
- }
336
- checkAndMarkElseClause(elseClause) {
337
- if (!elseClause) {
338
- return;
339
- }
340
- this.processedElseNodes.add(elseClause);
341
- if (!this.statementsHaveContent(elseClause.statements)) {
342
- this.checkEmptyStatements(elseClause, elseClause.statements, "else");
343
- }
344
- }
345
- markIfChainAsProcessed(node) {
346
- this.processedIfNodes.add(node);
347
- this.traverseSubsequentNodes(node.subsequent, (current) => {
348
- if (current.type === 'AST_ERB_IF_NODE') {
349
- this.processedIfNodes.add(current);
350
- }
351
- });
352
- }
353
- markElseNodesInIfChain(node) {
354
- this.traverseSubsequentNodes(node.subsequent, (current) => {
355
- if (current.type === 'AST_ERB_ELSE_NODE') {
356
- this.processedElseNodes.add(current);
357
- }
358
- });
183
+
184
+ function lspPosition(herbPosition) {
185
+ return node.Position.create(herbPosition.line - 1, herbPosition.column);
186
+ }
187
+ function lspLine(herbPosition) {
188
+ return herbPosition.line - 1;
189
+ }
190
+ function lspRangeFromLocation(herbLocation) {
191
+ return node.Range.create(lspPosition(herbLocation.start), lspPosition(herbLocation.end));
192
+ }
193
+ function erbTagToRange(node$1) {
194
+ if (!node$1.tag_opening || !node$1.tag_closing)
195
+ return null;
196
+ return node.Range.create(lspPosition(node$1.tag_opening.location.start), lspPosition(node$1.tag_closing.location.end));
197
+ }
198
+ function tokenToRange(token) {
199
+ if (!token)
200
+ return null;
201
+ return lspRangeFromLocation(token.location);
202
+ }
203
+ function nodeToRange(node) {
204
+ return lspRangeFromLocation(node.location);
205
+ }
206
+ function openTagRanges(tag) {
207
+ const ranges = [];
208
+ if (tag.tag_opening && tag.tag_name) {
209
+ ranges.push(node.Range.create(lspPosition(tag.tag_opening.location.start), lspPosition(tag.tag_name.location.end)));
359
210
  }
360
- traverseSubsequentNodes(startNode, callback) {
361
- let current = startNode;
362
- while (current) {
363
- callback(current);
364
- if ('subsequent' in current) {
365
- current = current.subsequent;
366
- }
367
- else {
368
- break;
369
- }
370
- }
211
+ ranges.push(tokenToRange(tag.tag_closing));
212
+ return ranges;
213
+ }
214
+ function isPositionInRange(position, range) {
215
+ if (position.line < range.start.line || position.line > range.end.line) {
216
+ return false;
371
217
  }
372
- checkIfChainParts(node) {
373
- if (!this.statementsHaveContent(node.statements)) {
374
- this.checkEmptyStatementsWithEndLocation(node, node.statements, "if", node.subsequent);
375
- }
376
- this.traverseSubsequentNodes(node.subsequent, (current) => {
377
- if (!('statements' in current) || !Array.isArray(current.statements)) {
378
- return;
379
- }
380
- if (this.statementsHaveContent(current.statements)) {
381
- return;
382
- }
383
- const blockType = current.type === 'AST_ERB_IF_NODE' ? 'elsif' : 'else';
384
- const nextSubsequent = 'subsequent' in current ? current.subsequent : null;
385
- if (nextSubsequent) {
386
- this.checkEmptyStatementsWithEndLocation(current, current.statements, blockType, nextSubsequent);
387
- }
388
- else {
389
- this.checkEmptyStatements(current, current.statements, blockType);
390
- }
391
- });
218
+ if (position.line === range.start.line && position.character < range.start.character) {
219
+ return false;
392
220
  }
393
- isEntireIfChainEmpty(node) {
394
- if (this.statementsHaveContent(node.statements)) {
395
- return false;
396
- }
397
- let hasContent = false;
398
- this.traverseSubsequentNodes(node.subsequent, (current) => {
399
- if ('statements' in current && Array.isArray(current.statements)) {
400
- if (this.statementsHaveContent(current.statements)) {
401
- hasContent = true;
402
- }
403
- }
404
- });
405
- return !hasContent;
221
+ if (position.line === range.end.line && position.character > range.end.character) {
222
+ return false;
406
223
  }
407
- toZeroBased(line) {
408
- return line - 1;
224
+ return true;
225
+ }
226
+ function rangeSize(range) {
227
+ if (range.start.line === range.end.line) {
228
+ return range.end.character - range.start.character;
409
229
  }
230
+ return (range.end.line - range.start.line) * 10000 + range.end.character;
231
+ }
232
+ /**
233
+ * Returns a Range that spans the entire document
234
+ */
235
+ function getFullDocumentRange(document) {
236
+ const lastLine = document.lineCount - 1;
237
+ const lastLineText = document.getText({
238
+ start: node.Position.create(lastLine, 0),
239
+ end: node.Position.create(lastLine + 1, 0)
240
+ });
241
+ const lastLineLength = lastLineText.length;
242
+ return {
243
+ start: node.Position.create(0, 0),
244
+ end: node.Position.create(lastLine, lastLineLength)
245
+ };
410
246
  }
411
247
 
412
248
  class ErrorVisitor extends nodeWasm.Visitor {
@@ -423,7 +259,7 @@ class ErrorVisitor extends nodeWasm.Visitor {
423
259
  const diagnostic = {
424
260
  source: this.source,
425
261
  severity: node.DiagnosticSeverity.Error,
426
- range: this.rangeFromHerbError(error),
262
+ range: lspRangeFromLocation(error.location),
427
263
  message: error.message,
428
264
  code: error.type,
429
265
  data: {
@@ -433,9 +269,6 @@ class ErrorVisitor extends nodeWasm.Visitor {
433
269
  };
434
270
  this.diagnostics.push(diagnostic);
435
271
  }
436
- rangeFromHerbError(error) {
437
- return node.Range.create(node.Position.create(error.location.start.line - 1, error.location.start.column), node.Position.create(error.location.end.line - 1, error.location.end.column));
438
- }
439
272
  }
440
273
  class ParserService {
441
274
  parseDocument(textDocument) {
@@ -448,6 +281,9 @@ class ParserService {
448
281
  diagnostics: errorVisitor.diagnostics
449
282
  };
450
283
  }
284
+ parseContent(content, options) {
285
+ return nodeWasm.Herb.parse(content, options);
286
+ }
451
287
  }
452
288
 
453
289
  function camelize(value) {
@@ -467,20 +303,16 @@ function lintToDignosticSeverity(severity) {
467
303
  case "hint": return node.DiagnosticSeverity.Hint;
468
304
  }
469
305
  }
470
- /**
471
- * Returns a Range that spans the entire document
472
- */
473
- function getFullDocumentRange(document) {
474
- const lastLine = document.lineCount - 1;
475
- const lastLineText = document.getText({
476
- start: node.Position.create(lastLine, 0),
477
- end: node.Position.create(lastLine + 1, 0)
306
+ function lintToDignosticTags(tags) {
307
+ if (!tags)
308
+ return [];
309
+ return tags.flatMap(tag => {
310
+ switch (tag) {
311
+ case "unnecessary": return [node.DiagnosticTag.Unnecessary];
312
+ case "deprecated": return [node.DiagnosticTag.Deprecated];
313
+ default: return [];
314
+ }
478
315
  });
479
- const lastLineLength = lastLineText.length;
480
- return {
481
- start: node.Position.create(0, 0),
482
- end: node.Position.create(lastLine, lastLineLength)
483
- };
484
316
  }
485
317
 
486
318
  const OPEN_CONFIG_ACTION$1 = 'Open .herb.yml';
@@ -563,8 +395,24 @@ class LinterService {
563
395
  this.connection.window.showWarningMessage(message);
564
396
  }
565
397
  }
398
+ shouldLintFile(uri) {
399
+ const filePath = uri.replace(/^file:\/\//, '');
400
+ if (filePath.endsWith('.herb.yml'))
401
+ return false;
402
+ const config$1 = this.settings.projectConfig;
403
+ if (!config$1)
404
+ return true;
405
+ const hasConfigFile = config.Config.exists(config$1.projectPath);
406
+ if (!hasConfigFile)
407
+ return true;
408
+ const relativePath = filePath.replace(this.project.projectPath + '/', '');
409
+ return config$1.isLinterEnabledForPath(relativePath);
410
+ }
566
411
  async lintDocument(textDocument) {
567
412
  var _a, _b, _c, _d;
413
+ if (!this.shouldLintFile(textDocument.uri)) {
414
+ return { diagnostics: [] };
415
+ }
568
416
  const settings = await this.settings.getDocumentSettings(textDocument.uri);
569
417
  const linterEnabled = (_b = (_a = settings === null || settings === void 0 ? void 0 : settings.linter) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
570
418
  if (!linterEnabled) {
@@ -590,14 +438,14 @@ class LinterService {
590
438
  const content = textDocument.getText();
591
439
  const lintResult = this.linter.lint(content, { fileName: textDocument.uri });
592
440
  const diagnostics = lintResult.offenses.map(offense => {
593
- const range = node.Range.create(node.Position.create(offense.location.start.line - 1, offense.location.start.column), node.Position.create(offense.location.end.line - 1, offense.location.end.column));
441
+ const range = lspRangeFromLocation(offense.location);
594
442
  const customRulePath = this.customRulePaths.get(offense.rule);
595
443
  const codeDescription = {
596
444
  href: customRulePath
597
445
  ? `file://${customRulePath}`
598
- : `https://herb-tools.dev/linter/rules/${offense.rule}`
446
+ : linter.ruleDocumentationUrl(offense.rule)
599
447
  };
600
- return {
448
+ const diagnostic = {
601
449
  source: this.source,
602
450
  severity: lintToDignosticSeverity(offense.severity),
603
451
  range,
@@ -606,6 +454,11 @@ class LinterService {
606
454
  data: { rule: offense.rule },
607
455
  codeDescription
608
456
  };
457
+ const tags = lintToDignosticTags(offense.tags);
458
+ if (tags.length > 0) {
459
+ diagnostic.tags = tags;
460
+ }
461
+ return diagnostic;
609
462
  });
610
463
  return { diagnostics };
611
464
  }
@@ -770,7 +623,7 @@ class FormattingService {
770
623
  this.postRewriters = [];
771
624
  }
772
625
  }
773
- async formatOnSave(document, reason) {
626
+ async formatOnSave(document, reason, textOverride) {
774
627
  this.connection.console.log(`[Formatting] formatOnSave called for ${document.uri}`);
775
628
  if (reason !== node.TextDocumentSaveReason.Manual) {
776
629
  this.connection.console.log(`[Formatting] Skipping: reason=${reason} (not manual)`);
@@ -781,7 +634,7 @@ class FormattingService {
781
634
  this.connection.console.log(`[Formatting] Skipping: file not in formatter config`);
782
635
  return [];
783
636
  }
784
- return this.performFormatting({ textDocument: { uri: document.uri }, options: { tabSize: 2, insertSpaces: true } });
637
+ return this.performFormatting({ textDocument: { uri: document.uri }, options: { tabSize: 2, insertSpaces: true } }, textOverride);
785
638
  }
786
639
  shouldFormatFile(filePath) {
787
640
  if (filePath.endsWith('.herb.yml'))
@@ -809,13 +662,13 @@ class FormattingService {
809
662
  }
810
663
  };
811
664
  }
812
- async performFormatting(params) {
665
+ async performFormatting(params, textOverride) {
813
666
  const document = this.documents.get(params.textDocument.uri);
814
667
  if (!document) {
815
668
  return [];
816
669
  }
817
670
  try {
818
- const text = document.getText();
671
+ const text = textOverride !== null && textOverride !== void 0 ? textOverride : document.getText();
819
672
  const config = await this.getConfigWithSettings(params.textDocument.uri);
820
673
  this.connection.console.log(`[Formatting] Creating formatter with ${this.preRewriters.length} pre-rewriters, ${this.postRewriters.length} post-rewriters`);
821
674
  if (this.failedRewriters.size > 0) {
@@ -1287,10 +1140,7 @@ class CodeActionService {
1287
1140
  };
1288
1141
  }
1289
1142
  offenseToRange(offense) {
1290
- return {
1291
- start: node.Position.create(offense.location.start.line - 1, offense.location.start.column),
1292
- end: node.Position.create(offense.location.end.line - 1, offense.location.end.column)
1293
- };
1143
+ return lspRangeFromLocation(offense.location);
1294
1144
  }
1295
1145
  rangesEqual(r1, r2) {
1296
1146
  return (r1.start.line === r2.start.line &&
@@ -1309,6 +1159,14 @@ class CodeActionService {
1309
1159
 
1310
1160
  class DocumentSaveService {
1311
1161
  constructor(connection, settings, autofixService, formattingService) {
1162
+ /**
1163
+ * Tracks documents that were recently autofixed via applyFixesAndFormatting
1164
+ * (triggered by onDocumentFormatting). When editor.formatOnSave is enabled,
1165
+ * onDocumentFormatting fires BEFORE willSaveWaitUntil. If applyFixesAndFormatting
1166
+ * already applied autofix, applyFixes must skip to avoid conflicting edits
1167
+ * (since this.documents hasn't been updated between the two events).
1168
+ */
1169
+ this.recentlyAutofixedViaFormatting = new Set();
1312
1170
  this.connection = connection;
1313
1171
  this.settings = settings;
1314
1172
  this.autofixService = autofixService;
@@ -1325,6 +1183,10 @@ class DocumentSaveService {
1325
1183
  this.connection.console.log(`[DocumentSave] applyFixes fixOnSave=${fixOnSave}`);
1326
1184
  if (!fixOnSave)
1327
1185
  return [];
1186
+ if (this.recentlyAutofixedViaFormatting.delete(document.uri)) {
1187
+ this.connection.console.log(`[DocumentSave] applyFixes skipping: already autofixed via formatting`);
1188
+ return [];
1189
+ }
1328
1190
  return this.autofixService.autofix(document);
1329
1191
  }
1330
1192
  /**
@@ -1340,97 +1202,1150 @@ class DocumentSaveService {
1340
1202
  let autofixEdits = [];
1341
1203
  if (fixOnSave) {
1342
1204
  autofixEdits = await this.autofixService.autofix(document);
1205
+ if (autofixEdits.length > 0) {
1206
+ this.recentlyAutofixedViaFormatting.add(document.uri);
1207
+ }
1343
1208
  }
1344
1209
  if (!formatterEnabled)
1345
1210
  return autofixEdits;
1346
1211
  if (autofixEdits.length === 0) {
1347
1212
  return this.formattingService.formatOnSave(document, reason);
1348
1213
  }
1349
- const autofixedDocument = {
1350
- ...document,
1351
- uri: document.uri,
1352
- getText: () => autofixEdits[0].newText,
1353
- };
1354
- return this.formattingService.formatOnSave(autofixedDocument, reason);
1214
+ return this.formattingService.formatOnSave(document, reason, autofixEdits[0].newText);
1355
1215
  }
1356
1216
  }
1357
1217
 
1358
- class Service {
1359
- constructor(connection, params) {
1360
- this.connection = connection;
1361
- this.settings = new Settings(params, this.connection);
1362
- this.documentService = new DocumentService(this.connection);
1363
- this.project = new Project(connection, this.settings.projectPath.replace("file://", ""));
1364
- this.parserService = new ParserService();
1365
- this.linterService = new LinterService(this.connection, this.settings, this.project);
1366
- this.formattingService = new FormattingService(this.connection, this.documentService.documents, this.project, this.settings);
1367
- this.autofixService = new AutofixService(this.connection, this.config);
1368
- this.configService = new ConfigService(this.project.projectPath);
1369
- this.codeActionService = new CodeActionService(this.project, this.config);
1370
- this.diagnostics = new Diagnostics(this.connection, this.documentService, this.parserService, this.linterService, this.configService);
1371
- this.documentSaveService = new DocumentSaveService(this.connection, this.settings, this.autofixService, this.formattingService);
1372
- if (params.initializationOptions) {
1373
- this.settings.globalSettings = params.initializationOptions;
1374
- }
1218
+ class FoldingRangeService {
1219
+ constructor(parserService) {
1220
+ this.parserService = parserService;
1375
1221
  }
1376
- async init() {
1377
- await this.project.initialize();
1378
- await this.formattingService.initialize();
1379
- try {
1380
- this.config = await config.Config.loadForEditor(this.project.projectPath, version);
1381
- this.codeActionService.setConfig(this.config);
1382
- this.autofixService.setConfig(this.config);
1383
- if (this.config.version && this.config.version !== version) {
1384
- this.connection.console.warn(`Config file version (${this.config.version}) does not match current version (${version}). ` +
1385
- `Consider updating your .herb.yml file.`);
1386
- }
1387
- }
1388
- catch (error) {
1389
- this.connection.console.warn(`Failed to load config: ${error instanceof Error ? error.message : String(error)}. Using personal settings with defaults.`);
1390
- this.config = config.Config.fromObject({
1391
- linter: this.settings.globalSettings.linter,
1392
- formatter: this.settings.globalSettings.formatter
1393
- }, { projectPath: this.project.projectPath, version });
1394
- this.codeActionService.setConfig(this.config);
1395
- this.autofixService.setConfig(this.config);
1222
+ getFoldingRanges(textDocument) {
1223
+ const parseResult = this.parserService.parseDocument(textDocument);
1224
+ const collector = new FoldingRangeCollector();
1225
+ collector.visit(parseResult.document);
1226
+ return collector.ranges;
1227
+ }
1228
+ }
1229
+ class FoldingRangeCollector extends core.Visitor {
1230
+ constructor() {
1231
+ super(...arguments);
1232
+ this.ranges = [];
1233
+ this.processedIfNodes = new Set();
1234
+ }
1235
+ visitHTMLElementNode(node) {
1236
+ if (node.body.length > 0 && node.open_tag && node.close_tag) {
1237
+ this.addRange(node.open_tag.location.end, node.close_tag.location.start);
1396
1238
  }
1397
- await this.settings.initializeProjectConfig(this.config);
1398
- await this.formattingService.refreshConfig(this.config);
1399
- this.linterService.rebuildLinter();
1400
- this.documentService.onDidClose((change) => {
1401
- this.settings.documentSettings.delete(change.document.uri);
1402
- });
1403
- this.documentService.onDidChangeContent(async (change) => {
1404
- await this.diagnostics.refreshDocument(change.document);
1405
- });
1239
+ this.visitChildNodes(node);
1406
1240
  }
1407
- async refresh() {
1408
- await this.project.refresh();
1409
- await this.formattingService.refreshConfig(this.config);
1410
- await this.diagnostics.refreshAllDocuments();
1241
+ visitHTMLOpenTagNode(node) {
1242
+ if (node.children.length > 0 && node.tag_opening && node.tag_closing) {
1243
+ this.addRange(node.tag_opening.location.end, node.tag_closing.location.start);
1244
+ }
1245
+ this.visitChildNodes(node);
1411
1246
  }
1412
- async refreshConfig() {
1413
- try {
1414
- this.config = await config.Config.loadForEditor(this.project.projectPath, version);
1415
- this.codeActionService.setConfig(this.config);
1416
- this.autofixService.setConfig(this.config);
1417
- if (this.config.version && this.config.version !== version) {
1418
- this.connection.console.warn(`Config file version (${this.config.version}) does not match current version (${version}). ` +
1419
- `Consider updating your .herb.yml file.`);
1420
- }
1247
+ visitHTMLCommentNode(node$1) {
1248
+ if (node$1.comment_start && node$1.comment_end) {
1249
+ this.addRange(node$1.comment_start.location.end, node$1.comment_end.location.start, node.FoldingRangeKind.Comment);
1421
1250
  }
1422
- catch (error) {
1423
- this.connection.console.warn(`Failed to load config: ${error instanceof Error ? error.message : String(error)}. Using personal settings with defaults.`);
1424
- this.config = config.Config.fromObject({
1425
- linter: this.settings.globalSettings.linter,
1426
- formatter: this.settings.globalSettings.formatter
1427
- }, { projectPath: this.project.projectPath, version });
1428
- this.codeActionService.setConfig(this.config);
1429
- this.autofixService.setConfig(this.config);
1251
+ this.visitChildNodes(node$1);
1252
+ }
1253
+ visitHTMLAttributeValueNode(node) {
1254
+ if (node.children.length > 0) {
1255
+ const first = node.children[0];
1256
+ const last = node.children[node.children.length - 1];
1257
+ this.addRange(first.location.start, last.location.end);
1258
+ }
1259
+ this.visitChildNodes(node);
1260
+ }
1261
+ visitCDATANode(node) {
1262
+ this.addRange(node.location.start, node.location.end);
1263
+ this.visitChildNodes(node);
1264
+ }
1265
+ visitHTMLConditionalElementNode(node) {
1266
+ this.addRange(node.location.start, node.location.end);
1267
+ this.visitChildNodes(node);
1268
+ }
1269
+ visitERBNode(node) {
1270
+ var _a;
1271
+ if (node.tag_closing && 'end_node' in node && ((_a = node.end_node) === null || _a === void 0 ? void 0 : _a.tag_opening)) {
1272
+ this.addRange(node.tag_closing.location.end, node.end_node.tag_opening.location.start);
1273
+ }
1274
+ else {
1275
+ this.addRange(node.location.start, node.location.end);
1276
+ }
1277
+ }
1278
+ visitERBContentNode(node) {
1279
+ if (node.tag_opening && node.tag_closing) {
1280
+ this.addRange(node.tag_opening.location.end, node.tag_closing.location.start);
1281
+ }
1282
+ this.visitChildNodes(node);
1283
+ }
1284
+ visitERBIfNode(node) {
1285
+ var _a, _b;
1286
+ if (this.processedIfNodes.has(node)) {
1287
+ this.visitChildNodes(node);
1288
+ return;
1289
+ }
1290
+ this.markIfChainAsProcessed(node);
1291
+ const nextAfterIf = (_a = node.subsequent) !== null && _a !== void 0 ? _a : node.end_node;
1292
+ if (node.tag_closing && (nextAfterIf === null || nextAfterIf === void 0 ? void 0 : nextAfterIf.tag_opening)) {
1293
+ this.addRange(node.tag_closing.location.end, nextAfterIf.tag_opening.location.start);
1294
+ }
1295
+ let current = node.subsequent;
1296
+ while (current) {
1297
+ if (core.isERBIfNode(current)) {
1298
+ const nextAfterElsif = (_b = current.subsequent) !== null && _b !== void 0 ? _b : node.end_node;
1299
+ if (current.tag_closing && (nextAfterElsif === null || nextAfterElsif === void 0 ? void 0 : nextAfterElsif.tag_opening)) {
1300
+ this.addRange(current.tag_closing.location.end, nextAfterElsif.tag_opening.location.start);
1301
+ }
1302
+ current = current.subsequent;
1303
+ }
1304
+ else {
1305
+ break;
1306
+ }
1307
+ }
1308
+ this.visitChildNodes(node);
1309
+ }
1310
+ visitERBUnlessNode(node) {
1311
+ var _a, _b;
1312
+ const nextAfterUnless = (_a = node.else_clause) !== null && _a !== void 0 ? _a : node.end_node;
1313
+ if (node.tag_closing && (nextAfterUnless === null || nextAfterUnless === void 0 ? void 0 : nextAfterUnless.tag_opening)) {
1314
+ this.addRange(node.tag_closing.location.end, nextAfterUnless.tag_opening.location.start);
1315
+ }
1316
+ if (node.else_clause) {
1317
+ if (node.else_clause.tag_closing && ((_b = node.end_node) === null || _b === void 0 ? void 0 : _b.tag_opening)) {
1318
+ this.addRange(node.else_clause.tag_closing.location.end, node.end_node.tag_opening.location.start);
1319
+ }
1320
+ }
1321
+ this.visitChildNodes(node);
1322
+ }
1323
+ visitERBCaseNode(node) {
1324
+ this.addCaseFoldingRanges(node);
1325
+ this.visitChildNodes(node);
1326
+ }
1327
+ visitERBCaseMatchNode(node) {
1328
+ this.addCaseFoldingRanges(node);
1329
+ this.visitChildNodes(node);
1330
+ }
1331
+ visitERBWhenNode(node) {
1332
+ this.visitChildNodes(node);
1333
+ }
1334
+ visitERBInNode(node) {
1335
+ this.visitChildNodes(node);
1336
+ }
1337
+ visitERBBeginNode(node) {
1338
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1339
+ const nextAfterBegin = (_c = (_b = (_a = node.rescue_clause) !== null && _a !== void 0 ? _a : node.else_clause) !== null && _b !== void 0 ? _b : node.ensure_clause) !== null && _c !== void 0 ? _c : node.end_node;
1340
+ if (node.tag_closing && (nextAfterBegin === null || nextAfterBegin === void 0 ? void 0 : nextAfterBegin.tag_opening)) {
1341
+ this.addRange(node.tag_closing.location.end, nextAfterBegin.tag_opening.location.start);
1342
+ }
1343
+ let rescue = node.rescue_clause;
1344
+ while (rescue) {
1345
+ const nextAfterRescue = (_f = (_e = (_d = rescue.subsequent) !== null && _d !== void 0 ? _d : node.else_clause) !== null && _e !== void 0 ? _e : node.ensure_clause) !== null && _f !== void 0 ? _f : node.end_node;
1346
+ if (rescue.tag_closing && (nextAfterRescue === null || nextAfterRescue === void 0 ? void 0 : nextAfterRescue.tag_opening)) {
1347
+ this.addRange(rescue.tag_closing.location.end, nextAfterRescue.tag_opening.location.start);
1348
+ }
1349
+ rescue = rescue.subsequent;
1350
+ }
1351
+ if (node.else_clause) {
1352
+ const nextAfterElse = (_g = node.ensure_clause) !== null && _g !== void 0 ? _g : node.end_node;
1353
+ if (node.else_clause.tag_closing && (nextAfterElse === null || nextAfterElse === void 0 ? void 0 : nextAfterElse.tag_opening)) {
1354
+ this.addRange(node.else_clause.tag_closing.location.end, nextAfterElse.tag_opening.location.start);
1355
+ }
1356
+ }
1357
+ if (node.ensure_clause) {
1358
+ if (node.ensure_clause.tag_closing && ((_h = node.end_node) === null || _h === void 0 ? void 0 : _h.tag_opening)) {
1359
+ this.addRange(node.ensure_clause.tag_closing.location.end, node.end_node.tag_opening.location.start);
1360
+ }
1361
+ }
1362
+ this.visitChildNodes(node);
1363
+ }
1364
+ visitERBRescueNode(node) {
1365
+ this.visitChildNodes(node);
1366
+ }
1367
+ visitERBElseNode(node) {
1368
+ this.addRange(node.location.start, node.location.end);
1369
+ this.visitChildNodes(node);
1370
+ }
1371
+ visitERBEnsureNode(node) {
1372
+ this.visitChildNodes(node);
1373
+ }
1374
+ markIfChainAsProcessed(node) {
1375
+ this.processedIfNodes.add(node);
1376
+ let current = node.subsequent;
1377
+ while (current) {
1378
+ if (core.isERBIfNode(current)) {
1379
+ this.processedIfNodes.add(current);
1380
+ current = current.subsequent;
1381
+ }
1382
+ else {
1383
+ break;
1384
+ }
1385
+ }
1386
+ }
1387
+ addCaseFoldingRanges(node) {
1388
+ var _a, _b, _c, _d;
1389
+ const conditions = node.conditions;
1390
+ const firstCondition = conditions[0];
1391
+ const nextAfterCase = (_a = firstCondition !== null && firstCondition !== void 0 ? firstCondition : node.else_clause) !== null && _a !== void 0 ? _a : node.end_node;
1392
+ if (node.tag_closing && (nextAfterCase === null || nextAfterCase === void 0 ? void 0 : nextAfterCase.tag_opening)) {
1393
+ this.addRange(node.tag_closing.location.end, nextAfterCase.tag_opening.location.start);
1394
+ }
1395
+ for (let i = 0; i < conditions.length; i++) {
1396
+ const condition = conditions[i];
1397
+ const nextCondition = (_c = (_b = conditions[i + 1]) !== null && _b !== void 0 ? _b : node.else_clause) !== null && _c !== void 0 ? _c : node.end_node;
1398
+ if (condition.tag_closing && (nextCondition === null || nextCondition === void 0 ? void 0 : nextCondition.tag_opening)) {
1399
+ this.addRange(condition.tag_closing.location.end, nextCondition.tag_opening.location.start);
1400
+ }
1401
+ }
1402
+ if (node.else_clause) {
1403
+ if (node.else_clause.tag_closing && ((_d = node.end_node) === null || _d === void 0 ? void 0 : _d.tag_opening)) {
1404
+ this.addRange(node.else_clause.tag_closing.location.end, node.end_node.tag_opening.location.start);
1405
+ }
1406
+ }
1407
+ }
1408
+ addRange(start, end, kind) {
1409
+ const startLine = lspLine(start);
1410
+ const endLine = lspLine(end) - 1;
1411
+ if (endLine > startLine) {
1412
+ this.ranges.push({
1413
+ startLine,
1414
+ startCharacter: start.column,
1415
+ endLine,
1416
+ endCharacter: end.column,
1417
+ kind,
1418
+ });
1430
1419
  }
1431
- await this.settings.refreshProjectConfig(this.config);
1432
- await this.formattingService.refreshConfig(this.config);
1433
- this.linterService.rebuildLinter();
1420
+ }
1421
+ }
1422
+
1423
+ class DocumentHighlightCollector extends core.Visitor {
1424
+ constructor() {
1425
+ super(...arguments);
1426
+ this.groups = [];
1427
+ this.processedIfNodes = new Set();
1428
+ }
1429
+ visitERBNode(node) {
1430
+ if ('end_node' in node && node.end_node) {
1431
+ this.addGroup([erbTagToRange(node), erbTagToRange(node.end_node)]);
1432
+ }
1433
+ }
1434
+ visitERBContentNode(node) {
1435
+ this.addGroup([tokenToRange(node.tag_opening), tokenToRange(node.tag_closing)]);
1436
+ this.visitChildNodes(node);
1437
+ }
1438
+ visitHTMLCommentNode(node) {
1439
+ this.addGroup([tokenToRange(node.comment_start), tokenToRange(node.comment_end)]);
1440
+ this.visitChildNodes(node);
1441
+ }
1442
+ visitHTMLElementNode(node) {
1443
+ const ranges = [];
1444
+ if (node.open_tag && core.isHTMLOpenTagNode(node.open_tag)) {
1445
+ ranges.push(...openTagRanges(node.open_tag));
1446
+ }
1447
+ else if (node.open_tag) {
1448
+ ranges.push(nodeToRange(node.open_tag));
1449
+ }
1450
+ if (node.close_tag) {
1451
+ ranges.push(nodeToRange(node.close_tag));
1452
+ }
1453
+ this.addGroup(ranges);
1454
+ this.visitChildNodes(node);
1455
+ }
1456
+ visitHTMLAttributeNode(node) {
1457
+ const ranges = [];
1458
+ if (node.name) {
1459
+ ranges.push(nodeToRange(node.name));
1460
+ }
1461
+ if (node.equals) {
1462
+ ranges.push(tokenToRange(node.equals));
1463
+ }
1464
+ if (node.value) {
1465
+ ranges.push(nodeToRange(node.value));
1466
+ }
1467
+ this.addGroup(ranges);
1468
+ this.visitChildNodes(node);
1469
+ }
1470
+ visitHTMLConditionalElementNode(node) {
1471
+ const ranges = [];
1472
+ if (node.open_conditional) {
1473
+ ranges.push(erbTagToRange(node.open_conditional));
1474
+ }
1475
+ if (node.open_tag) {
1476
+ ranges.push(...openTagRanges(node.open_tag));
1477
+ }
1478
+ if (node.close_tag) {
1479
+ ranges.push(nodeToRange(node.close_tag));
1480
+ }
1481
+ if (node.close_conditional) {
1482
+ ranges.push(erbTagToRange(node.close_conditional));
1483
+ }
1484
+ this.addGroup(ranges);
1485
+ this.visitChildNodes(node);
1486
+ }
1487
+ visitERBIfNode(node) {
1488
+ if (this.processedIfNodes.has(node)) {
1489
+ this.visitChildNodes(node);
1490
+ return;
1491
+ }
1492
+ this.markIfChainAsProcessed(node);
1493
+ const ranges = [];
1494
+ ranges.push(erbTagToRange(node));
1495
+ let current = node.subsequent;
1496
+ while (current) {
1497
+ if (core.isERBIfNode(current)) {
1498
+ ranges.push(erbTagToRange(current));
1499
+ current = current.subsequent;
1500
+ }
1501
+ else if (core.isERBElseNode(current)) {
1502
+ ranges.push(erbTagToRange(current));
1503
+ break;
1504
+ }
1505
+ else {
1506
+ break;
1507
+ }
1508
+ }
1509
+ if (node.end_node) {
1510
+ ranges.push(erbTagToRange(node.end_node));
1511
+ }
1512
+ this.addGroup(ranges);
1513
+ this.visitChildNodes(node);
1514
+ }
1515
+ visitERBUnlessNode(node) {
1516
+ const ranges = [];
1517
+ ranges.push(erbTagToRange(node));
1518
+ if (node.else_clause) {
1519
+ ranges.push(erbTagToRange(node.else_clause));
1520
+ }
1521
+ if (node.end_node) {
1522
+ ranges.push(erbTagToRange(node.end_node));
1523
+ }
1524
+ this.addGroup(ranges);
1525
+ this.visitChildNodes(node);
1526
+ }
1527
+ visitERBCaseNode(node) {
1528
+ this.visitERBAnyCaseNode(node);
1529
+ }
1530
+ visitERBCaseMatchNode(node) {
1531
+ this.visitERBAnyCaseNode(node);
1532
+ }
1533
+ visitERBAnyCaseNode(node) {
1534
+ const ranges = [];
1535
+ ranges.push(erbTagToRange(node));
1536
+ for (const condition of node.conditions) {
1537
+ ranges.push(erbTagToRange(condition));
1538
+ }
1539
+ if (node.else_clause) {
1540
+ ranges.push(erbTagToRange(node.else_clause));
1541
+ }
1542
+ if (node.end_node) {
1543
+ ranges.push(erbTagToRange(node.end_node));
1544
+ }
1545
+ this.addGroup(ranges);
1546
+ this.visitChildNodes(node);
1547
+ }
1548
+ visitERBBeginNode(node) {
1549
+ const ranges = [];
1550
+ ranges.push(erbTagToRange(node));
1551
+ let rescue = node.rescue_clause;
1552
+ while (rescue) {
1553
+ ranges.push(erbTagToRange(rescue));
1554
+ rescue = rescue.subsequent;
1555
+ }
1556
+ if (node.else_clause) {
1557
+ ranges.push(erbTagToRange(node.else_clause));
1558
+ }
1559
+ if (node.ensure_clause) {
1560
+ ranges.push(erbTagToRange(node.ensure_clause));
1561
+ }
1562
+ if (node.end_node) {
1563
+ ranges.push(erbTagToRange(node.end_node));
1564
+ }
1565
+ this.addGroup(ranges);
1566
+ this.visitChildNodes(node);
1567
+ }
1568
+ markIfChainAsProcessed(node) {
1569
+ this.processedIfNodes.add(node);
1570
+ let current = node.subsequent;
1571
+ while (current) {
1572
+ if (core.isERBIfNode(current)) {
1573
+ this.processedIfNodes.add(current);
1574
+ current = current.subsequent;
1575
+ }
1576
+ else {
1577
+ break;
1578
+ }
1579
+ }
1580
+ }
1581
+ addGroup(ranges) {
1582
+ const filtered = ranges.filter((r) => r !== null);
1583
+ if (filtered.length >= 2) {
1584
+ this.groups.push(filtered);
1585
+ }
1586
+ }
1587
+ }
1588
+ class DocumentHighlightService {
1589
+ constructor(parserService) {
1590
+ this.parserService = parserService;
1591
+ }
1592
+ getDocumentHighlights(textDocument, position) {
1593
+ const parseResult = this.parserService.parseDocument(textDocument);
1594
+ const collector = new DocumentHighlightCollector();
1595
+ collector.visit(parseResult.document);
1596
+ let bestGroup = null;
1597
+ let bestSize = Infinity;
1598
+ for (const group of collector.groups) {
1599
+ const matchingRange = group.find(range => isPositionInRange(position, range));
1600
+ if (matchingRange) {
1601
+ const size = rangeSize(matchingRange);
1602
+ if (size < bestSize) {
1603
+ bestSize = size;
1604
+ bestGroup = group;
1605
+ }
1606
+ }
1607
+ }
1608
+ if (bestGroup) {
1609
+ return bestGroup.map(range => node.DocumentHighlight.create(range, node.DocumentHighlightKind.Text));
1610
+ }
1611
+ return [];
1612
+ }
1613
+ }
1614
+
1615
+ const ACTION_VIEW_HELPERS = {
1616
+ "ActionView::Helpers::TagHelper#tag": {
1617
+ signature: "tag.<tag name>(optional content, options)",
1618
+ documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag",
1619
+ },
1620
+ "ActionView::Helpers::TagHelper#content_tag": {
1621
+ signature: "content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)",
1622
+ documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag",
1623
+ },
1624
+ "ActionView::Helpers::UrlHelper#link_to": {
1625
+ signature: "link_to(name = nil, options = nil, html_options = nil, &block)",
1626
+ documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to",
1627
+ },
1628
+ "Turbo::FramesHelper#turbo_frame_tag": {
1629
+ signature: "turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)",
1630
+ documentationURL: "https://www.rubydoc.info/github/hotwired/turbo-rails/Turbo/FramesHelper:turbo_frame_tag",
1631
+ },
1632
+ };
1633
+
1634
+ class ActionViewElementCollector extends nodeWasm.Visitor {
1635
+ constructor() {
1636
+ super(...arguments);
1637
+ this.elements = [];
1638
+ }
1639
+ visitHTMLElementNode(node) {
1640
+ if (node.element_source && node.element_source !== "HTML" && core.isERBOpenTagNode(node.open_tag)) {
1641
+ this.elements.push({
1642
+ node,
1643
+ range: nodeToRange(node),
1644
+ });
1645
+ }
1646
+ this.visitChildNodes(node);
1647
+ }
1648
+ }
1649
+ class HoverService {
1650
+ constructor(parserService) {
1651
+ this.parserService = parserService;
1652
+ }
1653
+ getHover(textDocument, position) {
1654
+ const parseResult = this.parserService.parseContent(textDocument.getText(), {
1655
+ action_view_helpers: true,
1656
+ track_whitespace: true,
1657
+ });
1658
+ const collector = new ActionViewElementCollector();
1659
+ collector.visit(parseResult.value);
1660
+ let bestElement = null;
1661
+ let bestSize = Infinity;
1662
+ for (const element of collector.elements) {
1663
+ if (isPositionInRange(position, element.range)) {
1664
+ const size = rangeSize(element.range);
1665
+ if (size < bestSize) {
1666
+ bestSize = size;
1667
+ bestElement = element;
1668
+ }
1669
+ }
1670
+ }
1671
+ if (!bestElement) {
1672
+ return null;
1673
+ }
1674
+ const elementSource = bestElement.node.element_source;
1675
+ const rewriter$1 = new rewriter.ActionViewTagHelperToHTMLRewriter();
1676
+ const rewrittenNode = rewriter$1.rewrite(bestElement.node, { baseDir: process.cwd() });
1677
+ const htmlOutput = printer.IdentityPrinter.print(rewrittenNode);
1678
+ const helper = ACTION_VIEW_HELPERS[elementSource];
1679
+ const parts = [];
1680
+ if (helper) {
1681
+ parts.push(`\`\`\`ruby\n${helper.signature}\n\`\`\``);
1682
+ }
1683
+ parts.push(`**HTML equivalent**\n\`\`\`erb\n${htmlOutput.trim()}\n\`\`\``);
1684
+ if (helper) {
1685
+ parts.push(`[${elementSource}](${helper.documentationURL})`);
1686
+ }
1687
+ return {
1688
+ contents: {
1689
+ kind: node.MarkupKind.Markdown,
1690
+ value: parts.join("\n\n"),
1691
+ },
1692
+ range: bestElement.range,
1693
+ };
1694
+ }
1695
+ }
1696
+
1697
+ class ElementCollector extends nodeWasm.Visitor {
1698
+ constructor() {
1699
+ super(...arguments);
1700
+ this.actionViewElements = [];
1701
+ this.htmlElements = [];
1702
+ }
1703
+ visitHTMLElementNode(node) {
1704
+ if (node.element_source && node.element_source !== "HTML" && core.isERBOpenTagNode(node.open_tag)) {
1705
+ this.actionViewElements.push({
1706
+ node,
1707
+ range: nodeToRange(node),
1708
+ });
1709
+ }
1710
+ else if (core.isHTMLOpenTagNode(node.open_tag) && node.open_tag.tag_name) {
1711
+ this.htmlElements.push({
1712
+ node,
1713
+ range: nodeToRange(node),
1714
+ });
1715
+ }
1716
+ this.visitChildNodes(node);
1717
+ }
1718
+ }
1719
+ class RewriteCodeActionService {
1720
+ constructor(parserService) {
1721
+ this.parserService = parserService;
1722
+ }
1723
+ getCodeActions(document, requestedRange) {
1724
+ const parseResult = this.parserService.parseContent(document.getText(), {
1725
+ action_view_helpers: true,
1726
+ track_whitespace: true,
1727
+ });
1728
+ const collector = new ElementCollector();
1729
+ collector.visit(parseResult.value);
1730
+ const actions = [];
1731
+ for (const element of collector.actionViewElements) {
1732
+ if (!this.rangesOverlap(element.range, requestedRange))
1733
+ continue;
1734
+ const action = this.createActionViewToHTMLAction(document, element);
1735
+ if (action) {
1736
+ actions.push(action);
1737
+ }
1738
+ }
1739
+ for (const element of collector.htmlElements) {
1740
+ if (!this.rangesOverlap(element.range, requestedRange))
1741
+ continue;
1742
+ const action = this.createHTMLToActionViewAction(document, element);
1743
+ if (action) {
1744
+ actions.push(action);
1745
+ }
1746
+ }
1747
+ return actions;
1748
+ }
1749
+ createActionViewToHTMLAction(document, element) {
1750
+ var _a;
1751
+ const originalText = document.getText(element.range);
1752
+ const parseResult = this.parserService.parseContent(originalText, {
1753
+ action_view_helpers: true,
1754
+ track_whitespace: true,
1755
+ });
1756
+ if (parseResult.failed)
1757
+ return null;
1758
+ const rewriter$1 = new rewriter.ActionViewTagHelperToHTMLRewriter();
1759
+ rewriter$1.rewrite(parseResult.value, { baseDir: process.cwd() });
1760
+ const rewrittenText = printer.IdentityPrinter.print(parseResult.value);
1761
+ if (rewrittenText === originalText)
1762
+ return null;
1763
+ const edit = {
1764
+ changes: {
1765
+ [document.uri]: [node.TextEdit.replace(element.range, rewrittenText)]
1766
+ }
1767
+ };
1768
+ const tagName = (_a = element.node.tag_name) === null || _a === void 0 ? void 0 : _a.value;
1769
+ const title = tagName
1770
+ ? `Herb: Convert to \`<${tagName}>\``
1771
+ : "Herb: Convert to HTML";
1772
+ return {
1773
+ title,
1774
+ kind: node.CodeActionKind.RefactorRewrite,
1775
+ edit,
1776
+ };
1777
+ }
1778
+ createHTMLToActionViewAction(document, element) {
1779
+ var _a;
1780
+ const originalText = document.getText(element.range);
1781
+ const parseResult = this.parserService.parseContent(originalText, {
1782
+ track_whitespace: true,
1783
+ });
1784
+ if (parseResult.failed)
1785
+ return null;
1786
+ const rewriter$1 = new rewriter.HTMLToActionViewTagHelperRewriter();
1787
+ rewriter$1.rewrite(parseResult.value, { baseDir: process.cwd() });
1788
+ const rewrittenText = printer.IdentityPrinter.print(parseResult.value);
1789
+ if (rewrittenText === originalText)
1790
+ return null;
1791
+ const edit = {
1792
+ changes: {
1793
+ [document.uri]: [node.TextEdit.replace(element.range, rewrittenText)]
1794
+ }
1795
+ };
1796
+ const tagName = (_a = element.node.tag_name) === null || _a === void 0 ? void 0 : _a.value;
1797
+ const isAnchor = tagName === "a";
1798
+ const isTurboFrame = tagName === "turbo-frame";
1799
+ const methodName = tagName === null || tagName === void 0 ? void 0 : tagName.replace(/-/g, "_");
1800
+ const title = isAnchor
1801
+ ? "Herb: Convert to `link_to`"
1802
+ : isTurboFrame
1803
+ ? "Herb: Convert to `turbo_frame_tag`"
1804
+ : methodName
1805
+ ? `Herb: Convert to \`tag.${methodName}\``
1806
+ : "Herb: Convert to tag helper";
1807
+ return {
1808
+ title,
1809
+ kind: node.CodeActionKind.RefactorRewrite,
1810
+ edit,
1811
+ };
1812
+ }
1813
+ rangesOverlap(r1, r2) {
1814
+ if (r1.end.line < r2.start.line)
1815
+ return false;
1816
+ if (r1.start.line > r2.end.line)
1817
+ return false;
1818
+ if (r1.end.line === r2.start.line && r1.end.character < r2.start.character)
1819
+ return false;
1820
+ if (r1.start.line === r2.end.line && r1.start.character > r2.end.character)
1821
+ return false;
1822
+ return true;
1823
+ }
1824
+ }
1825
+
1826
+ class LineContextCollector extends core.Visitor {
1827
+ constructor() {
1828
+ super(...arguments);
1829
+ this.lineMap = new Map();
1830
+ this.erbNodesPerLine = new Map();
1831
+ this.htmlCommentNodesPerLine = new Map();
1832
+ }
1833
+ visitERBNode(node) {
1834
+ if (!node.tag_opening || !node.tag_closing)
1835
+ return;
1836
+ const startLine = lspLine(node.tag_opening.location.start);
1837
+ const nodes = this.erbNodesPerLine.get(startLine) || [];
1838
+ nodes.push(node);
1839
+ this.erbNodesPerLine.set(startLine, nodes);
1840
+ if (core.isERBCommentNode(node)) {
1841
+ this.setLine(startLine, "erb-comment", node);
1842
+ }
1843
+ else {
1844
+ this.setLine(startLine, "erb-tag", node);
1845
+ }
1846
+ }
1847
+ visitERBContentNode(node) {
1848
+ this.visitERBNode(node);
1849
+ this.visitChildNodes(node);
1850
+ }
1851
+ visitHTMLCommentNode(node) {
1852
+ const startLine = lspLine(node.location.start);
1853
+ const endLine = lspLine(node.location.end);
1854
+ for (let line = startLine; line <= endLine; line++) {
1855
+ this.htmlCommentNodesPerLine.set(line, node);
1856
+ this.setLine(line, "html-comment", node);
1857
+ }
1858
+ this.visitChildNodes(node);
1859
+ }
1860
+ visitHTMLElementNode(node) {
1861
+ const startLine = lspLine(node.location.start);
1862
+ const endLine = lspLine(node.location.end);
1863
+ for (let line = startLine; line <= endLine; line++) {
1864
+ if (!this.lineMap.has(line)) {
1865
+ this.setLine(line, "html-content", node);
1866
+ }
1867
+ }
1868
+ this.visitChildNodes(node);
1869
+ }
1870
+ visitHTMLTextNode(node) {
1871
+ const startLine = lspLine(node.location.start);
1872
+ const endLine = lspLine(node.location.end);
1873
+ for (let line = startLine; line <= endLine; line++) {
1874
+ if (!this.lineMap.has(line)) {
1875
+ this.setLine(line, "html-content", node);
1876
+ }
1877
+ }
1878
+ this.visitChildNodes(node);
1879
+ }
1880
+ setLine(line, context, node) {
1881
+ const existing = this.lineMap.get(line);
1882
+ if (existing) {
1883
+ if (existing.context === "erb-comment" || existing.context === "erb-tag")
1884
+ return;
1885
+ if (context === "erb-comment" || context === "erb-tag") {
1886
+ this.lineMap.set(line, { line, context, node });
1887
+ return;
1888
+ }
1889
+ if (existing.context === "html-comment")
1890
+ return;
1891
+ }
1892
+ this.lineMap.set(line, { line, context, node });
1893
+ }
1894
+ }
1895
+
1896
+ function commentERBNode(node) {
1897
+ const mutable = rewriter.asMutable(node);
1898
+ if (mutable.tag_opening) {
1899
+ const currentValue = mutable.tag_opening.value;
1900
+ mutable.tag_opening = core.createSyntheticToken(currentValue.substring(0, 2) + "#" + currentValue.substring(2), mutable.tag_opening.type);
1901
+ }
1902
+ }
1903
+ function uncommentERBNode(node) {
1904
+ var _a;
1905
+ const mutable = rewriter.asMutable(node);
1906
+ if (mutable.tag_opening && mutable.tag_opening.value === "<%#") {
1907
+ const contentValue = ((_a = mutable.content) === null || _a === void 0 ? void 0 : _a.value) || "";
1908
+ if (contentValue.startsWith(" graphql ") ||
1909
+ contentValue.startsWith(" %= ") ||
1910
+ contentValue.startsWith(" == ") ||
1911
+ contentValue.startsWith(" % ") ||
1912
+ contentValue.startsWith(" = ") ||
1913
+ contentValue.startsWith(" - ")) {
1914
+ mutable.tag_opening = core.createSyntheticToken("<%", mutable.tag_opening.type);
1915
+ mutable.content = core.createSyntheticToken(contentValue.substring(1), mutable.content.type);
1916
+ }
1917
+ else {
1918
+ mutable.tag_opening = core.createSyntheticToken("<%", mutable.tag_opening.type);
1919
+ }
1920
+ }
1921
+ }
1922
+ function determineStrategy(erbNodes, lineText) {
1923
+ if (erbNodes.length === 0) {
1924
+ return "html-only";
1925
+ }
1926
+ if (erbNodes.length === 1) {
1927
+ const node = erbNodes[0];
1928
+ if (!node.tag_opening || !node.tag_closing)
1929
+ return "html-only";
1930
+ const nodeStart = node.tag_opening.location.start.column;
1931
+ const nodeEnd = node.tag_closing.location.end.column;
1932
+ const isSoleContent = lineText.substring(0, nodeStart).trim() === "" && lineText.substring(nodeEnd).trim() === "";
1933
+ if (isSoleContent) {
1934
+ return "single-erb";
1935
+ }
1936
+ return "whole-line";
1937
+ }
1938
+ const segments = getLineSegments(lineText, erbNodes);
1939
+ const hasHTML = segments.some(segment => !segment.isERB && segment.text.trim() !== "");
1940
+ if (!hasHTML) {
1941
+ return "all-erb";
1942
+ }
1943
+ const allControlTags = erbNodes.every(node => { var _a; return ((_a = node.tag_opening) === null || _a === void 0 ? void 0 : _a.value) === "<%"; });
1944
+ if (allControlTags) {
1945
+ return "per-segment";
1946
+ }
1947
+ return "whole-line";
1948
+ }
1949
+ function getLineSegments(lineText, erbNodes) {
1950
+ const segments = [];
1951
+ const sorted = [...erbNodes].sort((a, b) => a.tag_opening.location.start.column - b.tag_opening.location.start.column);
1952
+ let position = 0;
1953
+ for (const node of sorted) {
1954
+ const nodeStart = node.tag_opening.location.start.column;
1955
+ const nodeEnd = node.tag_closing.location.end.column;
1956
+ if (nodeStart > position) {
1957
+ segments.push({ text: lineText.substring(position, nodeStart), isERB: false });
1958
+ }
1959
+ segments.push({ text: lineText.substring(nodeStart, nodeEnd), isERB: true, node });
1960
+ position = nodeEnd;
1961
+ }
1962
+ if (position < lineText.length) {
1963
+ segments.push({ text: lineText.substring(position), isERB: false });
1964
+ }
1965
+ return segments;
1966
+ }
1967
+ /**
1968
+ * Comment a line using AST mutation for strategies where the parser produces flat children,
1969
+ * and text-segment manipulation for per-segment (where the parser nests nodes).
1970
+ */
1971
+ function commentLineContent(content, erbNodes, strategy, parserService) {
1972
+ if (strategy === "per-segment") {
1973
+ return commentPerSegment(content, erbNodes);
1974
+ }
1975
+ const parseResult = parserService.parseContent(content, { track_whitespace: true });
1976
+ const lineCollector = new LineContextCollector();
1977
+ parseResult.visit(lineCollector);
1978
+ const lineERBNodes = lineCollector.erbNodesPerLine.get(0) || [];
1979
+ const document = parseResult.value;
1980
+ const children = rewriter.asMutable(document).children;
1981
+ switch (strategy) {
1982
+ case "all-erb":
1983
+ for (const node of lineERBNodes) {
1984
+ commentERBNode(node);
1985
+ }
1986
+ break;
1987
+ case "whole-line": {
1988
+ for (const node of lineERBNodes) {
1989
+ commentERBNode(node);
1990
+ }
1991
+ const commentNode = new core.HTMLCommentNode({
1992
+ type: "AST_HTML_COMMENT_NODE",
1993
+ location: core.Location.zero,
1994
+ errors: [],
1995
+ comment_start: core.createSyntheticToken("<!--", "TOKEN_HTML_COMMENT_START"),
1996
+ children: [core.createLiteral(" "), ...children.slice(), core.createLiteral(" ")],
1997
+ comment_end: core.createSyntheticToken("-->", "TOKEN_HTML_COMMENT_END"),
1998
+ });
1999
+ children.length = 0;
2000
+ children.push(commentNode);
2001
+ break;
2002
+ }
2003
+ case "html-only": {
2004
+ const commentNode = new core.HTMLCommentNode({
2005
+ type: "AST_HTML_COMMENT_NODE",
2006
+ location: core.Location.zero,
2007
+ errors: [],
2008
+ comment_start: core.createSyntheticToken("<!--", "TOKEN_HTML_COMMENT_START"),
2009
+ children: [core.createLiteral(" "), ...children.slice(), core.createLiteral(" ")],
2010
+ comment_end: core.createSyntheticToken("-->", "TOKEN_HTML_COMMENT_END"),
2011
+ });
2012
+ children.length = 0;
2013
+ children.push(commentNode);
2014
+ break;
2015
+ }
2016
+ }
2017
+ return printer.IdentityPrinter.print(document, { ignoreErrors: true });
2018
+ }
2019
+ /**
2020
+ * Per-segment commenting uses text segments because the parser creates nested
2021
+ * structures (e.g., ERBIfNode) that don't allow flat child iteration.
2022
+ */
2023
+ function commentPerSegment(content, erbNodes) {
2024
+ const segments = getLineSegments(content, erbNodes);
2025
+ return segments.map(segment => {
2026
+ if (segment.isERB) {
2027
+ return segment.text.substring(0, 2) + "#" + segment.text.substring(2);
2028
+ }
2029
+ else if (segment.text.trim() !== "") {
2030
+ return `<!-- ${segment.text} -->`;
2031
+ }
2032
+ return segment.text;
2033
+ }).join("");
2034
+ }
2035
+ function uncommentLineContent(content, parserService) {
2036
+ const parseResult = parserService.parseContent(content, { track_whitespace: true });
2037
+ const lineCollector = new LineContextCollector();
2038
+ parseResult.visit(lineCollector);
2039
+ const lineERBNodes = lineCollector.erbNodesPerLine.get(0) || [];
2040
+ const document = parseResult.value;
2041
+ const children = rewriter.asMutable(document).children;
2042
+ for (const node of lineERBNodes) {
2043
+ if (core.isERBCommentNode(node)) {
2044
+ uncommentERBNode(node);
2045
+ }
2046
+ }
2047
+ let index = 0;
2048
+ while (index < children.length) {
2049
+ const child = children[index];
2050
+ if (child.type === "AST_HTML_COMMENT_NODE") {
2051
+ const commentNode = child;
2052
+ const innerChildren = [...commentNode.children];
2053
+ if (innerChildren.length > 0) {
2054
+ const first = innerChildren[0];
2055
+ if (core.isLiteralNode(first) && first.content.startsWith(" ")) {
2056
+ const trimmed = first.content.substring(1);
2057
+ if (trimmed === "") {
2058
+ innerChildren.shift();
2059
+ }
2060
+ else {
2061
+ innerChildren[0] = core.createLiteral(trimmed);
2062
+ }
2063
+ }
2064
+ }
2065
+ if (innerChildren.length > 0) {
2066
+ const last = innerChildren[innerChildren.length - 1];
2067
+ if (core.isLiteralNode(last) && last.content.endsWith(" ")) {
2068
+ const trimmed = last.content.substring(0, last.content.length - 1);
2069
+ if (trimmed === "") {
2070
+ innerChildren.pop();
2071
+ }
2072
+ else {
2073
+ innerChildren[innerChildren.length - 1] = core.createLiteral(trimmed);
2074
+ }
2075
+ }
2076
+ }
2077
+ const innerERBNodes = [];
2078
+ const innerCollector = new LineContextCollector();
2079
+ for (const innerChild of innerChildren) {
2080
+ innerCollector.visit(innerChild);
2081
+ }
2082
+ innerERBNodes.push(...(innerCollector.erbNodesPerLine.get(0) || []));
2083
+ for (const erbNode of innerERBNodes) {
2084
+ if (core.isERBCommentNode(erbNode)) {
2085
+ uncommentERBNode(erbNode);
2086
+ }
2087
+ }
2088
+ children.splice(index, 1, ...innerChildren);
2089
+ index += innerChildren.length;
2090
+ continue;
2091
+ }
2092
+ index++;
2093
+ }
2094
+ return printer.IdentityPrinter.print(document, { ignoreErrors: true });
2095
+ }
2096
+
2097
+ class CommentService {
2098
+ constructor(parserService) {
2099
+ this.parserService = parserService;
2100
+ }
2101
+ toggleLineComment(document, range) {
2102
+ const parseResult = this.parserService.parseDocument(document);
2103
+ const collector = new LineContextCollector();
2104
+ collector.visit(parseResult.document);
2105
+ const startLine = range.start.line;
2106
+ const endLine = range.end.line;
2107
+ const lineInfos = [];
2108
+ for (let line = startLine; line <= endLine; line++) {
2109
+ const lineText = document.getText(node.Range.create(line, 0, line + 1, 0)).replace(/\n$/, "");
2110
+ if (lineText.trim() === "") {
2111
+ continue;
2112
+ }
2113
+ if (this.lineIsIfFalseWrapped(lineText) !== null) {
2114
+ lineInfos.push({ line, context: "erb-comment", node: null });
2115
+ continue;
2116
+ }
2117
+ const htmlCommentNode = collector.htmlCommentNodesPerLine.get(line);
2118
+ const info = collector.lineMap.get(line);
2119
+ if (htmlCommentNode && this.htmlCommentSpansLine(htmlCommentNode, lineText)) {
2120
+ lineInfos.push({ line, context: "html-comment", node: htmlCommentNode });
2121
+ }
2122
+ else if (info) {
2123
+ if (info.context === "html-comment") {
2124
+ lineInfos.push({ line, context: "html-content", node: null });
2125
+ }
2126
+ else {
2127
+ lineInfos.push(info);
2128
+ }
2129
+ }
2130
+ else {
2131
+ lineInfos.push({ line, context: "html-content", node: null });
2132
+ }
2133
+ }
2134
+ if (lineInfos.length === 0)
2135
+ return [];
2136
+ const allCommented = lineInfos.every(info => info.context === "erb-comment" || info.context === "html-comment");
2137
+ const edits = [];
2138
+ if (allCommented) {
2139
+ for (const info of lineInfos) {
2140
+ const lineText = document.getText(node.Range.create(info.line, 0, info.line + 1, 0)).replace(/\n$/, "");
2141
+ const edit = this.uncommentLine(info, lineText, collector);
2142
+ if (edit)
2143
+ edits.push(edit);
2144
+ }
2145
+ }
2146
+ else {
2147
+ for (const info of lineInfos) {
2148
+ if (info.context === "erb-comment" || info.context === "html-comment")
2149
+ continue;
2150
+ const lineText = document.getText(node.Range.create(info.line, 0, info.line + 1, 0)).replace(/\n$/, "");
2151
+ const erbNodes = collector.erbNodesPerLine.get(info.line) || [];
2152
+ const edit = this.commentLine(info, lineText, erbNodes, collector);
2153
+ if (edit)
2154
+ edits.push(edit);
2155
+ }
2156
+ }
2157
+ return edits;
2158
+ }
2159
+ toggleBlockComment(document, range) {
2160
+ const startLine = range.start.line;
2161
+ const endLine = range.end.line;
2162
+ const firstLineText = document.getText(node.Range.create(startLine, 0, startLine + 1, 0)).replace(/\n$/, "");
2163
+ const lastLineText = document.getText(node.Range.create(endLine, 0, endLine + 1, 0)).replace(/\n$/, "");
2164
+ const isWrapped = firstLineText.trim() === "<% if false %>" && lastLineText.trim() === "<% end %>";
2165
+ if (isWrapped) {
2166
+ return [
2167
+ node.TextEdit.del(node.Range.create(endLine, 0, endLine + 1, 0)),
2168
+ node.TextEdit.del(node.Range.create(startLine, 0, startLine + 1, 0)),
2169
+ ];
2170
+ }
2171
+ else {
2172
+ const firstLineIndent = this.getIndentation(firstLineText);
2173
+ return [
2174
+ node.TextEdit.insert(node.Position.create(endLine + 1, 0), `${firstLineIndent}<% end %>\n`),
2175
+ node.TextEdit.insert(node.Position.create(startLine, 0), `${firstLineIndent}<% if false %>\n`),
2176
+ ];
2177
+ }
2178
+ }
2179
+ commentLine(info, lineText, erbNodes, collector) {
2180
+ const lineRange = node.Range.create(info.line, 0, info.line, lineText.length);
2181
+ const indent = this.getIndentation(lineText);
2182
+ const content = lineText.trimStart();
2183
+ const htmlCommentNode = collector.htmlCommentNodesPerLine.get(info.line);
2184
+ if (htmlCommentNode) {
2185
+ return node.TextEdit.replace(lineRange, `${indent}<% if false %>${content}<% end %>`);
2186
+ }
2187
+ const strategy = determineStrategy(erbNodes, lineText);
2188
+ if (strategy === "single-erb") {
2189
+ const node$1 = erbNodes[0];
2190
+ const insertColumn = node$1.tag_opening.location.start.column + 2;
2191
+ return node.TextEdit.insert(node.Position.create(info.line, insertColumn), "#");
2192
+ }
2193
+ const result = commentLineContent(content, erbNodes, strategy, this.parserService);
2194
+ return node.TextEdit.replace(lineRange, indent + result);
2195
+ }
2196
+ lineIsIfFalseWrapped(lineText) {
2197
+ const trimmed = lineText.trimStart();
2198
+ const indent = this.getIndentation(lineText);
2199
+ if (trimmed.startsWith("<% if false %>") && trimmed.endsWith("<% end %>")) {
2200
+ const inner = trimmed.slice("<% if false %>".length, -"<% end %>".length);
2201
+ return indent + inner;
2202
+ }
2203
+ return null;
2204
+ }
2205
+ uncommentLine(info, lineText, collector) {
2206
+ var _a;
2207
+ const lineRange = node.Range.create(info.line, 0, info.line, lineText.length);
2208
+ const indent = this.getIndentation(lineText);
2209
+ const ifFalseContent = this.lineIsIfFalseWrapped(lineText);
2210
+ if (ifFalseContent !== null) {
2211
+ return node.TextEdit.replace(lineRange, ifFalseContent);
2212
+ }
2213
+ if (info.context === "erb-comment") {
2214
+ const node$1 = info.node;
2215
+ if (!(node$1 === null || node$1 === void 0 ? void 0 : node$1.tag_opening) || !(node$1 === null || node$1 === void 0 ? void 0 : node$1.tag_closing))
2216
+ return null;
2217
+ const contentValue = (_a = node$1.content) === null || _a === void 0 ? void 0 : _a.value;
2218
+ const trimmedContent = (contentValue === null || contentValue === void 0 ? void 0 : contentValue.trim()) || "";
2219
+ if (trimmedContent.startsWith("<") && !trimmedContent.startsWith("<%")) {
2220
+ return node.TextEdit.replace(lineRange, `${indent}${trimmedContent}`);
2221
+ }
2222
+ if (lspLine(node$1.tag_opening.location.start) !== info.line)
2223
+ return null;
2224
+ const erbNodes = collector.erbNodesPerLine.get(info.line) || [];
2225
+ if (erbNodes.length > 1) {
2226
+ const content = lineText.trimStart();
2227
+ const result = uncommentLineContent(content, this.parserService);
2228
+ return node.TextEdit.replace(lineRange, indent + result);
2229
+ }
2230
+ const hashColumn = node$1.tag_opening.location.start.column + 2;
2231
+ if ((contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" graphql ")) ||
2232
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" %= ")) ||
2233
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" == ")) ||
2234
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" % ")) ||
2235
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" = ")) ||
2236
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" - "))) {
2237
+ return node.TextEdit.del(node.Range.create(info.line, hashColumn, info.line, hashColumn + 2));
2238
+ }
2239
+ return node.TextEdit.del(node.Range.create(info.line, hashColumn, info.line, hashColumn + 1));
2240
+ }
2241
+ if (info.context === "html-comment") {
2242
+ const commentNode = info.node;
2243
+ if ((commentNode === null || commentNode === void 0 ? void 0 : commentNode.comment_start) && (commentNode === null || commentNode === void 0 ? void 0 : commentNode.comment_end)) {
2244
+ const contentStart = commentNode.comment_start.location.end.column;
2245
+ const contentEnd = commentNode.comment_end.location.start.column;
2246
+ const innerContent = lineText.substring(contentStart, contentEnd).trim();
2247
+ const result = uncommentLineContent(innerContent, this.parserService);
2248
+ return node.TextEdit.replace(lineRange, `${indent}${result}`);
2249
+ }
2250
+ }
2251
+ return null;
2252
+ }
2253
+ htmlCommentSpansLine(node, lineText) {
2254
+ if (!node.comment_start || !node.comment_end)
2255
+ return false;
2256
+ const commentStart = node.comment_start.location.start.column;
2257
+ const commentEnd = node.comment_end.location.end.column;
2258
+ const contentBefore = lineText.substring(0, commentStart).trim();
2259
+ const contentAfter = lineText.substring(commentEnd).trim();
2260
+ return contentBefore === "" && contentAfter === "";
2261
+ }
2262
+ getIndentation(lineText) {
2263
+ const match = lineText.match(/^(\s*)/);
2264
+ return match ? match[1] : "";
2265
+ }
2266
+ }
2267
+
2268
+ class Service {
2269
+ constructor(connection, params) {
2270
+ this.connection = connection;
2271
+ this.settings = new Settings(params, this.connection);
2272
+ this.documentService = new DocumentService(this.connection);
2273
+ this.project = new Project(connection, this.settings.projectPath.replace("file://", ""));
2274
+ this.parserService = new ParserService();
2275
+ this.linterService = new LinterService(this.connection, this.settings, this.project);
2276
+ this.formattingService = new FormattingService(this.connection, this.documentService.documents, this.project, this.settings);
2277
+ this.autofixService = new AutofixService(this.connection, this.config);
2278
+ this.configService = new ConfigService(this.project.projectPath);
2279
+ this.codeActionService = new CodeActionService(this.project, this.config);
2280
+ this.diagnostics = new Diagnostics(this.connection, this.documentService, this.parserService, this.linterService, this.configService);
2281
+ this.documentSaveService = new DocumentSaveService(this.connection, this.settings, this.autofixService, this.formattingService);
2282
+ this.foldingRangeService = new FoldingRangeService(this.parserService);
2283
+ this.documentHighlightService = new DocumentHighlightService(this.parserService);
2284
+ this.hoverService = new HoverService(this.parserService);
2285
+ this.rewriteCodeActionService = new RewriteCodeActionService(this.parserService);
2286
+ this.commentService = new CommentService(this.parserService);
2287
+ if (params.initializationOptions) {
2288
+ this.settings.globalSettings = params.initializationOptions;
2289
+ }
2290
+ }
2291
+ async init() {
2292
+ await this.project.initialize();
2293
+ await this.formattingService.initialize();
2294
+ try {
2295
+ this.config = await config.Config.loadForEditor(this.project.projectPath, version);
2296
+ this.codeActionService.setConfig(this.config);
2297
+ this.autofixService.setConfig(this.config);
2298
+ if (this.config.version && this.config.version !== version) {
2299
+ this.connection.console.warn(`Config file version (${this.config.version}) does not match current version (${version}). ` +
2300
+ `Consider updating your .herb.yml file.`);
2301
+ }
2302
+ }
2303
+ catch (error) {
2304
+ this.connection.console.warn(`Failed to load config: ${error instanceof Error ? error.message : String(error)}. Using personal settings with defaults.`);
2305
+ this.config = config.Config.fromObject({
2306
+ linter: this.settings.globalSettings.linter,
2307
+ formatter: this.settings.globalSettings.formatter
2308
+ }, { projectPath: this.project.projectPath, version });
2309
+ this.codeActionService.setConfig(this.config);
2310
+ this.autofixService.setConfig(this.config);
2311
+ }
2312
+ await this.settings.initializeProjectConfig(this.config);
2313
+ await this.formattingService.refreshConfig(this.config);
2314
+ this.linterService.rebuildLinter();
2315
+ this.documentService.onDidClose((change) => {
2316
+ this.settings.documentSettings.delete(change.document.uri);
2317
+ });
2318
+ this.documentService.onDidChangeContent(async (change) => {
2319
+ await this.diagnostics.refreshDocument(change.document);
2320
+ });
2321
+ }
2322
+ async refresh() {
2323
+ await this.project.refresh();
2324
+ await this.formattingService.refreshConfig(this.config);
2325
+ await this.diagnostics.refreshAllDocuments();
2326
+ }
2327
+ async refreshConfig() {
2328
+ try {
2329
+ this.config = await config.Config.loadForEditor(this.project.projectPath, version);
2330
+ this.codeActionService.setConfig(this.config);
2331
+ this.autofixService.setConfig(this.config);
2332
+ if (this.config.version && this.config.version !== version) {
2333
+ this.connection.console.warn(`Config file version (${this.config.version}) does not match current version (${version}). ` +
2334
+ `Consider updating your .herb.yml file.`);
2335
+ }
2336
+ }
2337
+ catch (error) {
2338
+ this.connection.console.warn(`Failed to load config: ${error instanceof Error ? error.message : String(error)}. Using personal settings with defaults.`);
2339
+ this.config = config.Config.fromObject({
2340
+ linter: this.settings.globalSettings.linter,
2341
+ formatter: this.settings.globalSettings.formatter
2342
+ }, { projectPath: this.project.projectPath, version });
2343
+ this.codeActionService.setConfig(this.config);
2344
+ this.autofixService.setConfig(this.config);
2345
+ }
2346
+ await this.settings.refreshProjectConfig(this.config);
2347
+ await this.formattingService.refreshConfig(this.config);
2348
+ this.linterService.rebuildLinter();
1434
2349
  }
1435
2350
  }
1436
2351
 
@@ -1460,8 +2375,11 @@ class Server {
1460
2375
  documentFormattingProvider: true,
1461
2376
  documentRangeFormattingProvider: true,
1462
2377
  codeActionProvider: {
1463
- codeActionKinds: [node.CodeActionKind.QuickFix, node.CodeActionKind.SourceFixAll]
2378
+ codeActionKinds: [node.CodeActionKind.QuickFix, node.CodeActionKind.SourceFixAll, node.CodeActionKind.RefactorRewrite]
1464
2379
  },
2380
+ foldingRangeProvider: true,
2381
+ documentHighlightProvider: true,
2382
+ hoverProvider: true,
1465
2383
  },
1466
2384
  };
1467
2385
  if (this.service.settings.hasWorkspaceFolderCapability) {
@@ -1543,15 +2461,49 @@ class Server {
1543
2461
  this.connection.onDocumentRangeFormatting((params) => {
1544
2462
  return this.service.formattingService.formatRange(params);
1545
2463
  });
2464
+ this.connection.onDocumentHighlight((params) => {
2465
+ const document = this.service.documentService.get(params.textDocument.uri);
2466
+ if (!document)
2467
+ return [];
2468
+ return this.service.documentHighlightService.getDocumentHighlights(document, params.position);
2469
+ });
2470
+ this.connection.onHover((params) => {
2471
+ const document = this.service.documentService.get(params.textDocument.uri);
2472
+ if (!document)
2473
+ return null;
2474
+ return this.service.hoverService.getHover(document, params.position);
2475
+ });
1546
2476
  this.connection.onCodeAction((params) => {
1547
2477
  const document = this.service.documentService.get(params.textDocument.uri);
1548
2478
  if (!document)
1549
2479
  return [];
2480
+ const parseResult = this.service.parserService.parseDocument(document);
2481
+ if (parseResult.diagnostics.length > 0)
2482
+ return [];
1550
2483
  const diagnostics = params.context.diagnostics;
1551
2484
  const documentText = document.getText();
1552
2485
  const linterDisableCodeActions = this.service.codeActionService.createCodeActions(params.textDocument.uri, diagnostics, documentText);
1553
2486
  const autofixCodeActions = this.service.codeActionService.autofixCodeActions(params, document);
1554
- return autofixCodeActions.concat(linterDisableCodeActions);
2487
+ const rewriteCodeActions = this.service.rewriteCodeActionService.getCodeActions(document, params.range);
2488
+ return autofixCodeActions.concat(linterDisableCodeActions).concat(rewriteCodeActions);
2489
+ });
2490
+ this.connection.onFoldingRanges((params) => {
2491
+ const document = this.service.documentService.get(params.textDocument.uri);
2492
+ if (!document)
2493
+ return [];
2494
+ return this.service.foldingRangeService.getFoldingRanges(document);
2495
+ });
2496
+ this.connection.onRequest('herb/toggleLineComment', (params) => {
2497
+ const document = this.service.documentService.get(params.textDocument.uri);
2498
+ if (!document)
2499
+ return [];
2500
+ return this.service.commentService.toggleLineComment(document, params.range);
2501
+ });
2502
+ this.connection.onRequest('herb/toggleBlockComment', (params) => {
2503
+ const document = this.service.documentService.get(params.textDocument.uri);
2504
+ if (!document)
2505
+ return [];
2506
+ return this.service.commentService.toggleBlockComment(document, params.range);
1555
2507
  });
1556
2508
  }
1557
2509
  listen() {
@@ -1582,17 +2534,31 @@ class CLI {
1582
2534
  }
1583
2535
 
1584
2536
  exports.CLI = CLI;
2537
+ exports.CommentService = CommentService;
1585
2538
  exports.Diagnostics = Diagnostics;
2539
+ exports.DocumentHighlightCollector = DocumentHighlightCollector;
2540
+ exports.DocumentHighlightService = DocumentHighlightService;
1586
2541
  exports.DocumentService = DocumentService;
2542
+ exports.FoldingRangeCollector = FoldingRangeCollector;
2543
+ exports.FoldingRangeService = FoldingRangeService;
1587
2544
  exports.FormattingService = FormattingService;
1588
2545
  exports.Project = Project;
1589
2546
  exports.Server = Server;
1590
2547
  exports.Service = Service;
1591
2548
  exports.Settings = Settings;
1592
- exports.UnreachableCodeCollector = UnreachableCodeCollector;
1593
2549
  exports.camelize = camelize;
1594
2550
  exports.capitalize = capitalize;
1595
2551
  exports.dasherize = dasherize;
2552
+ exports.erbTagToRange = erbTagToRange;
1596
2553
  exports.getFullDocumentRange = getFullDocumentRange;
2554
+ exports.isPositionInRange = isPositionInRange;
1597
2555
  exports.lintToDignosticSeverity = lintToDignosticSeverity;
2556
+ exports.lintToDignosticTags = lintToDignosticTags;
2557
+ exports.lspLine = lspLine;
2558
+ exports.lspPosition = lspPosition;
2559
+ exports.lspRangeFromLocation = lspRangeFromLocation;
2560
+ exports.nodeToRange = nodeToRange;
2561
+ exports.openTagRanges = openTagRanges;
2562
+ exports.rangeSize = rangeSize;
2563
+ exports.tokenToRange = tokenToRange;
1598
2564
  //# sourceMappingURL=index.cjs.map