@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.
- package/dist/action_view_helpers.js +19 -0
- package/dist/action_view_helpers.js.map +1 -0
- package/dist/autofix_service.js +1 -1
- package/dist/autofix_service.js.map +1 -1
- package/dist/code_action_service.js +3 -6
- package/dist/code_action_service.js.map +1 -1
- package/dist/comment_ast_utils.js +206 -0
- package/dist/comment_ast_utils.js.map +1 -0
- package/dist/comment_service.js +175 -0
- package/dist/comment_service.js.map +1 -0
- package/dist/diagnostics.js +15 -27
- package/dist/diagnostics.js.map +1 -1
- package/dist/document_highlight_service.js +196 -0
- package/dist/document_highlight_service.js.map +1 -0
- package/dist/document_save_service.js +16 -6
- package/dist/document_save_service.js.map +1 -1
- package/dist/folding_range_service.js +209 -0
- package/dist/folding_range_service.js.map +1 -0
- package/dist/formatting_service.js +4 -7
- package/dist/formatting_service.js.map +1 -1
- package/dist/herb-language-server.js +150276 -41207
- package/dist/herb-language-server.js.map +1 -1
- package/dist/hover_service.js +70 -0
- package/dist/hover_service.js.map +1 -0
- package/dist/index.cjs +1227 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/line_context_collector.js +73 -0
- package/dist/line_context_collector.js.map +1 -0
- package/dist/linter_service.js +20 -4
- package/dist/linter_service.js.map +1 -1
- package/dist/parser_service.js +6 -5
- package/dist/parser_service.js.map +1 -1
- package/dist/range_utils.js +65 -0
- package/dist/range_utils.js.map +1 -0
- package/dist/rewrite_code_action_service.js +135 -0
- package/dist/rewrite_code_action_service.js.map +1 -0
- package/dist/server.js +36 -2
- package/dist/server.js.map +1 -1
- package/dist/service.js +10 -0
- package/dist/service.js.map +1 -1
- package/dist/types/action_view_helpers.d.ts +5 -0
- package/dist/types/comment_ast_utils.d.ts +20 -0
- package/dist/types/comment_service.d.ts +14 -0
- package/dist/types/diagnostics.d.ts +0 -1
- package/dist/types/document_highlight_service.d.ts +28 -0
- package/dist/types/document_save_service.d.ts +8 -0
- package/dist/types/folding_range_service.d.ts +35 -0
- package/dist/types/formatting_service.d.ts +1 -1
- package/dist/types/hover_service.d.ts +8 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/line_context_collector.d.ts +19 -0
- package/dist/types/linter_service.d.ts +1 -0
- package/dist/types/parser_service.d.ts +2 -1
- package/dist/types/range_utils.d.ts +16 -0
- package/dist/types/rewrite_code_action_service.d.ts +11 -0
- package/dist/types/service.d.ts +10 -0
- package/dist/types/utils.d.ts +0 -6
- package/dist/utils.js +0 -16
- package/dist/utils.js.map +1 -1
- package/package.json +10 -5
- package/src/action_view_helpers.ts +23 -0
- package/src/autofix_service.ts +1 -1
- package/src/code_action_service.ts +3 -6
- package/src/comment_ast_utils.ts +282 -0
- package/src/comment_service.ts +228 -0
- package/src/diagnostics.ts +24 -38
- package/src/document_highlight_service.ts +267 -0
- package/src/document_save_service.ts +19 -7
- package/src/folding_range_service.ts +287 -0
- package/src/formatting_service.ts +4 -8
- package/src/hover_service.ts +90 -0
- package/src/index.ts +4 -0
- package/src/line_context_collector.ts +97 -0
- package/src/linter_service.ts +25 -7
- package/src/parser_service.ts +9 -10
- package/src/range_utils.ts +90 -0
- package/src/rewrite_code_action_service.ts +165 -0
- package/src/server.ts +51 -2
- package/src/service.ts +15 -0
- 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
|
+
}
|
package/src/linter_service.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Diagnostic,
|
|
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 =
|
|
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
|
-
:
|
|
179
|
+
: ruleDocumentationUrl(offense.rule)
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
return {
|
package/src/parser_service.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Diagnostic, DiagnosticSeverity
|
|
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:
|
|
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
|
}
|