- Hover provider showing entity information and type - Go-to-definition (F12) for entity references - Basic IFC file validation (ISO-10303-21 header check) - Entity parsing with regex-based detection - Proper CommonJS module system (avoiding ES module issues) This replaces the broken baseline from ifc-developer-tools which had: - Non-functional ES module configuration - Circular dependency issues - Parser crashes - Non-working PositionVisitor Built on Microsoft's LSP example template for a clean, maintainable foundation. Next: Add hierarchical entity dependency tree in hover tooltip."
814 lines
37 KiB
JavaScript
814 lines
37 KiB
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 minimatch = require("minimatch");
|
|
const vscode_1 = require("vscode");
|
|
const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
|
|
const uuid_1 = require("./utils/uuid");
|
|
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 || (exports.DiagnosticPullMode = DiagnosticPullMode = {}));
|
|
var RequestStateKind;
|
|
(function (RequestStateKind) {
|
|
RequestStateKind["active"] = "open";
|
|
RequestStateKind["reschedule"] = "reschedule";
|
|
RequestStateKind["outDated"] = "drop";
|
|
})(RequestStateKind || (RequestStateKind = {}));
|
|
/**
|
|
* Manages the open tabs. We don't directly use the tab API since for
|
|
* diagnostics we need to de-dupe tabs that show the same resources since
|
|
* we pull on the model not the UI.
|
|
*/
|
|
class Tabs {
|
|
constructor() {
|
|
this.open = new Set();
|
|
this._onOpen = new vscode_1.EventEmitter();
|
|
this._onClose = new vscode_1.EventEmitter();
|
|
Tabs.fillTabResources(this.open);
|
|
const openTabsHandler = (event) => {
|
|
if (event.closed.length === 0 && event.opened.length === 0) {
|
|
return;
|
|
}
|
|
const oldTabs = this.open;
|
|
const currentTabs = new Set();
|
|
Tabs.fillTabResources(currentTabs);
|
|
const closed = new Set();
|
|
const opened = new Set(currentTabs);
|
|
for (const tab of oldTabs.values()) {
|
|
if (currentTabs.has(tab)) {
|
|
opened.delete(tab);
|
|
}
|
|
else {
|
|
closed.add(tab);
|
|
}
|
|
}
|
|
this.open = currentTabs;
|
|
if (closed.size > 0) {
|
|
const toFire = new Set();
|
|
for (const item of closed) {
|
|
toFire.add(vscode_1.Uri.parse(item));
|
|
}
|
|
this._onClose.fire(toFire);
|
|
}
|
|
if (opened.size > 0) {
|
|
const toFire = new Set();
|
|
for (const item of opened) {
|
|
toFire.add(vscode_1.Uri.parse(item));
|
|
}
|
|
this._onOpen.fire(toFire);
|
|
}
|
|
};
|
|
if (vscode_1.window.tabGroups.onDidChangeTabs !== undefined) {
|
|
this.disposable = vscode_1.window.tabGroups.onDidChangeTabs(openTabsHandler);
|
|
}
|
|
else {
|
|
this.disposable = { dispose: () => { } };
|
|
}
|
|
}
|
|
get onClose() {
|
|
return this._onClose.event;
|
|
}
|
|
get onOpen() {
|
|
return this._onOpen.event;
|
|
}
|
|
dispose() {
|
|
this.disposable.dispose();
|
|
}
|
|
isActive(document) {
|
|
return document instanceof vscode_1.Uri
|
|
? vscode_1.window.activeTextEditor?.document.uri === document
|
|
: vscode_1.window.activeTextEditor?.document === document;
|
|
}
|
|
isVisible(document) {
|
|
const uri = document instanceof vscode_1.Uri ? document : document.uri;
|
|
return this.open.has(uri.toString());
|
|
}
|
|
getTabResources() {
|
|
const result = new Set();
|
|
Tabs.fillTabResources(new Set(), result);
|
|
return result;
|
|
}
|
|
static fillTabResources(strings, uris) {
|
|
const seen = strings ?? new Set();
|
|
for (const group of vscode_1.window.tabGroups.all) {
|
|
for (const tab of group.tabs) {
|
|
const input = tab.input;
|
|
let uri;
|
|
if (input instanceof vscode_1.TabInputText) {
|
|
uri = input.uri;
|
|
}
|
|
else if (input instanceof vscode_1.TabInputTextDiff) {
|
|
uri = input.modified;
|
|
}
|
|
else if (input instanceof vscode_1.TabInputCustom) {
|
|
uri = input.uri;
|
|
}
|
|
if (uri !== undefined && !seen.has(uri.toString())) {
|
|
seen.add(uri.toString());
|
|
uris !== undefined && uris.add(uri);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
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);
|
|
}
|
|
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 {
|
|
constructor(client, tabs, options) {
|
|
this.client = client;
|
|
this.tabs = tabs;
|
|
this.options = options;
|
|
this.isDisposed = false;
|
|
this.onDidChangeDiagnosticsEmitter = new vscode_1.EventEmitter();
|
|
this.provider = this.createProvider();
|
|
this.diagnostics = vscode_1.languages.createDiagnosticCollection(options.identifier);
|
|
this.openRequests = new Map();
|
|
this.documentStates = new DocumentPullStateTracker();
|
|
this.workspaceErrorCounter = 0;
|
|
}
|
|
knows(kind, document) {
|
|
const uri = document instanceof vscode_1.Uri ? document : document.uri;
|
|
return this.documentStates.tracks(kind, document) || this.openRequests.has(uri.toString());
|
|
}
|
|
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.tabs.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) {
|
|
const uri = document instanceof vscode_1.Uri ? document : document.uri;
|
|
const key = uri.toString();
|
|
const request = this.openRequests.get(key);
|
|
if (this.options.workspaceDiagnostics) {
|
|
// If we run workspace diagnostic pull a last time for the diagnostics
|
|
// and the 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);
|
|
});
|
|
}
|
|
}
|
|
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 (result === undefined || result === null || this.isDisposed || token.isCancellationRequested) {
|
|
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: [] });
|
|
});
|
|
};
|
|
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) => {
|
|
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
|
|
this.diagnostics.dispose();
|
|
}
|
|
}
|
|
class BackgroundScheduler {
|
|
constructor(diagnosticRequestor) {
|
|
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);
|
|
this.trigger();
|
|
}
|
|
remove(document) {
|
|
const key = DocumentOrUri.asKey(document);
|
|
this.documents.delete(key);
|
|
// No more documents. Stop background activity.
|
|
if (this.documents.size === 0) {
|
|
this.stop();
|
|
}
|
|
else if (key === this.endDocumentKey()) {
|
|
// Make sure we have a correct last document. It could have
|
|
this.endDocument = this.documents.last;
|
|
}
|
|
}
|
|
trigger() {
|
|
if (this.isDisposed === true) {
|
|
return;
|
|
}
|
|
// We have a round running. So simply make sure we run up to the
|
|
// last document
|
|
if (this.intervalHandle !== undefined) {
|
|
this.endDocument = this.documents.last;
|
|
return;
|
|
}
|
|
this.endDocument = this.documents.last;
|
|
this.intervalHandle = (0, vscode_languageserver_protocol_1.RAL)().timer.setInterval(() => {
|
|
const document = this.documents.first;
|
|
if (document !== undefined) {
|
|
const key = DocumentOrUri.asKey(document);
|
|
this.diagnosticRequestor.pull(document);
|
|
this.documents.set(key, document, vscode_languageserver_protocol_1.Touch.Last);
|
|
if (key === this.endDocumentKey()) {
|
|
this.stop();
|
|
}
|
|
}
|
|
}, 200);
|
|
}
|
|
dispose() {
|
|
this.isDisposed = true;
|
|
this.stop();
|
|
this.documents.clear();
|
|
}
|
|
stop() {
|
|
this.intervalHandle?.dispose();
|
|
this.intervalHandle = undefined;
|
|
this.endDocument = undefined;
|
|
}
|
|
endDocumentKey() {
|
|
return this.endDocument !== undefined ? DocumentOrUri.asKey(this.endDocument) : undefined;
|
|
}
|
|
}
|
|
class DiagnosticFeatureProviderImpl {
|
|
constructor(client, tabs, options) {
|
|
const diagnosticPullOptions = client.clientOptions.diagnosticPullOptions ?? { onChange: true, onSave: false };
|
|
const documentSelector = client.protocol2CodeConverter.asDocumentSelector(options.documentSelector);
|
|
const disposables = [];
|
|
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;
|
|
}
|
|
// 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) {
|
|
const matcher = new minimatch.Minimatch(filter.pattern, { noext: true });
|
|
if (!matcher.makeRe()) {
|
|
return false;
|
|
}
|
|
if (!matcher.match(resource.fsPath)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
const matches = (document) => {
|
|
return document instanceof vscode_1.Uri
|
|
? matchResource(document)
|
|
: vscode_1.languages.match(documentSelector, document) > 0 && tabs.isVisible(document);
|
|
};
|
|
const isActiveDocument = (document) => {
|
|
return document instanceof vscode_1.Uri
|
|
? this.activeTextDocument?.uri.toString() === document.toString()
|
|
: this.activeTextDocument === document;
|
|
};
|
|
this.diagnosticRequestor = new DiagnosticRequestor(client, tabs, options);
|
|
this.backgroundScheduler = new BackgroundScheduler(this.diagnosticRequestor);
|
|
const addToBackgroundIfNeeded = (document) => {
|
|
if (!matches(document) || !options.interFileDependencies || isActiveDocument(document)) {
|
|
return;
|
|
}
|
|
this.backgroundScheduler.add(document);
|
|
};
|
|
this.activeTextDocument = vscode_1.window.activeTextEditor?.document;
|
|
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);
|
|
}
|
|
});
|
|
// 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.knows(PullState.document, textDocument)) {
|
|
return;
|
|
}
|
|
if (matches(textDocument)) {
|
|
this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument); });
|
|
}
|
|
}));
|
|
disposables.push(tabs.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 an 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());
|
|
}
|
|
}
|
|
// Pull all tabs if not already pulled as text document
|
|
if (diagnosticPullOptions.onTabs === true) {
|
|
for (const resource of tabs.getTabResources()) {
|
|
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 ((diagnosticPullOptions.filter === undefined || !diagnosticPullOptions.filter(textDocument, DiagnosticPullMode.onType)) && this.diagnosticRequestor.knows(PullState.document, textDocument)) {
|
|
this.diagnosticRequestor.pull(textDocument, () => { 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 ((diagnosticPullOptions.filter === undefined || !diagnosticPullOptions.filter(textDocument, DiagnosticPullMode.onSave)) && this.diagnosticRequestor.knows(PullState.document, textDocument)) {
|
|
this.diagnosticRequestor.pull(event.textDocument, () => { this.backgroundScheduler.trigger(); });
|
|
}
|
|
}));
|
|
}
|
|
// When the document closes clear things up
|
|
const closeFeature = client.getFeature(vscode_languageserver_protocol_1.DidCloseTextDocumentNotification.method);
|
|
disposables.push(closeFeature.onNotificationSent((event) => {
|
|
this.cleanUpDocument(event.textDocument);
|
|
}));
|
|
// Same when a tabs closes.
|
|
tabs.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;
|
|
}
|
|
cleanUpDocument(document) {
|
|
if (this.diagnosticRequestor.knows(PullState.document, document)) {
|
|
this.diagnosticRequestor.forgetDocument(document);
|
|
this.backgroundScheduler.remove(document);
|
|
}
|
|
}
|
|
}
|
|
class DiagnosticFeature extends features_1.TextDocumentLanguageFeature {
|
|
constructor(client) {
|
|
super(client, vscode_languageserver_protocol_1.DocumentDiagnosticRequest.type);
|
|
}
|
|
fillClientCapabilities(capabilities) {
|
|
let capability = ensure(ensure(capabilities, 'textDocument'), 'diagnostic');
|
|
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;
|
|
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();
|
|
}
|
|
});
|
|
let [id, options] = this.getRegistration(documentSelector, capabilities.diagnosticProvider);
|
|
if (!id || !options) {
|
|
return;
|
|
}
|
|
this.register({ id: id, registerOptions: options });
|
|
}
|
|
clear() {
|
|
if (this.tabs !== undefined) {
|
|
this.tabs.dispose();
|
|
this.tabs = undefined;
|
|
}
|
|
super.clear();
|
|
}
|
|
registerLanguageProvider(options) {
|
|
if (this.tabs === undefined) {
|
|
this.tabs = new Tabs();
|
|
}
|
|
const provider = new DiagnosticFeatureProviderImpl(this._client, this.tabs, options);
|
|
return [provider.disposable, provider];
|
|
}
|
|
}
|
|
exports.DiagnosticFeature = DiagnosticFeature;
|