vscode-languageclient
Version:
VSCode Language client implementation
896 lines (895 loc) • 42.8 kB
JavaScript
"use strict";
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticFeature = exports.DiagnosticPullMode = exports.vsdiag = void 0;
const vscode_1 = require("vscode");
const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
const uuid_1 = require("./utils/uuid");
const globPattern_1 = require("./utils/globPattern");
const features_1 = require("./features");
function ensure(target, key) {
if (target[key] === void 0) {
target[key] = {};
}
return target[key];
}
var vsdiag;
(function (vsdiag) {
let DocumentDiagnosticReportKind;
(function (DocumentDiagnosticReportKind) {
DocumentDiagnosticReportKind["full"] = "full";
DocumentDiagnosticReportKind["unChanged"] = "unChanged";
})(DocumentDiagnosticReportKind = vsdiag.DocumentDiagnosticReportKind || (vsdiag.DocumentDiagnosticReportKind = {}));
})(vsdiag || (exports.vsdiag = vsdiag = {}));
var DiagnosticPullMode;
(function (DiagnosticPullMode) {
DiagnosticPullMode["onType"] = "onType";
DiagnosticPullMode["onSave"] = "onSave";
DiagnosticPullMode["onFocus"] = "onFocus";
})(DiagnosticPullMode || (exports.DiagnosticPullMode = DiagnosticPullMode = {}));
var RequestStateKind;
(function (RequestStateKind) {
RequestStateKind["active"] = "open";
RequestStateKind["reschedule"] = "reschedule";
RequestStateKind["outDated"] = "drop";
})(RequestStateKind || (RequestStateKind = {}));
var PullState;
(function (PullState) {
PullState[PullState["document"] = 1] = "document";
PullState[PullState["workspace"] = 2] = "workspace";
})(PullState || (PullState = {}));
var DocumentOrUri;
(function (DocumentOrUri) {
function asKey(document) {
return document instanceof vscode_1.Uri ? document.toString() : document.uri.toString();
}
DocumentOrUri.asKey = asKey;
})(DocumentOrUri || (DocumentOrUri = {}));
class DocumentPullStateTracker {
documentPullStates;
workspacePullStates;
constructor() {
this.documentPullStates = new Map();
this.workspacePullStates = new Map();
}
track(kind, document, arg1) {
const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates;
const [key, uri, version] = document instanceof vscode_1.Uri
? [document.toString(), document, arg1]
: [document.uri.toString(), document.uri, document.version];
let state = states.get(key);
if (state === undefined) {
state = { document: uri, pulledVersion: version, resultId: undefined };
states.set(key, state);
}
return state;
}
update(kind, document, arg1, arg2) {
const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates;
const [key, uri, version, resultId] = document instanceof vscode_1.Uri
? [document.toString(), document, arg1, arg2]
: [document.uri.toString(), document.uri, document.version, arg1];
let state = states.get(key);
if (state === undefined) {
state = { document: uri, pulledVersion: version, resultId };
states.set(key, state);
}
else {
state.pulledVersion = version;
state.resultId = resultId;
}
}
unTrack(kind, document) {
const key = DocumentOrUri.asKey(document);
const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates;
states.delete(key);
}
tracks(kind, document) {
const key = DocumentOrUri.asKey(document);
const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates;
return states.has(key);
}
tracksSameVersion(kind, document) {
const key = document.uri.toString();
const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates;
const state = states.get(key);
return state !== undefined && state.pulledVersion === document.version;
}
getResultId(kind, document) {
const key = DocumentOrUri.asKey(document);
const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates;
return states.get(key)?.resultId;
}
getAllResultIds() {
const result = [];
for (let [uri, value] of this.workspacePullStates) {
if (this.documentPullStates.has(uri)) {
value = this.documentPullStates.get(uri);
}
if (value.resultId !== undefined) {
result.push({ uri, value: value.resultId });
}
}
return result;
}
}
class DiagnosticRequestor {
isDisposed;
client;
visibleDocuments;
options;
onDidChangeDiagnosticsEmitter;
provider;
diagnostics;
openRequests;
documentStates;
workspaceErrorCounter;
workspaceCancellation;
workspaceTimeout;
constructor(client, visibleDocuments, options) {
this.client = client;
this.visibleDocuments = visibleDocuments;
this.options = options;
this.isDisposed = false;
this.onDidChangeDiagnosticsEmitter = new vscode_1.EventEmitter();
this.provider = this.createProvider();
this.diagnostics = this.createDiagnosticCollection();
this.openRequests = new Map();
this.documentStates = new DocumentPullStateTracker();
this.workspaceErrorCounter = 0;
}
createDiagnosticCollection() {
if (this.client.clientOptions.diagnosticCollectionProvider === undefined) {
return vscode_1.languages.createDiagnosticCollection(this.options.identifier);
}
else {
return this.client.clientOptions.diagnosticCollectionProvider.create(this.options.identifier, features_1.DiagnosticCollectionSource.pull);
}
}
knows(kind, document) {
const uri = document instanceof vscode_1.Uri ? document : document.uri;
return this.documentStates.tracks(kind, document) || this.openRequests.has(uri.toString());
}
knowsSameVersion(kind, document) {
const requestState = this.openRequests.get(document.uri.toString());
if (requestState === undefined) {
return this.documentStates.tracksSameVersion(kind, document);
}
// A reschedule request is independent of a version so it will trigger
// on the latest version no matter what.
if (requestState.state === RequestStateKind.reschedule) {
return true;
}
if (requestState.state === RequestStateKind.outDated) {
return false;
}
return requestState.version === document.version;
}
forget(kind, document) {
this.documentStates.unTrack(kind, document);
}
pull(document, cb) {
if (this.isDisposed) {
return;
}
const uri = document instanceof vscode_1.Uri ? document : document.uri;
this.pullAsync(document).then(() => {
if (cb) {
cb();
}
}, (error) => {
this.client.error(`Document pull failed for text document ${uri.toString()}`, error, false);
});
}
async pullAsync(document, version) {
if (this.isDisposed) {
return;
}
const isUri = document instanceof vscode_1.Uri;
const uri = isUri ? document : document.uri;
const key = uri.toString();
version = isUri ? version : document.version;
const currentRequestState = this.openRequests.get(key);
const documentState = isUri
? this.documentStates.track(PullState.document, document, version)
: this.documentStates.track(PullState.document, document);
if (currentRequestState === undefined) {
const tokenSource = new vscode_1.CancellationTokenSource();
this.openRequests.set(key, { state: RequestStateKind.active, document: document, version: version, tokenSource });
let report;
let afterState;
try {
report = await this.provider.provideDiagnostics(document, documentState.resultId, tokenSource.token) ?? { kind: vsdiag.DocumentDiagnosticReportKind.full, items: [] };
}
catch (error) {
if (error instanceof features_1.LSPCancellationError && vscode_languageserver_protocol_1.DiagnosticServerCancellationData.is(error.data) && error.data.retriggerRequest === false) {
afterState = { state: RequestStateKind.outDated, document };
}
if (afterState === undefined && error instanceof vscode_1.CancellationError) {
afterState = { state: RequestStateKind.reschedule, document };
}
else {
throw error;
}
}
afterState = afterState ?? this.openRequests.get(key);
if (afterState === undefined) {
// This shouldn't happen. Log it
this.client.error(`Lost request state in diagnostic pull model. Clearing diagnostics for ${key}`);
this.diagnostics.delete(uri);
return;
}
this.openRequests.delete(key);
if (!this.visibleDocuments.isVisible(document)) {
this.documentStates.unTrack(PullState.document, document);
return;
}
if (afterState.state === RequestStateKind.outDated) {
return;
}
// report is only undefined if the request has thrown.
if (report !== undefined) {
if (report.kind === vsdiag.DocumentDiagnosticReportKind.full) {
this.diagnostics.set(uri, report.items);
}
documentState.pulledVersion = version;
documentState.resultId = report.resultId;
}
if (afterState.state === RequestStateKind.reschedule) {
this.pull(document);
}
}
else {
if (currentRequestState.state === RequestStateKind.active) {
// Cancel the current request and reschedule a new one when the old one returned.
currentRequestState.tokenSource.cancel();
this.openRequests.set(key, { state: RequestStateKind.reschedule, document: currentRequestState.document });
}
else if (currentRequestState.state === RequestStateKind.outDated) {
this.openRequests.set(key, { state: RequestStateKind.reschedule, document: currentRequestState.document });
}
}
}
forgetDocument(document) {
if (this.isDisposed) {
return;
}
const uri = document instanceof vscode_1.Uri ? document : document.uri;
const key = uri.toString();
const request = this.openRequests.get(key);
if (this.options.workspaceDiagnostics && uri.scheme !== 'untitled') {
// If we run workspace diagnostic pull a last time for the diagnostics
// and then rely on getting them from the workspace result.
if (request !== undefined) {
this.openRequests.set(key, { state: RequestStateKind.reschedule, document: document });
}
else {
this.pull(document, () => {
this.forget(PullState.document, document);
});
}
// The previous resultId from the workspace pull state can map to diagnostics we no longer have
// (e.g. they came from a workspace report but were overwritten by a later document pull request).
// Clear the workspace pull state for this document as well to ensure we get fresh diagnostics.
this.forget(PullState.workspace, document);
}
else {
// We have normal pull or inter file dependencies. In this case we
// clear the diagnostics (to have the same start as after startup).
// We also cancel outstanding requests.
if (request !== undefined) {
if (request.state === RequestStateKind.active) {
request.tokenSource.cancel();
}
this.openRequests.set(key, { state: RequestStateKind.outDated, document: document });
}
this.diagnostics.delete(uri);
this.forget(PullState.document, document);
}
}
pullWorkspace() {
if (this.isDisposed) {
return;
}
this.pullWorkspaceAsync().then(() => {
this.workspaceTimeout = (0, vscode_languageserver_protocol_1.RAL)().timer.setTimeout(() => {
this.pullWorkspace();
}, 2000);
}, (error) => {
if (!(error instanceof features_1.LSPCancellationError) && !vscode_languageserver_protocol_1.DiagnosticServerCancellationData.is(error.data)) {
this.client.error(`Workspace diagnostic pull failed.`, error, false);
this.workspaceErrorCounter++;
}
if (this.workspaceErrorCounter <= 5) {
this.workspaceTimeout = (0, vscode_languageserver_protocol_1.RAL)().timer.setTimeout(() => {
this.pullWorkspace();
}, 2000);
}
});
}
async pullWorkspaceAsync() {
if (!this.provider.provideWorkspaceDiagnostics || this.isDisposed) {
return;
}
if (this.workspaceCancellation !== undefined) {
this.workspaceCancellation.cancel();
this.workspaceCancellation = undefined;
}
this.workspaceCancellation = new vscode_1.CancellationTokenSource();
const previousResultIds = this.documentStates.getAllResultIds().map((item) => {
return {
uri: this.client.protocol2CodeConverter.asUri(item.uri),
value: item.value
};
});
await this.provider.provideWorkspaceDiagnostics(previousResultIds, this.workspaceCancellation.token, (chunk) => {
if (!chunk || this.isDisposed) {
return;
}
for (const item of chunk.items) {
if (item.kind === vsdiag.DocumentDiagnosticReportKind.full) {
// Favour document pull result over workspace results. So skip if it is tracked
// as a document result.
if (!this.documentStates.tracks(PullState.document, item.uri)) {
this.diagnostics.set(item.uri, item.items);
}
}
this.documentStates.update(PullState.workspace, item.uri, item.version ?? undefined, item.resultId);
}
});
}
createProvider() {
const result = {
onDidChangeDiagnostics: this.onDidChangeDiagnosticsEmitter.event,
provideDiagnostics: (document, previousResultId, token) => {
const provideDiagnostics = (document, previousResultId, token) => {
const params = {
identifier: this.options.identifier,
textDocument: { uri: this.client.code2ProtocolConverter.asUri(document instanceof vscode_1.Uri ? document : document.uri) },
previousResultId: previousResultId
};
if (this.isDisposed === true || !this.client.isRunning()) {
return { kind: vsdiag.DocumentDiagnosticReportKind.full, items: [] };
}
return this.client.sendRequest(vscode_languageserver_protocol_1.DocumentDiagnosticRequest.type, params, token).then(async (result) => {
if (this.isDisposed) {
return { kind: vsdiag.DocumentDiagnosticReportKind.full, items: [] };
}
if (token.isCancellationRequested) {
throw new vscode_1.CancellationError();
}
if (result === undefined || result === null) {
return { kind: vsdiag.DocumentDiagnosticReportKind.full, items: [] };
}
if (result.kind === vscode_languageserver_protocol_1.DocumentDiagnosticReportKind.Full) {
return { kind: vsdiag.DocumentDiagnosticReportKind.full, resultId: result.resultId, items: await this.client.protocol2CodeConverter.asDiagnostics(result.items, token) };
}
else {
return { kind: vsdiag.DocumentDiagnosticReportKind.unChanged, resultId: result.resultId };
}
}, (error) => {
return this.client.handleFailedRequest(vscode_languageserver_protocol_1.DocumentDiagnosticRequest.type, token, error, { kind: vsdiag.DocumentDiagnosticReportKind.full, items: [] }, true, true);
});
};
const middleware = this.client.middleware;
return middleware.provideDiagnostics
? middleware.provideDiagnostics(document, previousResultId, token, provideDiagnostics)
: provideDiagnostics(document, previousResultId, token);
}
};
if (this.options.workspaceDiagnostics) {
result.provideWorkspaceDiagnostics = (resultIds, token, resultReporter) => {
const convertReport = async (report) => {
if (report.kind === vscode_languageserver_protocol_1.DocumentDiagnosticReportKind.Full) {
return {
kind: vsdiag.DocumentDiagnosticReportKind.full,
uri: this.client.protocol2CodeConverter.asUri(report.uri),
resultId: report.resultId,
version: report.version,
items: await this.client.protocol2CodeConverter.asDiagnostics(report.items, token)
};
}
else {
return {
kind: vsdiag.DocumentDiagnosticReportKind.unChanged,
uri: this.client.protocol2CodeConverter.asUri(report.uri),
resultId: report.resultId,
version: report.version
};
}
};
const convertPreviousResultIds = (resultIds) => {
const converted = [];
for (const item of resultIds) {
converted.push({ uri: this.client.code2ProtocolConverter.asUri(item.uri), value: item.value });
}
return converted;
};
const provideDiagnostics = (resultIds, token, resultReporter) => {
const partialResultToken = (0, uuid_1.generateUuid)();
const disposable = this.client.onProgress(vscode_languageserver_protocol_1.WorkspaceDiagnosticRequest.partialResult, partialResultToken, async (partialResult) => {
if (partialResult === undefined || partialResult === null) {
resultReporter(null);
return;
}
const converted = {
items: []
};
for (const item of partialResult.items) {
try {
converted.items.push(await convertReport(item));
}
catch (error) {
this.client.error(`Converting workspace diagnostics failed.`, error);
}
}
resultReporter(converted);
});
const params = {
identifier: this.options.identifier,
previousResultIds: convertPreviousResultIds(resultIds),
partialResultToken: partialResultToken
};
if (this.isDisposed === true || !this.client.isRunning()) {
return { items: [] };
}
return this.client.sendRequest(vscode_languageserver_protocol_1.WorkspaceDiagnosticRequest.type, params, token).then(async (result) => {
if (token.isCancellationRequested) {
return { items: [] };
}
const converted = {
items: []
};
for (const item of result.items) {
converted.items.push(await convertReport(item));
}
disposable.dispose();
resultReporter(converted);
return { items: [] };
}, (error) => {
disposable.dispose();
return this.client.handleFailedRequest(vscode_languageserver_protocol_1.DocumentDiagnosticRequest.type, token, error, { items: [] });
});
};
const middleware = this.client.middleware;
return middleware.provideWorkspaceDiagnostics
? middleware.provideWorkspaceDiagnostics(resultIds, token, resultReporter, provideDiagnostics)
: provideDiagnostics(resultIds, token, resultReporter);
};
}
return result;
}
dispose() {
this.isDisposed = true;
// Cancel and clear workspace pull if present.
this.workspaceCancellation?.cancel();
this.workspaceTimeout?.dispose();
// Cancel all request and mark open requests as outdated.
for (const [key, request] of this.openRequests) {
if (request.state === RequestStateKind.active) {
request.tokenSource.cancel();
}
this.openRequests.set(key, { state: RequestStateKind.outDated, document: request.document });
}
// cleanup old diagnostics
if (this.client.clientOptions.diagnosticCollectionProvider !== undefined) {
this.client.clientOptions.diagnosticCollectionProvider.dispose(this.diagnostics, features_1.DiagnosticCollectionSource.pull);
}
else {
this.diagnostics.dispose();
}
}
}
class BackgroundScheduler {
client;
diagnosticRequestor;
lastDocumentToPull;
documents;
timeoutHandle;
// The problem is that there could be outstanding diagnostic requests
// when we shutdown which when we receive the result will trigger a
// reschedule. So we remember if the background scheduler got disposed
// and ignore those re-schedules
isDisposed;
constructor(client, diagnosticRequestor) {
this.client = client;
this.diagnosticRequestor = diagnosticRequestor;
this.documents = new vscode_languageserver_protocol_1.LinkedMap();
this.isDisposed = false;
}
add(document) {
if (this.isDisposed === true) {
return;
}
const key = DocumentOrUri.asKey(document);
if (this.documents.has(key)) {
return;
}
this.documents.set(key, document, vscode_languageserver_protocol_1.Touch.Last);
// Make sure we run up to that document. We could
// consider inserting it after the current last
// document for performance reasons but it might not catch
// all interfile dependencies.
this.lastDocumentToPull = document;
}
remove(document) {
const key = DocumentOrUri.asKey(document);
this.documents.delete(key);
// No more documents. Stop background activity.
if (this.documents.size === 0) {
this.stop();
return;
}
else if (key === this.lastDocumentToPullKey()) {
// The remove document was the one we would run up to. So
// take the one before it.
const before = this.documents.before(key);
if (before === undefined) {
this.stop();
}
else {
this.lastDocumentToPull = before;
}
}
}
trigger() {
this.lastDocumentToPull = this.documents.last;
this.runLoop();
}
runLoop() {
if (this.isDisposed === true) {
return;
}
// We have an empty background list. Make sure we stop
// background activity.
if (this.documents.size === 0) {
this.stop();
return;
}
// We have no last document anymore so stop the loop
if (this.lastDocumentToPull === undefined) {
return;
}
// We have a timeout in the loop. So we should not schedule
// another run.
if (this.timeoutHandle !== undefined) {
return;
}
this.timeoutHandle = (0, vscode_languageserver_protocol_1.RAL)().timer.setTimeout(() => {
const document = this.documents.first;
if (document === undefined) {
return;
}
const key = DocumentOrUri.asKey(document);
this.diagnosticRequestor.pullAsync(document).catch((error) => {
this.client.error(`Document pull failed for text document ${key}`, error, false);
}).finally(() => {
this.timeoutHandle = undefined;
this.documents.set(key, document, vscode_languageserver_protocol_1.Touch.Last);
if (key !== this.lastDocumentToPullKey()) {
this.runLoop();
}
});
}, 500);
}
dispose() {
this.isDisposed = true;
this.stop();
this.documents.clear();
this.lastDocumentToPull = undefined;
}
stop() {
this.timeoutHandle?.dispose();
this.timeoutHandle = undefined;
this.lastDocumentToPull = undefined;
}
lastDocumentToPullKey() {
return this.lastDocumentToPull !== undefined ? DocumentOrUri.asKey(this.lastDocumentToPull) : undefined;
}
}
class DiagnosticFeatureProviderImpl {
disposable;
diagnosticRequestor;
activeTextDocument;
backgroundScheduler;
constructor(client, visibleDocuments, options) {
const diagnosticPullOptions = Object.assign({ onChange: false, onSave: false, onFocus: false }, client.clientOptions.diagnosticPullOptions);
const documentSelector = client.protocol2CodeConverter.asDocumentSelector(options.documentSelector);
const disposables = [];
const matchFilter = (filter, resource) => {
// The filter is a language id. We can't determine if it matches
// so we return false.
if (typeof filter === 'string') {
return false;
}
if (filter.language !== undefined && filter.language !== '*') {
return false;
}
if (filter.scheme !== undefined && filter.scheme !== '*' && filter.scheme !== resource.scheme) {
return false;
}
if (filter.pattern !== undefined && !(0, globPattern_1.matchGlobPattern)(filter.pattern, resource)) {
return false;
}
return true;
};
const matchResource = (resource) => {
const selector = options.documentSelector;
if (diagnosticPullOptions.match !== undefined) {
return diagnosticPullOptions.match(selector, resource);
}
for (const filter of selector) {
if (!vscode_languageserver_protocol_1.TextDocumentFilter.is(filter)) {
continue;
}
if (matchFilter(filter, resource)) {
return true;
}
}
return false;
};
const matches = (document) => {
return document instanceof vscode_1.Uri
? matchResource(document)
: vscode_1.languages.match(documentSelector, document) > 0 && visibleDocuments.isVisible(document);
};
const matchesCell = (cell) => {
// Cells match if the language is allowed and if the containing notebook is visible.
return vscode_1.languages.match(documentSelector, cell.document) > 0 && visibleDocuments.isVisible(cell.notebook.uri);
};
const isActiveDocument = (document) => {
return document instanceof vscode_1.Uri
? this.activeTextDocument?.uri.toString() === document.toString()
: this.activeTextDocument === document;
};
this.diagnosticRequestor = new DiagnosticRequestor(client, visibleDocuments, options);
this.backgroundScheduler = new BackgroundScheduler(client, this.diagnosticRequestor);
const addToBackgroundIfNeeded = (document) => {
if (!matches(document) || !options.interFileDependencies || isActiveDocument(document) || diagnosticPullOptions.onChange === false) {
return;
}
this.backgroundScheduler.add(document);
};
const considerDocument = (textDocument, mode) => {
return (diagnosticPullOptions.filter === undefined || !diagnosticPullOptions.filter(textDocument, mode)) && this.diagnosticRequestor.knows(PullState.document, textDocument);
};
this.activeTextDocument = vscode_1.window.activeTextEditor?.document;
disposables.push(vscode_1.window.onDidChangeActiveTextEditor((editor) => {
const oldActive = this.activeTextDocument;
this.activeTextDocument = editor?.document;
if (oldActive !== undefined) {
addToBackgroundIfNeeded(oldActive);
}
if (this.activeTextDocument !== undefined) {
this.backgroundScheduler.remove(this.activeTextDocument);
if (diagnosticPullOptions.onFocus === true && matches(this.activeTextDocument) && considerDocument(this.activeTextDocument, DiagnosticPullMode.onFocus)) {
this.diagnosticRequestor.pull(this.activeTextDocument);
}
}
}));
// For pull model diagnostics we pull for documents visible in the UI.
// From an eventing point of view we still rely on open document events
// and filter the documents that are not visible in the UI instead of
// listening to Tab events. Major reason is event timing since we need
// to ensure that the pull is send after the document open has reached
// the server.
// We always pull on open.
const openFeature = client.getFeature(vscode_languageserver_protocol_1.DidOpenTextDocumentNotification.method);
disposables.push(openFeature.onNotificationSent((event) => {
const textDocument = event.textDocument;
// We already know about this document. This can happen via a tab open.
if (this.diagnosticRequestor.knowsSameVersion(PullState.document, textDocument)) {
return;
}
if (matches(textDocument)) {
this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument); });
}
}));
const notebookFeature = client.getFeature(vscode_languageserver_protocol_1.NotebookDocumentSyncRegistrationType.method);
disposables.push(notebookFeature.onOpenNotificationSent((event) => {
// Send a pull for all opened cells in the notebook.
for (const cell of event.getCells()) {
if (matchesCell(cell)) {
this.diagnosticRequestor.pull(cell.document, () => { addToBackgroundIfNeeded(cell.document); });
}
}
}));
disposables.push(visibleDocuments.onOpen((opened) => {
for (const resource of opened) {
// We already know about this document. This can happen via a document open.
if (this.diagnosticRequestor.knows(PullState.document, resource)) {
continue;
}
const uriStr = resource.toString();
let textDocument;
for (const item of vscode_1.workspace.textDocuments) {
if (uriStr === item.uri.toString()) {
textDocument = item;
break;
}
}
// In VS Code the event timing is as follows:
// 1. tab events are fired.
// 2. open document events are fired and internal data structures like
// workspace.textDocuments and Window.activeTextEditor are updated.
//
// This means: for newly created tab/editors we don't find the underlying
// document yet. So we do nothing and rely on the underlying open document event
// to be fired.
if (textDocument !== undefined && matches(textDocument)) {
this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument); });
}
}
}));
// Pull all diagnostics for documents that are already open
const pulledTextDocuments = new Set();
for (const textDocument of vscode_1.workspace.textDocuments) {
if (matches(textDocument)) {
this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument); });
pulledTextDocuments.add(textDocument.uri.toString());
}
}
// Do the same for already open notebook cells.
for (const notebookDocument of vscode_1.workspace.notebookDocuments) {
for (const cell of notebookDocument.getCells()) {
if (matchesCell(cell)) {
this.diagnosticRequestor.pull(cell.document, () => { addToBackgroundIfNeeded(cell.document); });
pulledTextDocuments.add(cell.document.uri.toString());
}
}
}
// Pull all visible documents if not already pulled as text document
if (diagnosticPullOptions.onTabs === true) {
for (const resource of visibleDocuments.getResources()) {
if (!pulledTextDocuments.has(resource.toString()) && matches(resource)) {
this.diagnosticRequestor.pull(resource, () => { addToBackgroundIfNeeded(resource); });
}
}
}
// We don't need to pull on tab open since we will receive a document open as well later on
// and that event allows us to use a document for a match check which will have a set
// language id.
if (diagnosticPullOptions.onChange === true) {
const changeFeature = client.getFeature(vscode_languageserver_protocol_1.DidChangeTextDocumentNotification.method);
disposables.push(changeFeature.onNotificationSent(async (event) => {
const textDocument = event.textDocument;
if (considerDocument(textDocument, DiagnosticPullMode.onType)) {
this.diagnosticRequestor.pull(textDocument, () => { this.backgroundScheduler.trigger(); });
}
}));
disposables.push(notebookFeature.onChangeNotificationSent(async (event) => {
// Send a pull for all changed cells in the notebook.
const textEvents = event.cells?.textContent || [];
const changedCells = textEvents.map((c) => event.notebook.getCells().find(cell => cell.document.uri.toString() === c.document.uri.toString()));
for (const cell of changedCells) {
if (cell && matchesCell(cell)) {
this.diagnosticRequestor.pull(cell.document, () => { this.backgroundScheduler.trigger(); });
}
}
// Clear out any closed cells.
const closedCells = event.cells?.structure?.didClose || [];
for (const cell of closedCells) {
this.diagnosticRequestor.forgetDocument(cell.document);
}
// Send a pull for any new opened cells.
const openedCells = event.cells?.structure?.didOpen || [];
for (const cell of openedCells) {
if (matchesCell(cell)) {
this.diagnosticRequestor.pull(cell.document, () => { this.backgroundScheduler.trigger(); });
}
}
}));
}
if (diagnosticPullOptions.onSave === true) {
const saveFeature = client.getFeature(vscode_languageserver_protocol_1.DidSaveTextDocumentNotification.method);
disposables.push(saveFeature.onNotificationSent((event) => {
const textDocument = event.textDocument;
if (considerDocument(textDocument, DiagnosticPullMode.onSave)) {
this.diagnosticRequestor.pull(event.textDocument);
}
}));
disposables.push(notebookFeature.onSaveNotificationSent((event) => {
for (const cell of event.getCells()) {
if (matchesCell(cell)) {
this.diagnosticRequestor.pull(cell.document);
}
}
}));
}
// When the document closes clear things up. We do that right before the
// close notification is sent to the server so that it is not taken
// from the pull queue in case it is still there.
const closeFeature = client.getFeature(vscode_languageserver_protocol_1.DidCloseTextDocumentNotification.method);
disposables.push(closeFeature.onAboutToSendNotification((event) => {
this.cleanUpDocument(event.textDocument);
}));
disposables.push(notebookFeature.onCloseNotificationSent((event) => {
for (const cell of event.getCells()) {
this.cleanUpDocument(cell.document);
}
}));
// Same when a visible document closes.
disposables.push(visibleDocuments.onClose((closed) => {
for (const document of closed) {
this.cleanUpDocument(document);
}
}));
// We received a did change from the server.
this.diagnosticRequestor.onDidChangeDiagnosticsEmitter.event(() => {
for (const textDocument of vscode_1.workspace.textDocuments) {
if (matches(textDocument)) {
this.diagnosticRequestor.pull(textDocument);
}
}
});
// da348dc5-c30a-4515-9d98-31ff3be38d14 is the test UUID to test the middle ware. So don't auto trigger pulls.
if (options.workspaceDiagnostics === true && options.identifier !== 'da348dc5-c30a-4515-9d98-31ff3be38d14') {
this.diagnosticRequestor.pullWorkspace();
}
this.disposable = vscode_1.Disposable.from(...disposables, this.backgroundScheduler, this.diagnosticRequestor);
}
get onDidChangeDiagnosticsEmitter() {
return this.diagnosticRequestor.onDidChangeDiagnosticsEmitter;
}
get diagnostics() {
return this.diagnosticRequestor.provider;
}
forget(document) {
this.cleanUpDocument(document);
}
cleanUpDocument(document) {
this.backgroundScheduler.remove(document);
if (this.diagnosticRequestor.knows(PullState.document, document)) {
this.diagnosticRequestor.forgetDocument(document);
}
}
}
class DiagnosticFeature extends features_1.TextDocumentLanguageFeature {
constructor(client) {
super(client, vscode_languageserver_protocol_1.DocumentDiagnosticRequest.type);
}
fillClientCapabilities(capabilities) {
const capability = ensure(ensure(capabilities, 'textDocument'), 'diagnostic');
capability.relatedInformation = true;
capability.tagSupport = { valueSet: [vscode_languageserver_protocol_1.DiagnosticTag.Unnecessary, vscode_languageserver_protocol_1.DiagnosticTag.Deprecated] };
capability.codeDescriptionSupport = true;
capability.dataSupport = true;
capability.dynamicRegistration = true;
// We first need to decide how a UI will look with related documents.
// An easy implementation would be to only show related diagnostics for
// the active editor.
capability.relatedDocumentSupport = false;
// VS Code has no support for markup content in diagnostic messages.
capability.markupMessageSupport = false;
ensure(ensure(capabilities, 'workspace'), 'diagnostics').refreshSupport = true;
}
initialize(capabilities, documentSelector) {
const client = this._client;
client.onRequest(vscode_languageserver_protocol_1.DiagnosticRefreshRequest.type, async () => {
for (const provider of this.getAllProviders()) {
provider.onDidChangeDiagnosticsEmitter.fire();
}
});
const [id, options] = this.getRegistration(documentSelector, capabilities.diagnosticProvider);
if (!id || !options) {
return;
}
this.register({ id: id, registerOptions: options });
}
clear() {
super.clear();
}
refresh() {
for (const provider of this.getAllProviders()) {
provider.onDidChangeDiagnosticsEmitter.fire();
}
}
registerLanguageProvider(options) {
const provider = new DiagnosticFeatureProviderImpl(this._client, this._client.visibleDocuments, options);
return [provider.disposable, provider];
}
}
exports.DiagnosticFeature = DiagnosticFeature;