Skip to content
This repository was archived by the owner on Jul 31, 2023. It is now read-only.

Commit fc7e89a

Browse files
committed
feat: Convert FoldingRangeAnalyzer to tree-sitter queries
This fixes a few outstanding bugs + should be simpler/more performant
1 parent 056d6db commit fc7e89a

File tree

5 files changed

+130
-51
lines changed

5 files changed

+130
-51
lines changed

packages/language-server-ruby/spec/analyzers/FoldingRangeAnalyzer.spec.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from 'chai';
22
import { FoldingRange } from 'vscode-languageserver';
33

44
import FoldingRangeAnalyzer, { FoldHeuristic } from '../../src/analyzers/FoldingRangeAnalyzer';
5-
import { loadFixture, walkTSTree, Fixture } from '../helper';
5+
import { getParser, loadFixture, Fixture } from '../helper';
66

77
/**
88
* NOTICE
@@ -60,8 +60,8 @@ describe('FoldingRangeAnalyzer', () => {
6060

6161
before(() => {
6262
fixture = loadFixture('folds.rb');
63-
analyzer = new FoldingRangeAnalyzer();
64-
walkTSTree(fixture.tree, analyzer.analyze.bind(analyzer));
63+
analyzer = new FoldingRangeAnalyzer(getParser().getLanguage());
64+
analyzer.analyze(fixture.tree.rootNode);
6565
});
6666

6767
after(() => {
@@ -85,7 +85,11 @@ describe('FoldingRangeAnalyzer', () => {
8585
]);
8686
});
8787

88-
describe('blocks', () => {});
88+
describe('requires', () => {
89+
itIdentifiesFolds([
90+
FoldingRange.create(0, 2, 0, 14, 'imports'), // require()...
91+
]);
92+
});
8993

9094
describe('case statements', () => {
9195
itIdentifiesFolds([
@@ -96,12 +100,12 @@ describe('FoldingRangeAnalyzer', () => {
96100

97101
describe('when statements', () => {
98102
itIdentifiesFolds([
99-
FoldingRange.create(51, 52, 12, 18, 'region'), // method1, case a, when 1
100-
FoldingRange.create(53, 54, 12, 18, 'region'), // method1, case a, when 2
101-
FoldingRange.create(55, 56, 12, 18, 'region'), // method1, case a, when 3
102-
FoldingRange.create(60, 61, 17, 18, 'region'), // method1, case, when a == 1
103-
FoldingRange.create(62, 63, 17, 18, 'region'), // method1, case, when a == 2
104-
FoldingRange.create(64, 65, 17, 18, 'region'), // method1, case, when a == 3
103+
FoldingRange.create(51, 52, 6, 18, 'region'), // method1, case a, when 1
104+
FoldingRange.create(53, 54, 6, 18, 'region'), // method1, case a, when 2
105+
FoldingRange.create(55, 56, 6, 18, 'region'), // method1, case a, when 3
106+
FoldingRange.create(60, 61, 6, 18, 'region'), // method1, case, when a == 1
107+
FoldingRange.create(62, 63, 6, 18, 'region'), // method1, case, when a == 2
108+
FoldingRange.create(64, 65, 6, 18, 'region'), // method1, case, when a == 3
105109
]);
106110
});
107111

@@ -121,13 +125,13 @@ describe('FoldingRangeAnalyzer', () => {
121125

122126
describe('begin blocks', () => {
123127
itIdentifiesFolds([
124-
FoldingRange.create(127, 130, 6, 9, 'region'), // method5, begin...
128+
FoldingRange.create(127, 128, 6, undefined, 'region'), // method5, begin...
125129
]);
126130
});
127131

128132
describe('rescue blocks', () => {
129133
itIdentifiesFolds([
130-
FoldingRange.create(129, 130, 12, 33, 'region'), // method5, rescue...
134+
FoldingRange.create(129, 130, 6, 33, 'region'), // method5, rescue...
131135
]);
132136
});
133137

@@ -145,21 +149,21 @@ describe('FoldingRangeAnalyzer', () => {
145149

146150
describe('heredocs', () => {
147151
itIdentifiesFolds([
148-
FoldingRange.create(69, 72, 19, 4, 'region'), // method2, text
149-
FoldingRange.create(73, 75, 21, 10, 'region'), // method2, text2
150-
FoldingRange.create(77, 79, 21, 10, 'region'), // method2, text3
152+
FoldingRange.create(70, 72, 19, 4, 'region'), // method2, text
153+
FoldingRange.create(74, 75, 21, 10, 'region'), // method2, text2
154+
FoldingRange.create(78, 79, 21, 10, 'region'), // method2, text3
151155
]);
152156
});
153157

154158
describe('if blocks', () => {
155159
itIdentifiesFolds([
156-
FoldingRange.create(84, 88, 12, 11, 'region'), // method3, if (a)
157-
FoldingRange.create(116, 117, 10, 18, 'region'), // method4, if a
160+
FoldingRange.create(84, 88, 6, undefined, 'region'), // method3, if (a)
161+
FoldingRange.create(116, 117, 6, 9, 'region'), // method4, if a
158162
]);
159163

160164
describe('then blocks', () => {
161165
itIdentifiesFolds([
162-
FoldingRange.create(97, 98, 13, 22, 'region'), // method3, if (b) then
166+
FoldingRange.create(97, 98, 6, 9, 'region'), // method3, if (b) then
163167
]);
164168
});
165169

@@ -174,13 +178,13 @@ describe('FoldingRangeAnalyzer', () => {
174178

175179
describe('unless blocks', () => {
176180
itIdentifiesFolds([
177-
FoldingRange.create(93, 94, 16, 22, 'region'), // method3, unless (a)
178-
FoldingRange.create(119, 120, 15, 18, 'region'), // method4, unless a
181+
FoldingRange.create(93, 94, 6, 9, 'region'), // method3, unless (a)
182+
FoldingRange.create(119, 120, 6, 9, 'region'), // method4, unless a
179183
]);
180184

181185
describe('then blocks', () => {
182186
itIdentifiesFolds([
183-
FoldingRange.create(101, 102, 17, 26, 'region'), // method3, unless (b) then
187+
FoldingRange.create(101, 102, 6, 9, 'region'), // method3, unless (b) then
184188
]);
185189
});
186190
});

packages/language-server-ruby/spec/helper.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SyntaxNode, Tree } from 'web-tree-sitter';
1+
import Parser, { SyntaxNode, Tree } from 'web-tree-sitter';
22

33
export interface Fixture {
44
content: string;
@@ -23,3 +23,7 @@ export function walkTSTree(tree: Tree, action: (node: SyntaxNode) => void): void
2323
walk(0);
2424
cursor.delete();
2525
}
26+
27+
export function getParser(): Parser {
28+
return (global as any).loader.parser;
29+
}

packages/language-server-ruby/src/Analyzer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Observer } from 'rxjs';
44
import { map } from 'rxjs/operators';
55
import { Tree, SyntaxNode } from 'web-tree-sitter';
66
import DocumentSymbolAnalyzer from './analyzers/DocumentSymbolAnalyzer';
7-
import { forestStream, ForestEventKind } from './Forest';
7+
import { forest, forestStream, ForestEventKind } from './Forest';
88
import FoldingRangeAnalyzer from './analyzers/FoldingRangeAnalyzer';
99

1010
interface Analysis {
@@ -18,7 +18,7 @@ class Analyzer {
1818
private documentSymbolAnalyzer: DocumentSymbolAnalyzer;
1919

2020
constructor(public uri: string) {
21-
this.foldingRangeAnalyzer = new FoldingRangeAnalyzer();
21+
this.foldingRangeAnalyzer = new FoldingRangeAnalyzer(forest.parser.getLanguage());
2222
this.documentSymbolAnalyzer = new DocumentSymbolAnalyzer();
2323
}
2424

@@ -31,6 +31,8 @@ class Analyzer {
3131
}
3232

3333
public analyze(tree: Tree): Analysis {
34+
this.foldingRangeAnalyzer.analyze(tree.rootNode);
35+
3436
const cursor = tree.walk();
3537
const walk = (depth: number): void => {
3638
this.analyzeNode(cursor.currentNode());
@@ -48,7 +50,6 @@ class Analyzer {
4850
}
4951

5052
private analyzeNode(node: SyntaxNode): void {
51-
this.foldingRangeAnalyzer.analyze(node);
5253
this.documentSymbolAnalyzer.analyze(node);
5354
}
5455
}

packages/language-server-ruby/src/analyzers/FoldingRangeAnalyzer.ts

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FoldingRange, FoldingRangeKind } from 'vscode-languageserver';
2-
import { SyntaxNode } from 'web-tree-sitter';
2+
import Parser, { Query, SyntaxNode } from 'web-tree-sitter';
33
import BaseAnalyzer from './BaseAnalyzer';
4+
import FOLDS_QUERY from './queries/folds';
45

56
interface IFoldLocation {
67
row?: number;
@@ -38,7 +39,22 @@ export class FoldHeuristic {
3839
}
3940
}
4041

42+
/**
43+
* This is an analyzer for determining fold regions for VSCode
44+
*
45+
* The FOLD_NODES map is used to offset the tree-sitter end points to make sure
46+
* keywords like "end" are not folded. This works similarly to the way {} are folded
47+
* for languages like JavaScript in VSCode
48+
*/
49+
4150
export default class FoldingRangeAnalyzer extends BaseAnalyzer<FoldingRange> {
51+
private readonly tsQuery: Query;
52+
53+
constructor(language: Parser.Language) {
54+
super();
55+
this.tsQuery = language.query(FOLDS_QUERY);
56+
}
57+
4258
private readonly FOLD_NODES: Map<string, FoldHeuristic> = new Map([
4359
[
4460
'array',
@@ -48,7 +64,6 @@ export default class FoldingRangeAnalyzer extends BaseAnalyzer<FoldingRange> {
4864
},
4965
}),
5066
],
51-
['block', new FoldHeuristic()],
5267
[
5368
'case',
5469
new FoldHeuristic({
@@ -57,7 +72,6 @@ export default class FoldingRangeAnalyzer extends BaseAnalyzer<FoldingRange> {
5772
},
5873
}),
5974
],
60-
['when', new FoldHeuristic()],
6175
[
6276
'class',
6377
new FoldHeuristic({
@@ -66,7 +80,6 @@ export default class FoldingRangeAnalyzer extends BaseAnalyzer<FoldingRange> {
6680
},
6781
}),
6882
],
69-
['comment', new FoldHeuristic()],
7083
[
7184
'begin',
7285
new FoldHeuristic({
@@ -75,7 +88,6 @@ export default class FoldingRangeAnalyzer extends BaseAnalyzer<FoldingRange> {
7588
},
7689
}),
7790
],
78-
['do_block', new FoldHeuristic()],
7991
[
8092
'hash',
8193
new FoldHeuristic({
@@ -87,16 +99,11 @@ export default class FoldingRangeAnalyzer extends BaseAnalyzer<FoldingRange> {
8799
[
88100
'heredoc_body',
89101
new FoldHeuristic({
90-
start: {
91-
row: -1,
92-
},
93102
end: {
94103
row: -1,
95104
},
96105
}),
97106
],
98-
['then', new FoldHeuristic()], // body of an if and unless statement
99-
['else', new FoldHeuristic()],
100107
[
101108
'method',
102109
new FoldHeuristic({
@@ -121,37 +128,64 @@ export default class FoldingRangeAnalyzer extends BaseAnalyzer<FoldingRange> {
121128
},
122129
}),
123130
],
131+
[
132+
'if',
133+
new FoldHeuristic({
134+
end: {
135+
row: -1,
136+
},
137+
}),
138+
],
139+
[
140+
'unless',
141+
new FoldHeuristic({
142+
end: {
143+
row: -1,
144+
},
145+
}),
146+
],
124147
]);
125148

149+
// Used to build "implicit blocks", such as a block comment
150+
// made up of multiple lines of # style comments
126151
private lastNodeAnalyzed: SyntaxNode;
127152

128153
get foldingRanges(): FoldingRange[] {
129154
return this.diagnostics;
130155
}
131156

132157
public analyze(node: SyntaxNode): void {
133-
if (this.FOLD_NODES.has(node.type)) {
134-
const heuristic: IFoldHeuristic = this.FOLD_NODES.get(node.type);
158+
const captures = this.tsQuery.captures(node);
159+
for (const capture of captures) {
160+
const { name, node } = capture;
161+
162+
// The tree-sitter query captures the call and the identifier nodes
163+
// just skip the named identifier node
164+
if (name === 'require') continue;
165+
166+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
167+
const heuristic: IFoldHeuristic = this.FOLD_NODES.get(node.type) || new FoldHeuristic();
135168
if (this.determineImplicitBlock(node, this.lastNodeAnalyzed) && this.diagnostics.length > 0) {
136169
const foldingRange: FoldingRange = this.diagnostics[this.diagnostics.length - 1];
137170
foldingRange.endLine = node.endPosition.row;
138171
foldingRange.endCharacter = node.endPosition.column;
139172
} else {
140-
const foldingRange = {
173+
const foldingRange: FoldingRange = {
141174
startLine: node.startPosition.row + heuristic.start.row,
142175
startCharacter: node.startPosition.column + heuristic.start.column,
143176
endLine: node.endPosition.row + heuristic.end.row,
144177
endCharacter: node.endPosition.column + heuristic.end.column,
145178
kind: this.getFoldKind(node.type),
146179
};
147180

148-
// Fold end - fold start must be >= 1 or they must be comments
149-
if (
150-
foldingRange.endLine > foldingRange.startLine ||
151-
foldingRange.kind === FoldingRangeKind.Comment
152-
) {
153-
this.diagnostics.push(foldingRange);
181+
// handle shortening a fold for nested nodes
182+
if (node.type === 'begin') {
183+
this.handleNestedNode(node, 'rescue', foldingRange);
184+
} else if (node.type === 'if') {
185+
this.handleNestedNode(node, 'else', foldingRange);
154186
}
187+
188+
this.diagnostics.push(foldingRange);
155189
}
156190
this.lastNodeAnalyzed = node;
157191
}
@@ -161,25 +195,39 @@ export default class FoldingRangeAnalyzer extends BaseAnalyzer<FoldingRange> {
161195
switch (nodeType) {
162196
case 'comment':
163197
return FoldingRangeKind.Comment;
164-
case 'require':
198+
case 'call':
165199
return FoldingRangeKind.Imports;
166200
default:
167201
return FoldingRangeKind.Region;
168202
}
169203
}
170204

205+
private handleNestedNode(node: SyntaxNode, nodeType: string, range: FoldingRange): void {
206+
const descendants = node.descendantsOfType(nodeType);
207+
if (descendants.length > 0) {
208+
const {
209+
startPosition: { row },
210+
} = descendants[0];
211+
range.endLine = row - 1;
212+
delete range.endCharacter;
213+
}
214+
}
215+
171216
/**
172217
* Tree Sitter does not identify block comments as blocks. This will collect them together
173218
* into one fold
174219
*/
175220
private determineImplicitBlock(node: SyntaxNode, lastNode: SyntaxNode): boolean {
176221
return (
177-
node.type === 'comment' &&
178-
node.text[0] === '#' &&
179-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
180-
lastNode &&
181-
lastNode.type === 'comment' &&
182-
lastNode.text[0] === '#'
222+
lastNode !== undefined &&
223+
((node.type === 'comment' &&
224+
node.text[0] === '#' &&
225+
lastNode.type === 'comment' &&
226+
lastNode.text[0] === '#') ||
227+
(node.type === 'call' &&
228+
node.text.indexOf('require') === 0 &&
229+
lastNode.type === 'call' &&
230+
lastNode.text.indexOf('require') === 0))
183231
);
184232
}
185233
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const FOLDS_QUERY = `[
2+
(method)
3+
(singleton_method)
4+
(class)
5+
(module)
6+
(if)
7+
(else)
8+
(unless)
9+
(case)
10+
(when)
11+
(do_block)
12+
(singleton_class)
13+
(lambda)
14+
(comment)
15+
(heredoc_body)
16+
(begin)
17+
(rescue)
18+
(hash)
19+
(array)
20+
(call method: (identifier) @require (#match? @require "require"))
21+
] @fold`;
22+
export default FOLDS_QUERY;

0 commit comments

Comments
 (0)