diff --git a/extensions/json/client/src/jsonMain.ts b/extensions/json/client/src/jsonMain.ts index c6b51503df9..ae87ca72f37 100644 --- a/extensions/json/client/src/jsonMain.ts +++ b/extensions/json/client/src/jsonMain.ts @@ -9,7 +9,7 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); import { workspace, languages, ExtensionContext, extensions, Uri, LanguageConfiguration, TextDocument, FoldingRangeList, FoldingRange, Disposable } from 'vscode'; -import { LanguageClient, LanguageClientOptions, RequestType, ServerOptions, TransportKind, NotificationType, DidChangeConfigurationNotification } from 'vscode-languageclient'; +import { LanguageClient, LanguageClientOptions, RequestType, ServerOptions, TransportKind, NotificationType, DidChangeConfigurationNotification, CancellationToken } from 'vscode-languageclient'; import TelemetryReporter from 'vscode-extension-telemetry'; import { FoldingRangesRequest } from './protocol/foldingProvider.proposed'; @@ -154,12 +154,15 @@ export function activate(context: ExtensionContext) { if (enable) { if (!foldingProviderRegistration) { foldingProviderRegistration = languages.registerFoldingProvider(documentSelector, { - provideFoldingRanges(document: TextDocument) { - return client.sendRequest(FoldingRangesRequest.type, { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) }).then(res => { + provideFoldingRanges(document: TextDocument, token: CancellationToken) { + return client.sendRequest(FoldingRangesRequest.type, { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) }, token).then(res => { if (res && Array.isArray(res.ranges)) { return new FoldingRangeList(res.ranges.map(r => new FoldingRange(r.startLine, r.endLine, r.type))); } return null; + }, error => { + client.logFailedRequest(FoldingRangesRequest.type, error); + return null; }); } }); diff --git a/extensions/json/server/src/folding.ts b/extensions/json/server/src/jsonFolding.ts similarity index 91% rename from extensions/json/server/src/folding.ts rename to extensions/json/server/src/jsonFolding.ts index 958e2f7388a..5f99290d8bd 100644 --- a/extensions/json/server/src/folding.ts +++ b/extensions/json/server/src/jsonFolding.ts @@ -4,17 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TextDocument, Position } from 'vscode-languageserver'; +import { TextDocument, Position, CancellationToken } from 'vscode-languageserver'; import { createScanner, SyntaxKind, ScanError } from 'jsonc-parser'; import { FoldingRangeType, FoldingRange, FoldingRangeList } from './protocol/foldingProvider.proposed'; -export function getFoldingRegions(document: TextDocument) { +export function getFoldingRegions(document: TextDocument, cancellationToken: CancellationToken | null) { let ranges: FoldingRange[] = []; let stack: FoldingRange[] = []; let prevStart = -1; let scanner = createScanner(document.getText(), false); let token = scanner.scan(); while (token !== SyntaxKind.EOF) { + if (cancellationToken && cancellationToken.isCancellationRequested) { + return null; + } switch (token) { case SyntaxKind.OpenBraceToken: case SyntaxKind.OpenBracketToken: { diff --git a/extensions/json/server/src/jsonServerMain.ts b/extensions/json/server/src/jsonServerMain.ts index 67160f689e5..69afb08a459 100644 --- a/extensions/json/server/src/jsonServerMain.ts +++ b/extensions/json/server/src/jsonServerMain.ts @@ -18,7 +18,7 @@ import { startsWith } from './utils/strings'; import { formatError, runSafe, runSafeAsync } from './utils/errors'; import { JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration } from 'vscode-json-languageservice'; import { getLanguageModelCache } from './languageModelCache'; -import { getFoldingRegions } from './folding'; +import { getFoldingRegions } from './jsonFolding'; import { FoldingRangesRequest, FoldingProviderServerCapabilities } from './protocol/foldingProvider.proposed'; @@ -260,17 +260,21 @@ function validateTextDocument(textDocument: TextDocument): void { connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] }); return; } - try { - let jsonDocument = getJSONDocument(textDocument); + let jsonDocument = getJSONDocument(textDocument); + let version = textDocument.version; - let documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'ignore' } : { comments: 'error', trailingCommas: 'error' }; - languageService.doValidation(textDocument, jsonDocument, documentSettings).then(diagnostics => { - // Send the computed diagnostics to VSCode. - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - }); - } catch (e) { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); - } + let documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'ignore' } : { comments: 'error', trailingCommas: 'error' }; + languageService.doValidation(textDocument, jsonDocument, documentSettings).then(diagnostics => { + setTimeout(() => { + let currDocument = documents.get(textDocument.uri); + if (currDocument && currDocument.version === version) { + // Send the computed diagnostics to VSCode. + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } + }, 100); + }, error => { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, error)); + }); } connection.onDidChangeWatchedFiles((change) => { @@ -282,7 +286,7 @@ connection.onDidChangeWatchedFiles((change) => { } }); if (hasChanges) { - documents.all().forEach(validateTextDocument); + documents.all().forEach(triggerValidation); } }); @@ -320,19 +324,19 @@ connection.onHover(textDocumentPositionParams => { }, null, `Error while computing hover for ${textDocumentPositionParams.textDocument.uri}`); }); -connection.onDocumentSymbol(documentSymbolParams => { +connection.onDocumentSymbol((documentSymbolParams, token) => { return runSafe(() => { let document = documents.get(documentSymbolParams.textDocument.uri); let jsonDocument = getJSONDocument(document); return languageService.findDocumentSymbols(document, jsonDocument); - }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`); + }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token); }); -connection.onDocumentRangeFormatting(formatParams => { +connection.onDocumentRangeFormatting((formatParams, token) => { return runSafe(() => { let document = documents.get(formatParams.textDocument.uri); return languageService.format(document, formatParams.range, formatParams.options); - }, [], `Error while formatting range for ${formatParams.textDocument.uri}`); + }, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); }); connection.onRequest(DocumentColorRequest.type, params => { @@ -346,7 +350,7 @@ connection.onRequest(DocumentColorRequest.type, params => { }, [], `Error while computing document colors for ${params.textDocument.uri}`); }); -connection.onRequest(ColorPresentationRequest.type, params => { +connection.onRequest(ColorPresentationRequest.type, (params, token) => { return runSafe(() => { let document = documents.get(params.textDocument.uri); if (document) { @@ -354,17 +358,17 @@ connection.onRequest(ColorPresentationRequest.type, params => { return languageService.getColorPresentations(document, jsonDocument, params.color, params.range); } return []; - }, [], `Error while computing color presentations for ${params.textDocument.uri}`); + }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); }); -connection.onRequest(FoldingRangesRequest.type, params => { +connection.onRequest(FoldingRangesRequest.type, (params, token) => { return runSafe(() => { let document = documents.get(params.textDocument.uri); if (document) { - return getFoldingRegions(document); + return getFoldingRegions(document, token); } return null; - }, null, `Error while computing folding ranges for ${params.textDocument.uri}`); + }, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token); }); // Listen on the connection diff --git a/extensions/json/server/src/test/folding.test.ts b/extensions/json/server/src/test/folding.test.ts index 0ef69577cc0..2cf2b1269bd 100644 --- a/extensions/json/server/src/test/folding.test.ts +++ b/extensions/json/server/src/test/folding.test.ts @@ -8,7 +8,7 @@ import 'mocha'; import * as assert from 'assert'; import { TextDocument } from 'vscode-languageserver'; -import { getFoldingRegions } from '../folding'; +import { getFoldingRegions } from '../jsonFolding'; interface ExpectedIndentRange { startLine: number; @@ -18,8 +18,7 @@ interface ExpectedIndentRange { function assertRanges(lines: string[], expected: ExpectedIndentRange[]): void { let document = TextDocument.create('test://foo/bar.json', 'json', 1, lines.join('\n')); - let actual = getFoldingRegions(document).ranges; - + let actual = getFoldingRegions(document, null)!.ranges; let actualRanges = []; for (let i = 0; i < actual.length; i++) { diff --git a/extensions/json/server/src/utils/errors.ts b/extensions/json/server/src/utils/errors.ts index 27ece60c051..c7e78cec6df 100644 --- a/extensions/json/server/src/utils/errors.ts +++ b/extensions/json/server/src/utils/errors.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import { CancellationToken, ResponseError, ErrorCodes } from 'vscode-languageserver'; + export function formatError(message: string, err: any): string { if (err instanceof Error) { let error = err; @@ -23,11 +25,31 @@ export function runSafeAsync(func: () => Thenable, errorVal: T, errorMessa return errorVal; }); } -export function runSafe(func: () => T, errorVal: T, errorMessage: string): T { - try { - return func(); - } catch (e) { - console.error(formatError(errorMessage, e)); - return errorVal; - } -} \ No newline at end of file +export function runSafe(func: () => T, errorVal: T, errorMessage: string, token: CancellationToken): Thenable> { + return new Promise>((resolve, reject) => { + setTimeout(() => { + if (token.isCancellationRequested) { + resolve(cancelValue()); + } else { + try { + let result = func(); + if (token.isCancellationRequested) { + resolve(cancelValue()); + return; + } else { + resolve(result); + } + + } catch (e) { + console.error(formatError(errorMessage, e)); + resolve(errorVal); + } + } + }, 100); + }); +} + +function cancelValue() { + console.log('cancelled'); + return new ResponseError(ErrorCodes.RequestCancelled, 'Request cancelled'); +} diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 9570882bfa1..ad50a41f37a 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -9,7 +9,7 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; -import { RunOnceScheduler, Delayer } from 'vs/base/common/async'; +import { RunOnceScheduler, Delayer, asWinJsPromise } from 'vs/base/common/async'; import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -30,11 +30,12 @@ import { IndentRangeProvider } from 'vs/editor/contrib/folding/indentRangeProvid import { IPosition } from 'vs/editor/common/core/position'; import { FoldingProviderRegistry, FoldingRangeType } from 'vs/editor/common/modes'; import { SyntaxRangeProvider } from './syntaxRangeProvider'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const ID = 'editor.contrib.folding'; export interface RangeProvider { - compute(editorModel: ITextModel): TPromise; + compute(editorModel: ITextModel, cancelationToken: CancellationToken): Thenable; } export class FoldingController implements IEditorContribution { @@ -56,6 +57,7 @@ export class FoldingController implements IEditorContribution { private hiddenRangeModel: HiddenRangeModel; private rangeProvider: RangeProvider; + private foldingRegionPromise: TPromise; private foldingModelPromise: TPromise; private updateScheduler: Delayer; @@ -171,6 +173,10 @@ export class FoldingController implements IEditorContribution { this.localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); this.localToDispose.push({ dispose: () => { + if (this.foldingRegionPromise) { + this.foldingRegionPromise.cancel(); + this.foldingRegionPromise = null; + } this.updateScheduler.cancel(); this.updateScheduler = null; this.foldingModel = null; @@ -178,6 +184,7 @@ export class FoldingController implements IEditorContribution { this.hiddenRangeModel = null; this.cursorChangedScheduler = null; this.rangeProvider = null; + } }); this.onModelContentChanged(); @@ -201,19 +208,24 @@ export class FoldingController implements IEditorContribution { private onModelContentChanged() { if (this.updateScheduler) { + if (this.foldingRegionPromise) { + this.foldingRegionPromise.cancel(); + this.foldingRegionPromise = null; + } this.foldingModelPromise = this.updateScheduler.trigger(() => { - if (this.foldingModel) { // null if editor has been disposed, or folding turned off - // some cursors might have moved into hidden regions, make sure they are in expanded regions - let selections = this.editor.getSelections(); - let selectionLineNumbers = selections ? selections.map(s => s.startLineNumber) : []; - return this.getRangeProvider().compute(this.foldingModel.textModel).then(foldingRanges => { - if (this.foldingModel) { // null if editor has been disposed, or folding turned off - this.foldingModel.update(foldingRanges, selectionLineNumbers); - } - return this.foldingModel; - }); + if (!this.foldingModel) { // null if editor has been disposed, or folding turned off + return null; } - return null; + let foldingRegionPromise = this.foldingRegionPromise = asWinJsPromise(token => this.getRangeProvider().compute(this.foldingModel.textModel, token)); + return foldingRegionPromise.then(foldingRanges => { + if (foldingRanges && foldingRegionPromise === this.foldingRegionPromise) { // new request or cancelled in the meantime? + // some cursors might have moved into hidden regions, make sure they are in expanded regions + let selections = this.editor.getSelections(); + let selectionLineNumbers = selections ? selections.map(s => s.startLineNumber) : []; + this.foldingModel.update(foldingRanges, selectionLineNumbers); + } + return this.foldingModel; + }); }); } } diff --git a/src/vs/editor/contrib/folding/indentRangeProvider.ts b/src/vs/editor/contrib/folding/indentRangeProvider.ts index 9ec730af0e2..f2c7871cbb2 100644 --- a/src/vs/editor/contrib/folding/indentRangeProvider.ts +++ b/src/vs/editor/contrib/folding/indentRangeProvider.ts @@ -12,11 +12,12 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { RangeProvider } from './folding'; import { TPromise } from 'vs/base/common/winjs.base'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { CancellationToken } from 'vs/base/common/cancellation'; const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000; export class IndentRangeProvider implements RangeProvider { - compute(editorModel: ITextModel): TPromise { + compute(editorModel: ITextModel, cancelationToken: CancellationToken): Thenable { let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageIdentifier().id); let offSide = foldingRules && foldingRules.offSide; let markers = foldingRules && foldingRules.markers; diff --git a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts index dd6f7bdf81e..2f82d78d034 100644 --- a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts @@ -7,11 +7,12 @@ import { FoldingProvider, IFoldingRange } from 'vs/editor/common/modes'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { toThenable } from 'vs/base/common/async'; import { ITextModel } from 'vs/editor/common/model'; import { RangeProvider } from './folding'; import { TPromise } from 'vs/base/common/winjs.base'; import { MAX_LINE_NUMBER, FoldingRegions } from './foldingRanges'; +import { CancellationToken } from 'vs/base/common/cancellation'; const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000; @@ -24,32 +25,42 @@ export class SyntaxRangeProvider implements RangeProvider { constructor(private providers: FoldingProvider[]) { } - compute(model: ITextModel): TPromise { - return collectSyntaxRanges(this.providers, model).then(ranges => { - let res = sanitizeRanges(ranges); - //console.log(res.toString()); - return res; + compute(model: ITextModel, cancellationToken: CancellationToken): Thenable { + return collectSyntaxRanges(this.providers, model, cancellationToken).then(ranges => { + if (ranges) { + let res = sanitizeRanges(ranges); + return res; + } + return null; }); } } -function collectSyntaxRanges(providers: FoldingProvider[], model: ITextModel): TPromise { - const rangeData: IFoldingRangeData[] = []; - let promises = providers.map((provider, rank) => asWinJsPromise(token => provider.provideFoldingRanges(model, token)).then(list => { - if (list && Array.isArray(list.ranges)) { - let nLines = model.getLineCount(); - for (let r of list.ranges) { - if (r.startLineNumber > 0 && r.endLineNumber > r.startLineNumber && r.endLineNumber <= nLines) { - rangeData.push({ startLineNumber: r.startLineNumber, endLineNumber: r.endLineNumber, rank, type: r.type }); +function collectSyntaxRanges(providers: FoldingProvider[], model: ITextModel, cancellationToken: CancellationToken): Thenable { + let promises = providers.map(provider => toThenable(provider.provideFoldingRanges(model, cancellationToken))); + return TPromise.join(promises).then(lists => { + let rangeData: IFoldingRangeData[] = null; + if (cancellationToken.isCancellationRequested) { + return null; + } + for (let i = 0; i < lists.length; i++) { + let list = lists[i]; + if (list && Array.isArray(list.ranges)) { + if (!Array.isArray(rangeData)) { + rangeData = []; + } + let nLines = model.getLineCount(); + for (let r of list.ranges) { + if (r.startLineNumber > 0 && r.endLineNumber > r.startLineNumber && r.endLineNumber <= nLines) { + rangeData.push({ startLineNumber: r.startLineNumber, endLineNumber: r.endLineNumber, rank: i, type: r.type }); + } } } } - }, onUnexpectedExternalError)); - - return TPromise.join(promises).then(() => { return rangeData; - }); + + }, onUnexpectedExternalError); } export class RangesCollector { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 185a5195ad4..2962ace2b5b 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -373,6 +373,9 @@ declare module 'vscode' { } } export interface FoldingProvider { + /** + * Returns a list of folding ranges or null if the provider does not want to participate or was cancelled. + */ provideFoldingRanges(document: TextDocument, token: CancellationToken): ProviderResult; }