Skip to content

Commit 2b074bc

Browse files
committed
Add formatter
1 parent bd54d76 commit 2b074bc

File tree

11 files changed

+496
-8
lines changed

11 files changed

+496
-8
lines changed

.vscode/launch.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@
5757
"printParseTree": true,
5858
"visualParseTree": true
5959
},
60+
{
61+
"name": "Debug ANTLR4 Fmt grammar",
62+
"type": "antlr-debug",
63+
"request": "launch",
64+
"input": "${file}",
65+
"grammar": "server/src/antlr/vbafmt.g4",
66+
"startRule": "startRule",
67+
"printParseTree": true,
68+
"visualParseTree": true
69+
},
6070
{
6171
"name": "Extension Tests",
6272
"type": "extensionHost",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"devMate": "npx js-yaml client/src/syntaxes/dev.tmLanguage.yaml > client/out/vba.tmLanguage.json",
128128
"antlr4ng": "antlr4ng -Dlanguage=TypeScript -visitor ./server/src/antlr/vba.g4 -o ./server/src/antlr/out/",
129129
"antlr4ngPre": "antlr4ng -Dlanguage=TypeScript -visitor ./server/src/antlr/vbapre.g4 -o ./server/src/antlr/out/",
130+
"antlr4ngFmt": "antlr4ng -Dlanguage=TypeScript -visitor ./server/src/antlr/vbafmt.g4 -o ./server/src/antlr/out/",
130131
"test": "npm run tmSnapTest && npm run tmUnitTest && npm run vsctest",
131132
"testsh": "sh ./scripts/e2e.sh",
132133
"testps": "powershell ./scripts/e2e.ps1",

server/src/antlr/vbafmt.g4

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
grammar vbafmt;
2+
3+
options {
4+
caseInsensitive = true;
5+
}
6+
7+
// PARSER RULES
8+
startRule
9+
: document EOF
10+
;
11+
12+
endOfFile
13+
: ws? EOF
14+
;
15+
16+
document
17+
: documentElement* endOfFile
18+
;
19+
20+
basicStatement
21+
: ws (ambiguousComponent ws?)+ endOfStatement
22+
;
23+
24+
// endOfStatement
25+
// : bols? NEWLINE
26+
// ;
27+
28+
documentElement
29+
: comment
30+
| methodDeclaration
31+
| attributeStatement
32+
| basicStatement
33+
;
34+
35+
attributeStatement
36+
: ws? ATTRIBUTE (ANYCHARS | ws | continuation | STRINGLITERAL)* endOfStatement
37+
;
38+
39+
asType
40+
: ws AS ws (ANYCHARS | LPAREN | RPAREN | continuation)+
41+
;
42+
43+
visibility
44+
: PUBLIC
45+
| PRIVATE
46+
| GLOBAL
47+
;
48+
49+
methodOpen
50+
: ws? (visibility ws)? (SUB | FUNCTION | (PROPERTY ws ANYCHARS)) ws ANYCHARS
51+
;
52+
53+
methodParameters
54+
: LPAREN (ANYCHARS | AS | STRINGLITERAL | ws)* RPAREN asType? endOfStatement
55+
;
56+
57+
methodClose
58+
: ws? END ws (SUB | FUNCTION | PROPERTY) endOfStatement
59+
;
60+
61+
methodBody
62+
: documentElement*
63+
;
64+
65+
methodDeclaration
66+
: methodOpen
67+
methodParameters
68+
attributeStatement?
69+
methodBody
70+
methodClose
71+
;
72+
73+
comment
74+
: ws? (COMMENT | REMCOMMENT) (ws | ANYCHARS)*
75+
;
76+
77+
endOfStatement
78+
: comment? (NEWLINE+ | endOfFile)
79+
;
80+
81+
continuation
82+
: LINE_CONTINUATION
83+
;
84+
85+
ambiguousComponent
86+
: ANYCHARS
87+
| STRINGLITERAL
88+
;
89+
90+
ws
91+
: (TAB
92+
| SPACE
93+
| continuation)+
94+
;
95+
96+
// COMPUND LEXER RULES
97+
98+
REMCOMMENT
99+
: COLON? REM (TAB | SPACE) (LINE_CONTINUATION | ~[\r\n\u2028\u2029])*
100+
;
101+
102+
COMMENT
103+
: SINGLEQUOTE (LINE_CONTINUATION | ~[\r\n\u2028\u2029])*
104+
;
105+
106+
107+
// LEXER RULES
108+
109+
STRINGLITERAL
110+
: '"' (~["\r\n] | '""')* '"'
111+
;
112+
113+
LPAREN
114+
: '('
115+
;
116+
117+
RPAREN
118+
: ')'
119+
;
120+
121+
COLON
122+
: ':'
123+
;
124+
125+
AS
126+
: 'AS'
127+
;
128+
129+
ATTRIBUTE
130+
: 'ATTRIBUTE'
131+
;
132+
133+
END
134+
: 'END'
135+
;
136+
137+
ENUM
138+
: 'ENUM'
139+
;
140+
141+
FUNCTION
142+
: 'FUNCTION'
143+
;
144+
145+
PROPERTY
146+
: 'PROPERTY'
147+
;
148+
149+
GLOBAL
150+
: 'GLOBAL'
151+
;
152+
153+
LINE_CONTINUATION
154+
: (TAB | SPACE) UNDERSCORE (TAB | SPACE)? '\r'? '\n'
155+
;
156+
157+
PRIVATE
158+
: 'PRIVATE'
159+
;
160+
161+
PUBLIC
162+
: 'PUBLIC'
163+
;
164+
165+
REM
166+
: 'REM'
167+
;
168+
169+
SINGLEQUOTE
170+
: '\''
171+
;
172+
173+
SUB
174+
: 'SUB'
175+
;
176+
177+
UNDERSCORE
178+
: '_'
179+
;
180+
181+
TAB
182+
: '\t'+
183+
;
184+
185+
SPACE
186+
: [ \u3000]+
187+
;
188+
189+
NEWLINE
190+
: [\r\n\u2028\u2029\u0019]+
191+
;
192+
193+
// Any non-whitespace or new line characters.
194+
ANYCHARS
195+
: ANYCHAR+
196+
;
197+
198+
fragment ANYCHAR
199+
: ~[\r\n\u2028\u2029 \t\u0019\u3000()]
200+
;

server/src/extensions/parserExtensions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ declare module 'antlr4ng' {
3030
toRange(doc: TextDocument): Range;
3131
startIndex(): number;
3232
stopIndex(): number;
33+
hasPositionOf(ctx: ParserRuleContext): boolean;
3334
}
3435

3536
interface TerminalNode {
@@ -120,6 +121,10 @@ ParserRuleContext.prototype.stopIndex = function (): number {
120121
return this.stop?.stop ?? this.startIndex();
121122
}
122123

124+
ParserRuleContext.prototype.hasPositionOf = function (ctx: ParserRuleContext): boolean {
125+
return this.startIndex() === ctx.startIndex() && this.stopIndex() === ctx.stopIndex();
126+
}
127+
123128

124129
TerminalNode.prototype.toRange = function (doc: TextDocument): Range {
125130
return Range.create(

server/src/project/document.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { PropertyDeclarationElement,
2525
PropertyLetDeclarationElement,
2626
PropertySetDeclarationElement
2727
} from './elements/procedure';
28+
import { VbaFmtListener } from './parser/vbaListener';
2829

2930

3031
export interface DocumentSettings {
@@ -177,7 +178,7 @@ export abstract class BaseProjectDocument {
177178
}
178179

179180
// Parse the document.
180-
await (new SyntaxParser()).parseAsync(this)
181+
await (new SyntaxParser()).parseAsync(this);
181182

182183
// Evaluate the diagnostics.
183184
this.diagnostics = this.hasDiagnosticElements
@@ -187,6 +188,28 @@ export abstract class BaseProjectDocument {
187188
this._isBusy = false;
188189
};
189190

191+
async formatParseAsync(token: CancellationToken): Promise<VbaFmtListener | undefined> {
192+
// Handle already cancelled.
193+
if (token.isCancellationRequested) {
194+
throw new ParseCancellationException(Error('Parse operation cancelled before it started.'));
195+
}
196+
197+
// Listen for cancellation event.
198+
token.onCancellationRequested(() => {
199+
throw new ParseCancellationException(new Error('Parse operation cancelled during parse.'));
200+
})
201+
202+
// Don't parse oversize documents.
203+
if (await this.isOversize) {
204+
console.log(`Document oversize: ${this.textDocument.lineCount} lines.`);
205+
console.warn(`Syntax parsing has been disabled to prevent crashing.`);
206+
return;
207+
}
208+
209+
// Parse the document.
210+
return await (new SyntaxParser()).formatParseAsync(this);
211+
}
212+
190213
/**
191214
* Auto registers the element based on capabilities.
192215
* @returns This for chaining.

server/src/project/formatter.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Core
2+
import { Range, TextEdit } from 'vscode-languageserver';
3+
import { TextDocument } from 'vscode-languageserver-textdocument';
4+
import { VbaFmtListener } from './parser/vbaListener';
5+
6+
7+
export function getFormattingEdits(document: TextDocument, listener: VbaFmtListener, range?: Range): TextEdit[] {
8+
// Return nothing if we have nothing.
9+
if (document.getText() == '')
10+
return [];
11+
12+
const result: TextEdit[] = [];
13+
14+
const startLine = range?.start.line ?? 0;
15+
const endLine = (range?.end.line ?? document.lineCount) + 1;
16+
17+
let trackedIndentLevel = 0;
18+
const baseIndentLevel = getIndentLevel(document.getText(range));
19+
20+
for (let i = startLine; i < endLine; i++) {
21+
const text = getLine(document, i);
22+
23+
// Ignore comment lines.
24+
if (/^\s*'/.test(text)) continue;
25+
26+
// Actual indent level
27+
const currentIndentLevel = getIndentLevel(text);
28+
const newIndentLevel = listener.getIndent(i);
29+
if (currentIndentLevel != newIndentLevel) {
30+
result.push({
31+
range: getIndentRange(text, i)!,
32+
newText: ' '.repeat(newIndentLevel * 2)
33+
});
34+
}
35+
}
36+
37+
return result;
38+
}
39+
40+
function getExpectedIndent(listener: VbaFmtListener, range: Range, n: number) {
41+
// The listener will be offset by the range, e.g., if the range.start.line
42+
// is 5 then getting line 2 from the listener will be document line 7.
43+
return listener.getIndent(n - range.start.line + 1)
44+
}
45+
46+
function getIndentRange(text: string, n: number): Range | undefined {
47+
const match = /^(?!\s*')(\s*)/m.exec(text);
48+
if (match) {
49+
return {
50+
start: { line: n, character: 0 },
51+
end: { line: n, character: match[0].length }
52+
}
53+
}
54+
}
55+
56+
function getIndentLevel(text: string): number {
57+
// Get spaces at start of non-comment lines (tab is four spaces)
58+
const normalised = text.replace(/\t/g, ' ')
59+
const match = /^(?!\s*')(\s*)/m.exec(normalised);
60+
61+
// Default is no indent.
62+
if (!match) {
63+
return 0;
64+
}
65+
66+
// Four spaces per indent.
67+
return (match[0].length / 4) | 0;
68+
}
69+
70+
function getLine(d: TextDocument, n: number): string {
71+
return d.getText({
72+
start: { line: n, character: 0 },
73+
end: { line: n + 1, character: 0
74+
}
75+
})
76+
}

0 commit comments

Comments
 (0)