Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions client/src/test/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';
import { toRange } from './util';

suite('Should get diagnostics', () => {
test('diagnostics.class.missingNameAttributeError', async () => {
Expand Down Expand Up @@ -143,12 +144,6 @@ suite('Should get diagnostics', () => {
});
});

function toRange(sLine: number, sChar: number, eLine: number, eChar: number) {
const start = new vscode.Position(sLine - 1, sChar);
const end = new vscode.Position(eLine - 1, eChar);
return new vscode.Range(start, end);
}

async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.Diagnostic[]) {
await activate(docUri);

Expand Down
38 changes: 38 additions & 0 deletions client/src/test/foldingRanges.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';

suite('Should get text edits', () => {
test('formatting.class.template', async () => {
const subFoo = {start: 23, end: 42};
const subBar = {start: 44, end: 58};
const ifBlockOuter = {start: 33, end: 41};
const ifBlockInner = {start: 34, end: 36};
const whileWend = {start: 54, end: 57};
const propFoobar = {start: 61, end: 63};

await testFoldingRanges(getDocUri('FoldingRanges.bas'), [
subFoo,
ifBlockOuter,
ifBlockInner,
subBar,
whileWend,
propFoobar
]);
});
});

async function testFoldingRanges(docUri: vscode.Uri, expectedFoldingRanges: vscode.FoldingRange[]) {
await activate(docUri);
const actualFoldingRanges = await vscode.commands.executeCommand<vscode.FoldingRange[]>(
'vscode.executeFoldingRangeProvider',
docUri
)

assert.equal(actualFoldingRanges.length ?? 0, expectedFoldingRanges.length, "Count");

expectedFoldingRanges.forEach((expectedFoldingRange, i) => {
const actualFoldingRange = actualFoldingRanges[i];
assert.deepEqual(actualFoldingRange, expectedFoldingRange, `FoldingRange ${i}`);
});
}
7 changes: 1 addition & 6 deletions client/src/test/formatting.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';
import { toRange } from './util';

suite('Should get text edits', () => {
test('formatting.class.template', async () => {
Expand Down Expand Up @@ -39,12 +40,6 @@ suite('Should get text edits', () => {
});
});

function toRange(sLine: number, sChar: number, eLine: number, eChar: number) {
const start = new vscode.Position(sLine - 1, sChar);
const end = new vscode.Position(eLine - 1, eChar);
return new vscode.Range(start, end);
}

async function testTextEdits(docUri: vscode.Uri, expectedTextEdits: vscode.TextEdit[]) {
await activate(docUri);
const actualEdits = await vscode.commands.executeCommand<vscode.TextEdit[]>(
Expand Down
7 changes: 7 additions & 0 deletions client/src/test/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Position, Range } from 'vscode';

export function toRange(sLine: number, sChar: number, eLine: number, eChar: number) {
const start = new Position(sLine - 1, sChar);
const end = new Position(eLine - 1, eChar);
return new Range(start, end);
}
64 changes: 64 additions & 0 deletions client/testFixture/FoldingRanges.bas
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Attribute VB_Name = "FoldingRanges"
' Copyright 2024 Sam Vanderslink
'
' Permission is hereby granted, free of charge, to any person obtaining a copy
' of this software and associated documentation files (the "Software"), to deal
' in the Software without restriction, including without limitation the rights
' to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
' copies of the Software, and to permit persons to whom the Software is
' furnished to do so, subject to the following conditions:
'
' The above copyright notice and this permission notice shall be included in
' all copies or substantial portions of the Software.
'
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
' IN THE SOFTWARE.

Option Explicit

Public Sub Foo()
Attribute Foo.VB_Description = "Tests a folding range."
' Tests a folding range.
'
' Args:
' param1:
'
' Raises:
'

If FoldingRanges Then
If FoldingRanges Then
Debug.Print "Great!"
End If
ElseIf UnfoldingRanges Then
Debug.Print "What does this even mean?"
Else
Debug.Print "Not great..."
End If
End Sub

Public Sub Bar()
Attribute Bar.VB_Description = "Tests more folding ranges."
' Tests more folding ranges.
'
' Args:
' param1:
'
' Raises:
'

While True
Debug.Print "You ain't never going home!"
DoEvents
Wend
End Sub


Public Function FooBar() As Long

End Function
11 changes: 10 additions & 1 deletion server/src/capabilities/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,18 @@ abstract class BaseCapability {

export class FoldingRangeCapability extends BaseCapability {
foldingRangeKind?: FoldingRangeKind;
openWord?: string;
closeWord?: string;

get foldingRange(): FoldingRange {
return new FoldingRange(this.element.context.range, this.foldingRangeKind);
const trailingLineCount = this.element.context.rule.countTrailingLineEndings();
const start = this.element.context.range.start;
const end = {
line: this.element.context.range.end.line - trailingLineCount,
character: this.element.context.range.end.character
}
const range = Range.create(start, end);
return new FoldingRange(range, this.foldingRangeKind, this.openWord, this.closeWord);
}

constructor(element: BaseContextSyntaxElement<ParserRuleContext>, foldingRangeKind?: FoldingRangeKind) {
Expand Down
45 changes: 28 additions & 17 deletions server/src/capabilities/folding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,44 @@ export class FoldingRange implements VscFoldingRange {
/**
* The zero-based line number from where the folded range starts.
*/
startLine: number;

/**
* The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line.
*/
startCharacter?: number;
get startLine(): number {
return this._range.start.line;
}

/**
* The zero-based line number where the folded range ends.
*/
endLine: number;

/**
* The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line.
*/
endCharacter?: number;
get endLine(): number {
return this._range.end.line;
}

/**
* Describes the kind of the folding range such as 'comment' or 'region'. The kind
* is used to categorize folding ranges and used by commands like 'Fold all comments'. See
* [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds.
*/
kind?: string;
get kind(): string | undefined {
return this._foldingRangeKind
}

get openWord(): string {
return this._openWord ?? '';
}

constructor(range: Range, foldingRangeKind?: FoldingRangeKind) {
this.startLine = range.start.line;
this.endLine = range.end.line;
this.kind = foldingRangeKind;
get closeWord(): string {
return this._closeWord ?? '';
}

get range() {
return {
startLine: this.startLine,
endLine: this.endLine
}
}

constructor(
private _range: Range,
private _foldingRangeKind?: FoldingRangeKind,
private _openWord?: string,
private _closeWord?: string) { }
}
88 changes: 88 additions & 0 deletions server/src/extensions/parserExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ import {
BuiltinTypeContext,
ClassTypeNameContext,
ConstItemContext,
EndOfStatementContext,
EndOfStatementNoWsContext,
GlobalVariableDeclarationContext,
PrivateConstDeclarationContext,
PrivateVariableDeclarationContext,
ProcedureTailContext,
PublicConstDeclarationContext,
PublicVariableDeclarationContext,
TypeSpecContext,
TypeSuffixContext,
VariableDclContext,
WitheventsVariableDclContext
} from '../antlr/out/vbaParser';
import { LineEndingContext } from '../antlr/out/vbafmtParser';


declare module 'antlr4ng' {
Expand All @@ -31,6 +35,8 @@ declare module 'antlr4ng' {
startIndex(): number;
stopIndex(): number;
hasPositionOf(ctx: ParserRuleContext): boolean;
endsWithLineEnding: boolean;
countTrailingLineEndings(): number;
}

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

Object.defineProperty(ParserRuleContext.prototype, 'endsWithLineEnding', {
get: function endsWithLineEnding() {
// Ensure we have a context.
if (!(this instanceof ParserRuleContext))
return false;

// Check last child is a line ending.
const child = this.children.at(-1);
if (!child)
return false;

// Check the various line ending contexts.
if (child instanceof LineEndingContext)
return true;
if (child instanceof EndOfStatementContext)
return true;
if (child instanceof EndOfStatementNoWsContext)
return true;
if (child instanceof ProcedureTailContext)
return true;

// Run it again!
if (child.getChildCount() > 0)
return (child as ParserRuleContext).endsWithLineEnding;

// Not a line ending and no more children.
return false;
}
})

interface LineEndingParserRuleContext {
NEWLINE(): TerminalNode | null;
}

function isLineEndingParserRuleContext(ctx: unknown): ctx is LineEndingParserRuleContext {
return typeof ctx === 'object'
&& ctx !== null
&& typeof (ctx as any).NEWLINE === 'function';
}

function countTrailingLineEndings(ctx: ParserRuleContext): number {
// This function recursively loops through last child of
// the context to find one that has a NEWLINE terminal node.

// Check if we have a NEWLINE node.
if (isLineEndingParserRuleContext(ctx)) {
const lines = ctx.NEWLINE()?.getText();
if (!lines) {
return 0;
}

let i = 0;
let result = 0;
while (i < lines.length) {
const char = lines[i];

if (char === '\r') {
result++;
i += lines[i + 1] === '\n' ? 2 : 1;
} else if (char === '\n') {
result++;
i++;
}
}

return result;
}

// Recursive call on last child.
const lastChild = ctx.children.at(-1);
if (!!(lastChild instanceof ParserRuleContext)) {
return countTrailingLineEndings(lastChild);
}

// If we get here, we have no trailing lines.
return 0;
}

ParserRuleContext.prototype.countTrailingLineEndings = function (): number {
return countTrailingLineEndings(this);
}


TerminalNode.prototype.toRange = function (doc: TextDocument): Range {
return Range.create(
Expand Down
16 changes: 14 additions & 2 deletions server/src/project/elements/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@
import { TextDocument } from 'vscode-languageserver-textdocument';

// Antlr
import { AnyOperatorContext, WhileStatementContext } from '../../antlr/out/vbaParser';
import { AnyOperatorContext, IfStatementContext, WhileStatementContext } from '../../antlr/out/vbaParser';

// Project
import { DiagnosticCapability } from '../../capabilities/capabilities';
import { DiagnosticCapability, FoldingRangeCapability } from '../../capabilities/capabilities';
import { BaseContextSyntaxElement, HasDiagnosticCapability } from './base';
import { MultipleOperatorsDiagnostic, WhileWendDeprecatedDiagnostic } from '../../capabilities/diagnostics';

export class IfElseBlock extends BaseContextSyntaxElement<IfStatementContext> {
constructor(context: IfStatementContext, document: TextDocument) {
super(context, document);
this.foldingRangeCapability = new FoldingRangeCapability(this);
this.foldingRangeCapability.openWord = 'If';
this.foldingRangeCapability.closeWord = 'End If';
}
}


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

constructor(context: WhileStatementContext, document: TextDocument) {
super(context, document);
this.foldingRangeCapability = new FoldingRangeCapability(this);
this.foldingRangeCapability.openWord = 'While';
this.foldingRangeCapability.closeWord = 'Wend';
this.diagnosticCapability = new DiagnosticCapability(this, () => {
this.diagnosticCapability.diagnostics.push(new WhileWendDeprecatedDiagnostic(this.context.range));
return this.diagnosticCapability.diagnostics;
Expand Down
Loading