@herb-tools/language-server 0.8.10 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/action_view_helpers.js +19 -0
  2. package/dist/action_view_helpers.js.map +1 -0
  3. package/dist/autofix_service.js +1 -1
  4. package/dist/autofix_service.js.map +1 -1
  5. package/dist/code_action_service.js +3 -6
  6. package/dist/code_action_service.js.map +1 -1
  7. package/dist/comment_ast_utils.js +206 -0
  8. package/dist/comment_ast_utils.js.map +1 -0
  9. package/dist/comment_service.js +175 -0
  10. package/dist/comment_service.js.map +1 -0
  11. package/dist/diagnostics.js +15 -27
  12. package/dist/diagnostics.js.map +1 -1
  13. package/dist/document_highlight_service.js +196 -0
  14. package/dist/document_highlight_service.js.map +1 -0
  15. package/dist/document_save_service.js +16 -6
  16. package/dist/document_save_service.js.map +1 -1
  17. package/dist/folding_range_service.js +209 -0
  18. package/dist/folding_range_service.js.map +1 -0
  19. package/dist/formatting_service.js +4 -4
  20. package/dist/formatting_service.js.map +1 -1
  21. package/dist/herb-language-server.js +150189 -41260
  22. package/dist/herb-language-server.js.map +1 -1
  23. package/dist/hover_service.js +70 -0
  24. package/dist/hover_service.js.map +1 -0
  25. package/dist/index.cjs +1227 -63
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/line_context_collector.js +73 -0
  30. package/dist/line_context_collector.js.map +1 -0
  31. package/dist/linter_service.js +20 -4
  32. package/dist/linter_service.js.map +1 -1
  33. package/dist/parser_service.js +6 -5
  34. package/dist/parser_service.js.map +1 -1
  35. package/dist/range_utils.js +65 -0
  36. package/dist/range_utils.js.map +1 -0
  37. package/dist/rewrite_code_action_service.js +135 -0
  38. package/dist/rewrite_code_action_service.js.map +1 -0
  39. package/dist/server.js +36 -2
  40. package/dist/server.js.map +1 -1
  41. package/dist/service.js +10 -0
  42. package/dist/service.js.map +1 -1
  43. package/dist/types/action_view_helpers.d.ts +5 -0
  44. package/dist/types/comment_ast_utils.d.ts +20 -0
  45. package/dist/types/comment_service.d.ts +14 -0
  46. package/dist/types/diagnostics.d.ts +0 -1
  47. package/dist/types/document_highlight_service.d.ts +28 -0
  48. package/dist/types/document_save_service.d.ts +8 -0
  49. package/dist/types/folding_range_service.d.ts +35 -0
  50. package/dist/types/formatting_service.d.ts +1 -1
  51. package/dist/types/hover_service.d.ts +8 -0
  52. package/dist/types/index.d.ts +4 -0
  53. package/dist/types/line_context_collector.d.ts +19 -0
  54. package/dist/types/linter_service.d.ts +1 -0
  55. package/dist/types/parser_service.d.ts +2 -1
  56. package/dist/types/range_utils.d.ts +16 -0
  57. package/dist/types/rewrite_code_action_service.d.ts +11 -0
  58. package/dist/types/service.d.ts +10 -0
  59. package/dist/types/utils.d.ts +0 -6
  60. package/dist/utils.js +0 -16
  61. package/dist/utils.js.map +1 -1
  62. package/package.json +10 -5
  63. package/src/action_view_helpers.ts +23 -0
  64. package/src/autofix_service.ts +1 -1
  65. package/src/code_action_service.ts +3 -6
  66. package/src/comment_ast_utils.ts +282 -0
  67. package/src/comment_service.ts +228 -0
  68. package/src/diagnostics.ts +24 -38
  69. package/src/document_highlight_service.ts +267 -0
  70. package/src/document_save_service.ts +19 -7
  71. package/src/folding_range_service.ts +287 -0
  72. package/src/formatting_service.ts +4 -4
  73. package/src/hover_service.ts +90 -0
  74. package/src/index.ts +4 -0
  75. package/src/line_context_collector.ts +97 -0
  76. package/src/linter_service.ts +25 -7
  77. package/src/parser_service.ts +9 -10
  78. package/src/range_utils.ts +90 -0
  79. package/src/rewrite_code_action_service.ts +165 -0
  80. package/src/server.ts +51 -2
  81. package/src/service.ts +15 -0
  82. package/src/utils.ts +0 -22
package/dist/index.cjs CHANGED
@@ -9,8 +9,10 @@ var nodeWasm = require('@herb-tools/node-wasm');
9
9
  var linter = require('@herb-tools/linter');
10
10
  var loader = require('@herb-tools/linter/loader');
11
11
  var loader$1 = require('@herb-tools/rewriter/loader');
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.0";
14
16
 
