Skip to content

Commit 493ebd2

Browse files
committed
Better handling of cancelled requests
1 parent 242619c commit 493ebd2

File tree

4 files changed

+109
-43
lines changed

4 files changed

+109
-43
lines changed

server/src/project/document.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,6 @@ export abstract class BaseProjectDocument {
158158
}
159159

160160
async parseAsync(token: CancellationToken): Promise<void> {
161-
// Handle already cancelled.
162-
if (token.isCancellationRequested) {
163-
throw new ParseCancellationException(Error('Parse operation cancelled before it started.'));
164-
}
165-
166-
// Listen for cancellation event.
167-
token.onCancellationRequested(() => {
168-
throw new ParseCancellationException(new Error('Parse operation cancelled during parse.'));
169-
})
170-
171161
// Don't parse oversize documents.
172162
if (await this.isOversize) {
173163
this.workspace.logger.debug(`Document oversize: ${this.textDocument.lineCount} lines.`);
@@ -177,7 +167,7 @@ export abstract class BaseProjectDocument {
177167
}
178168

179169
// Parse the document.
180-
await (new SyntaxParser()).parseAsync(this);
170+
await (new SyntaxParser(this.workspace.logger)).parseAsync(token, this);
181171

182172
// Evaluate the diagnostics.
183173
this.diagnostics = this.hasDiagnosticElements
@@ -188,16 +178,6 @@ export abstract class BaseProjectDocument {
188178
};
189179

190180
async formatParseAsync(token: CancellationToken): Promise<VbaFmtListener | undefined> {
191-
// Handle already cancelled.
192-
if (token.isCancellationRequested) {
193-
throw new ParseCancellationException(Error('Parse operation cancelled before it started.'));
194-
}
195-
196-
// Listen for cancellation event.
197-
token.onCancellationRequested(() => {
198-
throw new ParseCancellationException(new Error('Parse operation cancelled during parse.'));
199-
})
200-
201181
// Don't parse oversize documents.
202182
if (await this.isOversize) {
203183
this.workspace.logger.debug(`Document oversize: ${this.textDocument.lineCount} lines.`);
@@ -206,7 +186,7 @@ export abstract class BaseProjectDocument {
206186
}
207187

208188
// Parse the document.
209-
return await (new SyntaxParser()).formatParseAsync(this);
189+
return await (new SyntaxParser(this.workspace.logger)).formatParseAsync(token, this);
210190
}
211191

212192
/**

server/src/project/parser/vbaParser.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,64 @@
11
// Antlr
2-
import { ParseTreeWalker } from 'antlr4ng';
2+
import { ParseCancellationException, ParseTreeWalker } from 'antlr4ng';
33
import { VbaFmtParser, VbaParser, VbaPreParser } from './vbaAntlr';
44
import { VbaFmtListener, VbaListener, VbaPreListener } from './vbaListener';
55

66
// Project
77
import { VbaClassDocument, VbaModuleDocument } from '../document';
8-
import { Range } from 'vscode-languageserver';
8+
import { CancellationToken, Range } from 'vscode-languageserver';
9+
import { LspLogger } from '../../utils/logger';
910

1011

1112
export class SyntaxParser {
12-
async parseAsync(document: VbaClassDocument | VbaModuleDocument): Promise<void> {
13+
logger: LspLogger
14+
constructor(logger: LspLogger) {
15+
this.logger = logger;
16+
}
17+
18+
async parseAsync(token: CancellationToken, document: VbaClassDocument | VbaModuleDocument): Promise<void> {
1319
// Preparse the document if we find a precompiler statement.
1420
const regexp = new RegExp(/^\s*#If/gmi)
1521
let docText = document.textDocument.getText();
1622
if (regexp.test(docText)) {
23+
this.logger.debug(`Beginning pre-parse.`);
1724
const prelistener = await VbaPreListener.createAsync(document);
1825
const preparser = VbaPreParser.create(docText);
19-
await this.parseDocumentAsync(prelistener, preparser);
26+
await this.parseDocumentAsync(token, prelistener, preparser);
2027
docText = prelistener.text;
28+
this.logger.debug(`Completed pre-parse.`);
2129
}
2230

2331
// Perform main document parse without compiler directives.
32+
this.logger.debug(`Beginning main parse.`);
2433
const listener = await VbaListener.createAsync(document);
2534
const parser = VbaParser.create(docText);
26-
await this.parseDocumentAsync(listener, parser);
35+
await this.parseDocumentAsync(token, listener, parser);
36+
this.logger.debug(`Completed main parse.`);
2737
}
2838

29-
async formatParseAsync(document: VbaClassDocument | VbaModuleDocument, range?: Range): Promise<VbaFmtListener> {
39+
async formatParseAsync(token: CancellationToken, document: VbaClassDocument | VbaModuleDocument, range?: Range): Promise<VbaFmtListener> {
3040
// Special parser focused on document format.
41+
this.logger.debug(`Beginning format parse.`);
3142
const listener = await VbaFmtListener.createAsync(document);
3243
const parser = VbaFmtParser.create(document.textDocument.getText(range));
33-
await this.parseDocumentAsync(listener, parser);
44+
await this.parseDocumentAsync(token, listener, parser);
45+
this.logger.debug(`Completed format parse.`);
3446
return listener;
3547
}
3648

37-
private async parseDocumentAsync(listener: VbaListener | VbaPreListener | VbaFmtListener, parser: VbaParser | VbaPreParser | VbaFmtParser) {
49+
private async parseDocumentAsync(token: CancellationToken, listener: VbaListener | VbaPreListener | VbaFmtListener, parser: VbaParser | VbaPreParser | VbaFmtParser) {
50+
// Handle already cancelled.
51+
if (token.isCancellationRequested) {
52+
this.logger.debug(`Cancellation requested before parsing.`);
53+
throw new ParseCancellationException(Error('Parse operation cancelled before it started.'));
54+
}
55+
56+
// Listen for cancellation event.
57+
token.onCancellationRequested(() => {
58+
this.logger.debug(`Cancellation requested during parsing.`);
59+
throw new ParseCancellationException(new Error('Parse operation cancelled during parse.'));
60+
})
61+
3862
ParseTreeWalker.DEFAULT.walk(
3963
listener,
4064
parser.startRule()

server/src/project/workspace.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import { NamespaceManager } from './scope';
3333
import { ParseCancellationException } from 'antlr4ng';
3434
import { getFormattingEdits } from './formatter';
3535
import { VbaFmtListener } from './parser/vbaListener';
36-
import { LspLogger } from '../logger';
36+
import { LspLogger } from '../utils/logger';
37+
import { returnDefaultOnCancelClientRequest } from '../utils/wrappers';
3738

3839

3940
/**
@@ -93,17 +94,13 @@ export class Workspace {
9394

9495
// Exceptions thrown by the parser should be ignored.
9596
try {
96-
this.logger.debug(`Parsing document: ${this.activeDocument.name}`);
9797
await this.activeDocument.parseAsync(this.parseCancellationTokenSource.token);
9898
this.logger.info(`Parsed ${this.activeDocument.name}`);
9999
} catch (e) {
100-
if (e instanceof ParseCancellationException) {
101-
this.logger.debug('Parse cancelled successfully.')
102-
} else if (e instanceof Error) {
103-
this.logger.stack(e);
104-
} else {
105-
this.logger.error(`Parse failed: ${e}`)
106-
}
100+
// Swallow cancellation exceptions. They're good. We like these.
101+
if (e instanceof ParseCancellationException) { }
102+
else if (e instanceof Error) { this.logger.stack(e); }
103+
else { this.logger.error('Something went wrong.')}
107104
}
108105

109106
this.parseCancellationTokenSource = undefined;
@@ -116,7 +113,6 @@ export class Workspace {
116113
// Exceptions thrown by the parser should be ignored.
117114
let result: VbaFmtListener | undefined;
118115
try {
119-
this.logger.debug(`Format parsing document: ${document.uri}`);
120116
result = await this.activeDocument?.formatParseAsync(this.parseCancellationTokenSource.token);
121117
this.logger.info(`Formatted ${document.uri}`);
122118
}
@@ -244,19 +240,31 @@ class WorkspaceEvents {
244240
}
245241

246242
private initialiseConnectionEvents(connection: _Connection) {
243+
const cancellableOnDocSymbol = returnDefaultOnCancelClientRequest(
244+
(p: DocumentSymbolParams, t) => this.onDocumentSymbolAsync(p, t), [], this.workspace.logger, 'Document Symbols');
245+
246+
const cancellableOnDiagnostics = returnDefaultOnCancelClientRequest(
247+
(p: DocumentDiagnosticParams, t) => this.onDiagnosticAsync(p, t),
248+
{kind: DocumentDiagnosticReportKind.Full, items: []},
249+
this.workspace.logger,
250+
'Diagnostics');
251+
252+
const cancellableOnFoldingRanges = returnDefaultOnCancelClientRequest(
253+
(p: FoldingRangeParams, t) => this.onFoldingRangesAsync(p, t), [], this.workspace.logger, 'Folding Range')
254+
247255
connection.onInitialized(() => this.onInitialized());
248256
connection.onDidOpenTextDocument(params => this.onDidOpenTextDocumentAsync(params));
249257
connection.onCompletion(params => this.onCompletion(params));
250258
connection.onCompletionResolve(item => this.onCompletionResolve(item));
251259
connection.onDidChangeConfiguration(_ => this.workspace.clearDocumentsConfiguration());
252260
connection.onDidChangeWatchedFiles(params => this.onDidChangeWatchedFiles(params));
253-
connection.onDocumentSymbol(async (params, token) => await this.onDocumentSymbolAsync(params, token));
261+
connection.onDocumentSymbol(async (params, token) => await cancellableOnDocSymbol(params, token));
254262
connection.onHover(params => this.onHover(params));
255-
connection.languages.diagnostics.on(async (params, token) => await this.onDiagnosticAsync(params, token));
263+
connection.languages.diagnostics.on(async (params, token) => await cancellableOnDiagnostics(params, token));
256264
connection.onDocumentFormatting(params => this.onDocumentFormatting(params));
257265

258266
if (hasConfigurationCapability(this.configuration)) {
259-
connection.onFoldingRanges(async (params, token) => this.onFoldingRangesAsync(params, token));
267+
connection.onFoldingRanges(async (params, token) => await cancellableOnFoldingRanges(params, token));
260268
}
261269

262270
connection.onRequest((method: string, params: object | object[] | any) => {
@@ -303,6 +311,7 @@ class WorkspaceEvents {
303311
}
304312

305313
private async onDiagnosticAsync(params: DocumentDiagnosticParams, token: CancellationToken): Promise<DocumentDiagnosticReport> {
314+
// const document = await this.withTimeout(this.activeParsedDocument(0, token), 10000).catch(() => null);
306315
const document = await this.activeParsedDocument(0, token);
307316
return document?.languageServerDiagnostics() ?? {
308317
kind: DocumentDiagnosticReportKind.Full,

server/src/utils/wrappers.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { CancellationToken } from 'vscode-languageserver';
2+
import { LspLogger } from './logger';
3+
4+
type signature<T,A extends any[]> = (token: CancellationToken, ...args: A) => Promise<T|void>;
5+
type paramsSignature<T,P> = (params: P, token: CancellationToken) => Promise<T>;
6+
7+
/**
8+
* A wrapper to give cancellation token handling to an async function.
9+
* @param fn An async function that requires cancellation token handling.
10+
* @param defaultValue The value to return when cancelled.
11+
*/
12+
function returnDefaultOnCancel<T, A extends any[]>(fn: signature<T,A>, logger?: LspLogger, name?: string, defaultValue?: T, cancelError?: Error): signature<T,A> {
13+
return async (token: CancellationToken, ...args: A): Promise<T|void> => {
14+
if (token.isCancellationRequested) {
15+
if (logger) logger.debug(`Cancellation requested before start for ${name ?? 'unknown'}. Returning default.`);
16+
if (cancelError) throw cancelError;
17+
return defaultValue;
18+
}
19+
20+
return new Promise<T|void>((resolve) => {
21+
const onCancel = () =>{
22+
if (logger) logger.debug(`Cancellation requested during processing for ${name ?? 'unknown'}. Returning default.`);
23+
if (cancelError) throw cancelError;
24+
resolve(defaultValue);
25+
}
26+
token.onCancellationRequested(onCancel);
27+
fn(token, ...args).then(resolve).catch(() => resolve(defaultValue));
28+
});
29+
};
30+
}
31+
32+
/**
33+
* A wrapper to give cancellation token handling to an async client request.
34+
* @param fn An async function that requires cancellation token handling.
35+
* @param defaultValue The value to return when cancelled.
36+
*/
37+
export function returnDefaultOnCancelClientRequest<T, P>(fn: paramsSignature<T,P>, defaultValue: T, logger: LspLogger, name: string): paramsSignature<T,P> {
38+
return async (params: P, token: CancellationToken): Promise<T> => {
39+
if (token.isCancellationRequested) {
40+
logger.debug(`Cancellation requested before start for ${name}. Returning default.`)
41+
return defaultValue;
42+
}
43+
44+
return new Promise<T>((resolve) => {
45+
const onCancel = () =>{
46+
logger.debug(`Cancellation requested during processing for ${name}. Returning default.`);
47+
resolve(defaultValue);
48+
}
49+
token.onCancellationRequested(onCancel);
50+
fn(params, token).then(resolve).catch(() => resolve(defaultValue));
51+
});
52+
};
53+
}

0 commit comments

Comments
 (0)