@herb-tools/language-server 0.8.10 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +0 -233
- 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 -4
- package/dist/formatting_service.js.map +1 -1
- package/dist/herb-language-server.js +152936 -41156
- 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 +1299 -333
- 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 +27 -6
- 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 +39 -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 +1 -35
- 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 +4 -8
- package/dist/utils.js +10 -15
- 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 +1 -305
- 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 -4
- 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 +35 -9
- 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 +54 -2
- package/src/service.ts +15 -0
- package/src/utils.ts +12 -21
|
@@ -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,14 +1,15 @@
|
|
|
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"
|
|
8
8
|
|
|
9
9
|
import { Settings } from "./settings"
|
|
10
10
|
import { Project } from "./project"
|
|
11
|
-
import { lintToDignosticSeverity } from "./utils"
|
|
11
|
+
import { lintToDignosticSeverity, lintToDignosticTags } 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,19 +170,16 @@ 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
|
+
const diagnostic: Diagnostic = {
|
|
165
183
|
source: this.source,
|
|
166
184
|
severity: lintToDignosticSeverity(offense.severity),
|
|
167
185
|
range,
|
|
@@ -170,6 +188,14 @@ export class LinterService {
|
|
|
170
188
|
data: { rule: offense.rule },
|
|
171
189
|
codeDescription
|
|
172
190
|
}
|
|
191
|
+
|
|
192
|
+
const tags = lintToDignosticTags(offense.tags)
|
|
193
|
+
|
|
194
|
+
if (tags.length > 0) {
|
|
195
|
+
diagnostic.tags = tags
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return diagnostic
|
|
173
199
|
})
|
|
174
200
|
|
|
175
201
|
return { diagnostics }
|
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
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Position, Range } from "vscode-languageserver/node"
|
|
2
|
+
import type { TextDocument } from "vscode-languageserver-textdocument"
|
|
3
|
+
import type { SerializedPosition, SerializedLocation, Node, Token, ERBNode, HTMLOpenTagNode } from "@herb-tools/core"
|
|
4
|
+
|
|
5
|
+
export function lspPosition(herbPosition: SerializedPosition): Position {
|
|
6
|
+
return Position.create(herbPosition.line - 1, herbPosition.column)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function lspLine(herbPosition: SerializedPosition): number {
|
|
10
|
+
return herbPosition.line - 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function lspRangeFromLocation(herbLocation: SerializedLocation): Range {
|
|
14
|
+
return Range.create(lspPosition(herbLocation.start), lspPosition(herbLocation.end))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function erbTagToRange(node: ERBNode): Range | null {
|
|
18
|
+
if (!node.tag_opening || !node.tag_closing) return null
|
|
19
|
+
|
|
20
|
+
return Range.create(
|
|
21
|
+
lspPosition(node.tag_opening.location.start),
|
|
22
|
+
lspPosition(node.tag_closing.location.end),
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tokenToRange(token: Token | null): Range | null {
|
|
27
|
+
if (!token) return null
|
|
28
|
+
|
|
29
|
+
return lspRangeFromLocation(token.location)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function nodeToRange(node: Node): Range {
|
|
33
|
+
return lspRangeFromLocation(node.location)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function openTagRanges(tag: HTMLOpenTagNode): (Range | null)[] {
|
|
37
|
+
const ranges: (Range | null)[] = []
|
|
38
|
+
|
|
39
|
+
if (tag.tag_opening && tag.tag_name) {
|
|
40
|
+
ranges.push(Range.create(
|
|
41
|
+
lspPosition(tag.tag_opening.location.start),
|
|
42
|
+
lspPosition(tag.tag_name.location.end),
|
|
43
|
+
))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ranges.push(tokenToRange(tag.tag_closing))
|
|
47
|
+
|
|
48
|
+
return ranges
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isPositionInRange(position: Position, range: Range): boolean {
|
|
52
|
+
if (position.line < range.start.line || position.line > range.end.line) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (position.line === range.start.line && position.character < range.start.character) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (position.line === range.end.line && position.character > range.end.character) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function rangeSize(range: Range): number {
|
|
68
|
+
if (range.start.line === range.end.line) {
|
|
69
|
+
return range.end.character - range.start.character
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (range.end.line - range.start.line) * 10000 + range.end.character
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns a Range that spans the entire document
|
|
77
|
+
*/
|
|
78
|
+
export function getFullDocumentRange(document: TextDocument): Range {
|
|
79
|
+
const lastLine = document.lineCount - 1
|
|
80
|
+
const lastLineText = document.getText({
|
|
81
|
+
start: Position.create(lastLine, 0),
|
|
82
|
+
end: Position.create(lastLine + 1, 0)
|
|
83
|
+
})
|
|
84
|
+
const lastLineLength = lastLineText.length
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
start: Position.create(0, 0),
|
|
88
|
+
end: Position.create(lastLine, lastLineLength)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { CodeAction, CodeActionKind, TextEdit, WorkspaceEdit, Range } from "vscode-languageserver/node"
|
|
2
|
+
import { TextDocument } from "vscode-languageserver-textdocument"
|
|
3
|
+
|
|
4
|
+
import { Visitor, Herb } from "@herb-tools/node-wasm"
|
|
5
|
+
import { IdentityPrinter } from "@herb-tools/printer"
|
|
6
|
+
import { ActionViewTagHelperToHTMLRewriter, HTMLToActionViewTagHelperRewriter } from "@herb-tools/rewriter"
|
|
7
|
+
import { isERBOpenTagNode, isHTMLOpenTagNode } from "@herb-tools/core"
|
|
8
|
+
import { ParserService } from "./parser_service"
|
|
9
|
+
import { nodeToRange } from "./range_utils"
|
|
10
|
+
|
|
11
|
+
import type { Node, HTMLElementNode } from "@herb-tools/core"
|
|
12
|
+
|
|
13
|
+
interface CollectedElement {
|
|
14
|
+
node: HTMLElementNode
|
|
15
|
+
range: Range
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class ElementCollector extends Visitor {
|
|
19
|
+
public actionViewElements: CollectedElement[] = []
|
|
20
|
+
public htmlElements: CollectedElement[] = []
|
|
21
|
+
|
|
22
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
23
|
+
if (node.element_source && node.element_source !== "HTML" && isERBOpenTagNode(node.open_tag)) {
|
|
24
|
+
this.actionViewElements.push({
|
|
25
|
+
node,
|
|
26
|
+
range: nodeToRange(node),
|
|
27
|
+
})
|
|
28
|
+
} else if (isHTMLOpenTagNode(node.open_tag) && node.open_tag.tag_name) {
|
|
29
|
+
this.htmlElements.push({
|
|
30
|
+
node,
|
|
31
|
+
range: nodeToRange(node),
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.visitChildNodes(node)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class RewriteCodeActionService {
|
|
40
|
+
private parserService: ParserService
|
|
41
|
+
|
|
42
|
+
constructor(parserService: ParserService) {
|
|
43
|
+
this.parserService = parserService
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getCodeActions(document: TextDocument, requestedRange: Range): CodeAction[] {
|
|
47
|
+
const parseResult = this.parserService.parseContent(document.getText(), {
|
|
48
|
+
action_view_helpers: true,
|
|
49
|
+
track_whitespace: true,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const collector = new ElementCollector()
|
|
53
|
+
collector.visit(parseResult.value)
|
|
54
|
+
|
|
55
|
+
const actions: CodeAction[] = []
|
|
56
|
+
|
|
57
|
+
for (const element of collector.actionViewElements) {
|
|
58
|
+
if (!this.rangesOverlap(element.range, requestedRange)) continue
|
|
59
|
+
|
|
60
|
+
const action = this.createActionViewToHTMLAction(document, element)
|
|
61
|
+
|
|
62
|
+
if (action) {
|
|
63
|
+
actions.push(action)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const element of collector.htmlElements) {
|
|
68
|
+
if (!this.rangesOverlap(element.range, requestedRange)) continue
|
|
69
|
+
|
|
70
|
+
const action = this.createHTMLToActionViewAction(document, element)
|
|
71
|
+
|
|
72
|
+
if (action) {
|
|
73
|
+
actions.push(action)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return actions
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private createActionViewToHTMLAction(document: TextDocument, element: CollectedElement): CodeAction | null {
|
|
81
|
+
const originalText = document.getText(element.range)
|
|
82
|
+
|
|
83
|
+
const parseResult = this.parserService.parseContent(originalText, {
|
|
84
|
+
action_view_helpers: true,
|
|
85
|
+
track_whitespace: true,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (parseResult.failed) return null
|
|
89
|
+
|
|
90
|
+
const rewriter = new ActionViewTagHelperToHTMLRewriter()
|
|
91
|
+
rewriter.rewrite(parseResult.value as Node, { baseDir: process.cwd() })
|
|
92
|
+
|
|
93
|
+
const rewrittenText = IdentityPrinter.print(parseResult.value)
|
|
94
|
+
|
|
95
|
+
if (rewrittenText === originalText) return null
|
|
96
|
+
|
|
97
|
+
const edit: WorkspaceEdit = {
|
|
98
|
+
changes: {
|
|
99
|
+
[document.uri]: [TextEdit.replace(element.range, rewrittenText)]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const tagName = element.node.tag_name?.value
|
|
104
|
+
const title = tagName
|
|
105
|
+
? `Herb: Convert to \`<${tagName}>\``
|
|
106
|
+
: "Herb: Convert to HTML"
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
title,
|
|
110
|
+
kind: CodeActionKind.RefactorRewrite,
|
|
111
|
+
edit,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private createHTMLToActionViewAction(document: TextDocument, element: CollectedElement): CodeAction | null {
|
|
116
|
+
const originalText = document.getText(element.range)
|
|
117
|
+
|
|
118
|
+
const parseResult = this.parserService.parseContent(originalText, {
|
|
119
|
+
track_whitespace: true,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if (parseResult.failed) return null
|
|
123
|
+
|
|
124
|
+
const rewriter = new HTMLToActionViewTagHelperRewriter()
|
|
125
|
+
rewriter.rewrite(parseResult.value as Node, { baseDir: process.cwd() })
|
|
126
|
+
|
|
127
|
+
const rewrittenText = IdentityPrinter.print(parseResult.value)
|
|
128
|
+
|
|
129
|
+
if (rewrittenText === originalText) return null
|
|
130
|
+
|
|
131
|
+
const edit: WorkspaceEdit = {
|
|
132
|
+
changes: {
|
|
133
|
+
[document.uri]: [TextEdit.replace(element.range, rewrittenText)]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tagName = element.node.tag_name?.value
|
|
138
|
+
const isAnchor = tagName === "a"
|
|
139
|
+
const isTurboFrame = tagName === "turbo-frame"
|
|
140
|
+
const methodName = tagName?.replace(/-/g, "_")
|
|
141
|
+
const title = isAnchor
|
|
142
|
+
? "Herb: Convert to `link_to`"
|
|
143
|
+
: isTurboFrame
|
|
144
|
+
? "Herb: Convert to `turbo_frame_tag`"
|
|
145
|
+
: methodName
|
|
146
|
+
? `Herb: Convert to \`tag.${methodName}\``
|
|
147
|
+
: "Herb: Convert to tag helper"
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
title,
|
|
151
|
+
kind: CodeActionKind.RefactorRewrite,
|
|
152
|
+
edit,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private rangesOverlap(r1: Range, r2: Range): boolean {
|
|
157
|
+
if (r1.end.line < r2.start.line) return false
|
|
158
|
+
if (r1.start.line > r2.end.line) return false
|
|
159
|
+
|
|
160
|
+
if (r1.end.line === r2.start.line && r1.end.character < r2.start.character) return false
|
|
161
|
+
if (r1.start.line === r2.end.line && r1.start.character > r2.end.character) return false
|
|
162
|
+
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
DocumentRangeFormattingParams,
|
|
13
13
|
CodeActionParams,
|
|
14
14
|
CodeActionKind,
|
|
15
|
+
FoldingRangeParams,
|
|
16
|
+
DocumentHighlightParams,
|
|
17
|
+
HoverParams,
|
|
18
|
+
TextDocumentIdentifier,
|
|
19
|
+
Range,
|
|
15
20
|
} from "vscode-languageserver/node"
|
|
16
21
|
|
|
17
22
|
import { Service } from "./service"
|
|
@@ -51,8 +56,11 @@ export class Server {
|
|
|
51
56
|
documentFormattingProvider: true,
|
|
52
57
|
documentRangeFormattingProvider: true,
|
|
53
58
|
codeActionProvider: {
|
|
54
|
-
codeActionKinds: [CodeActionKind.QuickFix, CodeActionKind.SourceFixAll]
|
|
59
|
+
codeActionKinds: [CodeActionKind.QuickFix, CodeActionKind.SourceFixAll, CodeActionKind.RefactorRewrite]
|
|
55
60
|
},
|
|
61
|
+
foldingRangeProvider: true,
|
|
62
|
+
documentHighlightProvider: true,
|
|
63
|
+
hoverProvider: true,
|
|
56
64
|
},
|
|
57
65
|
}
|
|
58
66
|
|
|
@@ -157,11 +165,30 @@ export class Server {
|
|
|
157
165
|
return this.service.formattingService.formatRange(params)
|
|
158
166
|
})
|
|
159
167
|
|
|
168
|
+
this.connection.onDocumentHighlight((params: DocumentHighlightParams) => {
|
|
169
|
+
const document = this.service.documentService.get(params.textDocument.uri)
|
|
170
|
+
|
|
171
|
+
if (!document) return []
|
|
172
|
+
|
|
173
|
+
return this.service.documentHighlightService.getDocumentHighlights(document, params.position)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
this.connection.onHover((params: HoverParams) => {
|
|
177
|
+
const document = this.service.documentService.get(params.textDocument.uri)
|
|
178
|
+
|
|
179
|
+
if (!document) return null
|
|
180
|
+
|
|
181
|
+
return this.service.hoverService.getHover(document, params.position)
|
|
182
|
+
})
|
|
183
|
+
|
|
160
184
|
this.connection.onCodeAction((params: CodeActionParams) => {
|
|
161
185
|
const document = this.service.documentService.get(params.textDocument.uri)
|
|
162
186
|
|
|
163
187
|
if (!document) return []
|
|
164
188
|
|
|
189
|
+
const parseResult = this.service.parserService.parseDocument(document)
|
|
190
|
+
if (parseResult.diagnostics.length > 0) return []
|
|
191
|
+
|
|
165
192
|
const diagnostics = params.context.diagnostics
|
|
166
193
|
const documentText = document.getText()
|
|
167
194
|
|
|
@@ -172,8 +199,33 @@ export class Server {
|
|
|
172
199
|
)
|
|
173
200
|
|
|
174
201
|
const autofixCodeActions = this.service.codeActionService.autofixCodeActions(params, document)
|
|
202
|
+
const rewriteCodeActions = this.service.rewriteCodeActionService.getCodeActions(document, params.range)
|
|
203
|
+
|
|
204
|
+
return autofixCodeActions.concat(linterDisableCodeActions).concat(rewriteCodeActions)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
this.connection.onFoldingRanges((params: FoldingRangeParams) => {
|
|
208
|
+
const document = this.service.documentService.get(params.textDocument.uri)
|
|
209
|
+
|
|
210
|
+
if (!document) return []
|
|
211
|
+
|
|
212
|
+
return this.service.foldingRangeService.getFoldingRanges(document)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
this.connection.onRequest('herb/toggleLineComment', (params: { textDocument: TextDocumentIdentifier, range: Range }) => {
|
|
216
|
+
const document = this.service.documentService.get(params.textDocument.uri)
|
|
217
|
+
|
|
218
|
+
if (!document) return []
|
|
219
|
+
|
|
220
|
+
return this.service.commentService.toggleLineComment(document, params.range)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
this.connection.onRequest('herb/toggleBlockComment', (params: { textDocument: TextDocumentIdentifier, range: Range }) => {
|
|
224
|
+
const document = this.service.documentService.get(params.textDocument.uri)
|
|
225
|
+
|
|
226
|
+
if (!document) return []
|
|
175
227
|
|
|
176
|
-
return
|
|
228
|
+
return this.service.commentService.toggleBlockComment(document, params.range)
|
|
177
229
|
})
|
|
178
230
|
}
|
|
179
231
|
|