Skip to content

Commit 8c4ccaf

Browse files
authored
Fixes #1013: Document diagnostics report not used when text document closed (#1020)
1 parent ae18fa7 commit 8c4ccaf

2 files changed

Lines changed: 128 additions & 43 deletions

File tree

client/src/common/diagnostic.ts

Lines changed: 118 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import * as minimatch from 'minimatch';
77

88
import {
99
Disposable, languages as Languages, window as Window, workspace as Workspace, CancellationToken, ProviderResult, Diagnostic as VDiagnostic,
10-
CancellationTokenSource, TextDocument, CancellationError, Event as VEvent, EventEmitter, DiagnosticCollection, Uri, TabInputText, TabInputTextDiff
10+
CancellationTokenSource, TextDocument, CancellationError, Event as VEvent, EventEmitter, DiagnosticCollection, Uri, TabInputText, TabInputTextDiff,
11+
TabChangeEvent, Event
1112
} from 'vscode';
1213

1314
import {
@@ -174,27 +175,57 @@ type RequestState = {
174175
document: TextDocument | Uri;
175176
};
176177

178+
179+
/**
180+
* Manages the open tabs. We don't directly use the tab API since for
181+
* diagnostics we need to de-dupe tabs that show the same resources since
182+
* we pull on the model not the UI.
183+
*/
177184
class Tabs {
178185

179-
private readonly open: Set<string>;
186+
private open: Set<string>;
187+
private readonly _onOpen: EventEmitter<Set<Uri>>;
188+
private readonly _onClose: EventEmitter<Set<Uri>>;
180189
private readonly disposable: Disposable;
181190

182191
constructor() {
183192
this.open = new Set();
184-
const openTabsHandler = () => {
185-
this.open.clear();
186-
for (const group of Window.tabGroups.all) {
187-
for (const tab of group.tabs) {
188-
const input = tab.input;
189-
if (input instanceof TabInputText) {
190-
this.open.add(input.uri.toString());
191-
} else if (input instanceof TabInputTextDiff) {
192-
this.open.add(input.modified.toString());
193-
}
193+
this._onOpen = new EventEmitter();
194+
this._onClose = new EventEmitter();
195+
Tabs.fillTabResources(this.open);
196+
const openTabsHandler = (event: TabChangeEvent) => {
197+
if (event.closed.length === 0 && event.opened.length === 0) {
198+
return;
199+
}
200+
const oldTabs = this.open;
201+
const currentTabs: Set<string> = new Set();
202+
Tabs.fillTabResources(currentTabs);
203+
204+
const closed: Set<string> = new Set();
205+
const opened: Set<string> = new Set(currentTabs);
206+
for (const tab of oldTabs.values()) {
207+
if (currentTabs.has(tab)) {
208+
opened.delete(tab);
209+
} else {
210+
closed.add(tab);
211+
}
212+
}
213+
this.open = currentTabs;
214+
if (closed.size > 0) {
215+
const toFire: Set<Uri> = new Set();
216+
for (const item of closed) {
217+
toFire.add(Uri.parse(item));
218+
}
219+
this._onClose.fire(toFire);
220+
}
221+
if (opened.size > 0) {
222+
const toFire: Set<Uri> = new Set();
223+
for (const item of opened) {
224+
toFire.add(Uri.parse(item));
194225
}
226+
this._onOpen.fire(toFire);
195227
}
196228
};
197-
openTabsHandler();
198229

199230
if (Window.tabGroups.onDidChangeTabs !== undefined) {
200231
this.disposable = Window.tabGroups.onDidChangeTabs(openTabsHandler);
@@ -203,6 +234,14 @@ class Tabs {
203234
}
204235
}
205236

237+
public get onClose(): Event<Set<Uri>> {
238+
return this._onClose.event;
239+
}
240+
241+
public get onOpen(): Event<Set<Uri>> {
242+
return this._onOpen.event;
243+
}
244+
206245
public dispose(): void {
207246
this.disposable.dispose();
208247
}
@@ -218,19 +257,29 @@ class Tabs {
218257
return this.open.has(uri.toString());
219258
}
220259

221-
public getTabResources(): Uri[] {
222-
const result: Uri[] = [];
260+
public getTabResources(): Set<Uri> {
261+
const result: Set<Uri> = new Set();
262+
Tabs.fillTabResources(new Set(), result);
263+
return result;
264+
}
265+
266+
private static fillTabResources(strings: Set<string> | undefined, uris?: Set<Uri>): void {
267+
const seen = strings ?? new Set();
223268
for (const group of Window.tabGroups.all) {
224269
for (const tab of group.tabs) {
225270
const input = tab.input;
271+
let uri: Uri | undefined;
226272
if (input instanceof TabInputText) {
227-
result.push(input.uri);
273+
uri = input.uri;
228274
} else if (input instanceof TabInputTextDiff) {
229-
result.push(input.modified);
275+
uri = input.modified;
276+
}
277+
if (uri !== undefined && !seen.has(uri.toString())) {
278+
seen.add(uri.toString());
279+
uris !== undefined && uris.add(uri);
230280
}
231281
}
232282
}
233-
return result;
234283
}
235284
}
236285

@@ -294,16 +343,12 @@ class DocumentPullStateTracker {
294343
states.delete(key);
295344
}
296345

297-
public tracks(kind: PullState, textDocument: TextDocument): boolean;
298-
public tracks(kind: PullState, uri: Uri): boolean;
299346
public tracks(kind: PullState, document: TextDocument | Uri): boolean {
300347
const key = document instanceof Uri ? document.toString() : document.uri.toString();
301348
const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates;
302349
return states.has(key);
303350
}
304351

305-
public getResultId(kind: PullState, document: TextDocument): string | undefined;
306-
public getResultId(kind: PullState, document: Uri): string | undefined;
307352
public getResultId(kind: PullState, document: TextDocument | Uri): string | undefined {
308353
const key = document instanceof Uri ? document.toString() : document.uri.toString();
309354
const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates;
@@ -356,8 +401,12 @@ class DiagnosticRequestor implements Disposable {
356401
this.workspaceErrorCounter = 0;
357402
}
358403

359-
public knows(kind: PullState, textDocument: TextDocument): boolean {
360-
return this.documentStates.tracks(kind, textDocument);
404+
public knows(kind: PullState, document: TextDocument | Uri): boolean {
405+
return this.documentStates.tracks(kind, document);
406+
}
407+
408+
public forget(kind: PullState, document: TextDocument | Uri): void {
409+
this.documentStates.unTrack(kind, document);
361410
}
362411

363412
public pull(document: TextDocument | Uri, cb?: () => void): void {
@@ -440,24 +489,32 @@ class DiagnosticRequestor implements Disposable {
440489
}
441490
}
442491

443-
public cleanupPull(document: TextDocument | Uri): void {
492+
public forgetDocument(document: TextDocument | Uri): void {
444493
const uri = document instanceof Uri ? document : document.uri;
445494
const key = uri.toString();
446495
const request = this.openRequests.get(key);
447-
if (this.options.workspaceDiagnostics || this.options.interFileDependencies) {
496+
if (this.options.workspaceDiagnostics) {
497+
// If we run workspace diagnostic pull a last time for the diagnostics
498+
// and the rely on getting them from the workspace result.
448499
if (request !== undefined) {
449500
this.openRequests.set(key, { state: RequestStateKind.reschedule, document: document });
450501
} else {
451-
this.pull(document);
502+
this.pull(document, () => {
503+
this.forget(PullState.document, document);
504+
});
452505
}
453506
} else {
507+
// We have normal pull or inter file dependencies. In this case we
508+
// clear the diagnostics (to have the same start as after startup).
509+
// We also cancel outstanding requests.
454510
if (request !== undefined) {
455511
if (request.state === RequestStateKind.active) {
456512
request.tokenSource.cancel();
457513
}
458514
this.openRequests.set(key, { state: RequestStateKind.outDated, document: document });
459515
}
460516
this.diagnostics.delete(uri);
517+
this.forget(PullState.document, document);
461518
}
462519
}
463520

@@ -814,6 +871,13 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
814871
}
815872
});
816873

874+
// For pull model diagnostics we pull for documents visible in the UI.
875+
// From an eventing point of view we still rely on open document events
876+
// and filter the documents that are not visible in the UI instead of
877+
// listening to Tab events. Major reason is event timing since we need
878+
// to ensure that the pull is send after the document open has reached
879+
// the server.
880+
817881
// We always pull on open.
818882
const openFeature = client.getFeature(DidOpenTextDocumentNotification.method);
819883
disposables.push(openFeature.onNotificationSent((event) => {
@@ -824,23 +888,31 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
824888
}));
825889

826890
// Pull all diagnostics for documents that are already open
827-
const pullTextDocuments: Set<string> = new Set();
891+
const pulledTextDocuments: Set<string> = new Set();
828892
for (const textDocument of Workspace.textDocuments) {
829893
if (matches(textDocument)) {
830894
this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument); });
831-
pullTextDocuments.add(textDocument.uri.toString());
895+
pulledTextDocuments.add(textDocument.uri.toString());
832896
}
833897
}
834898

835-
// Pull all tabs if not already pull as text document
899+
// Pull all tabs if not already pulled as text document
836900
if (diagnosticPullOptions.onTabs === true) {
837901
for (const resource of tabs.getTabResources()) {
838-
if (!pullTextDocuments.has(resource.toString()) && matches(resource)) {
902+
if (!pulledTextDocuments.has(resource.toString()) && matches(resource)) {
839903
this.diagnosticRequestor.pull(resource, () => { addToBackgroundIfNeeded(resource); });
840904
}
841905
}
842906
}
843907

908+
tabs.onOpen((opened) => {
909+
for (const document of opened) {
910+
if (matches(document) && !this.diagnosticRequestor.knows(PullState.document, document)) {
911+
this.diagnosticRequestor.pull(document, () => { addToBackgroundIfNeeded(document); });
912+
}
913+
}
914+
});
915+
844916
if (diagnosticPullOptions.onChange === true) {
845917
const changeFeature = client.getFeature(DidChangeTextDocumentNotification.method);
846918
disposables.push(changeFeature.onNotificationSent(async (event) => {
@@ -864,11 +936,16 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
864936
// When the document closes clear things up
865937
const closeFeature = client.getFeature(DidCloseTextDocumentNotification.method);
866938
disposables.push(closeFeature.onNotificationSent((event) => {
867-
const textDocument = event.original;
868-
this.diagnosticRequestor.cleanupPull(textDocument);
869-
this.backgroundScheduler.remove(textDocument);
939+
this.cleanUpDocument(event.original);
870940
}));
871941

942+
// Same when a tabs closes.
943+
tabs.onClose((closed) => {
944+
for (const document of closed) {
945+
this.cleanUpDocument(document);
946+
}
947+
});
948+
872949
// We received a did change from the server.
873950
this.diagnosticRequestor.onDidChangeDiagnosticsEmitter.event(() => {
874951
for (const textDocument of Workspace.textDocuments) {
@@ -893,6 +970,13 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
893970
public get diagnostics(): vsdiag.DiagnosticProvider {
894971
return this.diagnosticRequestor.provider;
895972
}
973+
974+
private cleanUpDocument(document: TextDocument | Uri): void {
975+
if (this.diagnosticRequestor.knows(PullState.document, document)) {
976+
this.diagnosticRequestor.forgetDocument(document);
977+
this.backgroundScheduler.remove(document);
978+
}
979+
}
896980
}
897981

898982
export class DiagnosticFeature extends TextDocumentLanguageFeature<DiagnosticOptions, DiagnosticRegistrationOptions, DiagnosticProviderShape, DiagnosticProviderMiddleware, $DiagnosticPullOptions> {

testbed/server/src/server.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -259,15 +259,16 @@ function validate(document: TextDocument): Diagnostic[] {
259259
// clearInterval(interval);
260260
// });
261261
// });
262-
connection.console.log('Validating document ' + document.uri);
263-
return [ {
264-
range: Range.create(0, 0, 0, 10),
265-
message: 'An error message',
266-
tags: [
267-
DiagnosticTag.Unnecessary
268-
],
269-
data: '11316630-392c-4227-a2c7-3b26cd68f241'
270-
}];
262+
// connection.console.log('Validating document ' + document.uri);
263+
// return [ {
264+
// range: Range.create(0, 0, 0, 10),
265+
// message: 'An error message',
266+
// tags: [
267+
// DiagnosticTag.Unnecessary
268+
// ],
269+
// data: '11316630-392c-4227-a2c7-3b26cd68f241'
270+
// }];
271+
return [];
271272
}
272273

273274
connection.onHover((textPosition): Hover => {

0 commit comments

Comments
 (0)