diff --git a/extensions/css-language-features/server/src/cssServer.ts b/extensions/css-language-features/server/src/cssServer.ts index 3eed3962879..b4ce51c162a 100644 --- a/extensions/css-language-features/server/src/cssServer.ts +++ b/extensions/css-language-features/server/src/cssServer.ts @@ -9,7 +9,8 @@ import { import { URI } from 'vscode-uri'; import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, TextDocument, Position, CSSFormatConfiguration } from 'vscode-css-languageservice'; import { getLanguageModelCache } from './languageModelCache'; -import { formatError, runSafeAsync } from './utils/runner'; +import { runSafeAsync } from './utils/runner'; +import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; import { getDocumentContext } from './utils/documentContext'; import { fetchDataProviders } from './customData'; import { RequestService, getRequestService } from './requests'; @@ -56,6 +57,8 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let dataProvidersReady: Promise = Promise.resolve(); + let diagnosticPushSupport: DiagnosticsSupport | undefined; + const languageServices: { [id: string]: LanguageService } = {}; const notReady = () => Promise.reject('Not Ready'); @@ -91,12 +94,20 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) const snippetSupport = !!getClientCapability('textDocument.completion.completionItem.snippetSupport', false); scopedSettingsSupport = !!getClientCapability('workspace.configuration', false); foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); + formatterMaxNumberOfEdits = initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; languageServices.css = getCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); languageServices.scss = getSCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); languageServices.less = getLESSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); + const pullDiagnosticSupport = getClientCapability('textDocument.diagnostic', undefined); + if (pullDiagnosticSupport === undefined) { + diagnosticPushSupport = registerDiagnosticsPushSupport(documents, connection, runtime, validateTextDocument); + } else { + diagnosticPushSupport = registerDiagnosticsPullSupport(documents, connection, runtime, validateTextDocument); + } + const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, completionProvider: snippetSupport ? { resolveProvider: false, triggerCharacters: ['/', '-', ':'] } : undefined, @@ -113,6 +124,11 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) colorProvider: {}, foldingRangeProvider: true, selectionRangeProvider: true, + diagnosticProvider: { + documentSelector: null, + interFileDependencies: false, + workspaceDiagnostics: false + }, documentRangeFormattingProvider: initializationOptions?.provideFormatter === true, documentFormattingProvider: initializationOptions?.provideFormatter === true, }; @@ -157,53 +173,16 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } // reset all document settings documentSettings = {}; - // Revalidate any open text documents - documents.all().forEach(triggerValidation); + diagnosticPushSupport?.requestRefresh(); } - const pendingValidationRequests: { [uri: string]: Disposable } = {}; - const validationDelayMs = 500; - - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - documents.onDidChangeContent(change => { - triggerValidation(change.document); - }); - - // a document has closed: clear all diagnostics - documents.onDidClose(event => { - cleanPendingValidation(event.document); - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); - }); - - function cleanPendingValidation(textDocument: TextDocument): void { - const request = pendingValidationRequests[textDocument.uri]; - if (request) { - request.dispose(); - delete pendingValidationRequests[textDocument.uri]; - } - } - - function triggerValidation(textDocument: TextDocument): void { - cleanPendingValidation(textDocument); - pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(() => { - delete pendingValidationRequests[textDocument.uri]; - validateTextDocument(textDocument); - }, validationDelayMs); - } - - function validateTextDocument(textDocument: TextDocument): void { + async function validateTextDocument(textDocument: TextDocument): Promise { const settingsPromise = getDocumentSettings(textDocument); - Promise.all([settingsPromise, dataProvidersReady]).then(async ([settings]) => { - const stylesheet = stylesheets.get(textDocument); - const diagnostics = getLanguageService(textDocument).doValidation(textDocument, stylesheet, settings) as Diagnostic[]; - // Send the computed diagnostics to VSCode. - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - }, e => { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); - }); - } + const [settings] = await Promise.all([settingsPromise, dataProvidersReady]); + const stylesheet = stylesheets.get(textDocument); + return getLanguageService(textDocument).doValidation(textDocument, stylesheet, settings); + } function updateDataProviders(dataPaths: string[]) { dataProvidersReady = fetchDataProviders(dataPaths, requestService).then(customDataProviders => { diff --git a/extensions/css-language-features/server/src/utils/validation.ts b/extensions/css-language-features/server/src/utils/validation.ts new file mode 100644 index 00000000000..2658f74016a --- /dev/null +++ b/extensions/css-language-features/server/src/utils/validation.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, Connection, Diagnostic, Disposable, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, TextDocuments } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-css-languageservice'; +import { formatError, runSafeAsync } from './runner'; +import { RuntimeEnvironment } from '../cssServer'; + +export type Validator = (textDocument: TextDocument) => Promise; +export type DiagnosticsSupport = { + dispose(): void; + requestRefresh(): void; +}; + +export function registerDiagnosticsPushSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + const pendingValidationRequests: { [uri: string]: Disposable } = {}; + const validationDelayMs = 500; + + const disposables: Disposable[] = []; + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + triggerValidation(change.document); + }, undefined, disposables); + + // a document has closed: clear all diagnostics + documents.onDidClose(event => { + cleanPendingValidation(event.document); + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); + }, undefined, disposables); + + function cleanPendingValidation(textDocument: TextDocument): void { + const request = pendingValidationRequests[textDocument.uri]; + if (request) { + request.dispose(); + delete pendingValidationRequests[textDocument.uri]; + } + } + + function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => { + if (request === pendingValidationRequests[textDocument.uri]) { + try { + const diagnostics = await validate(textDocument); + if (request === pendingValidationRequests[textDocument.uri]) { + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } + delete pendingValidationRequests[textDocument.uri]; + } catch (e) { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); + } + } + }, validationDelayMs); + } + + documents.all().forEach(triggerValidation); + + return { + requestRefresh: () => { + documents.all().forEach(triggerValidation); + }, + dispose: () => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + const keys = Object.keys(pendingValidationRequests); + for (const key of keys) { + pendingValidationRequests[key].dispose(); + delete pendingValidationRequests[key]; + } + } + }; +} + +export function registerDiagnosticsPullSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + function newDocumentDiagnosticReport(diagnostics: Diagnostic[]): DocumentDiagnosticReport { + return { + kind: DocumentDiagnosticReportKind.Full, + items: diagnostics + }; + } + + connection.languages.diagnostics.on(async (params: DocumentDiagnosticParams, token: CancellationToken) => { + return runSafeAsync(runtime, async () => { + const document = documents.get(params.textDocument.uri); + if (document) { + return newDocumentDiagnosticReport(await validate(document)); + } + return newDocumentDiagnosticReport([]); + + }, newDocumentDiagnosticReport([]), `Error while computing diagnostics for ${params.textDocument.uri}`, token); + }); + + function requestRefresh(): void { + connection.languages.diagnostics.refresh(); + } + + return { + requestRefresh, + dispose: () => { + } + }; + +} diff --git a/extensions/html-language-features/server/src/utils/validation.ts b/extensions/html-language-features/server/src/utils/validation.ts new file mode 100644 index 00000000000..5c5261cf9e5 --- /dev/null +++ b/extensions/html-language-features/server/src/utils/validation.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Connection, Diagnostic, Disposable, TextDocuments } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-html-languageservice'; +import { formatError } from './runner'; +import { RuntimeEnvironment } from '../htmlServer'; + +export type Validator = (textDocument: TextDocument) => Promise; +export type DiagnosticPushSupport = { dispose(): void; triggerValidation(textDocument: TextDocument): void }; + +export function registerDiagnosticPushSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticPushSupport { + + const pendingValidationRequests: { [uri: string]: Disposable } = {}; + const validationDelayMs = 500; + + const disposables: Disposable[] = []; + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + triggerValidation(change.document); + }, undefined, disposables); + + // a document has closed: clear all diagnostics + documents.onDidClose(event => { + cleanPendingValidation(event.document); + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); + }, undefined, disposables); + + function cleanPendingValidation(textDocument: TextDocument): void { + const request = pendingValidationRequests[textDocument.uri]; + if (request) { + request.dispose(); + delete pendingValidationRequests[textDocument.uri]; + } + } + + function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => { + if (request === pendingValidationRequests[textDocument.uri]) { + try { + const diagnostics = await validate(textDocument); + if (request === pendingValidationRequests[textDocument.uri]) { + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } + delete pendingValidationRequests[textDocument.uri]; + } catch (e) { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); + } + } + }, validationDelayMs); + } + + documents.all().forEach(triggerValidation); + + return { + triggerValidation, + dispose: () => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + const keys = Object.keys(pendingValidationRequests); + for (const key of keys) { + pendingValidationRequests[key].dispose(); + delete pendingValidationRequests[key]; + } + } + }; +} diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index b818bcefbf2..df8bb6faa35 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -10,6 +10,7 @@ import { } from 'vscode-languageserver'; import { formatError, runSafe, runSafeAsync } from './utils/runner'; +import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Range, Position } from 'vscode-json-languageservice'; import { getLanguageModelCache } from './languageModelCache'; import { Utils, URI } from 'vscode-uri'; @@ -113,6 +114,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let resultLimit = Number.MAX_VALUE; let formatterMaxNumberOfEdits = Number.MAX_VALUE; + let diagnosticSupport: DiagnosticsSupport | undefined; + + // After the server has started the client sends an initialize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities. connection.onInitialize((params: InitializeParams): InitializeResult => { @@ -147,6 +151,15 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) foldingRangeLimitDefault = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); hierarchicalDocumentSymbolSupport = getClientCapability('textDocument.documentSymbol.hierarchicalDocumentSymbolSupport', false); formatterMaxNumberOfEdits = initializationOptions.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; + + const pullDiagnosticSupport = getClientCapability('textDocument.diagnostic', undefined); + if (pullDiagnosticSupport === undefined) { + diagnosticSupport = registerDiagnosticsPushSupport(documents, connection, runtime, validateTextDocument); + } else { + diagnosticSupport = registerDiagnosticsPullSupport(documents, connection, runtime, validateTextDocument); + } + + const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, completionProvider: clientSnippetSupport ? { @@ -160,7 +173,12 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) colorProvider: {}, foldingRangeProvider: true, selectionRangeProvider: true, - documentLinkProvider: {} + documentLinkProvider: {}, + diagnosticProvider: { + documentSelector: null, + interFileDependencies: false, + workspaceDiagnostics: false + } }; return { capabilities }; @@ -351,68 +369,13 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) documents.all().forEach(triggerValidation); } - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - documents.onDidChangeContent((change) => { - limitExceededWarnings.cancel(change.document.uri); - triggerValidation(change.document); - }); - - // a document has closed: clear all diagnostics - documents.onDidClose(event => { - limitExceededWarnings.cancel(event.document.uri); - cleanPendingValidation(event.document); - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); - }); - - const pendingValidationRequests: { [uri: string]: Disposable } = {}; - const validationDelayMs = 300; - - function cleanPendingValidation(textDocument: TextDocument): void { - const request = pendingValidationRequests[textDocument.uri]; - if (request) { - request.dispose(); - delete pendingValidationRequests[textDocument.uri]; - } - } - - function triggerValidation(textDocument: TextDocument): void { - cleanPendingValidation(textDocument); - if (validateEnabled) { - pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(() => { - delete pendingValidationRequests[textDocument.uri]; - validateTextDocument(textDocument); - }, validationDelayMs); - } else { - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] }); - } - } - - function validateTextDocument(textDocument: TextDocument, callback?: (diagnostics: Diagnostic[]) => void): void { - const respond = (diagnostics: Diagnostic[]) => { - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - if (callback) { - callback(diagnostics); - } - }; + async function validateTextDocument(textDocument: TextDocument): Promise { if (textDocument.getText().length === 0) { - respond([]); // ignore empty documents - return; + return []; // ignore empty documents } const jsonDocument = getJSONDocument(textDocument); - const version = textDocument.version; - const documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'warning' } : { comments: 'error', trailingCommas: 'error' }; - languageService.doValidation(textDocument, jsonDocument, documentSettings).then(diagnostics => { - runtime.timer.setImmediate(() => { - const currDocument = documents.get(textDocument.uri); - if (currDocument && currDocument.version === version) { - respond(diagnostics as Diagnostic[]); // Send the computed diagnostics to VSCode. - } - }); - }, error => { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, error)); - }); + return await languageService.doValidation(textDocument, jsonDocument, documentSettings); } connection.onDidChangeWatchedFiles((change) => { diff --git a/extensions/json-language-features/server/src/utils/validation.ts b/extensions/json-language-features/server/src/utils/validation.ts new file mode 100644 index 00000000000..10130d2684f --- /dev/null +++ b/extensions/json-language-features/server/src/utils/validation.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, Connection, Diagnostic, Disposable, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, TextDocuments } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-json-languageservice'; +import { formatError, runSafeAsync } from './runner'; +import { RuntimeEnvironment } from '../jsonServer'; + +export type Validator = (textDocument: TextDocument) => Promise; +export type DiagnosticsSupport = { + dispose(): void; + requestRefresh(): void; +}; + +export function registerDiagnosticsPushSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + const pendingValidationRequests: { [uri: string]: Disposable } = {}; + const validationDelayMs = 500; + + const disposables: Disposable[] = []; + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + triggerValidation(change.document); + }, undefined, disposables); + + // a document has closed: clear all diagnostics + documents.onDidClose(event => { + cleanPendingValidation(event.document); + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); + }, undefined, disposables); + + function cleanPendingValidation(textDocument: TextDocument): void { + const request = pendingValidationRequests[textDocument.uri]; + if (request) { + request.dispose(); + delete pendingValidationRequests[textDocument.uri]; + } + } + + function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => { + if (request === pendingValidationRequests[textDocument.uri]) { + try { + const diagnostics = await validate(textDocument); + if (request === pendingValidationRequests[textDocument.uri]) { + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } + delete pendingValidationRequests[textDocument.uri]; + } catch (e) { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); + } + } + }, validationDelayMs); + } + + documents.all().forEach(triggerValidation); + + return { + requestRefresh: () => { + documents.all().forEach(triggerValidation); + }, + dispose: () => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + const keys = Object.keys(pendingValidationRequests); + for (const key of keys) { + pendingValidationRequests[key].dispose(); + delete pendingValidationRequests[key]; + } + } + }; +} + +export function registerDiagnosticsPullSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + function newDocumentDiagnosticReport(diagnostics: Diagnostic[]): DocumentDiagnosticReport { + return { + kind: DocumentDiagnosticReportKind.Full, + items: diagnostics + }; + } + + connection.languages.diagnostics.on(async (params: DocumentDiagnosticParams, token: CancellationToken) => { + return runSafeAsync(runtime, async () => { + const document = documents.get(params.textDocument.uri); + if (document) { + return newDocumentDiagnosticReport(await validate(document)); + } + return newDocumentDiagnosticReport([]); + + }, newDocumentDiagnosticReport([]), `Error while computing diagnostics for ${params.textDocument.uri}`, token); + }); + + function requestRefresh(): void { + connection.languages.diagnostics.refresh(); + } + + return { + requestRefresh, + dispose: () => { + } + }; + +}