@herb-tools/language-server 0.8.9 → 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 -7
  20. package/dist/formatting_service.js.map +1 -1
  21. package/dist/herb-language-server.js +150276 -41207
  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 -66
  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 -8
  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
@@ -0,0 +1,287 @@
1
+ import { FoldingRange, FoldingRangeKind } from "vscode-languageserver/node"
2
+ import { TextDocument } from "vscode-languageserver-textdocument"
3
+
4
+ import { Visitor } from "@herb-tools/core"
5
+ import { ParserService } from "./parser_service"
6
+
7
+ import { isERBIfNode } from "@herb-tools/core"
8
+ import { lspLine } from "./range_utils"
9
+
10
+ import type {
11
+ Node,
12
+ ERBNode,
13
+ ERBContentNode,
14
+ HTMLElementNode,
15
+ HTMLOpenTagNode,
16
+ HTMLAttributeValueNode,
17
+ HTMLCommentNode,
18
+ HTMLConditionalElementNode,
19
+ CDATANode,
20
+ ERBIfNode,
21
+ ERBUnlessNode,
22
+ ERBCaseNode,
23
+ ERBCaseMatchNode,
24
+ ERBBeginNode,
25
+ ERBRescueNode,
26
+ ERBEnsureNode,
27
+ ERBElseNode,
28
+ ERBWhenNode,
29
+ ERBInNode,
30
+ SerializedPosition,
31
+ } from "@herb-tools/core"
32
+
33
+ export class FoldingRangeService {
34
+ private parserService: ParserService
35
+
36
+ constructor(parserService: ParserService) {
37
+ this.parserService = parserService
38
+ }
39
+
40
+ getFoldingRanges(textDocument: TextDocument): FoldingRange[] {
41
+ const parseResult = this.parserService.parseDocument(textDocument)
42
+ const collector = new FoldingRangeCollector()
43
+
44
+ collector.visit(parseResult.document)
45
+
46
+ return collector.ranges
47
+ }
48
+ }
49
+
50
+ export class FoldingRangeCollector extends Visitor {
51
+ public ranges: FoldingRange[] = []
52
+ private processedIfNodes: Set<ERBIfNode> = new Set()
53
+
54
+ visitHTMLElementNode(node: HTMLElementNode): void {
55
+ if (node.body.length > 0 && node.open_tag && node.close_tag) {
56
+ this.addRange(node.open_tag.location.end, node.close_tag.location.start)
57
+ }
58
+
59
+ this.visitChildNodes(node)
60
+ }
61
+
62
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
63
+ if (node.children.length > 0 && node.tag_opening && node.tag_closing) {
64
+ this.addRange(node.tag_opening.location.end, node.tag_closing.location.start)
65
+ }
66
+
67
+ this.visitChildNodes(node)
68
+ }
69
+
70
+ visitHTMLCommentNode(node: HTMLCommentNode): void {
71
+ if (node.comment_start && node.comment_end) {
72
+ this.addRange(node.comment_start.location.end, node.comment_end.location.start, FoldingRangeKind.Comment)
73
+ }
74
+
75
+ this.visitChildNodes(node)
76
+ }
77
+
78
+ visitHTMLAttributeValueNode(node: HTMLAttributeValueNode): void {
79
+ if (node.children.length > 0) {
80
+ const first = node.children[0]
81
+ const last = node.children[node.children.length - 1]
82
+
83
+ this.addRange(first.location.start, last.location.end)
84
+ }
85
+
86
+ this.visitChildNodes(node)
87
+ }
88
+
89
+ visitCDATANode(node: CDATANode): void {
90
+ this.addRange(node.location.start, node.location.end)
91
+ this.visitChildNodes(node)
92
+ }
93
+
94
+ visitHTMLConditionalElementNode(node: HTMLConditionalElementNode): void {
95
+ this.addRange(node.location.start, node.location.end)
96
+ this.visitChildNodes(node)
97
+ }
98
+
99
+ visitERBNode(node: ERBNode): void {
100
+ if (node.tag_closing && 'end_node' in node && node.end_node?.tag_opening) {
101
+ this.addRange(node.tag_closing.location.end, node.end_node.tag_opening.location.start)
102
+ } else {
103
+ this.addRange(node.location.start, node.location.end)
104
+ }
105
+ }
106
+
107
+ visitERBContentNode(node: ERBContentNode): void {
108
+ if (node.tag_opening && node.tag_closing) {
109
+ this.addRange(node.tag_opening.location.end, node.tag_closing.location.start)
110
+ }
111
+
112
+ this.visitChildNodes(node)
113
+ }
114
+
115
+ visitERBIfNode(node: ERBIfNode): void {
116
+ if (this.processedIfNodes.has(node)) {
117
+ this.visitChildNodes(node)
118
+ return
119
+ }
120
+
121
+ this.markIfChainAsProcessed(node)
122
+
123
+ const nextAfterIf = node.subsequent ?? node.end_node
124
+
125
+ if (node.tag_closing && nextAfterIf?.tag_opening) {
126
+ this.addRange(node.tag_closing.location.end, nextAfterIf.tag_opening.location.start)
127
+ }
128
+
129
+ let current: ERBIfNode | ERBElseNode | null = node.subsequent
130
+
131
+ while (current) {
132
+ if (isERBIfNode(current)) {
133
+ const nextAfterElsif = current.subsequent ?? node.end_node
134
+
135
+ if (current.tag_closing && nextAfterElsif?.tag_opening) {
136
+ this.addRange(current.tag_closing.location.end, nextAfterElsif.tag_opening.location.start)
137
+ }
138
+
139
+ current = current.subsequent
140
+ } else {
141
+ break
142
+ }
143
+ }
144
+
145
+ this.visitChildNodes(node)
146
+ }
147
+
148
+ visitERBUnlessNode(node: ERBUnlessNode): void {
149
+ const nextAfterUnless = node.else_clause ?? node.end_node
150
+
151
+ if (node.tag_closing && nextAfterUnless?.tag_opening) {
152
+ this.addRange(node.tag_closing.location.end, nextAfterUnless.tag_opening.location.start)
153
+ }
154
+
155
+ if (node.else_clause) {
156
+ if (node.else_clause.tag_closing && node.end_node?.tag_opening) {
157
+ this.addRange(node.else_clause.tag_closing.location.end, node.end_node.tag_opening.location.start)
158
+ }
159
+ }
160
+
161
+ this.visitChildNodes(node)
162
+ }
163
+
164
+ visitERBCaseNode(node: ERBCaseNode): void {
165
+ this.addCaseFoldingRanges(node)
166
+ this.visitChildNodes(node)
167
+ }
168
+
169
+ visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
170
+ this.addCaseFoldingRanges(node)
171
+ this.visitChildNodes(node)
172
+ }
173
+
174
+ visitERBWhenNode(node: ERBWhenNode): void {
175
+ this.visitChildNodes(node)
176
+ }
177
+
178
+ visitERBInNode(node: ERBInNode): void {
179
+ this.visitChildNodes(node)
180
+ }
181
+
182
+ visitERBBeginNode(node: ERBBeginNode): void {
183
+ const nextAfterBegin = node.rescue_clause ?? node.else_clause ?? node.ensure_clause ?? node.end_node
184
+
185
+ if (node.tag_closing && nextAfterBegin?.tag_opening) {
186
+ this.addRange(node.tag_closing.location.end, nextAfterBegin.tag_opening.location.start)
187
+ }
188
+
189
+ let rescue: ERBRescueNode | null = node.rescue_clause
190
+
191
+ while (rescue) {
192
+ const nextAfterRescue = rescue.subsequent ?? node.else_clause ?? node.ensure_clause ?? node.end_node
193
+
194
+ if (rescue.tag_closing && nextAfterRescue?.tag_opening) {
195
+ this.addRange(rescue.tag_closing.location.end, nextAfterRescue.tag_opening.location.start)
196
+ }
197
+
198
+ rescue = rescue.subsequent
199
+ }
200
+
201
+ if (node.else_clause) {
202
+ const nextAfterElse = node.ensure_clause ?? node.end_node
203
+
204
+ if (node.else_clause.tag_closing && nextAfterElse?.tag_opening) {
205
+ this.addRange(node.else_clause.tag_closing.location.end, nextAfterElse.tag_opening.location.start)
206
+ }
207
+ }
208
+
209
+ if (node.ensure_clause) {
210
+ if (node.ensure_clause.tag_closing && node.end_node?.tag_opening) {
211
+ this.addRange(node.ensure_clause.tag_closing.location.end, node.end_node.tag_opening.location.start)
212
+ }
213
+ }
214
+
215
+ this.visitChildNodes(node)
216
+ }
217
+
218
+ visitERBRescueNode(node: ERBRescueNode): void {
219
+ this.visitChildNodes(node)
220
+ }
221
+
222
+ visitERBElseNode(node: ERBElseNode): void {
223
+ this.addRange(node.location.start, node.location.end)
224
+ this.visitChildNodes(node)
225
+ }
226
+
227
+ visitERBEnsureNode(node: ERBEnsureNode): void {
228
+ this.visitChildNodes(node)
229
+ }
230
+
231
+ private markIfChainAsProcessed(node: ERBIfNode): void {
232
+ this.processedIfNodes.add(node)
233
+
234
+ let current: Node | null = node.subsequent
235
+
236
+ while (current) {
237
+ if (isERBIfNode(current)) {
238
+ this.processedIfNodes.add(current)
239
+ current = current.subsequent
240
+ } else {
241
+ break
242
+ }
243
+ }
244
+ }
245
+
246
+ private addCaseFoldingRanges(node: ERBCaseNode | ERBCaseMatchNode): void {
247
+ type ConditionNode = ERBWhenNode | ERBInNode
248
+ const conditions = node.conditions as ConditionNode[]
249
+
250
+ const firstCondition = conditions[0]
251
+ const nextAfterCase = firstCondition ?? node.else_clause ?? node.end_node
252
+
253
+ if (node.tag_closing && nextAfterCase?.tag_opening) {
254
+ this.addRange(node.tag_closing.location.end, nextAfterCase.tag_opening.location.start)
255
+ }
256
+
257
+ for (let i = 0; i < conditions.length; i++) {
258
+ const condition = conditions[i]
259
+ const nextCondition = conditions[i + 1] ?? node.else_clause ?? node.end_node
260
+
261
+ if (condition.tag_closing && nextCondition?.tag_opening) {
262
+ this.addRange(condition.tag_closing.location.end, nextCondition.tag_opening.location.start)
263
+ }
264
+ }
265
+
266
+ if (node.else_clause) {
267
+ if (node.else_clause.tag_closing && node.end_node?.tag_opening) {
268
+ this.addRange(node.else_clause.tag_closing.location.end, node.end_node.tag_opening.location.start)
269
+ }
270
+ }
271
+ }
272
+
273
+ private addRange(start: SerializedPosition, end: SerializedPosition, kind?: FoldingRangeKind): void {
274
+ const startLine = lspLine(start)
275
+ const endLine = lspLine(end) - 1
276
+
277
+ if (endLine > startLine) {
278
+ this.ranges.push({
279
+ startLine,
280
+ startCharacter: start.column,
281
+ endLine,
282
+ endCharacter: end.column,
283
+ kind,
284
+ })
285
+ }
286
+ }
287
+ }
@@ -186,7 +186,7 @@ export class FormattingService {
186
186
  }
