Skip to content

Commit 0e4888e

Browse files
authored
Make pull diagnostics work for peek editors (#1703)
1 parent 1baf163 commit 0e4888e

File tree

6 files changed

+71
-53
lines changed

6 files changed

+71
-53
lines changed

client/src/common/client.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
OnTypeFormattingEditProvider, RenameProvider, DocumentSymbolProvider, DocumentLinkProvider, DeclarationProvider, ImplementationProvider,
1212
DocumentColorProvider, SelectionRangeProvider, TypeDefinitionProvider, CallHierarchyProvider, LinkedEditingRangeProvider, TypeHierarchyProvider, WorkspaceSymbolProvider,
1313
ProviderResult, TextEdit as VTextEdit, InlineCompletionItemProvider, EventEmitter, type TabChangeEvent, TabInputText, TabInputTextDiff, TabInputCustom,
14-
TabInputNotebook, type LogOutputChannel} from 'vscode';
14+
TabInputNotebook, type LogOutputChannel, type TextEditor
15+
} from 'vscode';
1516

1617
import {
1718
RAL, Message, MessageSignature, Logger, ResponseError, RequestType0, RequestType, NotificationType0, NotificationType,
@@ -48,8 +49,7 @@ import * as UUID from './utils/uuid';
4849
import { ProgressPart } from './progressPart';
4950
import {
5051
DynamicFeature, ensure, FeatureClient, LSPCancellationError, TextDocumentSendFeature, RegistrationData, StaticFeature,
51-
TextDocumentProviderFeature, WorkspaceProviderFeature,
52-
type TabsModel
52+
TextDocumentProviderFeature, WorkspaceProviderFeature, type VisibleDocuments
5353
} from './features';
5454

5555
import { DiagnosticFeature, DiagnosticProviderMiddleware, DiagnosticProviderShape, $DiagnosticPullOptions, DiagnosticFeatureShape } from './diagnostic';
@@ -498,25 +498,23 @@ export enum ShutdownMode {
498498
* diagnostics we need to de-dupe tabs that show the same resources since
499499
* we pull on the model not the UI.
500500
*/
501-
class Tabs implements TabsModel {
501+
class VisibleDocumentsImpl implements VisibleDocuments {
502502

503503
private open: Set<string>;
504504
private readonly _onOpen: EventEmitter<Set<Uri>>;
505505
private readonly _onClose: EventEmitter<Set<Uri>>;
506-
private readonly disposable: Disposable;
506+
private readonly disposables: Disposable[];
507507

508508
constructor() {
509+
this.disposables = [];
509510
this.open = new Set();
510511
this._onOpen = new EventEmitter();
511512
this._onClose = new EventEmitter();
512-
Tabs.fillTabResources(this.open);
513-
const openTabsHandler = (event: TabChangeEvent) => {
514-
if (event.closed.length === 0 && event.opened.length === 0) {
515-
return;
516-
}
513+
VisibleDocumentsImpl.fillVisibleResources(this.open);
514+
const updateVisibleDocuments = () => {
517515
const oldTabs = this.open;
518516
const currentTabs: Set<string> = new Set();
519-
Tabs.fillTabResources(currentTabs);
517+
VisibleDocumentsImpl.fillVisibleResources(currentTabs);
520518

521519
const closed: Set<string> = new Set();
522520
const opened: Set<string> = new Set(currentTabs);
@@ -544,11 +542,16 @@ class Tabs implements TabsModel {
544542
}
545543
};
546544

547-
if (Window.tabGroups.onDidChangeTabs !== undefined) {
548-
this.disposable = Window.tabGroups.onDidChangeTabs(openTabsHandler);
549-
} else {
550-
this.disposable = { dispose: () => {} };
551-
}
545+
this.disposables.push(Window.tabGroups.onDidChangeTabs((event: TabChangeEvent) => {
546+
if (event.closed.length === 0 && event.opened.length === 0) {
547+
return;
548+
}
549+
updateVisibleDocuments();
550+
}));
551+
552+
this.disposables.push(Window.onDidChangeVisibleTextEditors((_editors: readonly TextEditor[]) => {
553+
updateVisibleDocuments();
554+
}));
552555
}
553556

554557
public get onClose(): Event<Set<Uri>> {
@@ -560,7 +563,7 @@ class Tabs implements TabsModel {
560563
}
561564

562565
public dispose(): void {
563-
this.disposable.dispose();
566+
this.disposables.forEach(disposable => disposable.dispose());
564567
}
565568

566569
public isActive(document: TextDocument | Uri): boolean {
@@ -584,13 +587,13 @@ class Tabs implements TabsModel {
584587
return this.open.has(uri.toString());
585588
}
586589

587-
public getTabResources(): Set<Uri> {
590+
public getResources(): Set<Uri> {
588591
const result: Set<Uri> = new Set();
589-
Tabs.fillTabResources(new Set(), result);
592+
VisibleDocumentsImpl.fillVisibleResources(new Set(), result);
590593
return result;
591594
}
592595

593-
private static fillTabResources(strings: Set<string> | undefined, uris?: Set<Uri>): void {
596+
private static fillVisibleResources(strings: Set<string> | undefined, uris?: Set<Uri>): void {
594597
const seen = strings ?? new Set();
595598
for (const group of Window.tabGroups.all) {
596599
for (const tab of group.tabs) {
@@ -611,6 +614,14 @@ class Tabs implements TabsModel {
611614
}
612615
}
613616
}
617+
// Peek editors are not part of tabs but should be considered visible
618+
for (const editor of Window.visibleTextEditors) {
619+
const uri = editor.document.uri;
620+
if (!seen.has(uri.toString())) {
621+
seen.add(uri.toString());
622+
uris !== undefined && uris.add(uri);
623+
}
624+
}
614625
}
615626
}
616627

@@ -668,7 +679,7 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
668679

669680
private readonly _c2p: c2p.Converter;
670681
private readonly _p2c: p2c.Converter;
671-
private _tabsModel: TabsModel | undefined;
682+
private _visibleDocuments: VisibleDocuments | undefined;
672683

673684
public constructor(id: string, name: string, clientOptions: LanguageClientOptions) {
674685
this._id = id;
@@ -807,11 +818,11 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
807818
return this._c2p;
808819
}
809820

810-
public get tabsModel(): TabsModel {
811-
if (this._tabsModel === undefined) {
812-
this._tabsModel = new Tabs();
821+
public get visibleDocuments(): VisibleDocuments {
822+
if (this._visibleDocuments === undefined) {
823+
this._visibleDocuments = new VisibleDocumentsImpl();
813824
}
814-
return this._tabsModel;
825+
return this._visibleDocuments;
815826
}
816827

817828
public get onTelemetry(): Event<any> {

client/src/common/diagnostic.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { generateUuid } from './utils/uuid';
2020
import { matchGlobPattern } from './utils/globPattern';
2121

2222
import {
23-
TextDocumentLanguageFeature, FeatureClient, LSPCancellationError, type TabsModel
23+
TextDocumentLanguageFeature, FeatureClient, LSPCancellationError, type VisibleDocuments
2424
} from './features';
2525

2626
function ensure<T, K extends keyof T>(target: T, key: K): T[K] {
@@ -285,7 +285,7 @@ class DiagnosticRequestor implements Disposable {
285285

286286
private isDisposed: boolean;
287287
private readonly client: FeatureClient<DiagnosticProviderMiddleware, $DiagnosticPullOptions>;
288-
private readonly tabs: TabsModel;
288+
private readonly visibleDocuments: VisibleDocuments;
289289
private readonly options: DiagnosticRegistrationOptions;
290290

291291
public readonly onDidChangeDiagnosticsEmitter: EventEmitter<void>;
@@ -298,9 +298,9 @@ class DiagnosticRequestor implements Disposable {
298298
private workspaceCancellation: CancellationTokenSource | undefined;
299299
private workspaceTimeout: Disposable | undefined;
300300

301-
public constructor(client: FeatureClient<DiagnosticProviderMiddleware, $DiagnosticPullOptions>, tabs: TabsModel, options: DiagnosticRegistrationOptions) {
301+
public constructor(client: FeatureClient<DiagnosticProviderMiddleware, $DiagnosticPullOptions>, visibleDocuments: VisibleDocuments, options: DiagnosticRegistrationOptions) {
302302
this.client = client;
303-
this.tabs = tabs;
303+
this.visibleDocuments = visibleDocuments;
304304
this.options = options;
305305

306306
this.isDisposed = false;
@@ -373,7 +373,7 @@ class DiagnosticRequestor implements Disposable {
373373
return;
374374
}
375375
this.openRequests.delete(key);
376-
if (!this.tabs.isVisible(document)) {
376+
if (!this.visibleDocuments.isVisible(document)) {
377377
this.documentStates.unTrack(PullState.document, document);
378378
return;
379379
}
@@ -767,7 +767,7 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
767767
private activeTextDocument: TextDocument | undefined;
768768
private readonly backgroundScheduler: BackgroundScheduler;
769769

770-
constructor(client: FeatureClient<DiagnosticProviderMiddleware, $DiagnosticPullOptions>, tabs: TabsModel, options: DiagnosticRegistrationOptions) {
770+
constructor(client: FeatureClient<DiagnosticProviderMiddleware, $DiagnosticPullOptions>, visibleDocuments: VisibleDocuments, options: DiagnosticRegistrationOptions) {
771771
const diagnosticPullOptions = Object.assign({ onChange: false, onSave: false, onFocus: false }, client.clientOptions.diagnosticPullOptions);
772772
const documentSelector = client.protocol2CodeConverter.asDocumentSelector(options.documentSelector!);
773773
const disposables: Disposable[] = [];
@@ -810,12 +810,12 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
810810
const matches = (document: TextDocument | Uri): boolean => {
811811
return document instanceof Uri
812812
? matchResource(document)
813-
: Languages.match(documentSelector, document) > 0 && tabs.isVisible(document);
813+
: Languages.match(documentSelector, document) > 0 && visibleDocuments.isVisible(document);
814814
};
815815

816816
const matchesCell = (cell: NotebookCell): boolean => {
817817
// Cells match if the language is allowed and if the containing notebook is visible.
818-
return Languages.match(documentSelector, cell.document) > 0 && tabs.isVisible(cell.notebook.uri);
818+
return Languages.match(documentSelector, cell.document) > 0 && visibleDocuments.isVisible(cell.notebook.uri);
819819
};
820820

821821
const isActiveDocument = (document: TextDocument | Uri): boolean => {
@@ -824,7 +824,7 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
824824
: this.activeTextDocument === document;
825825
};
826826

827-
this.diagnosticRequestor = new DiagnosticRequestor(client, tabs, options);
827+
this.diagnosticRequestor = new DiagnosticRequestor(client, visibleDocuments, options);
828828
this.backgroundScheduler = new BackgroundScheduler(client, this.diagnosticRequestor);
829829

830830
const addToBackgroundIfNeeded = (document: TextDocument | Uri): void => {
@@ -882,7 +882,7 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
882882
}
883883
}));
884884

885-
disposables.push(tabs.onOpen((opened) => {
885+
disposables.push(visibleDocuments.onOpen((opened) => {
886886
for (const resource of opened) {
887887
// We already know about this document. This can happen via a document open.
888888
if (this.diagnosticRequestor.knows(PullState.document, resource)) {
@@ -902,7 +902,7 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
902902
// workspace.textDocuments and Window.activeTextEditor are updated.
903903
//
904904
// This means: for newly created tab/editors we don't find the underlying
905-
// document yet. So we do nothing an rely on the underlying open document event
905+
// document yet. So we do nothing and rely on the underlying open document event
906906
// to be fired.
907907
if (textDocument !== undefined && matches(textDocument)) {
908908
this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument!); });
@@ -928,9 +928,9 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
928928
}
929929
}
930930

931-
// Pull all tabs if not already pulled as text document
931+
// Pull all visible documents if not already pulled as text document
932932
if (diagnosticPullOptions.onTabs === true) {
933-
for (const resource of tabs.getTabResources()) {
933+
for (const resource of visibleDocuments.getResources()) {
934934
if (!pulledTextDocuments.has(resource.toString()) && matches(resource)) {
935935
this.diagnosticRequestor.pull(resource, () => { addToBackgroundIfNeeded(resource); });
936936
}
@@ -1004,8 +1004,8 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
10041004
}
10051005
}));
10061006

1007-
// Same when a tabs closes.
1008-
tabs.onClose((closed) => {
1007+
// Same when a visible document closes.
1008+
visibleDocuments.onClose((closed) => {
10091009
for (const document of closed) {
10101010
this.cleanUpDocument(document);
10111011
}
@@ -1099,7 +1099,7 @@ export class DiagnosticFeature extends TextDocumentLanguageFeature<DiagnosticOpt
10991099
}
11001100

11011101
protected registerLanguageProvider(options: DiagnosticRegistrationOptions): [Disposable, DiagnosticProviderShape] {
1102-
const provider = new DiagnosticFeatureProviderImpl(this._client, this._client.tabsModel, options);
1102+
const provider = new DiagnosticFeatureProviderImpl(this._client, this._client.visibleDocuments, options);
11031103
return [provider.disposable, provider];
11041104
}
11051105
}

client/src/common/features.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -643,12 +643,12 @@ export interface DedicatedTextSynchronizationFeature {
643643
handles(textDocument: TextDocument): boolean;
644644
}
645645

646-
export interface TabsModel {
646+
export interface VisibleDocuments {
647647
onClose: Event<Set<Uri>>;
648648
onOpen: Event<Set<Uri>>;
649649
isActive(document: TextDocument | Uri): boolean;
650650
isVisible(document: TextDocument | Uri): boolean;
651-
getTabResources(): Set<Uri>;
651+
getResources(): Set<Uri>;
652652
}
653653

654654
// Features can refer to other feature when implementing themselves.
@@ -671,7 +671,7 @@ export interface FeatureClient<M, CO = object> {
671671
clientOptions: CO;
672672
middleware: M;
673673

674-
tabsModel: TabsModel;
674+
visibleDocuments: VisibleDocuments;
675675

676676
start(): Promise<void>;
677677
isRunning(): boolean;

client/src/common/textSynchronization.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export class DidOpenTextDocumentFeature extends TextDocumentEventFeature<DidOpen
7373
if (!this.matches(document)) {
7474
return;
7575
}
76-
const tabsModel = this._client.tabsModel;
77-
if (tabsModel.isVisible(document)) {
76+
const visibleDocuments = this._client.visibleDocuments;
77+
if (visibleDocuments.isVisible(document)) {
7878
return super.callback(document);
7979
} else {
8080
this._pendingOpenNotifications.set(document.uri.toString(), document);
@@ -113,8 +113,8 @@ export class DidOpenTextDocumentFeature extends TextDocumentEventFeature<DidOpen
113113
return;
114114
}
115115
if (Languages.match(documentSelector, textDocument) > 0 && !this._client.hasDedicatedTextSynchronizationFeature(textDocument)) {
116-
const tabsModel = this._client.tabsModel;
117-
if (tabsModel.isVisible(textDocument)) {
116+
const visibleDocuments = this._client.visibleDocuments;
117+
if (visibleDocuments.isVisible(textDocument)) {
118118
const middleware = this._client.middleware;
119119
const didOpen = (textDocument: TextDocument): Promise<void> => {
120120
return this._client.sendNotification(this._type, this._createParams(textDocument));
@@ -130,13 +130,13 @@ export class DidOpenTextDocumentFeature extends TextDocumentEventFeature<DidOpen
130130
});
131131
if (this._delayOpen && this._pendingOpenListeners === undefined) {
132132
this._pendingOpenListeners = [];
133-
const tabsModel = this._client.tabsModel;
134-
this._pendingOpenListeners.push(tabsModel.onClose((closed) => {
133+
const visibleDocuments = this._client.visibleDocuments;
134+
this._pendingOpenListeners.push(visibleDocuments.onClose((closed) => {
135135
for (const uri of closed) {
136136
this._pendingOpenNotifications.delete(uri.toString());
137137
}
138138
}));
139-
this._pendingOpenListeners.push(tabsModel.onOpen((opened) => {
139+
this._pendingOpenListeners.push(visibleDocuments.onOpen((opened) => {
140140
for (const uri of opened) {
141141
const document = this._pendingOpenNotifications.get(uri.toString());
142142
if (document !== undefined) {

testbed/server/src/server.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,10 @@ connection.onSignatureHelp((item): SignatureHelp => {
438438
});
439439

440440
connection.onDefinition((params): DefinitionLink[] => {
441+
const uri = URI.parse(params.textDocument.uri);
442+
const target = path.join(path.dirname(uri.fsPath), 'peek.bat');
441443
return [{
442-
targetUri: params.textDocument.uri,
444+
targetUri: URI.file(target).toString(),
443445
targetRange: { start: { line: 0, character: 2}, end: {line: 5, character: 45 } },
444446
targetSelectionRange: { start: { line: 1, character: 5}, end: {line: 1, character: 10 } },
445447
originSelectionRange: {
@@ -450,8 +452,10 @@ connection.onDefinition((params): DefinitionLink[] => {
450452
});
451453

452454
connection.onDeclaration((params): DeclarationLink[] => {
455+
const uri = URI.parse(params.textDocument.uri);
456+
const target = path.join(path.dirname(uri.fsPath), 'peek.bat');
453457
return [{
454-
targetUri: params.textDocument.uri,
458+
targetUri: URI.file(target).toString(),
455459
targetRange: { start: { line: 3, character: 0}, end: {line: 3, character: 10 } },
456460
targetSelectionRange: { start: { line: 3, character: 0}, end: {line: 3, character: 10 } },
457461
originSelectionRange: {

testbed/workspace/peek.bat

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FOR %%f IN (*.jpg *.png *.bmp) DO XCOPY C:\source\"%%f" c:\images /m /y
2+
REM This moves any files with a .jpg, .png,
3+
REM or .bmp extension from c:\source to c:\images;;

0 commit comments

Comments
 (0)