@herb-tools/language-server 0.8.10 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/action_view_helpers.js +19 -0
  2. package/dist/action_view_helpers.js.map +1 -0
  3. package/dist/autofix_service.js +1 -1
  4. package/dist/autofix_service.js.map +1 -1
  5. package/dist/code_action_service.js +3 -6
  6. package/dist/code_action_service.js.map +1 -1
  7. package/dist/comment_ast_utils.js +206 -0
  8. package/dist/comment_ast_utils.js.map +1 -0
  9. package/dist/comment_service.js +175 -0
  10. package/dist/comment_service.js.map +1 -0
  11. package/dist/diagnostics.js +15 -27
  12. package/dist/diagnostics.js.map +1 -1
  13. package/dist/document_highlight_service.js +196 -0
  14. package/dist/document_highlight_service.js.map +1 -0
  15. package/dist/document_save_service.js +16 -6
  16. package/dist/document_save_service.js.map +1 -1
  17. package/dist/folding_range_service.js +209 -0
  18. package/dist/folding_range_service.js.map +1 -0
  19. package/dist/formatting_service.js +4 -4
  20. package/dist/formatting_service.js.map +1 -1
  21. package/dist/herb-language-server.js +150189 -41260
  22. package/dist/herb-language-server.js.map +1 -1
  23. package/dist/hover_service.js +70 -0
  24. package/dist/hover_service.js.map +1 -0
  25. package/dist/index.cjs +1227 -63
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/line_context_collector.js +73 -0
  30. package/dist/line_context_collector.js.map +1 -0
  31. package/dist/linter_service.js +20 -4
  32. package/dist/linter_service.js.map +1 -1
  33. package/dist/parser_service.js +6 -5
  34. package/dist/parser_service.js.map +1 -1
  35. package/dist/range_utils.js +65 -0
  36. package/dist/range_utils.js.map +1 -0
  37. package/dist/rewrite_code_action_service.js +135 -0
  38. package/dist/rewrite_code_action_service.js.map +1 -0
  39. package/dist/server.js +36 -2
  40. package/dist/server.js.map +1 -1
  41. package/dist/service.js +10 -0
  42. package/dist/service.js.map +1 -1
  43. package/dist/types/action_view_helpers.d.ts +5 -0
  44. package/dist/types/comment_ast_utils.d.ts +20 -0
  45. package/dist/types/comment_service.d.ts +14 -0
  46. package/dist/types/diagnostics.d.ts +0 -1
  47. package/dist/types/document_highlight_service.d.ts +28 -0
  48. package/dist/types/document_save_service.d.ts +8 -0
  49. package/dist/types/folding_range_service.d.ts +35 -0
  50. package/dist/types/formatting_service.d.ts +1 -1
  51. package/dist/types/hover_service.d.ts +8 -0
  52. package/dist/types/index.d.ts +4 -0
  53. package/dist/types/line_context_collector.d.ts +19 -0
  54. package/dist/types/linter_service.d.ts +1 -0
  55. package/dist/types/parser_service.d.ts +2 -1
  56. package/dist/types/range_utils.d.ts +16 -0
  57. package/dist/types/rewrite_code_action_service.d.ts +11 -0
  58. package/dist/types/service.d.ts +10 -0
  59. package/dist/types/utils.d.ts +0 -6
  60. package/dist/utils.js +0 -16
  61. package/dist/utils.js.map +1 -1
  62. package/package.json +10 -5
  63. package/src/action_view_helpers.ts +23 -0
  64. package/src/autofix_service.ts +1 -1
  65. package/src/code_action_service.ts +3 -6
  66. package/src/comment_ast_utils.ts +282 -0
  67. package/src/comment_service.ts +228 -0
  68. package/src/diagnostics.ts +24 -38
  69. package/src/document_highlight_service.ts +267 -0
  70. package/src/document_save_service.ts +19 -7
  71. package/src/folding_range_service.ts +287 -0
  72. package/src/formatting_service.ts +4 -4
  73. package/src/hover_service.ts +90 -0
  74. package/src/index.ts +4 -0
  75. package/src/line_context_collector.ts +97 -0
  76. package/src/linter_service.ts +25 -7
  77. package/src/parser_service.ts +9 -10
  78. package/src/range_utils.ts +90 -0
  79. package/src/rewrite_code_action_service.ts +165 -0
  80. package/src/server.ts +51 -2
  81. package/src/service.ts +15 -0
  82. package/src/utils.ts +0 -22
@@ -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,6 +165,22 @@ 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
 
@@ -172,8 +196,33 @@ export class Server {
172
196
  )
173
197
 
174
198
  const autofixCodeActions = this.service.codeActionService.autofixCodeActions(params, document)
