Skip to content

Commit 93f384f

Browse files
authored
Merge pull request #53 from SSlinky/dev
Add folding ranges
2 parents d5df6a0 + 774b577 commit 93f384f

File tree

14 files changed

+291
-77
lines changed

14 files changed

+291
-77
lines changed

client/src/test/diagnostics.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as vscode from 'vscode';
77
import * as assert from 'assert';
88
import { getDocUri, activate } from './helper';
9+
import { toRange } from './util';
910

1011
suite('Should get diagnostics', () => {
1112
test('diagnostics.class.missingNameAttributeError', async () => {
@@ -143,12 +144,6 @@ suite('Should get diagnostics', () => {
143144
});
144145
});
145146

146-
function toRange(sLine: number, sChar: number, eLine: number, eChar: number) {
147-
const start = new vscode.Position(sLine - 1, sChar);
148-
const end = new vscode.Position(eLine - 1, eChar);
149-
return new vscode.Range(start, end);
150-
}
151-
152147
async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.Diagnostic[]) {
153148
await activate(docUri);
154149

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as vscode from 'vscode';
2+
import * as assert from 'assert';
3+
import { getDocUri, activate } from './helper';
4+
5+
suite('Should get text edits', () => {
6+
test('formatting.class.template', async () => {
7+
const subFoo = {start: 23, end: 42};
8+
const subBar = {start: 44, end: 58};
9+
const ifBlockOuter = {start: 33, end: 41};
10+
const ifBlockInner = {start: 34, end: 36};
11+
const whileWend = {start: 54, end: 57};
12+
const propFoobar = {start: 61, end: 63};
13+
14+
await testFoldingRanges(getDocUri('FoldingRanges.bas'), [
15+
subFoo,
16+
ifBlockOuter,
17+
ifBlockInner,
18+
subBar,
19+
whileWend,
20+
propFoobar
21+
]);
22+
});
23+
});
24+
25+
async function testFoldingRanges(docUri: vscode.Uri, expectedFoldingRanges: vscode.FoldingRange[]) {
26+
await activate(docUri);
27+
const actualFoldingRanges = await vscode.commands.executeCommand<vscode.FoldingRange[]>(
28+
'vscode.executeFoldingRangeProvider',
29+
docUri
30+
)
31+
32+
assert.equal(actualFoldingRanges.length ?? 0, expectedFoldingRanges.length, "Count");
33+
34+
expectedFoldingRanges.forEach((expectedFoldingRange, i) => {
35+
const actualFoldingRange = actualFoldingRanges[i];
36+
assert.deepEqual(actualFoldingRange, expectedFoldingRange, `FoldingRange ${i}`);
37+
});
38+
}

client/src/test/formatting.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from 'vscode';
22
import * as assert from 'assert';
33
import { getDocUri, activate } from './helper';
4+
import { toRange } from './util';
45

56
suite('Should get text edits', () => {
67
test('formatting.class.template', async () => {
@@ -39,12 +40,6 @@ suite('Should get text edits', () => {
3940
});
4041
});
4142

42-
function toRange(sLine: number, sChar: number, eLine: number, eChar: number) {
43-
const start = new vscode.Position(sLine - 1, sChar);
44-
const end = new vscode.Position(eLine - 1, eChar);
45-
return new vscode.Range(start, end);
46-
}
47-
4843
async function testTextEdits(docUri: vscode.Uri, expectedTextEdits: vscode.TextEdit[]) {
4944
await activate(docUri);
5045
const actualEdits = await vscode.commands.executeCommand<vscode.TextEdit[]>(

client/src/test/util.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Position, Range } from 'vscode';
2+
3+
export function toRange(sLine: number, sChar: number, eLine: number, eChar: number) {
4+
const start = new Position(sLine - 1, sChar);
5+
const end = new Position(eLine - 1, eChar);
6+
return new Range(start, end);
7+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
Attribute VB_Name = "FoldingRanges"
2+
' Copyright 2024 Sam Vanderslink
3+
'
4+
' Permission is hereby granted, free of charge, to any person obtaining a copy
5+
' of this software and associated documentation files (the "Software"), to deal
6+
' in the Software without restriction, including without limitation the rights
7+
' to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
' copies of the Software, and to permit persons to whom the Software is
9+
' furnished to do so, subject to the following conditions:
10+
'
11+
' The above copyright notice and this permission notice shall be included in
12+
' all copies or substantial portions of the Software.
13+
'
14+
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20+
' IN THE SOFTWARE.
21+
22+
Option Explicit
23+
24+
Public Sub Foo()
25+
Attribute Foo.VB_Description = "Tests a folding range."
26+
' Tests a folding range.
27+
'
28+
' Args:
29+
' param1:
30+
'
31+
' Raises:
32+
'
33+
34+
If FoldingRanges Then
35+
If FoldingRanges Then
36+
Debug.Print "Great!"
37+
End If
38+
ElseIf UnfoldingRanges Then
39+
Debug.Print "What does this even mean?"
40+
Else
41+
Debug.Print "Not great..."
42+
End If
43+
End Sub
44+
45+
Public Sub Bar()
46+
Attribute Bar.VB_Description = "Tests more folding ranges."
47+
' Tests more folding ranges.
48+
'
49+
' Args:
50+
' param1:
51+
'
52+
' Raises:
53+
'
54+
55+
While True
56+
Debug.Print "You ain't never going home!"
57+
DoEvents
58+
Wend
59+
End Sub
60+
61+
62+
Public Function FooBar() As Long
63+
64+
End Function

server/src/capabilities/capabilities.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@ abstract class BaseCapability {
2828

2929
export class FoldingRangeCapability extends BaseCapability {
3030
foldingRangeKind?: FoldingRangeKind;
31+
openWord?: string;
32+
closeWord?: string;
3133

3234
get foldingRange(): FoldingRange {
33-
return new FoldingRange(this.element.context.range, this.foldingRangeKind);
35+
const trailingLineCount = this.element.context.rule.countTrailingLineEndings();
36+
const start = this.element.context.range.start;
37+
const end = {
38+
line: this.element.context.range.end.line - trailingLineCount,
39+
character: this.element.context.range.end.character
40+
}
41+
const range = Range.create(start, end);
42+
return new FoldingRange(range, this.foldingRangeKind, this.openWord, this.closeWord);
3443
}
3544

3645
constructor(element: BaseContextSyntaxElement<ParserRuleContext>, foldingRangeKind?: FoldingRangeKind) {

server/src/capabilities/folding.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,44 @@ export class FoldingRange implements VscFoldingRange {
2424
/**
2525
* The zero-based line number from where the folded range starts.
2626
*/
27-
startLine: number;
28-
29-
/**
30-
* The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line.
31-
*/
32-
startCharacter?: number;
27+
get startLine(): number {
28+
return this._range.start.line;
29+
}
3330

3431
/**
3532
* The zero-based line number where the folded range ends.
3633
*/
37-
endLine: number;
38-
39-
/**
40-
* The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line.
41-
*/
42-
endCharacter?: number;
34+
get endLine(): number {
35+
return this._range.end.line;
36+
}
4337

4438
/**
4539
* Describes the kind of the folding range such as 'comment' or 'region'. The kind
4640
* is used to categorize folding ranges and used by commands like 'Fold all comments'. See
4741
* [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds.
4842
*/
49-
kind?: string;
43+
get kind(): string | undefined {
44+
return this._foldingRangeKind
45+
}
46+
47+
get openWord(): string {
48+
return this._openWord ?? '';
49+
}
5050

51-
constructor(range: Range, foldingRangeKind?: FoldingRangeKind) {
52-
this.startLine = range.start.line;
53-
this.endLine = range.end.line;
54-
this.kind = foldingRangeKind;
51+
get closeWord(): string {
52+
return this._closeWord ?? '';
5553
}
54+
55+
get range() {
56+
return {
57+
startLine: this.startLine,
58+
endLine: this.endLine
59+
}
60+
}
61+
62+
constructor(
63+
private _range: Range,
64+
private _foldingRangeKind?: FoldingRangeKind,
65+
private _openWord?: string,
66+
private _closeWord?: string) { }
5667
}

server/src/extensions/parserExtensions.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,20 @@ import {
1212
BuiltinTypeContext,
1313
ClassTypeNameContext,
1414
ConstItemContext,
15+
EndOfStatementContext,
16+
EndOfStatementNoWsContext,
1517
GlobalVariableDeclarationContext,
1618
PrivateConstDeclarationContext,
1719
PrivateVariableDeclarationContext,
20+
ProcedureTailContext,
1821
PublicConstDeclarationContext,
1922
PublicVariableDeclarationContext,
2023
TypeSpecContext,
2124
TypeSuffixContext,
2225
VariableDclContext,
2326
WitheventsVariableDclContext
2427
} from '../antlr/out/vbaParser';
28+
import { LineEndingContext } from '../antlr/out/vbafmtParser';
2529

2630

2731
declare module 'antlr4ng' {
@@ -31,6 +35,8 @@ declare module 'antlr4ng' {
3135
startIndex(): number;
3236
stopIndex(): number;
3337
hasPositionOf(ctx: ParserRuleContext): boolean;
38+
endsWithLineEnding: boolean;
39+
countTrailingLineEndings(): number;
3440
}
3541

3642
interface TerminalNode {
@@ -125,6 +131,88 @@ ParserRuleContext.prototype.hasPositionOf = function (ctx: ParserRuleContext): b
125131
return this.startIndex() === ctx.startIndex() && this.stopIndex() === ctx.stopIndex();
126132
}
127133

134+
Object.defineProperty(ParserRuleContext.prototype, 'endsWithLineEnding', {
135+
get: function endsWithLineEnding() {
136+
// Ensure we have a context.
137+
if (!(this instanceof ParserRuleContext))
138+
return false;
139+
140+
// Check last child is a line ending.
141+
const child = this.children.at(-1);
142+
if (!child)
143+
return false;
144+
145+
// Check the various line ending contexts.
146+
if (child instanceof LineEndingContext)
147+
return true;
148+
if (child instanceof EndOfStatementContext)
149+
return true;
150+
if (child instanceof EndOfStatementNoWsContext)
151+
return true;
152+
if (child instanceof ProcedureTailContext)
153+
return true;
154+
155+
// Run it again!
156+
if (child.getChildCount() > 0)
157+
return (child as ParserRuleContext).endsWithLineEnding;
158+
159+
// Not a line ending and no more children.
160+
return false;
161+
}
162+
})
163+
164+
interface LineEndingParserRuleContext {
165+
NEWLINE(): TerminalNode | null;
166+
}
167+
168+
function isLineEndingParserRuleContext(ctx: unknown): ctx is LineEndingParserRuleContext {
169+
return typeof ctx === 'object'
170+
&& ctx !== null
171+
&& typeof (ctx as any).NEWLINE === 'function';
172+
}
173+
174+
function countTrailingLineEndings(ctx: ParserRuleContext): number {
175+
// This function recursively loops through last child of
176+
// the context to find one that has a NEWLINE terminal node.
177+
178+
// Check if we have a NEWLINE node.
179+
if (isLineEndingParserRuleContext(ctx)) {
180+
const lines = ctx.NEWLINE()?.getText();
181+
if (!lines) {
182+
return 0;
183+
}
184+
185+
let i = 0;
186+
let result = 0;
187+
while (i < lines.length) {
188+
const char = lines[i];
189+
190+
if (char === '\r') {
191+
result++;
192+
i += lines[i + 1] === '\n' ? 2 : 1;
193+
} else if (char === '\n') {
194+
result++;
195+
i++;
196+
}
197+
}
198+
199+
return result;
200+
}
201+
202+
// Recursive call on last child.
203+
const lastChild = ctx.children.at(-1);
204+
if (!!(lastChild instanceof ParserRuleContext)) {
205+
return countTrailingLineEndings(lastChild);
206+
}
207+
208+
// If we get here, we have no trailing lines.
209+
return 0;
210+
}
211+
212+
ParserRuleContext.prototype.countTrailingLineEndings = function (): number {
213+
return countTrailingLineEndings(this);
214+
}
215+
128216

129217
TerminalNode.prototype.toRange = function (doc: TextDocument): Range {
130218
return Range.create(

server/src/project/elements/flow.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,31 @@
22
import { TextDocument } from 'vscode-languageserver-textdocument';
33

44
// Antlr
5-
import { AnyOperatorContext, WhileStatementContext } from '../../antlr/out/vbaParser';
5+
import { AnyOperatorContext, IfStatementContext, WhileStatementContext } from '../../antlr/out/vbaParser';
66

77
// Project
8-
import { DiagnosticCapability } from '../../capabilities/capabilities';
8+
import { DiagnosticCapability, FoldingRangeCapability } from '../../capabilities/capabilities';
99
import { BaseContextSyntaxElement, HasDiagnosticCapability } from './base';
1010
import { MultipleOperatorsDiagnostic, WhileWendDeprecatedDiagnostic } from '../../capabilities/diagnostics';
1111

12+
export class IfElseBlock extends BaseContextSyntaxElement<IfStatementContext> {
13+
constructor(context: IfStatementContext, document: TextDocument) {
14+
super(context, document);
15+
this.foldingRangeCapability = new FoldingRangeCapability(this);
16+
this.foldingRangeCapability.openWord = 'If';
17+
this.foldingRangeCapability.closeWord = 'End If';
18+
}
19+
}
20+
1221

1322
export class WhileLoopElement extends BaseContextSyntaxElement<WhileStatementContext> implements HasDiagnosticCapability {
1423
diagnosticCapability: DiagnosticCapability;
1524

1625
constructor(context: WhileStatementContext, document: TextDocument) {
1726
super(context, document);
27+
this.foldingRangeCapability = new FoldingRangeCapability(this);
28+
this.foldingRangeCapability.openWord = 'While';
29+
this.foldingRangeCapability.closeWord = 'Wend';
1830
this.diagnosticCapability = new DiagnosticCapability(this, () => {
1931
this.diagnosticCapability.diagnostics.push(new WhileWendDeprecatedDiagnostic(this.context.range));
2032
return this.diagnosticCapability.diagnostics;

0 commit comments

Comments
 (0)