@herb-tools/language-server 0.8.10 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/action_view_helpers.js +19 -0
  2. package/dist/action_view_helpers.js.map +1 -0
  3. package/dist/autofix_service.js +1 -1
  4. package/dist/autofix_service.js.map +1 -1
  5. package/dist/code_action_service.js +3 -6
  6. package/dist/code_action_service.js.map +1 -1
  7. package/dist/comment_ast_utils.js +206 -0
  8. package/dist/comment_ast_utils.js.map +1 -0
  9. package/dist/comment_service.js +175 -0
  10. package/dist/comment_service.js.map +1 -0
  11. package/dist/diagnostics.js +0 -233
  12. package/dist/diagnostics.js.map +1 -1
  13. package/dist/document_highlight_service.js +196 -0
  14. package/dist/document_highlight_service.js.map +1 -0
  15. package/dist/document_save_service.js +16 -6
  16. package/dist/document_save_service.js.map +1 -1
  17. package/dist/folding_range_service.js +209 -0
  18. package/dist/folding_range_service.js.map +1 -0
  19. package/dist/formatting_service.js +4 -4
  20. package/dist/formatting_service.js.map +1 -1
  21. package/dist/herb-language-server.js +152936 -41156
  22. package/dist/herb-language-server.js.map +1 -1
  23. package/dist/hover_service.js +70 -0
  24. package/dist/hover_service.js.map +1 -0
  25. package/dist/index.cjs +1299 -333
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/line_context_collector.js +73 -0
  30. package/dist/line_context_collector.js.map +1 -0
  31. package/dist/linter_service.js +27 -6
  32. package/dist/linter_service.js.map +1 -1
  33. package/dist/parser_service.js +6 -5
  34. package/dist/parser_service.js.map +1 -1
  35. package/dist/range_utils.js +65 -0
  36. package/dist/range_utils.js.map +1 -0
  37. package/dist/rewrite_code_action_service.js +135 -0
  38. package/dist/rewrite_code_action_service.js.map +1 -0
  39. package/dist/server.js +39 -2
  40. package/dist/server.js.map +1 -1
  41. package/dist/service.js +10 -0
  42. package/dist/service.js.map +1 -1
  43. package/dist/types/action_view_helpers.d.ts +5 -0
  44. package/dist/types/comment_ast_utils.d.ts +20 -0
  45. package/dist/types/comment_service.d.ts +14 -0
  46. package/dist/types/diagnostics.d.ts +1 -35
  47. package/dist/types/document_highlight_service.d.ts +28 -0
  48. package/dist/types/document_save_service.d.ts +8 -0
  49. package/dist/types/folding_range_service.d.ts +35 -0
  50. package/dist/types/formatting_service.d.ts +1 -1
  51. package/dist/types/hover_service.d.ts +8 -0
  52. package/dist/types/index.d.ts +4 -0
  53. package/dist/types/line_context_collector.d.ts +19 -0
  54. package/dist/types/linter_service.d.ts +1 -0
  55. package/dist/types/parser_service.d.ts +2 -1
  56. package/dist/types/range_utils.d.ts +16 -0
  57. package/dist/types/rewrite_code_action_service.d.ts +11 -0
  58. package/dist/types/service.d.ts +10 -0
  59. package/dist/types/utils.d.ts +4 -8
  60. package/dist/utils.js +10 -15
  61. package/dist/utils.js.map +1 -1
  62. package/package.json +10 -5
  63. package/src/action_view_helpers.ts +23 -0
  64. package/src/autofix_service.ts +1 -1
  65. package/src/code_action_service.ts +3 -6
  66. package/src/comment_ast_utils.ts +282 -0
  67. package/src/comment_service.ts +228 -0
  68. package/src/diagnostics.ts +1 -305
  69. package/src/document_highlight_service.ts +267 -0
  70. package/src/document_save_service.ts +19 -7
  71. package/src/folding_range_service.ts +287 -0
  72. package/src/formatting_service.ts +4 -4
  73. package/src/hover_service.ts +90 -0
  74. package/src/index.ts +4 -0
  75. package/src/line_context_collector.ts +97 -0
  76. package/src/linter_service.ts +35 -9
  77. package/src/parser_service.ts +9 -10
  78. package/src/range_utils.ts +90 -0
  79. package/src/rewrite_code_action_service.ts +165 -0
  80. package/src/server.ts +54 -2
  81. package/src/service.ts +15 -0
  82. package/src/utils.ts +12 -21
@@ -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,14 +1,15 @@
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"
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 = 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
- return {
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 }
@@ -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
  }
@@ -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 autofixCodeActions.concat(linterDisableCodeActions)
228
+ return this.service.commentService.toggleBlockComment(document, params.range)
177
229
  })
178
230
  }
179
231