199
+ const rewriteCodeActions = this.service.rewriteCodeActionService.getCodeActions(document, params.range)
200
+
201
+ return autofixCodeActions.concat(linterDisableCodeActions).concat(rewriteCodeActions)
202
+ })
203
+
204
+ this.connection.onFoldingRanges((params: FoldingRangeParams) => {
205
+ const document = this.service.documentService.get(params.textDocument.uri)
206
+
207
+ if (!document) return []
208
+
209
+ return this.service.foldingRangeService.getFoldingRanges(document)
210
+ })
211
+
212
+ this.connection.onRequest('herb/toggleLineComment', (params: { textDocument: TextDocumentIdentifier, range: Range }) => {
213
+ const document = this.service.documentService.get(params.textDocument.uri)
214
+
215
+ if (!document) return []
216
+
217
+ return this.service.commentService.toggleLineComment(document, params.range)
218
+ })
219
+
220
+ this.connection.onRequest('herb/toggleBlockComment', (params: { textDocument: TextDocumentIdentifier, range: Range }) => {
221
+ const document = this.service.documentService.get(params.textDocument.uri)
222
+
223
+ if (!document) return []
175
224
 
176
- return autofixCodeActions.concat(linterDisableCodeActions)
225
+ return this.service.commentService.toggleBlockComment(document, params.range)
177
226
  })
178
227
  }
179
228
 
package/src/service.ts CHANGED
@@ -12,6 +12,11 @@ import { ConfigService } from "./config_service"
12
12
  import { AutofixService } from "./autofix_service"
13
13
  import { CodeActionService } from "./code_action_service"
14
14
  import { DocumentSaveService } from "./document_save_service"
15
+ import { FoldingRangeService } from "./folding_range_service"
16
+ import { DocumentHighlightService } from "./document_highlight_service"
17
+ import { HoverService } from "./hover_service"
18
+ import { RewriteCodeActionService } from "./rewrite_code_action_service"
19
+ import { CommentService } from "./comment_service"
15
20
 
16
21
  import { version } from "../package.json"
17
22
 
@@ -30,6 +35,11 @@ export class Service {
30
35
  configService: ConfigService
31
36
  codeActionService: CodeActionService
32
37
  documentSaveService: DocumentSaveService
38
+ foldingRangeService: FoldingRangeService
39
+ documentHighlightService: DocumentHighlightService
40
+ hoverService: HoverService
41
+ rewriteCodeActionService: RewriteCodeActionService
42
+ commentService: CommentService
33
43
 
34
44
  constructor(connection: Connection, params: InitializeParams) {
35
45
  this.connection = connection
@@ -44,6 +54,11 @@ export class Service {
44
54
  this.codeActionService = new CodeActionService(this.project, this.config)
45
55
  this.diagnostics = new Diagnostics(this.connection, this.documentService, this.parserService, this.linterService, this.configService)
46
56
  this.documentSaveService = new DocumentSaveService(this.connection, this.settings, this.autofixService, this.formattingService)
57
+ this.foldingRangeService = new FoldingRangeService(this.parserService)
58
+ this.documentHighlightService = new DocumentHighlightService(this.parserService)
59
+ this.hoverService = new HoverService(this.parserService)
60
+ this.rewriteCodeActionService = new RewriteCodeActionService(this.parserService)
61
+ this.commentService = new CommentService(this.parserService)
47
62
 
48
63
  if (params.initializationOptions) {
49
64
  this.settings.globalSettings = params.initializationOptions as PersonalHerbSettings
package/src/utils.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  import { DiagnosticSeverity } from "vscode-languageserver/node"
2
2
  import type { LintSeverity } from "@herb-tools/linter"
3
3
 
4
- import { Position } from "vscode-languageserver/node"
5
-
6
- import type { Range } from "vscode-languageserver/node"
7
- import type { TextDocument } from "vscode-languageserver-textdocument"
8
-
9
4
  export function camelize(value: string) {
10
5
  return value.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase())
11
6
  }
@@ -26,20 +21,3 @@ export function lintToDignosticSeverity(severity: LintSeverity): DiagnosticSever
26
21
  case "hint": return DiagnosticSeverity.Hint
27
22
  }
28
23
  }
29
-
30
- /**
31
- * Returns a Range that spans the entire document
32
- */
33
- export function getFullDocumentRange(document: TextDocument): Range {
34
- const lastLine = document.lineCount - 1
35
- const lastLineText = document.getText({
36
- start: Position.create(lastLine, 0),
37
- end: Position.create(lastLine + 1, 0)
38
- })
39
- const lastLineLength = lastLineText.length
40
-
41
- return {
42
- start: Position.create(0, 0),
43
- end: Position.create(lastLine, lastLineLength)
44
- }
45
- }