187
187
  }
188
188
 
189
- async formatOnSave(document: TextDocument, reason: TextDocumentSaveReason): Promise<TextEdit[]> {
189
+ async formatOnSave(document: TextDocument, reason: TextDocumentSaveReason, textOverride?: string): Promise<TextEdit[]> {
190
190
  this.connection.console.log(`[Formatting] formatOnSave called for ${document.uri}`)
191
191
 
192
192
  if (reason !== TextDocumentSaveReason.Manual) {
@@ -203,7 +203,7 @@ export class FormattingService {
203
203
  return []
204
204
  }
205
205
 
206
- return this.performFormatting({ textDocument: { uri: document.uri }, options: { tabSize: 2, insertSpaces: true } })
206
+ return this.performFormatting({ textDocument: { uri: document.uri }, options: { tabSize: 2, insertSpaces: true } }, textOverride)
207
207
  }
208
208
 
209
209
  private shouldFormatFile(filePath: string): boolean {
@@ -235,7 +235,7 @@ export class FormattingService {
235
235
  } as Config
236
236
  }
237
237
 
238
- private async performFormatting(params: DocumentFormattingParams): Promise<TextEdit[]> {
238
+ private async performFormatting(params: DocumentFormattingParams, textOverride?: string): Promise<TextEdit[]> {
239
239
  const document = this.documents.get(params.textDocument.uri)
240
240
 
241
241
  if (!document) {
@@ -243,7 +243,7 @@ export class FormattingService {
243
243
  }
244
244
 
245
245
  try {
246
- const text = document.getText()
246
+ const text = textOverride ?? document.getText()
247
247
  const config = await this.getConfigWithSettings(params.textDocument.uri)
248
248
 
249
249
  this.connection.console.log(`[Formatting] Creating formatter with ${this.preRewriters.length} pre-rewriters, ${this.postRewriters.length} post-rewriters`)
@@ -382,10 +382,6 @@ export class FormattingService {
382
382
  }).join('\n')
383
383
  }
384
384
 
385
- if (!formattedText.endsWith('\n')) {
386
- formattedText += '\n'
387
- }
388
-
389
385
  if (formattedText === rangeText) {
390
386
  return []
391
387
  }
@@ -0,0 +1,90 @@
1
+ import { Hover, MarkupKind, Position } from "vscode-languageserver/node"
2
+ import { TextDocument } from "vscode-languageserver-textdocument"
3
+
4
+ import { Visitor } from "@herb-tools/node-wasm"
5
+ import { IdentityPrinter } from "@herb-tools/printer"
6
+ import { ActionViewTagHelperToHTMLRewriter } from "@herb-tools/rewriter"
7
+ import { isERBOpenTagNode } from "@herb-tools/core"
8
+ import { ParserService } from "./parser_service"
9
+ import { nodeToRange, isPositionInRange, rangeSize } from "./range_utils"
10
+ import { ACTION_VIEW_HELPERS } from "./action_view_helpers"
11
+
12
+ import type { Node, HTMLElementNode } from "@herb-tools/core"
13
+ import type { Range } from "vscode-languageserver/node"
14
+
15
+ class ActionViewElementCollector extends Visitor {
16
+ public elements: { node: HTMLElementNode; range: Range }[] = []
17
+
18
+ visitHTMLElementNode(node: HTMLElementNode): void {
19
+ if (node.element_source && node.element_source !== "HTML" && isERBOpenTagNode(node.open_tag)) {
20
+ this.elements.push({
21
+ node,
22
+ range: nodeToRange(node),
23
+ })
24
+ }
25
+
26
+ this.visitChildNodes(node)
27
+ }
28
+ }
29
+
30
+ export class HoverService {
31
+ private parserService: ParserService
32
+
33
+ constructor(parserService: ParserService) {
34
+ this.parserService = parserService
35
+ }
36
+
37
+ getHover(textDocument: TextDocument, position: Position): Hover | null {
38
+ const parseResult = this.parserService.parseContent(textDocument.getText(), {
39
+ action_view_helpers: true,
40
+ track_whitespace: true,
41
+ })
42
+
43
+ const collector = new ActionViewElementCollector()
44
+ collector.visit(parseResult.value)
45
+
46
+ let bestElement: { node: HTMLElementNode; range: Range } | null = null
47
+ let bestSize = Infinity
48
+
49
+ for (const element of collector.elements) {
50
+ if (isPositionInRange(position, element.range)) {
51
+ const size = rangeSize(element.range)
52
+
53
+ if (size < bestSize) {
54
+ bestSize = size
55
+ bestElement = element
56
+ }
57
+ }
58
+ }
59
+
60
+ if (!bestElement) {
61
+ return null
62
+ }
63
+
64
+ const elementSource = bestElement.node.element_source
65
+ const rewriter = new ActionViewTagHelperToHTMLRewriter()
66
+ const rewrittenNode = rewriter.rewrite(bestElement.node as Node, { baseDir: process.cwd() })
67
+ const htmlOutput = IdentityPrinter.print(rewrittenNode)
68
+ const helper = ACTION_VIEW_HELPERS[elementSource]
69
+ const parts: string[] = []
70
+
71
+ if (helper) {
72
+ parts.push(`\`\`\`ruby\n${helper.signature}\n\`\`\``)
73
+ }
74
+
75
+ parts.push(`**HTML equivalent**\n\`\`\`erb\n${htmlOutput.trim()}\n\`\`\``)
76
+
77
+ if (helper) {
78
+ parts.push(`[${elementSource}](${helper.documentationURL})`)
79
+ }
80
+
81
+ return {
82
+ contents: {
83
+ kind: MarkupKind.Markdown,
84
+ value: parts.join("\n\n"),
85
+ },
86
+ range: bestElement.range,
87
+ }
88
+ }
89
+
90
+ }
package/src/index.ts CHANGED
@@ -3,7 +3,11 @@ export * from "./service"
3
3
  export * from "./diagnostics"
4
4
  export * from "./document_service"
5
5
  export * from "./formatting_service"
6
+ export * from "./folding_range_service"
6
7
  export * from "./project"
7
8
  export * from "./settings"
8
9
  export * from "./utils"
10
+ export * from "./range_utils"
9
11
  export * from "./cli"
12
+ export * from "./document_highlight_service"
13
+ export * from "./comment_service"
@@ -0,0 +1,97 @@
1
+ import { Visitor, HTMLCommentNode, } from "@herb-tools/core"
2
+
3
+ import { lspLine } from "./range_utils"
4
+ import { isERBCommentNode } from "@herb-tools/core"
5
+
6
+ import type { Node, ERBNode, ERBContentNode, HTMLTextNode, HTMLElementNode } from "@herb-tools/core"
7
+
8
+ export type LineContext = "erb-comment" | "html-comment" | "erb-tag" | "html-content" | "empty"
9
+
10
+ export interface LineInfo {
11
+ line: number
12
+ context: LineContext
13
+ node: Node | null
14
+ }
15
+
16
+ export class LineContextCollector extends Visitor {
17
+ public lineMap: Map<number, LineInfo> = new Map()
18
+ public erbNodesPerLine: Map<number, ERBContentNode[]> = new Map()
19
+ public htmlCommentNodesPerLine: Map<number, HTMLCommentNode> = new Map()
20
+
21
+ visitERBNode(node: ERBNode): void {
22
+ if (!node.tag_opening || !node.tag_closing) return
23
+
24
+ const startLine = lspLine(node.tag_opening.location.start)
25
+
26
+ const nodes = this.erbNodesPerLine.get(startLine) || []
27
+ nodes.push(node as ERBContentNode)
28
+ this.erbNodesPerLine.set(startLine, nodes)
29
+
30
+ if (isERBCommentNode(node)) {
31
+ this.setLine(startLine, "erb-comment", node)
32
+ } else {
33
+ this.setLine(startLine, "erb-tag", node)
34
+ }
35
+ }
36
+
37
+ visitERBContentNode(node: ERBContentNode): void {
38
+ this.visitERBNode(node)
39
+ this.visitChildNodes(node)
40
+ }
41
+
42
+ visitHTMLCommentNode(node: HTMLCommentNode): void {
43
+ const startLine = lspLine(node.location.start)
44
+ const endLine = lspLine(node.location.end)
45
+
46
+ for (let line = startLine; line <= endLine; line++) {
47
+ this.htmlCommentNodesPerLine.set(line, node)
48
+ this.setLine(line, "html-comment", node)
49
+ }
50
+
51
+ this.visitChildNodes(node)
52
+ }
53
+
54
+ visitHTMLElementNode(node: HTMLElementNode): void {
55
+ const startLine = lspLine(node.location.start)
56
+ const endLine = lspLine(node.location.end)
57
+
58
+ for (let line = startLine; line <= endLine; line++) {
59
+ if (!this.lineMap.has(line)) {
60
+ this.setLine(line, "html-content", node)
61
+ }
62
+ }
63
+
64
+ this.visitChildNodes(node)
65
+ }
66
+
67
+ visitHTMLTextNode(node: HTMLTextNode): void {
68
+ const startLine = lspLine(node.location.start)
69
+ const endLine = lspLine(node.location.end)
70
+
71
+ for (let line = startLine; line <= endLine; line++) {
72
+ if (!this.lineMap.has(line)) {
73
+ this.setLine(line, "html-content", node)
74
+ }
75
+ }
76
+
77
+ this.visitChildNodes(node)
78
+ }
79
+
80
+ private setLine(line: number, context: LineContext, node: Node): void {
81
+ const existing = this.lineMap.get(line)
82
+
83
+ if (existing) {
84
+ if (existing.context === "erb-comment" || existing.context === "erb-tag") return
85
+
86
+ if (context === "erb-comment" || context === "erb-tag") {
87
+ this.lineMap.set(line, { line, context, node })
88
+
89
+ return
90
+ }
91
+
92
+ if (existing.context === "html-comment") return
93
+ }
94
+
95
+ this.lineMap.set(line, { line, context, node })
96
+ }
97
+ }
@@ -1,7 +1,7 @@
1
- import { Diagnostic, Range, Position, CodeDescription, Connection } from "vscode-languageserver/node"
1
+ import { Diagnostic, CodeDescription, Connection } from "vscode-languageserver/node"
2
2
  import { TextDocument } from "vscode-languageserver-textdocument"
3
3
 
4
- import { Linter, rules, type RuleClass } from "@herb-tools/linter"
4
+ import { Linter, rules, ruleDocumentationUrl, type RuleClass } from "@herb-tools/linter"
5
5
  import { loadCustomRules as loadCustomRulesFromFs } from "@herb-tools/linter/loader"
6
6
  import { Herb } from "@herb-tools/node-wasm"
7
7
  import { Config } from "@herb-tools/config"
@@ -9,6 +9,7 @@ import { Config } from "@herb-tools/config"
9
9
  import { Settings } from "./settings"
10
10
  import { Project } from "./project"
11
11
  import { lintToDignosticSeverity } from "./utils"
12
+ import { lspRangeFromLocation } from "./range_utils"
12
13
 
13
14
  const OPEN_CONFIG_ACTION = 'Open .herb.yml'
14
15
 
@@ -113,7 +114,27 @@ export class LinterService {
113
114
  }
114
115
  }
115
116
 
117
+ private shouldLintFile(uri: string): boolean {
118
+ const filePath = uri.replace(/^file:\/\//, '')
119
+
120
+ if (filePath.endsWith('.herb.yml')) return false
121
+
122
+ const config = this.settings.projectConfig
123
+ if (!config) return true
124
+
125
+ const hasConfigFile = Config.exists(config.projectPath)
126
+ if (!hasConfigFile) return true
127
+
128
+ const relativePath = filePath.replace(this.project.projectPath + '/', '')
129
+
130
+ return config.isLinterEnabledForPath(relativePath)
131
+ }
132
+
116
133
  async lintDocument(textDocument: TextDocument): Promise<LintServiceResult> {
134
+ if (!this.shouldLintFile(textDocument.uri)) {
135
+ return { diagnostics: [] }
136
+ }
137
+
117
138
  const settings = await this.settings.getDocumentSettings(textDocument.uri)
118
139
  const linterEnabled = settings?.linter?.enabled ?? true
119
140
 
@@ -149,16 +170,13 @@ export class LinterService {
149
170
  const lintResult = this.linter.lint(content, { fileName: textDocument.uri })
150
171
 
151
172
  const diagnostics: Diagnostic[] = lintResult.offenses.map(offense => {
152
- const range = Range.create(
153
- Position.create(offense.location.start.line - 1, offense.location.start.column),
154
- Position.create(offense.location.end.line - 1, offense.location.end.column),
155
- )
173
+ const range = lspRangeFromLocation(offense.location)
156
174
 
157
175
  const customRulePath = this.customRulePaths.get(offense.rule)
158
176
  const codeDescription: CodeDescription = {
159
177
  href: customRulePath
160
178
  ? `file://${customRulePath}`
161
- : `https://herb-tools.dev/linter/rules/${offense.rule}`
179
+ : ruleDocumentationUrl(offense.rule)
162
180
  }
163
181
 
164
182
  return {
@@ -1,8 +1,10 @@
1
- import { Diagnostic, DiagnosticSeverity, Range, Position } from "vscode-languageserver/node"
1
+ import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver/node"
2
2
  import { TextDocument } from "vscode-languageserver-textdocument"
3
3
  import { Herb, Visitor } from "@herb-tools/node-wasm"
4
4
 
5
- import type { Node, HerbError, DocumentNode } from "@herb-tools/node-wasm"
5
+ import type { Node, HerbError, DocumentNode, ParseResult, ParseOptions } from "@herb-tools/node-wasm"
6
+
7
+ import { lspRangeFromLocation } from "./range_utils"
6
8
 
7
9
  class ErrorVisitor extends Visitor {
8
10
  private readonly source = "Herb Parser "
@@ -18,7 +20,7 @@ class ErrorVisitor extends Visitor {
18
20
  const diagnostic: Diagnostic = {
19
21
  source: this.source,
20
22
  severity: DiagnosticSeverity.Error,
21
- range: this.rangeFromHerbError(error),
23
+ range: lspRangeFromLocation(error.location),
22
24
  message: error.message,
23
25
  code: error.type,
24
26
  data: {
@@ -29,13 +31,6 @@ class ErrorVisitor extends Visitor {
29
31
 
30
32
  this.diagnostics.push(diagnostic)
31
33
  }
32
-
33
- private rangeFromHerbError(error: HerbError): Range {
34
- return Range.create(
35
- Position.create(error.location.start.line - 1, error.location.start.column),
36
- Position.create(error.location.end.line - 1, error.location.end.column),
37
- )
38
- }
39
34
  }
40
35
 
41
36
  export interface ParseServiceResult {
@@ -56,4 +51,8 @@ export class ParserService {
56
51
  diagnostics: errorVisitor.diagnostics
57
52
  }
58
53
  }
54
+
55
+ parseContent(content: string, options?: ParseOptions): ParseResult {
56
+ return Herb.parse(content, options)
57
+ }
59
58
  }