15
17
  class Settings {
16
18
  constructor(params, connection) {
@@ -137,6 +139,70 @@ class DocumentService {
137
139
  }
138
140
  }
139
141
 
142
+ function lspPosition(herbPosition) {
143
+ return node.Position.create(herbPosition.line - 1, herbPosition.column);
144
+ }
145
+ function lspLine(herbPosition) {
146
+ return herbPosition.line - 1;
147
+ }
148
+ function lspRangeFromLocation(herbLocation) {
149
+ return node.Range.create(lspPosition(herbLocation.start), lspPosition(herbLocation.end));
150
+ }
151
+ function erbTagToRange(node$1) {
152
+ if (!node$1.tag_opening || !node$1.tag_closing)
153
+ return null;
154
+ return node.Range.create(lspPosition(node$1.tag_opening.location.start), lspPosition(node$1.tag_closing.location.end));
155
+ }
156
+ function tokenToRange(token) {
157
+ if (!token)
158
+ return null;
159
+ return lspRangeFromLocation(token.location);
160
+ }
161
+ function nodeToRange(node) {
162
+ return lspRangeFromLocation(node.location);
163
+ }
164
+ function openTagRanges(tag) {
165
+ const ranges = [];
166
+ if (tag.tag_opening && tag.tag_name) {
167
+ ranges.push(node.Range.create(lspPosition(tag.tag_opening.location.start), lspPosition(tag.tag_name.location.end)));
168
+ }
169
+ ranges.push(tokenToRange(tag.tag_closing));
170
+ return ranges;
171
+ }
172
+ function isPositionInRange(position, range) {
173
+ if (position.line < range.start.line || position.line > range.end.line) {
174
+ return false;
175
+ }
176
+ if (position.line === range.start.line && position.character < range.start.character) {
177
+ return false;
178
+ }
179
+ if (position.line === range.end.line && position.character > range.end.character) {
180
+ return false;
181
+ }
182
+ return true;
183
+ }
184
+ function rangeSize(range) {
185
+ if (range.start.line === range.end.line) {
186
+ return range.end.character - range.start.character;
187
+ }
188
+ return (range.end.line - range.start.line) * 10000 + range.end.character;
189
+ }
190
+ /**
191
+ * Returns a Range that spans the entire document
192
+ */
193
+ function getFullDocumentRange(document) {
194
+ const lastLine = document.lineCount - 1;
195
+ const lastLineText = document.getText({
196
+ start: node.Position.create(lastLine, 0),
197
+ end: node.Position.create(lastLine + 1, 0)
198
+ });
199
+ const lastLineLength = lastLineText.length;
200
+ return {
201
+ start: node.Position.create(0, 0),
202
+ end: node.Position.create(lastLine, lastLineLength)
203
+ };
204
+ }
205
+
140
206
  class Diagnostics {
141
207
  constructor(connection, documentService, parserService, linterService, configService) {
142
208
  this.diagnostics = new Map();
@@ -308,16 +374,7 @@ class UnreachableCodeCollector extends core.Visitor {
308
374
  }
309
375
  addDiagnostic(location, message) {
310
376
  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
- },
377
+ range: lspRangeFromLocation(location),
321
378
  message,
322
379
  severity: node.DiagnosticSeverity.Hint,
323
380
  tags: [node.DiagnosticTag.Unnecessary],
@@ -345,14 +402,14 @@ class UnreachableCodeCollector extends core.Visitor {
345
402
  markIfChainAsProcessed(node) {
346
403
  this.processedIfNodes.add(node);
347
404
  this.traverseSubsequentNodes(node.subsequent, (current) => {
348
- if (current.type === 'AST_ERB_IF_NODE') {
405
+ if (core.isERBIfNode(current)) {
349
406
  this.processedIfNodes.add(current);
350
407
  }
351
408
  });
352
409
  }
353
410
  markElseNodesInIfChain(node) {
354
411
  this.traverseSubsequentNodes(node.subsequent, (current) => {
355
- if (current.type === 'AST_ERB_ELSE_NODE') {
412
+ if (core.isERBElseNode(current)) {
356
413
  this.processedElseNodes.add(current);
357
414
  }
358
415
  });
@@ -360,10 +417,14 @@ class UnreachableCodeCollector extends core.Visitor {
360
417
  traverseSubsequentNodes(startNode, callback) {
361
418
  let current = startNode;
362
419
  while (current) {
363
- callback(current);
364
- if ('subsequent' in current) {
420
+ if (core.isERBIfNode(current)) {
421
+ callback(current);
365
422
  current = current.subsequent;
366
423
  }
424
+ else if (core.isERBElseNode(current)) {
425
+ callback(current);
426
+ break;
427
+ }
367
428
  else {
368
429
  break;
369
430
  }
@@ -374,14 +435,11 @@ class UnreachableCodeCollector extends core.Visitor {
374
435
  this.checkEmptyStatementsWithEndLocation(node, node.statements, "if", node.subsequent);
375
436
  }
376
437
  this.traverseSubsequentNodes(node.subsequent, (current) => {
377
- if (!('statements' in current) || !Array.isArray(current.statements)) {
378
- return;
379
- }
380
438
  if (this.statementsHaveContent(current.statements)) {
381
439
  return;
382
440
  }
383
- const blockType = current.type === 'AST_ERB_IF_NODE' ? 'elsif' : 'else';
384
- const nextSubsequent = 'subsequent' in current ? current.subsequent : null;
441
+ const blockType = core.isERBIfNode(current) ? 'elsif' : 'else';
442
+ const nextSubsequent = core.isERBIfNode(current) ? current.subsequent : null;
385
443
  if (nextSubsequent) {
386
444
  this.checkEmptyStatementsWithEndLocation(current, current.statements, blockType, nextSubsequent);
387
445
  }
@@ -396,17 +454,12 @@ class UnreachableCodeCollector extends core.Visitor {
396
454
  }
397
455
  let hasContent = false;
398
456
  this.traverseSubsequentNodes(node.subsequent, (current) => {
399
- if ('statements' in current && Array.isArray(current.statements)) {
400
- if (this.statementsHaveContent(current.statements)) {
401
- hasContent = true;
402
- }
457
+ if (this.statementsHaveContent(current.statements)) {
458
+ hasContent = true;
403
459
  }
404
460
  });
405
461
  return !hasContent;
406
462
  }
407
- toZeroBased(line) {
408
- return line - 1;
409
- }
410
463
  }
411
464
 
412
465
  class ErrorVisitor extends nodeWasm.Visitor {
@@ -423,7 +476,7 @@ class ErrorVisitor extends nodeWasm.Visitor {
423
476
  const diagnostic = {
424
477
  source: this.source,
425
478
  severity: node.DiagnosticSeverity.Error,
426
- range: this.rangeFromHerbError(error),
479
+ range: lspRangeFromLocation(error.location),
427
480
  message: error.message,
428
481
  code: error.type,
429
482
  data: {
@@ -433,9 +486,6 @@ class ErrorVisitor extends nodeWasm.Visitor {
433
486
  };
434
487
  this.diagnostics.push(diagnostic);
435
488
  }
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
489
  }
440
490
  class ParserService {
441
491
  parseDocument(textDocument) {
@@ -448,6 +498,9 @@ class ParserService {
448
498
  diagnostics: errorVisitor.diagnostics
449
499
  };
450
500
  }
501
+ parseContent(content, options) {
502
+ return nodeWasm.Herb.parse(content, options);
503
+ }
451
504
  }
452
505
 
453
506
  function camelize(value) {
@@ -467,21 +520,6 @@ function lintToDignosticSeverity(severity) {
467
520
  case "hint": return node.DiagnosticSeverity.Hint;
468
521
  }
469
522
  }
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)
478
- });
479
- const lastLineLength = lastLineText.length;
480
- return {
481
- start: node.Position.create(0, 0),
482
- end: node.Position.create(lastLine, lastLineLength)
483
- };
484
- }
485
523
 
486
524
  const OPEN_CONFIG_ACTION$1 = 'Open .herb.yml';
487
525
  class LinterService {
@@ -563,8 +601,24 @@ class LinterService {
563
601
  this.connection.window.showWarningMessage(message);
564
602
  }
565
603
  }
604
+ shouldLintFile(uri) {
605
+ const filePath = uri.replace(/^file:\/\//, '');
606
+ if (filePath.endsWith('.herb.yml'))
607
+ return false;
608
+ const config$1 = this.settings.projectConfig;
609
+ if (!config$1)
610
+ return true;
611
+ const hasConfigFile = config.Config.exists(config$1.projectPath);
612
+ if (!hasConfigFile)
613
+ return true;
614
+ const relativePath = filePath.replace(this.project.projectPath + '/', '');
615
+ return config$1.isLinterEnabledForPath(relativePath);
616
+ }
566
617
  async lintDocument(textDocument) {
567
618
  var _a, _b, _c, _d;
619
+ if (!this.shouldLintFile(textDocument.uri)) {
620
+ return { diagnostics: [] };
621
+ }
568
622
  const settings = await this.settings.getDocumentSettings(textDocument.uri);
569
623
  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
624
  if (!linterEnabled) {
@@ -590,12 +644,12 @@ class LinterService {
590
644
  const content = textDocument.getText();
591
645
  const lintResult = this.linter.lint(content, { fileName: textDocument.uri });
592
646
  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));
647
+ const range = lspRangeFromLocation(offense.location);
594
648
  const customRulePath = this.customRulePaths.get(offense.rule);
595
649
  const codeDescription = {
596
650
  href: customRulePath
597
651
  ? `file://${customRulePath}`
598
- : `https://herb-tools.dev/linter/rules/${offense.rule}`
652
+ : linter.ruleDocumentationUrl(offense.rule)
599
653
  };
600
654
  return {
601
655
  source: this.source,
@@ -770,7 +824,7 @@ class FormattingService {
770
824
  this.postRewriters = [];
771
825
  }
772
826
  }
773
- async formatOnSave(document, reason) {
827
+ async formatOnSave(document, reason, textOverride) {
774
828
  this.connection.console.log(`[Formatting] formatOnSave called for ${document.uri}`);
775
829
  if (reason !== node.TextDocumentSaveReason.Manual) {
776
830
  this.connection.console.log(`[Formatting] Skipping: reason=${reason} (not manual)`);
@@ -781,7 +835,7 @@ class FormattingService {
781
835
  this.connection.console.log(`[Formatting] Skipping: file not in formatter config`);
782
836
  return [];
783
837
  }
784
- return this.performFormatting({ textDocument: { uri: document.uri }, options: { tabSize: 2, insertSpaces: true } });
838
+ return this.performFormatting({ textDocument: { uri: document.uri }, options: { tabSize: 2, insertSpaces: true } }, textOverride);
785
839
  }
786
840
  shouldFormatFile(filePath) {
787
841
  if (filePath.endsWith('.herb.yml'))
@@ -809,13 +863,13 @@ class FormattingService {
809
863
  }
810
864
  };
811
865
  }
812
- async performFormatting(params) {
866
+ async performFormatting(params, textOverride) {
813
867
  const document = this.documents.get(params.textDocument.uri);
814
868
  if (!document) {
815
869
  return [];
816
870
  }
817
871
  try {
818
- const text = document.getText();
872
+ const text = textOverride !== null && textOverride !== void 0 ? textOverride : document.getText();
819
873
  const config = await this.getConfigWithSettings(params.textDocument.uri);
820
874
  this.connection.console.log(`[Formatting] Creating formatter with ${this.preRewriters.length} pre-rewriters, ${this.postRewriters.length} post-rewriters`);
821
875
  if (this.failedRewriters.size > 0) {
@@ -1287,10 +1341,7 @@ class CodeActionService {
1287
1341
  };
1288
1342
  }
1289
1343
  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
- };
1344
+ return lspRangeFromLocation(offense.location);
1294
1345
  }
1295
1346
  rangesEqual(r1, r2) {
1296
1347
  return (r1.start.line === r2.start.line &&
@@ -1309,6 +1360,14 @@ class CodeActionService {
1309
1360
 
1310
1361
  class DocumentSaveService {
1311
1362
  constructor(connection, settings, autofixService, formattingService) {
1363
+ /**
1364
+ * Tracks documents that were recently autofixed via applyFixesAndFormatting
1365
+ * (triggered by onDocumentFormatting). When editor.formatOnSave is enabled,
1366
+ * onDocumentFormatting fires BEFORE willSaveWaitUntil. If applyFixesAndFormatting
1367
+ * already applied autofix, applyFixes must skip to avoid conflicting edits
1368
+ * (since this.documents hasn't been updated between the two events).
1369
+ */
1370
+ this.recentlyAutofixedViaFormatting = new Set();
1312
1371
  this.connection = connection;
1313
1372
  this.settings = settings;
1314
1373
  this.autofixService = autofixService;
@@ -1325,6 +1384,10 @@ class DocumentSaveService {
1325
1384
  this.connection.console.log(`[DocumentSave] applyFixes fixOnSave=${fixOnSave}`);
1326
1385
  if (!fixOnSave)
1327
1386
  return [];
1387
+ if (this.recentlyAutofixedViaFormatting.delete(document.uri)) {
1388
+ this.connection.console.log(`[DocumentSave] applyFixes skipping: already autofixed via formatting`);
1389
+ return [];
1390
+ }
1328
1391
  return this.autofixService.autofix(document);
1329
1392
  }
1330
1393
  /**
@@ -1340,18 +1403,1066 @@ class DocumentSaveService {
1340
1403
  let autofixEdits = [];
1341
1404
  if (fixOnSave) {
1342
1405
  autofixEdits = await this.autofixService.autofix(document);
1406
+ if (autofixEdits.length > 0) {
1407
+ this.recentlyAutofixedViaFormatting.add(document.uri);
1408
+ }
1343
1409
  }
1344
1410
  if (!formatterEnabled)
1345
1411
  return autofixEdits;
1346
1412
  if (autofixEdits.length === 0) {
1347
1413
  return this.formattingService.formatOnSave(document, reason);
1348
1414
  }
1349
- const autofixedDocument = {
1350
- ...document,
1351
- uri: document.uri,
1352
- getText: () => autofixEdits[0].newText,
1415
+ return this.formattingService.formatOnSave(document, reason, autofixEdits[0].newText);
1416
+ }
1417
+ }
1418
+
1419
+ class FoldingRangeService {
1420
+ constructor(parserService) {
1421
+ this.parserService = parserService;
1422
+ }
1423
+ getFoldingRanges(textDocument) {
1424
+ const parseResult = this.parserService.parseDocument(textDocument);
1425
+ const collector = new FoldingRangeCollector();
1426
+ collector.visit(parseResult.document);
1427
+ return collector.ranges;
1428
+ }
1429
+ }
1430
+ class FoldingRangeCollector extends core.Visitor {
1431
+ constructor() {
1432
+ super(...arguments);
1433
+ this.ranges = [];
1434
+ this.processedIfNodes = new Set();
1435
+ }
1436
+ visitHTMLElementNode(node) {
1437
+ if (node.body.length > 0 && node.open_tag && node.close_tag) {
1438
+ this.addRange(node.open_tag.location.end, node.close_tag.location.start);
1439
+ }
1440
+ this.visitChildNodes(node);
1441
+ }
1442
+ visitHTMLOpenTagNode(node) {
1443
+ if (node.children.length > 0 && node.tag_opening && node.tag_closing) {
1444
+ this.addRange(node.tag_opening.location.end, node.tag_closing.location.start);
1445
+ }
1446
+ this.visitChildNodes(node);
1447
+ }
1448
+ visitHTMLCommentNode(node$1) {
1449
+ if (node$1.comment_start && node$1.comment_end) {
1450
+ this.addRange(node$1.comment_start.location.end, node$1.comment_end.location.start, node.FoldingRangeKind.Comment);
1451
+ }
1452
+ this.visitChildNodes(node$1);
1453
+ }
1454
+ visitHTMLAttributeValueNode(node) {
1455
+ if (node.children.length > 0) {
1456
+ const first = node.children[0];
1457
+ const last = node.children[node.children.length - 1];
1458
+ this.addRange(first.location.start, last.location.end);
1459
+ }
1460
+ this.visitChildNodes(node);
1461
+ }
1462
+ visitCDATANode(node) {
1463
+ this.addRange(node.location.start, node.location.end);
1464
+ this.visitChildNodes(node);
1465
+ }
1466
+ visitHTMLConditionalElementNode(node) {
1467
+ this.addRange(node.location.start, node.location.end);
1468
+ this.visitChildNodes(node);
1469
+ }
1470
+ visitERBNode(node) {
1471
+ var _a;
1472
+ if (node.tag_closing && 'end_node' in node && ((_a = node.end_node) === null || _a === void 0 ? void 0 : _a.tag_opening)) {
1473
+ this.addRange(node.tag_closing.location.end, node.end_node.tag_opening.location.start);
1474
+ }
1475
+ else {
1476
+ this.addRange(node.location.start, node.location.end);
1477
+ }
1478
+ }
1479
+ visitERBContentNode(node) {
1480
+ if (node.tag_opening && node.tag_closing) {
1481
+ this.addRange(node.tag_opening.location.end, node.tag_closing.location.start);
1482
+ }
1483
+ this.visitChildNodes(node);
1484
+ }
1485
+ visitERBIfNode(node) {
1486
+ var _a, _b;
1487
+ if (this.processedIfNodes.has(node)) {
1488
+ this.visitChildNodes(node);
1489
+ return;
1490
+ }
1491
+ this.markIfChainAsProcessed(node);
1492
+ const nextAfterIf = (_a = node.subsequent) !== null && _a !== void 0 ? _a : node.end_node;
1493
+ if (node.tag_closing && (nextAfterIf === null || nextAfterIf === void 0 ? void 0 : nextAfterIf.tag_opening)) {
1494
+ this.addRange(node.tag_closing.location.end, nextAfterIf.tag_opening.location.start);
1495
+ }
1496
+ let current = node.subsequent;
1497
+ while (current) {
1498
+ if (core.isERBIfNode(current)) {
1499
+ const nextAfterElsif = (_b = current.subsequent) !== null && _b !== void 0 ? _b : node.end_node;
1500
+ if (current.tag_closing && (nextAfterElsif === null || nextAfterElsif === void 0 ? void 0 : nextAfterElsif.tag_opening)) {
1501
+ this.addRange(current.tag_closing.location.end, nextAfterElsif.tag_opening.location.start);
1502
+ }
1503
+ current = current.subsequent;
1504
+ }
1505
+ else {
1506
+ break;
1507
+ }
1508
+ }
1509
+ this.visitChildNodes(node);
1510
+ }
1511
+ visitERBUnlessNode(node) {
1512
+ var _a, _b;
1513
+ const nextAfterUnless = (_a = node.else_clause) !== null && _a !== void 0 ? _a : node.end_node;
1514
+ if (node.tag_closing && (nextAfterUnless === null || nextAfterUnless === void 0 ? void 0 : nextAfterUnless.tag_opening)) {
1515
+ this.addRange(node.tag_closing.location.end, nextAfterUnless.tag_opening.location.start);
1516
+ }
1517
+ if (node.else_clause) {
1518
+ if (node.else_clause.tag_closing && ((_b = node.end_node) === null || _b === void 0 ? void 0 : _b.tag_opening)) {
1519
+ this.addRange(node.else_clause.tag_closing.location.end, node.end_node.tag_opening.location.start);
1520
+ }
1521
+ }
1522
+ this.visitChildNodes(node);
1523
+ }
1524
+ visitERBCaseNode(node) {
1525
+ this.addCaseFoldingRanges(node);
1526
+ this.visitChildNodes(node);
1527
+ }
1528
+ visitERBCaseMatchNode(node) {
1529
+ this.addCaseFoldingRanges(node);
1530
+ this.visitChildNodes(node);
1531
+ }
1532
+ visitERBWhenNode(node) {
1533
+ this.visitChildNodes(node);
1534
+ }
1535
+ visitERBInNode(node) {
1536
+ this.visitChildNodes(node);
1537
+ }
1538
+ visitERBBeginNode(node) {
1539
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1540
+ 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;
1541
+ if (node.tag_closing && (nextAfterBegin === null || nextAfterBegin === void 0 ? void 0 : nextAfterBegin.tag_opening)) {
1542
+ this.addRange(node.tag_closing.location.end, nextAfterBegin.tag_opening.location.start);
1543
+ }
1544
+ let rescue = node.rescue_clause;
1545
+ while (rescue) {
1546
+ 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;
1547
+ if (rescue.tag_closing && (nextAfterRescue === null || nextAfterRescue === void 0 ? void 0 : nextAfterRescue.tag_opening)) {
1548
+ this.addRange(rescue.tag_closing.location.end, nextAfterRescue.tag_opening.location.start);
1549
+ }
1550
+ rescue = rescue.subsequent;
1551
+ }
1552
+ if (node.else_clause) {
1553
+ const nextAfterElse = (_g = node.ensure_clause) !== null && _g !== void 0 ? _g : node.end_node;
1554
+ if (node.else_clause.tag_closing && (nextAfterElse === null || nextAfterElse === void 0 ? void 0 : nextAfterElse.tag_opening)) {
1555
+ this.addRange(node.else_clause.tag_closing.location.end, nextAfterElse.tag_opening.location.start);
1556
+ }
1557
+ }
1558
+ if (node.ensure_clause) {
1559
+ if (node.ensure_clause.tag_closing && ((_h = node.end_node) === null || _h === void 0 ? void 0 : _h.tag_opening)) {
1560
+ this.addRange(node.ensure_clause.tag_closing.location.end, node.end_node.tag_opening.location.start);
1561
+ }
1562
+ }
1563
+ this.visitChildNodes(node);
1564
+ }
1565
+ visitERBRescueNode(node) {
1566
+ this.visitChildNodes(node);
1567
+ }
1568
+ visitERBElseNode(node) {
1569
+ this.addRange(node.location.start, node.location.end);
1570
+ this.visitChildNodes(node);
1571
+ }
1572
+ visitERBEnsureNode(node) {
1573
+ this.visitChildNodes(node);
1574
+ }
1575
+ markIfChainAsProcessed(node) {
1576
+ this.processedIfNodes.add(node);
1577
+ let current = node.subsequent;
1578
+ while (current) {
1579
+ if (core.isERBIfNode(current)) {
1580
+ this.processedIfNodes.add(current);
1581
+ current = current.subsequent;
1582
+ }
1583
+ else {
1584
+ break;
1585
+ }
1586
+ }
1587
+ }
1588
+ addCaseFoldingRanges(node) {
1589
+ var _a, _b, _c, _d;
1590
+ const conditions = node.conditions;
1591
+ const firstCondition = conditions[0];
1592
+ const nextAfterCase = (_a = firstCondition !== null && firstCondition !== void 0 ? firstCondition : node.else_clause) !== null && _a !== void 0 ? _a : node.end_node;
1593
+ if (node.tag_closing && (nextAfterCase === null || nextAfterCase === void 0 ? void 0 : nextAfterCase.tag_opening)) {
1594
+ this.addRange(node.tag_closing.location.end, nextAfterCase.tag_opening.location.start);
1595
+ }
1596
+ for (let i = 0; i < conditions.length; i++) {
1597
+ const condition = conditions[i];
1598
+ const nextCondition = (_c = (_b = conditions[i + 1]) !== null && _b !== void 0 ? _b : node.else_clause) !== null && _c !== void 0 ? _c : node.end_node;
1599
+ if (condition.tag_closing && (nextCondition === null || nextCondition === void 0 ? void 0 : nextCondition.tag_opening)) {
1600
+ this.addRange(condition.tag_closing.location.end, nextCondition.tag_opening.location.start);
1601
+ }
1602
+ }
1603
+ if (node.else_clause) {
1604
+ if (node.else_clause.tag_closing && ((_d = node.end_node) === null || _d === void 0 ? void 0 : _d.tag_opening)) {
1605
+ this.addRange(node.else_clause.tag_closing.location.end, node.end_node.tag_opening.location.start);
1606
+ }
1607
+ }
1608
+ }
1609
+ addRange(start, end, kind) {
1610
+ const startLine = lspLine(start);
1611
+ const endLine = lspLine(end) - 1;
1612
+ if (endLine > startLine) {
1613
+ this.ranges.push({
1614
+ startLine,
1615
+ startCharacter: start.column,
1616
+ endLine,
1617
+ endCharacter: end.column,
1618
+ kind,
1619
+ });
1620
+ }
1621
+ }
1622
+ }
1623
+
1624
+ class DocumentHighlightCollector extends core.Visitor {
1625
+ constructor() {
1626
+ super(...arguments);
1627
+ this.groups = [];
1628
+ this.processedIfNodes = new Set();
1629
+ }
1630
+ visitERBNode(node) {
1631
+ if ('end_node' in node && node.end_node) {
1632
+ this.addGroup([erbTagToRange(node), erbTagToRange(node.end_node)]);
1633
+ }
1634
+ }
1635
+ visitERBContentNode(node) {
1636
+ this.addGroup([tokenToRange(node.tag_opening), tokenToRange(node.tag_closing)]);
1637
+ this.visitChildNodes(node);
1638
+ }
1639
+ visitHTMLCommentNode(node) {
1640
+ this.addGroup([tokenToRange(node.comment_start), tokenToRange(node.comment_end)]);
1641
+ this.visitChildNodes(node);
1642
+ }
1643
+ visitHTMLElementNode(node) {
1644
+ const ranges = [];
1645
+ if (node.open_tag && core.isHTMLOpenTagNode(node.open_tag)) {
1646
+ ranges.push(...openTagRanges(node.open_tag));
1647
+ }
1648
+ else if (node.open_tag) {
1649
+ ranges.push(nodeToRange(node.open_tag));
1650
+ }
1651
+ if (node.close_tag) {
1652
+ ranges.push(nodeToRange(node.close_tag));
1653
+ }
1654
+ this.addGroup(ranges);
1655
+ this.visitChildNodes(node);
1656
+ }
1657
+ visitHTMLAttributeNode(node) {
1658
+ const ranges = [];
1659
+ if (node.name) {
1660
+ ranges.push(nodeToRange(node.name));
1661
+ }
1662
+ if (node.equals) {
1663
+ ranges.push(tokenToRange(node.equals));
1664
+ }
1665
+ if (node.value) {
1666
+ ranges.push(nodeToRange(node.value));
1667
+ }
1668
+ this.addGroup(ranges);
1669
+ this.visitChildNodes(node);
1670
+ }
1671
+ visitHTMLConditionalElementNode(node) {
1672
+ const ranges = [];
1673
+ if (node.open_conditional) {
1674
+ ranges.push(erbTagToRange(node.open_conditional));
1675
+ }
1676
+ if (node.open_tag) {
1677
+ ranges.push(...openTagRanges(node.open_tag));
1678
+ }
1679
+ if (node.close_tag) {
1680
+ ranges.push(nodeToRange(node.close_tag));
1681
+ }
1682
+ if (node.close_conditional) {
1683
+ ranges.push(erbTagToRange(node.close_conditional));
1684
+ }
1685
+ this.addGroup(ranges);
1686
+ this.visitChildNodes(node);
1687
+ }
1688
+ visitERBIfNode(node) {
1689
+ if (this.processedIfNodes.has(node)) {
1690
+ this.visitChildNodes(node);
1691
+ return;
1692
+ }
1693
+ this.markIfChainAsProcessed(node);
1694
+ const ranges = [];
1695
+ ranges.push(erbTagToRange(node));
1696
+ let current = node.subsequent;
1697
+ while (current) {
1698
+ if (core.isERBIfNode(current)) {
1699
+ ranges.push(erbTagToRange(current));
1700
+ current = current.subsequent;
1701
+ }
1702
+ else if (core.isERBElseNode(current)) {
1703
+ ranges.push(erbTagToRange(current));
1704
+ break;
1705
+ }
1706
+ else {
1707
+ break;
1708
+ }
1709
+ }
1710
+ if (node.end_node) {
1711
+ ranges.push(erbTagToRange(node.end_node));
1712
+ }
1713
+ this.addGroup(ranges);
1714
+ this.visitChildNodes(node);
1715
+ }
1716
+ visitERBUnlessNode(node) {
1717
+ const ranges = [];
1718
+ ranges.push(erbTagToRange(node));
1719
+ if (node.else_clause) {
1720
+ ranges.push(erbTagToRange(node.else_clause));
1721
+ }
1722
+ if (node.end_node) {
1723
+ ranges.push(erbTagToRange(node.end_node));
1724
+ }
1725
+ this.addGroup(ranges);
1726
+ this.visitChildNodes(node);
1727
+ }
1728
+ visitERBCaseNode(node) {
1729
+ this.visitERBAnyCaseNode(node);
1730
+ }
1731
+ visitERBCaseMatchNode(node) {
1732
+ this.visitERBAnyCaseNode(node);
1733
+ }
1734
+ visitERBAnyCaseNode(node) {
1735
+ const ranges = [];
1736
+ ranges.push(erbTagToRange(node));
1737
+ for (const condition of node.conditions) {
1738
+ ranges.push(erbTagToRange(condition));
1739
+ }
1740
+ if (node.else_clause) {
1741
+ ranges.push(erbTagToRange(node.else_clause));
1742
+ }
1743
+ if (node.end_node) {
1744
+ ranges.push(erbTagToRange(node.end_node));
1745
+ }
1746
+ this.addGroup(ranges);
1747
+ this.visitChildNodes(node);
1748
+ }
1749
+ visitERBBeginNode(node) {
1750
+ const ranges = [];
1751
+ ranges.push(erbTagToRange(node));
1752
+ let rescue = node.rescue_clause;
1753
+ while (rescue) {
1754
+ ranges.push(erbTagToRange(rescue));
1755
+ rescue = rescue.subsequent;
1756
+ }
1757
+ if (node.else_clause) {
1758
+ ranges.push(erbTagToRange(node.else_clause));
1759
+ }
1760
+ if (node.ensure_clause) {
1761
+ ranges.push(erbTagToRange(node.ensure_clause));
1762
+ }
1763
+ if (node.end_node) {
1764
+ ranges.push(erbTagToRange(node.end_node));
1765
+ }
1766
+ this.addGroup(ranges);
1767
+ this.visitChildNodes(node);
1768
+ }
1769
+ markIfChainAsProcessed(node) {
1770
+ this.processedIfNodes.add(node);
1771
+ let current = node.subsequent;
1772
+ while (current) {
1773
+ if (core.isERBIfNode(current)) {
1774
+ this.processedIfNodes.add(current);
1775
+ current = current.subsequent;
1776
+ }
1777
+ else {
1778
+ break;
1779
+ }
1780
+ }
1781
+ }
1782
+ addGroup(ranges) {
1783
+ const filtered = ranges.filter((r) => r !== null);
1784
+ if (filtered.length >= 2) {
1785
+ this.groups.push(filtered);
1786
+ }
1787
+ }
1788
+ }
1789
+ class DocumentHighlightService {
1790
+ constructor(parserService) {
1791
+ this.parserService = parserService;
1792
+ }
1793
+ getDocumentHighlights(textDocument, position) {
1794
+ const parseResult = this.parserService.parseDocument(textDocument);
1795
+ const collector = new DocumentHighlightCollector();
1796
+ collector.visit(parseResult.document);
1797
+ let bestGroup = null;
1798
+ let bestSize = Infinity;
1799
+ for (const group of collector.groups) {
1800
+ const matchingRange = group.find(range => isPositionInRange(position, range));
1801
+ if (matchingRange) {
1802
+ const size = rangeSize(matchingRange);
1803
+ if (size < bestSize) {
1804
+ bestSize = size;
1805
+ bestGroup = group;
1806
+ }
1807
+ }
1808
+ }
1809
+ if (bestGroup) {
1810
+ return bestGroup.map(range => node.DocumentHighlight.create(range, node.DocumentHighlightKind.Text));
1811
+ }
1812
+ return [];
1813
+ }
1814
+ }
1815
+
1816
+ const ACTION_VIEW_HELPERS = {
1817
+ "ActionView::Helpers::TagHelper#tag": {
1818
+ signature: "tag.<tag name>(optional content, options)",
1819
+ documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag",
1820
+ },
1821
+ "ActionView::Helpers::TagHelper#content_tag": {
1822
+ signature: "content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)",
1823
+ documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag",
1824
+ },
1825
+ "ActionView::Helpers::UrlHelper#link_to": {
1826
+ signature: "link_to(name = nil, options = nil, html_options = nil, &block)",
1827
+ documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to",
1828
+ },
1829
+ "Turbo::FramesHelper#turbo_frame_tag": {
1830
+ signature: "turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)",
1831
+ documentationURL: "https://www.rubydoc.info/github/hotwired/turbo-rails/Turbo/FramesHelper:turbo_frame_tag",
1832
+ },
1833
+ };
1834
+
1835
+ class ActionViewElementCollector extends nodeWasm.Visitor {
1836
+ constructor() {
1837
+ super(...arguments);
1838
+ this.elements = [];
1839
+ }
1840
+ visitHTMLElementNode(node) {
1841
+ if (node.element_source && node.element_source !== "HTML" && core.isERBOpenTagNode(node.open_tag)) {
1842
+ this.elements.push({
1843
+ node,
1844
+ range: nodeToRange(node),
1845
+ });
1846
+ }
1847
+ this.visitChildNodes(node);
1848
+ }
1849
+ }
1850
+ class HoverService {
1851
+ constructor(parserService) {
1852
+ this.parserService = parserService;
1853
+ }
1854
+ getHover(textDocument, position) {
1855
+ const parseResult = this.parserService.parseContent(textDocument.getText(), {
1856
+ action_view_helpers: true,
1857
+ track_whitespace: true,
1858
+ });
1859
+ const collector = new ActionViewElementCollector();
1860
+ collector.visit(parseResult.value);
1861
+ let bestElement = null;
1862
+ let bestSize = Infinity;
1863
+ for (const element of collector.elements) {
1864
+ if (isPositionInRange(position, element.range)) {
1865
+ const size = rangeSize(element.range);
1866
+ if (size < bestSize) {
1867
+ bestSize = size;
1868
+ bestElement = element;
1869
+ }
1870
+ }
1871
+ }
1872
+ if (!bestElement) {
1873
+ return null;
1874
+ }
1875
+ const elementSource = bestElement.node.element_source;
1876
+ const rewriter$1 = new rewriter.ActionViewTagHelperToHTMLRewriter();
1877
+ const rewrittenNode = rewriter$1.rewrite(bestElement.node, { baseDir: process.cwd() });
1878
+ const htmlOutput = printer.IdentityPrinter.print(rewrittenNode);
1879
+ const helper = ACTION_VIEW_HELPERS[elementSource];
1880
+ const parts = [];
1881
+ if (helper) {
1882
+ parts.push(`\`\`\`ruby\n${helper.signature}\n\`\`\``);
1883
+ }
1884
+ parts.push(`**HTML equivalent**\n\`\`\`erb\n${htmlOutput.trim()}\n\`\`\``);
1885
+ if (helper) {
1886
+ parts.push(`[${elementSource}](${helper.documentationURL})`);
1887
+ }
1888
+ return {
1889
+ contents: {
1890
+ kind: node.MarkupKind.Markdown,
1891
+ value: parts.join("\n\n"),
1892
+ },
1893
+ range: bestElement.range,
1894
+ };
1895
+ }
1896
+ }
1897
+
1898
+ class ElementCollector extends nodeWasm.Visitor {
1899
+ constructor() {
1900
+ super(...arguments);
1901
+ this.actionViewElements = [];
1902
+ this.htmlElements = [];
1903
+ }
1904
+ visitHTMLElementNode(node) {
1905
+ if (node.element_source && node.element_source !== "HTML" && core.isERBOpenTagNode(node.open_tag)) {
1906
+ this.actionViewElements.push({
1907
+ node,
1908
+ range: nodeToRange(node),
1909
+ });
1910
+ }
1911
+ else if (core.isHTMLOpenTagNode(node.open_tag) && node.open_tag.tag_name) {
1912
+ this.htmlElements.push({
1913
+ node,
1914
+ range: nodeToRange(node),
1915
+ });
1916
+ }
1917
+ this.visitChildNodes(node);
1918
+ }
1919
+ }
1920
+ class RewriteCodeActionService {
1921
+ constructor(parserService) {
1922
+ this.parserService = parserService;
1923
+ }
1924
+ getCodeActions(document, requestedRange) {
1925
+ const parseResult = this.parserService.parseContent(document.getText(), {
1926
+ action_view_helpers: true,
1927
+ track_whitespace: true,
1928
+ });
1929
+ const collector = new ElementCollector();
1930
+ collector.visit(parseResult.value);
1931
+ const actions = [];
1932
+ for (const element of collector.actionViewElements) {
1933
+ if (!this.rangesOverlap(element.range, requestedRange))
1934
+ continue;
1935
+ const action = this.createActionViewToHTMLAction(document, element);
1936
+ if (action) {
1937
+ actions.push(action);
1938
+ }
1939
+ }
1940
+ for (const element of collector.htmlElements) {
1941
+ if (!this.rangesOverlap(element.range, requestedRange))
1942
+ continue;
1943
+ const action = this.createHTMLToActionViewAction(document, element);
1944
+ if (action) {
1945
+ actions.push(action);
1946
+ }
1947
+ }
1948
+ return actions;
1949
+ }
1950
+ createActionViewToHTMLAction(document, element) {
1951
+ var _a;
1952
+ const originalText = document.getText(element.range);
1953
+ const parseResult = this.parserService.parseContent(originalText, {
1954
+ action_view_helpers: true,
1955
+ track_whitespace: true,
1956
+ });
1957
+ if (parseResult.failed)
1958
+ return null;
1959
+ const rewriter$1 = new rewriter.ActionViewTagHelperToHTMLRewriter();
1960
+ rewriter$1.rewrite(parseResult.value, { baseDir: process.cwd() });
1961
+ const rewrittenText = printer.IdentityPrinter.print(parseResult.value);
1962
+ if (rewrittenText === originalText)
1963
+ return null;
1964
+ const edit = {
1965
+ changes: {
1966
+ [document.uri]: [node.TextEdit.replace(element.range, rewrittenText)]
1967
+ }
1968
+ };
1969
+ const tagName = (_a = element.node.tag_name) === null || _a === void 0 ? void 0 : _a.value;
1970
+ const title = tagName
1971
+ ? `Herb: Convert to \`<${tagName}>\``
1972
+ : "Herb: Convert to HTML";
1973
+ return {
1974
+ title,
1975
+ kind: node.CodeActionKind.RefactorRewrite,
1976
+ edit,
1353
1977
  };
1354
- return this.formattingService.formatOnSave(autofixedDocument, reason);
1978
+ }
1979
+ createHTMLToActionViewAction(document, element) {
1980
+ var _a;
1981
+ const originalText = document.getText(element.range);
1982
+ const parseResult = this.parserService.parseContent(originalText, {
1983
+ track_whitespace: true,
1984
+ });
1985
+ if (parseResult.failed)
1986
+ return null;
1987
+ const rewriter$1 = new rewriter.HTMLToActionViewTagHelperRewriter();
1988
+ rewriter$1.rewrite(parseResult.value, { baseDir: process.cwd() });
1989
+ const rewrittenText = printer.IdentityPrinter.print(parseResult.value);
1990
+ if (rewrittenText === originalText)
1991
+ return null;
1992
+ const edit = {
1993
+ changes: {
1994
+ [document.uri]: [node.TextEdit.replace(element.range, rewrittenText)]
1995
+ }
1996
+ };
1997
+ const tagName = (_a = element.node.tag_name) === null || _a === void 0 ? void 0 : _a.value;
1998
+ const isAnchor = tagName === "a";
1999
+ const isTurboFrame = tagName === "turbo-frame";
2000
+ const methodName = tagName === null || tagName === void 0 ? void 0 : tagName.replace(/-/g, "_");
2001
+ const title = isAnchor
2002
+ ? "Herb: Convert to `link_to`"
2003
+ : isTurboFrame
2004
+ ? "Herb: Convert to `turbo_frame_tag`"
2005
+ : methodName
2006
+ ? `Herb: Convert to \`tag.${methodName}\``
2007
+ : "Herb: Convert to tag helper";
2008
+ return {
2009
+ title,
2010
+ kind: node.CodeActionKind.RefactorRewrite,
2011
+ edit,
2012
+ };
2013
+ }
2014
+ rangesOverlap(r1, r2) {
2015
+ if (r1.end.line < r2.start.line)
2016
+ return false;
2017
+ if (r1.start.line > r2.end.line)
2018
+ return false;
2019
+ if (r1.end.line === r2.start.line && r1.end.character < r2.start.character)
2020
+ return false;
2021
+ if (r1.start.line === r2.end.line && r1.start.character > r2.end.character)
2022
+ return false;
2023
+ return true;
2024
+ }
2025
+ }
2026
+
2027
+ class LineContextCollector extends core.Visitor {
2028
+ constructor() {
2029
+ super(...arguments);
2030
+ this.lineMap = new Map();
2031
+ this.erbNodesPerLine = new Map();
2032
+ this.htmlCommentNodesPerLine = new Map();
2033
+ }
2034
+ visitERBNode(node) {
2035
+ if (!node.tag_opening || !node.tag_closing)
2036
+ return;
2037
+ const startLine = lspLine(node.tag_opening.location.start);
2038
+ const nodes = this.erbNodesPerLine.get(startLine) || [];
2039
+ nodes.push(node);
2040
+ this.erbNodesPerLine.set(startLine, nodes);
2041
+ if (core.isERBCommentNode(node)) {
2042
+ this.setLine(startLine, "erb-comment", node);
2043
+ }
2044
+ else {
2045
+ this.setLine(startLine, "erb-tag", node);
2046
+ }
2047
+ }
2048
+ visitERBContentNode(node) {
2049
+ this.visitERBNode(node);
2050
+ this.visitChildNodes(node);
2051
+ }
2052
+ visitHTMLCommentNode(node) {
2053
+ const startLine = lspLine(node.location.start);
2054
+ const endLine = lspLine(node.location.end);
2055
+ for (let line = startLine; line <= endLine; line++) {
2056
+ this.htmlCommentNodesPerLine.set(line, node);
2057
+ this.setLine(line, "html-comment", node);
2058
+ }
2059
+ this.visitChildNodes(node);
2060
+ }
2061
+ visitHTMLElementNode(node) {
2062
+ const startLine = lspLine(node.location.start);
2063
+ const endLine = lspLine(node.location.end);
2064
+ for (let line = startLine; line <= endLine; line++) {
2065
+ if (!this.lineMap.has(line)) {
2066
+ this.setLine(line, "html-content", node);
2067
+ }
2068
+ }
2069
+ this.visitChildNodes(node);
2070
+ }
2071
+ visitHTMLTextNode(node) {
2072
+ const startLine = lspLine(node.location.start);
2073
+ const endLine = lspLine(node.location.end);
2074
+ for (let line = startLine; line <= endLine; line++) {
2075
+ if (!this.lineMap.has(line)) {
2076
+ this.setLine(line, "html-content", node);
2077
+ }
2078
+ }
2079
+ this.visitChildNodes(node);
2080
+ }
2081
+ setLine(line, context, node) {
2082
+ const existing = this.lineMap.get(line);
2083
+ if (existing) {
2084
+ if (existing.context === "erb-comment" || existing.context === "erb-tag")
2085
+ return;
2086
+ if (context === "erb-comment" || context === "erb-tag") {
2087
+ this.lineMap.set(line, { line, context, node });
2088
+ return;
2089
+ }
2090
+ if (existing.context === "html-comment")
2091
+ return;
2092
+ }
2093
+ this.lineMap.set(line, { line, context, node });
2094
+ }
2095
+ }
2096
+
2097
+ function commentERBNode(node) {
2098
+ const mutable = rewriter.asMutable(node);
2099
+ if (mutable.tag_opening) {
2100
+ const currentValue = mutable.tag_opening.value;
2101
+ mutable.tag_opening = core.createSyntheticToken(currentValue.substring(0, 2) + "#" + currentValue.substring(2), mutable.tag_opening.type);
2102
+ }
2103
+ }
2104
+ function uncommentERBNode(node) {
2105
+ var _a;
2106
+ const mutable = rewriter.asMutable(node);
2107
+ if (mutable.tag_opening && mutable.tag_opening.value === "<%#") {
2108
+ const contentValue = ((_a = mutable.content) === null || _a === void 0 ? void 0 : _a.value) || "";
2109
+ if (contentValue.startsWith(" graphql ") ||
2110
+ contentValue.startsWith(" %= ") ||
2111
+ contentValue.startsWith(" == ") ||
2112
+ contentValue.startsWith(" % ") ||
2113
+ contentValue.startsWith(" = ") ||
2114
+ contentValue.startsWith(" - ")) {
2115
+ mutable.tag_opening = core.createSyntheticToken("<%", mutable.tag_opening.type);
2116
+ mutable.content = core.createSyntheticToken(contentValue.substring(1), mutable.content.type);
2117
+ }
2118
+ else {
2119
+ mutable.tag_opening = core.createSyntheticToken("<%", mutable.tag_opening.type);
2120
+ }
2121
+ }
2122
+ }
2123
+ function determineStrategy(erbNodes, lineText) {
2124
+ if (erbNodes.length === 0) {
2125
+ return "html-only";
2126
+ }
2127
+ if (erbNodes.length === 1) {
2128
+ const node = erbNodes[0];
2129
+ if (!node.tag_opening || !node.tag_closing)
2130
+ return "html-only";
2131
+ const nodeStart = node.tag_opening.location.start.column;
2132
+ const nodeEnd = node.tag_closing.location.end.column;
2133
+ const isSoleContent = lineText.substring(0, nodeStart).trim() === "" && lineText.substring(nodeEnd).trim() === "";
2134
+ if (isSoleContent) {
2135
+ return "single-erb";
2136
+ }
2137
+ return "whole-line";
2138
+ }
2139
+ const segments = getLineSegments(lineText, erbNodes);
2140
+ const hasHTML = segments.some(segment => !segment.isERB && segment.text.trim() !== "");
2141
+ if (!hasHTML) {
2142
+ return "all-erb";
2143
+ }
2144
+ const allControlTags = erbNodes.every(node => { var _a; return ((_a = node.tag_opening) === null || _a === void 0 ? void 0 : _a.value) === "<%"; });
2145
+ if (allControlTags) {
2146
+ return "per-segment";
2147
+ }
2148
+ return "whole-line";
2149
+ }
2150
+ function getLineSegments(lineText, erbNodes) {
2151
+ const segments = [];
2152
+ const sorted = [...erbNodes].sort((a, b) => a.tag_opening.location.start.column - b.tag_opening.location.start.column);
2153
+ let position = 0;
2154
+ for (const node of sorted) {
2155
+ const nodeStart = node.tag_opening.location.start.column;
2156
+ const nodeEnd = node.tag_closing.location.end.column;
2157
+ if (nodeStart > position) {
2158
+ segments.push({ text: lineText.substring(position, nodeStart), isERB: false });
2159
+ }
2160
+ segments.push({ text: lineText.substring(nodeStart, nodeEnd), isERB: true, node });
2161
+ position = nodeEnd;
2162
+ }
2163
+ if (position < lineText.length) {
2164
+ segments.push({ text: lineText.substring(position), isERB: false });
2165
+ }
2166
+ return segments;
2167
+ }
2168
+ /**
2169
+ * Comment a line using AST mutation for strategies where the parser produces flat children,
2170
+ * and text-segment manipulation for per-segment (where the parser nests nodes).
2171
+ */
2172
+ function commentLineContent(content, erbNodes, strategy, parserService) {
2173
+ if (strategy === "per-segment") {
2174
+ return commentPerSegment(content, erbNodes);
2175
+ }
2176
+ const parseResult = parserService.parseContent(content, { track_whitespace: true });
2177
+ const lineCollector = new LineContextCollector();
2178
+ parseResult.visit(lineCollector);
2179
+ const lineERBNodes = lineCollector.erbNodesPerLine.get(0) || [];
2180
+ const document = parseResult.value;
2181
+ const children = rewriter.asMutable(document).children;
2182
+ switch (strategy) {
2183
+ case "all-erb":
2184
+ for (const node of lineERBNodes) {
2185
+ commentERBNode(node);
2186
+ }
2187
+ break;
2188
+ case "whole-line": {
2189
+ for (const node of lineERBNodes) {
2190
+ commentERBNode(node);
2191
+ }
2192
+ const commentNode = new core.HTMLCommentNode({
2193
+ type: "AST_HTML_COMMENT_NODE",
2194
+ location: core.Location.zero,
2195
+ errors: [],
2196
+ comment_start: core.createSyntheticToken("<!--", "TOKEN_HTML_COMMENT_START"),
2197
+ children: [core.createLiteral(" "), ...children.slice(), core.createLiteral(" ")],
2198
+ comment_end: core.createSyntheticToken("-->", "TOKEN_HTML_COMMENT_END"),
2199
+ });
2200
+ children.length = 0;
2201
+ children.push(commentNode);
2202
+ break;
2203
+ }
2204
+ case "html-only": {
2205
+ const commentNode = new core.HTMLCommentNode({
2206
+ type: "AST_HTML_COMMENT_NODE",
2207
+ location: core.Location.zero,
2208
+ errors: [],
2209
+ comment_start: core.createSyntheticToken("<!--", "TOKEN_HTML_COMMENT_START"),
2210
+ children: [core.createLiteral(" "), ...children.slice(), core.createLiteral(" ")],
2211
+ comment_end: core.createSyntheticToken("-->", "TOKEN_HTML_COMMENT_END"),
2212
+ });
2213
+ children.length = 0;
2214
+ children.push(commentNode);
2215
+ break;
2216
+ }
2217
+ }
2218
+ return printer.IdentityPrinter.print(document, { ignoreErrors: true });
2219
+ }
2220
+ /**
2221
+ * Per-segment commenting uses text segments because the parser creates nested
2222
+ * structures (e.g., ERBIfNode) that don't allow flat child iteration.
2223
+ */
2224
+ function commentPerSegment(content, erbNodes) {
2225
+ const segments = getLineSegments(content, erbNodes);
2226
+ return segments.map(segment => {
2227
+ if (segment.isERB) {
2228
+ return segment.text.substring(0, 2) + "#" + segment.text.substring(2);
2229
+ }
2230
+ else if (segment.text.trim() !== "") {
2231
+ return `<!-- ${segment.text} -->`;
2232
+ }
2233
+ return segment.text;
2234
+ }).join("");
2235
+ }
2236
+ function uncommentLineContent(content, parserService) {
2237
+ const parseResult = parserService.parseContent(content, { track_whitespace: true });
2238
+ const lineCollector = new LineContextCollector();
2239
+ parseResult.visit(lineCollector);
2240
+ const lineERBNodes = lineCollector.erbNodesPerLine.get(0) || [];
2241
+ const document = parseResult.value;
2242
+ const children = rewriter.asMutable(document).children;
2243
+ for (const node of lineERBNodes) {
2244
+ if (core.isERBCommentNode(node)) {
2245
+ uncommentERBNode(node);
2246
+ }
2247
+ }
2248
+ let index = 0;
2249
+ while (index < children.length) {
2250
+ const child = children[index];
2251
+ if (child.type === "AST_HTML_COMMENT_NODE") {
2252
+ const commentNode = child;
2253
+ const innerChildren = [...commentNode.children];
2254
+ if (innerChildren.length > 0) {
2255
+ const first = innerChildren[0];
2256
+ if (core.isLiteralNode(first) && first.content.startsWith(" ")) {
2257
+ const trimmed = first.content.substring(1);
2258
+ if (trimmed === "") {
2259
+ innerChildren.shift();
2260
+ }
2261
+ else {
2262
+ innerChildren[0] = core.createLiteral(trimmed);
2263
+ }
2264
+ }
2265
+ }
2266
+ if (innerChildren.length > 0) {
2267
+ const last = innerChildren[innerChildren.length - 1];
2268
+ if (core.isLiteralNode(last) && last.content.endsWith(" ")) {
2269
+ const trimmed = last.content.substring(0, last.content.length - 1);
2270
+ if (trimmed === "") {
2271
+ innerChildren.pop();
2272
+ }
2273
+ else {
2274
+ innerChildren[innerChildren.length - 1] = core.createLiteral(trimmed);
2275
+ }
2276
+ }
2277
+ }
2278
+ const innerERBNodes = [];
2279
+ const innerCollector = new LineContextCollector();
2280
+ for (const innerChild of innerChildren) {
2281
+ innerCollector.visit(innerChild);
2282
+ }
2283
+ innerERBNodes.push(...(innerCollector.erbNodesPerLine.get(0) || []));
2284
+ for (const erbNode of innerERBNodes) {
2285
+ if (core.isERBCommentNode(erbNode)) {
2286
+ uncommentERBNode(erbNode);
2287
+ }
2288
+ }
2289
+ children.splice(index, 1, ...innerChildren);
2290
+ index += innerChildren.length;
2291
+ continue;
2292
+ }
2293
+ index++;
2294
+ }
2295
+ return printer.IdentityPrinter.print(document, { ignoreErrors: true });
2296
+ }
2297
+
2298
+ class CommentService {
2299
+ constructor(parserService) {
2300
+ this.parserService = parserService;
2301
+ }
2302
+ toggleLineComment(document, range) {
2303
+ const parseResult = this.parserService.parseDocument(document);
2304
+ const collector = new LineContextCollector();
2305
+ collector.visit(parseResult.document);
2306
+ const startLine = range.start.line;
2307
+ const endLine = range.end.line;
2308
+ const lineInfos = [];
2309
+ for (let line = startLine; line <= endLine; line++) {
2310
+ const lineText = document.getText(node.Range.create(line, 0, line + 1, 0)).replace(/\n$/, "");
2311
+ if (lineText.trim() === "") {
2312
+ continue;
2313
+ }
2314
+ if (this.lineIsIfFalseWrapped(lineText) !== null) {
2315
+ lineInfos.push({ line, context: "erb-comment", node: null });
2316
+ continue;
2317
+ }
2318
+ const htmlCommentNode = collector.htmlCommentNodesPerLine.get(line);
2319
+ const info = collector.lineMap.get(line);
2320
+ if (htmlCommentNode && this.htmlCommentSpansLine(htmlCommentNode, lineText)) {
2321
+ lineInfos.push({ line, context: "html-comment", node: htmlCommentNode });
2322
+ }
2323
+ else if (info) {
2324
+ if (info.context === "html-comment") {
2325
+ lineInfos.push({ line, context: "html-content", node: null });
2326
+ }
2327
+ else {
2328
+ lineInfos.push(info);
2329
+ }
2330
+ }
2331
+ else {
2332
+ lineInfos.push({ line, context: "html-content", node: null });
2333
+ }
2334
+ }
2335
+ if (lineInfos.length === 0)
2336
+ return [];
2337
+ const allCommented = lineInfos.every(info => info.context === "erb-comment" || info.context === "html-comment");
2338
+ const edits = [];
2339
+ if (allCommented) {
2340
+ for (const info of lineInfos) {
2341
+ const lineText = document.getText(node.Range.create(info.line, 0, info.line + 1, 0)).replace(/\n$/, "");
2342
+ const edit = this.uncommentLine(info, lineText, collector);
2343
+ if (edit)
2344
+ edits.push(edit);
2345
+ }
2346
+ }
2347
+ else {
2348
+ for (const info of lineInfos) {
2349
+ if (info.context === "erb-comment" || info.context === "html-comment")
2350
+ continue;
2351
+ const lineText = document.getText(node.Range.create(info.line, 0, info.line + 1, 0)).replace(/\n$/, "");
2352
+ const erbNodes = collector.erbNodesPerLine.get(info.line) || [];
2353
+ const edit = this.commentLine(info, lineText, erbNodes, collector);
2354
+ if (edit)
2355
+ edits.push(edit);
2356
+ }
2357
+ }
2358
+ return edits;
2359
+ }
2360
+ toggleBlockComment(document, range) {
2361
+ const startLine = range.start.line;
2362
+ const endLine = range.end.line;
2363
+ const firstLineText = document.getText(node.Range.create(startLine, 0, startLine + 1, 0)).replace(/\n$/, "");
2364
+ const lastLineText = document.getText(node.Range.create(endLine, 0, endLine + 1, 0)).replace(/\n$/, "");
2365
+ const isWrapped = firstLineText.trim() === "<% if false %>" && lastLineText.trim() === "<% end %>";
2366
+ if (isWrapped) {
2367
+ return [
2368
+ node.TextEdit.del(node.Range.create(endLine, 0, endLine + 1, 0)),
2369
+ node.TextEdit.del(node.Range.create(startLine, 0, startLine + 1, 0)),
2370
+ ];
2371
+ }
2372
+ else {
2373
+ const firstLineIndent = this.getIndentation(firstLineText);
2374
+ return [
2375
+ node.TextEdit.insert(node.Position.create(endLine + 1, 0), `${firstLineIndent}<% end %>\n`),
2376
+ node.TextEdit.insert(node.Position.create(startLine, 0), `${firstLineIndent}<% if false %>\n`),
2377
+ ];
2378
+ }
2379
+ }
2380
+ commentLine(info, lineText, erbNodes, collector) {
2381
+ const lineRange = node.Range.create(info.line, 0, info.line, lineText.length);
2382
+ const indent = this.getIndentation(lineText);
2383
+ const content = lineText.trimStart();
2384
+ const htmlCommentNode = collector.htmlCommentNodesPerLine.get(info.line);
2385
+ if (htmlCommentNode) {
2386
+ return node.TextEdit.replace(lineRange, `${indent}<% if false %>${content}<% end %>`);
2387
+ }
2388
+ const strategy = determineStrategy(erbNodes, lineText);
2389
+ if (strategy === "single-erb") {
2390
+ const node$1 = erbNodes[0];
2391
+ const insertColumn = node$1.tag_opening.location.start.column + 2;
2392
+ return node.TextEdit.insert(node.Position.create(info.line, insertColumn), "#");
2393
+ }
2394
+ const result = commentLineContent(content, erbNodes, strategy, this.parserService);
2395
+ return node.TextEdit.replace(lineRange, indent + result);
2396
+ }
2397
+ lineIsIfFalseWrapped(lineText) {
2398
+ const trimmed = lineText.trimStart();
2399
+ const indent = this.getIndentation(lineText);
2400
+ if (trimmed.startsWith("<% if false %>") && trimmed.endsWith("<% end %>")) {
2401
+ const inner = trimmed.slice("<% if false %>".length, -"<% end %>".length);
2402
+ return indent + inner;
2403
+ }
2404
+ return null;
2405
+ }
2406
+ uncommentLine(info, lineText, collector) {
2407
+ var _a;
2408
+ const lineRange = node.Range.create(info.line, 0, info.line, lineText.length);
2409
+ const indent = this.getIndentation(lineText);
2410
+ const ifFalseContent = this.lineIsIfFalseWrapped(lineText);
2411
+ if (ifFalseContent !== null) {
2412
+ return node.TextEdit.replace(lineRange, ifFalseContent);
2413
+ }
2414
+ if (info.context === "erb-comment") {
2415
+ const node$1 = info.node;
2416
+ 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))
2417
+ return null;
2418
+ const contentValue = (_a = node$1.content) === null || _a === void 0 ? void 0 : _a.value;
2419
+ const trimmedContent = (contentValue === null || contentValue === void 0 ? void 0 : contentValue.trim()) || "";
2420
+ if (trimmedContent.startsWith("<") && !trimmedContent.startsWith("<%")) {
2421
+ return node.TextEdit.replace(lineRange, `${indent}${trimmedContent}`);
2422
+ }
2423
+ if (lspLine(node$1.tag_opening.location.start) !== info.line)
2424
+ return null;
2425
+ const erbNodes = collector.erbNodesPerLine.get(info.line) || [];
2426
+ if (erbNodes.length > 1) {
2427
+ const content = lineText.trimStart();
2428
+ const result = uncommentLineContent(content, this.parserService);
2429
+ return node.TextEdit.replace(lineRange, indent + result);
2430
+ }
2431
+ const hashColumn = node$1.tag_opening.location.start.column + 2;
2432
+ if ((contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" graphql ")) ||
2433
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" %= ")) ||
2434
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" == ")) ||
2435
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" % ")) ||
2436
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" = ")) ||
2437
+ (contentValue === null || contentValue === void 0 ? void 0 : contentValue.startsWith(" - "))) {
2438
+ return node.TextEdit.del(node.Range.create(info.line, hashColumn, info.line, hashColumn + 2));
2439
+ }
2440
+ return node.TextEdit.del(node.Range.create(info.line, hashColumn, info.line, hashColumn + 1));
2441
+ }
2442
+ if (info.context === "html-comment") {
2443
+ const commentNode = info.node;
2444
+ if ((commentNode === null || commentNode === void 0 ? void 0 : commentNode.comment_start) && (commentNode === null || commentNode === void 0 ? void 0 : commentNode.comment_end)) {
2445
+ const contentStart = commentNode.comment_start.location.end.column;
2446
+ const contentEnd = commentNode.comment_end.location.start.column;
2447
+ const innerContent = lineText.substring(contentStart, contentEnd).trim();
2448
+ const result = uncommentLineContent(innerContent, this.parserService);
2449
+ return node.TextEdit.replace(lineRange, `${indent}${result}`);
2450
+ }
2451
+ }
2452
+ return null;
2453
+ }
2454
+ htmlCommentSpansLine(node, lineText) {
2455
+ if (!node.comment_start || !node.comment_end)
2456
+ return false;
2457
+ const commentStart = node.comment_start.location.start.column;
2458
+ const commentEnd = node.comment_end.location.end.column;
2459
+ const contentBefore = lineText.substring(0, commentStart).trim();
2460
+ const contentAfter = lineText.substring(commentEnd).trim();
2461
+ return contentBefore === "" && contentAfter === "";
2462
+ }
2463
+ getIndentation(lineText) {
2464
+ const match = lineText.match(/^(\s*)/);
2465
+ return match ? match[1] : "";
1355
2466
  }
1356
2467
  }
1357
2468
 
@@ -1369,6 +2480,11 @@ class Service {
1369
2480
  this.codeActionService = new CodeActionService(this.project, this.config);
1370
2481
  this.diagnostics = new Diagnostics(this.connection, this.documentService, this.parserService, this.linterService, this.configService);
1371
2482
  this.documentSaveService = new DocumentSaveService(this.connection, this.settings, this.autofixService, this.formattingService);
2483
+ this.foldingRangeService = new FoldingRangeService(this.parserService);
2484
+ this.documentHighlightService = new DocumentHighlightService(this.parserService);
2485
+ this.hoverService = new HoverService(this.parserService);
2486
+ this.rewriteCodeActionService = new RewriteCodeActionService(this.parserService);
2487
+ this.commentService = new CommentService(this.parserService);
1372
2488
  if (params.initializationOptions) {
1373
2489
  this.settings.globalSettings = params.initializationOptions;
1374
2490
  }
@@ -1460,8 +2576,11 @@ class Server {
1460
2576
  documentFormattingProvider: true,
1461
2577
  documentRangeFormattingProvider: true,
1462
2578
  codeActionProvider: {
1463
- codeActionKinds: [node.CodeActionKind.QuickFix, node.CodeActionKind.SourceFixAll]
2579
+ codeActionKinds: [node.CodeActionKind.QuickFix, node.CodeActionKind.SourceFixAll, node.CodeActionKind.RefactorRewrite]
1464
2580
  },
2581
+ foldingRangeProvider: true,
2582
+ documentHighlightProvider: true,
2583
+ hoverProvider: true,
1465
2584
  },
1466
2585
  };
1467
2586
  if (this.service.settings.hasWorkspaceFolderCapability) {
@@ -1543,6 +2662,18 @@ class Server {
1543
2662
  this.connection.onDocumentRangeFormatting((params) => {
1544
2663
  return this.service.formattingService.formatRange(params);
1545
2664
  });
2665
+ this.connection.onDocumentHighlight((params) => {
2666
+ const document = this.service.documentService.get(params.textDocument.uri);
2667
+ if (!document)
2668
+ return [];
2669
+ return this.service.documentHighlightService.getDocumentHighlights(document, params.position);
2670
+ });
2671
+ this.connection.onHover((params) => {
2672
+ const document = this.service.documentService.get(params.textDocument.uri);
2673
+ if (!document)
2674
+ return null;
2675
+ return this.service.hoverService.getHover(document, params.position);
2676
+ });
1546
2677
  this.connection.onCodeAction((params) => {
1547
2678
  const document = this.service.documentService.get(params.textDocument.uri);
1548
2679
  if (!document)
@@ -1551,7 +2682,26 @@ class Server {
1551
2682
  const documentText = document.getText();
1552
2683
  const linterDisableCodeActions = this.service.codeActionService.createCodeActions(params.textDocument.uri, diagnostics, documentText);
1553
2684
  const autofixCodeActions = this.service.codeActionService.autofixCodeActions(params, document);
1554
- return autofixCodeActions.concat(linterDisableCodeActions);
2685
+ const rewriteCodeActions = this.service.rewriteCodeActionService.getCodeActions(document, params.range);
2686
+ return autofixCodeActions.concat(linterDisableCodeActions).concat(rewriteCodeActions);
2687
+ });
2688
+ this.connection.onFoldingRanges((params) => {
2689
+ const document = this.service.documentService.get(params.textDocument.uri);
2690
+ if (!document)
2691
+ return [];
2692
+ return this.service.foldingRangeService.getFoldingRanges(document);
2693
+ });
2694
+ this.connection.onRequest('herb/toggleLineComment', (params) => {
2695
+ const document = this.service.documentService.get(params.textDocument.uri);
2696
+ if (!document)
2697
+ return [];
2698
+ return this.service.commentService.toggleLineComment(document, params.range);
2699
+ });
2700
+ this.connection.onRequest('herb/toggleBlockComment', (params) => {
2701
+ const document = this.service.documentService.get(params.textDocument.uri);
2702
+ if (!document)
2703
+ return [];
2704
+ return this.service.commentService.toggleBlockComment(document, params.range);
1555
2705
  });
1556
2706
  }
1557
2707
  listen() {
@@ -1582,8 +2732,13 @@ class CLI {
1582
2732
  }
1583
2733
 
1584
2734
  exports.CLI = CLI;
2735
+ exports.CommentService = CommentService;
1585
2736
  exports.Diagnostics = Diagnostics;
2737
+ exports.DocumentHighlightCollector = DocumentHighlightCollector;
2738
+ exports.DocumentHighlightService = DocumentHighlightService;
1586
2739
  exports.DocumentService = DocumentService;
2740
+ exports.FoldingRangeCollector = FoldingRangeCollector;
2741
+ exports.FoldingRangeService = FoldingRangeService;
1587
2742
  exports.FormattingService = FormattingService;
1588
2743
  exports.Project = Project;
1589
2744
  exports.Server = Server;
@@ -1593,6 +2748,15 @@ exports.UnreachableCodeCollector = UnreachableCodeCollector;
1593
2748
  exports.camelize = camelize;
1594
2749
  exports.capitalize = capitalize;
1595
2750
  exports.dasherize = dasherize;
2751
+ exports.erbTagToRange = erbTagToRange;
1596
2752
  exports.getFullDocumentRange = getFullDocumentRange;
2753
+ exports.isPositionInRange = isPositionInRange;
1597
2754
  exports.lintToDignosticSeverity = lintToDignosticSeverity;
2755
+ exports.lspLine = lspLine;
2756
+ exports.lspPosition = lspPosition;
2757
+ exports.lspRangeFromLocation = lspRangeFromLocation;
2758
+ exports.nodeToRange = nodeToRange;
2759
+ exports.openTagRanges = openTagRanges;
2760
+ exports.rangeSize = rangeSize;
2761
+ exports.tokenToRange = tokenToRange;
1598
2762
  //# sourceMappingURL=index.cjs.map