Skip to content

Commit 76053a7

Browse files
authored
1 parent 377d241 commit 76053a7

File tree

6 files changed

+806
-0
lines changed

6 files changed

+806
-0
lines changed
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

javascript/packages/language-server/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from "./service"
33
export * from "./diagnostics"
44
export * from "./document_service"
55
export * from "./formatting_service"
6+
export * from "./folding_range_service"
67
export * from "./project"
78
export * from "./settings"
89
export * from "./utils"

javascript/packages/language-server/src/range_utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export function lspPosition(herbPosition: SerializedPosition): Position {
66
return Position.create(herbPosition.line - 1, herbPosition.column)
77
}
88

9+
export function lspLine(herbPosition: SerializedPosition): number {
10+
return herbPosition.line - 1
11+
}
12+
913
export function lspRangeFromLocation(herbLocation: SerializedLocation): Range {
1014
return Range.create(lspPosition(herbLocation.start), lspPosition(herbLocation.end))
1115
}

javascript/packages/language-server/src/server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
DocumentRangeFormattingParams,
1313
CodeActionParams,
1414
CodeActionKind,
15+
FoldingRangeParams,
1516
DocumentHighlightParams,
1617
} from "vscode-languageserver/node"
1718

@@ -54,6 +55,7 @@ export class Server {
5455
codeActionProvider: {
5556
codeActionKinds: [CodeActionKind.QuickFix, CodeActionKind.SourceFixAll]
5657
},
58+
foldingRangeProvider: true,
5759
documentHighlightProvider: true,
5860
},
5961
}
@@ -185,6 +187,14 @@ export class Server {
185187

186188
return autofixCodeActions.concat(linterDisableCodeActions)
187189
})
190+
191+
this.connection.onFoldingRanges((params: FoldingRangeParams) => {
192+
const document = this.service.documentService.get(params.textDocument.uri)
193+
194+
if (!document) return []
195+
196+
return this.service.foldingRangeService.getFoldingRanges(document)
197+
})
188198
}
189199

190200
listen() {

javascript/packages/language-server/src/service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ConfigService } from "./config_service"
1212
import { AutofixService } from "./autofix_service"
1313
import { CodeActionService } from "./code_action_service"
1414
import { DocumentSaveService } from "./document_save_service"
15+
import { FoldingRangeService } from "./folding_range_service"
1516
import { DocumentHighlightService } from "./document_highlight_service"
1617

1718
import { version } from "../package.json"
@@ -31,6 +32,7 @@ export class Service {
3132
configService: ConfigService
3233
codeActionService: CodeActionService
3334
documentSaveService: DocumentSaveService
35+
foldingRangeService: FoldingRangeService
3436
documentHighlightService: DocumentHighlightService
3537

3638
constructor(connection: Connection, params: InitializeParams) {
@@ -46,6 +48,7 @@ export class Service {
4648
this.codeActionService = new CodeActionService(this.project, this.config)
4749
this.diagnostics = new Diagnostics(this.connection, this.documentService, this.parserService, this.linterService, this.configService)
4850
this.documentSaveService = new DocumentSaveService(this.connection, this.settings, this.autofixService, this.formattingService)
51+
this.foldingRangeService = new FoldingRangeService(this.parserService)
4952
this.documentHighlightService = new DocumentHighlightService(this.parserService)
5053

5154
if (params.initializationOptions) {

0 commit comments

Comments
 (0)