Skip to content

Commit f4d4e94

Browse files
authored
Addf support to delay open notifications. (#1505)
* WIP * Update test bed
1 parent 4bf6033 commit f4d4e94

File tree

9 files changed

+301
-172
lines changed

9 files changed

+301
-172
lines changed

client/src/common/client.ts

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

1718
import {
@@ -36,7 +37,8 @@ import {
3637
TypeHierarchyPrepareRequest, InlineValueRequest, InlayHintRequest, WorkspaceSymbolRequest, TextDocumentRegistrationOptions, FileOperationRegistrationOptions,
3738
ConnectionOptions, PositionEncodingKind, DocumentDiagnosticRequest, NotebookDocumentSyncRegistrationType, NotebookDocumentSyncRegistrationOptions, ErrorCodes,
3839
MessageStrategy, DidOpenTextDocumentParams, CodeLensResolveRequest, CompletionResolveRequest, CodeActionResolveRequest, InlayHintResolveRequest, DocumentLinkResolveRequest, WorkspaceSymbolResolveRequest,
39-
CancellationToken as ProtocolCancellationToken, InlineCompletionRequest, InlineCompletionRegistrationOptions, ExecuteCommandRequest, ExecuteCommandOptions, HandlerResult
40+
CancellationToken as ProtocolCancellationToken, InlineCompletionRequest, InlineCompletionRegistrationOptions, ExecuteCommandRequest, ExecuteCommandOptions, HandlerResult,
41+
type DidCloseTextDocumentParams
4042
} from 'vscode-languageserver-protocol';
4143

4244
import * as c2p from './codeConverter';
@@ -48,7 +50,8 @@ import * as UUID from './utils/uuid';
4850
import { ProgressPart } from './progressPart';
4951
import {
5052
DynamicFeature, ensure, FeatureClient, LSPCancellationError, TextDocumentSendFeature, RegistrationData, StaticFeature,
51-
TextDocumentProviderFeature, WorkspaceProviderFeature
53+
TextDocumentProviderFeature, WorkspaceProviderFeature,
54+
type TabsModel
5255
} from './features';
5356

5457
import { DiagnosticFeature, DiagnosticProviderMiddleware, DiagnosticProviderShape, $DiagnosticPullOptions, DiagnosticFeatureShape } from './diagnostic';
@@ -373,6 +376,16 @@ export type LanguageClientOptions = {
373376
supportHtml?: boolean;
374377
supportThemeIcons?: boolean;
375378
};
379+
textSynchronization?: {
380+
/**
381+
* Delays sending the open notification until one of the following
382+
* conditions becomes `true`:
383+
* - document is visible in the editor.
384+
* - any of the other notifications or requests is sent to the server, except
385+
* a closed notification for the pending document.
386+
*/
387+
delayOpenNotifications?: boolean;
388+
};
376389
} & $NotebookDocumentOptions & $DiagnosticPullOptions & $ConfigurationOptions;
377390

378391
// type TestOptions = {
@@ -406,6 +419,9 @@ type ResolvedClientOptions = {
406419
supportHtml: boolean;
407420
supportThemeIcons: boolean;
408421
};
422+
textSynchronization: {
423+
delayOpenNotifications: boolean;
424+
};
409425
} & Required<$NotebookDocumentOptions> & Required<$DiagnosticPullOptions>;
410426
namespace ResolvedClientOptions {
411427
export function sanitizeIsTrusted(isTrusted?: boolean | { readonly enabledCommands: readonly string[] }): boolean | { readonly enabledCommands: readonly string[] } {
@@ -477,6 +493,127 @@ export enum ShutdownMode {
477493
Stop = 'stop'
478494
}
479495

496+
/**
497+
* Manages the open tabs. We don't directly use the tab API since for
498+
* diagnostics we need to de-dupe tabs that show the same resources since
499+
* we pull on the model not the UI.
500+
*/
501+
class Tabs implements TabsModel {
502+
503+
private open: Set<string>;
504+
private readonly _onOpen: EventEmitter<Set<Uri>>;
505+
private readonly _onClose: EventEmitter<Set<Uri>>;
506+
private readonly disposable: Disposable;
507+
508+
constructor() {
509+
this.open = new Set();
510+
this._onOpen = new EventEmitter();
511+
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+
}
517+
const oldTabs = this.open;
518+
const currentTabs: Set<string> = new Set();
519+
Tabs.fillTabResources(currentTabs);
520+
521+
const closed: Set<string> = new Set();
522+
const opened: Set<string> = new Set(currentTabs);
523+
for (const tab of oldTabs.values()) {
524+
if (currentTabs.has(tab)) {
525+
opened.delete(tab);
526+
} else {
527+
closed.add(tab);
528+
}
529+
}
530+
this.open = currentTabs;
531+
if (closed.size > 0) {
532+
const toFire: Set<Uri> = new Set();
533+
for (const item of closed) {
534+
toFire.add(Uri.parse(item));
535+
}
536+
this._onClose.fire(toFire);
537+
}
538+
if (opened.size > 0) {
539+
const toFire: Set<Uri> = new Set();
540+
for (const item of opened) {
541+
toFire.add(Uri.parse(item));
542+
}
543+
this._onOpen.fire(toFire);
544+
}
545+
};
546+
547+
if (Window.tabGroups.onDidChangeTabs !== undefined) {
548+
this.disposable = Window.tabGroups.onDidChangeTabs(openTabsHandler);
549+
} else {
550+
this.disposable = { dispose: () => {} };
551+
}
552+
}
553+
554+
public get onClose(): Event<Set<Uri>> {
555+
return this._onClose.event;
556+
}
557+
558+
public get onOpen(): Event<Set<Uri>> {
559+
return this._onOpen.event;
560+
}
561+
562+
public dispose(): void {
563+
this.disposable.dispose();
564+
}
565+
566+
public isActive(document: TextDocument | Uri): boolean {
567+
return document instanceof Uri
568+
? Window.activeTextEditor?.document.uri === document
569+
: Window.activeTextEditor?.document === document;
570+
}
571+
572+
public isVisible(document: TextDocument | Uri): boolean {
573+
const uri = document instanceof Uri ? document : document.uri;
574+
if (uri.scheme === NotebookDocumentSyncFeature.CellScheme) {
575+
// Notebook cells aren't in the list of tabs, but the notebook should be.
576+
return Workspace.notebookDocuments.some(notebook => {
577+
if (this.open.has(notebook.uri.toString())) {
578+
const cell = notebook.getCells().find(cell => cell.document.uri.toString() === uri.toString());
579+
return cell !== undefined;
580+
}
581+
return false;
582+
});
583+
}
584+
return this.open.has(uri.toString());
585+
}
586+
587+
public getTabResources(): Set<Uri> {
588+
const result: Set<Uri> = new Set();
589+
Tabs.fillTabResources(new Set(), result);
590+
return result;
591+
}
592+
593+
private static fillTabResources(strings: Set<string> | undefined, uris?: Set<Uri>): void {
594+
const seen = strings ?? new Set();
595+
for (const group of Window.tabGroups.all) {
596+
for (const tab of group.tabs) {
597+
const input = tab.input;
598+
let uri: Uri | undefined;
599+
if (input instanceof TabInputText) {
600+
uri = input.uri;
601+
} else if (input instanceof TabInputTextDiff) {
602+
uri = input.modified;
603+
} else if (input instanceof TabInputCustom) {
604+
uri = input.uri;
605+
} else if (input instanceof TabInputNotebook) {
606+
uri = input.uri;
607+
}
608+
if (uri !== undefined && !seen.has(uri.toString())) {
609+
seen.add(uri.toString());
610+
uris !== undefined && uris.add(uri);
611+
}
612+
}
613+
}
614+
}
615+
}
616+
480617
export abstract class BaseLanguageClient implements FeatureClient<Middleware, LanguageClientOptions> {
481618

482619
private _id: string;
@@ -513,9 +650,10 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
513650
private _syncedDocuments: Map<string, TextDocument>;
514651

515652
private _didChangeTextDocumentFeature: DidChangeTextDocumentFeature | undefined;
516-
private readonly _pendingOpenNotifications: Set<string>;
653+
private readonly _inFlightOpenNotifications: Set<string>;
517654
private readonly _pendingChangeSemaphore: Semaphore<void>;
518655
private readonly _pendingChangeDelayer: Delayer<void>;
656+
private _didOpenTextDocumentFeature: DidOpenTextDocumentFeature | undefined;
519657

520658
private _fileEvents: FileEvent[];
521659
private _fileEventDelayer: Delayer<void>;
@@ -529,6 +667,7 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
529667

530668
private readonly _c2p: c2p.Converter;
531669
private readonly _p2c: p2c.Converter;
670+
private _tabsModel: TabsModel | undefined;
532671

533672
public constructor(id: string, name: string, clientOptions: LanguageClientOptions) {
534673
this._id = id;
@@ -571,7 +710,8 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
571710
// interval: clientOptions.suspend?.interval ? Math.max(clientOptions.suspend.interval, defaultInterval) : defaultInterval
572711
// },
573712
diagnosticPullOptions: clientOptions.diagnosticPullOptions ?? { onChange: true, onSave: false },
574-
notebookDocumentOptions: clientOptions.notebookDocumentOptions ?? { }
713+
notebookDocumentOptions: clientOptions.notebookDocumentOptions ?? { },
714+
textSynchronization: this.createTextSynchronizationOptions(clientOptions.textSynchronization)
575715
};
576716
this._clientOptions.synchronize = this._clientOptions.synchronize || {};
577717

@@ -602,7 +742,7 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
602742
this._traceOutputChannel = clientOptions.traceOutputChannel;
603743
this._diagnostics = undefined;
604744

605-
this._pendingOpenNotifications = new Set();
745+
this._inFlightOpenNotifications = new Set();
606746
this._pendingChangeSemaphore = new Semaphore(1);
607747
this._pendingChangeDelayer = new Delayer<void>(250);
608748

@@ -631,6 +771,16 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
631771
this.registerBuiltinFeatures();
632772
}
633773

774+
private createTextSynchronizationOptions(options: LanguageClientOptions['textSynchronization']): ResolvedClientOptions['textSynchronization'] {
775+
if (!options) {
776+
return { delayOpenNotifications: false };
777+
}
778+
if (typeof options.delayOpenNotifications === 'boolean') {
779+
return { delayOpenNotifications: options.delayOpenNotifications };
780+
}
781+
return { delayOpenNotifications: false };
782+
}
783+
634784
public get name(): string {
635785
return this._name;
636786
}
@@ -651,6 +801,13 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
651801
return this._c2p;
652802
}
653803

804+
public get tabsModel(): TabsModel {
805+
if (this._tabsModel === undefined) {
806+
this._tabsModel = new Tabs();
807+
}
808+
return this._tabsModel;
809+
}
810+
654811
public get onTelemetry(): Event<any> {
655812
return this._telemetryEmitter.event;
656813
}
@@ -724,6 +881,10 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
724881

725882
// Ensure we have a connection before we force the document sync.
726883
const connection = await this.$start();
884+
885+
// Send ony depending open notifications
886+
await this._didOpenTextDocumentFeature!.sendPendingOpenNotifications();
887+
727888
// If any document is synced in full mode make sure we flush any pending
728889
// full document syncs.
729890
if (this._didChangeTextDocumentFeature!.syncKind === TextDocumentSyncKind.Full) {
@@ -828,10 +989,18 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
828989
let openNotification: string | undefined;
829990
if (needsPendingFullTextDocumentSync && typeof type !== 'string' && type.method === DidOpenTextDocumentNotification.method) {
830991
openNotification = (params as DidOpenTextDocumentParams)?.textDocument.uri;
831-
this._pendingOpenNotifications.add(openNotification);
992+
this._inFlightOpenNotifications.add(openNotification);
993+
}
994+
let documentToClose: string | undefined;
995+
if (typeof type !== 'string' && type.method === DidCloseTextDocumentNotification.method) {
996+
documentToClose = (params as DidCloseTextDocumentParams).textDocument.uri;
832997
}
833998
// Ensure we have a connection before we force the document sync.
834999
const connection = await this.$start();
1000+
1001+
// Send ony depending open notifications
1002+
await this._didOpenTextDocumentFeature!.sendPendingOpenNotifications(documentToClose);
1003+
8351004
// If any document is synced in full mode make sure we flush any pending
8361005
// full document syncs.
8371006
if (needsPendingFullTextDocumentSync) {
@@ -843,11 +1012,11 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
8431012
// onto the wire will ignore pending document changes.
8441013
//
8451014
// Since the code path of connection.sendNotification is actually sync
846-
// until the message is handed of to the writer and the writer as a semaphore
1015+
// until the message is handed off to the writer and the writer has a semaphore
8471016
// lock with a capacity of 1 no additional async scheduling can happen until
848-
// the message is actually handed of.
1017+
// the message is actually handed off.
8491018
if (openNotification !== undefined) {
850-
this._pendingOpenNotifications.delete(openNotification);
1019+
this._inFlightOpenNotifications.delete(openNotification);
8511020
}
8521021

8531022
const _sendNotification = this._clientOptions.middleware?.sendNotification;
@@ -1479,7 +1648,7 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
14791648
private async sendPendingFullTextDocumentChanges(connection: Connection): Promise<void> {
14801649
return this._pendingChangeSemaphore.lock(async () => {
14811650
try {
1482-
const changes = this._didChangeTextDocumentFeature!.getPendingDocumentChanges(this._pendingOpenNotifications);
1651+
const changes = this._didChangeTextDocumentFeature!.getPendingDocumentChanges(this._inFlightOpenNotifications);
14831652
if (changes.length === 0) {
14841653
return;
14851654
}
@@ -1773,7 +1942,8 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
17731942
protected registerBuiltinFeatures() {
17741943
const pendingFullTextDocumentChanges: Map<string, TextDocument> = new Map();
17751944
this.registerFeature(new ConfigurationFeature(this));
1776-
this.registerFeature(new DidOpenTextDocumentFeature(this, this._syncedDocuments));
1945+
this._didOpenTextDocumentFeature = new DidOpenTextDocumentFeature(this, this._syncedDocuments);
1946+
this.registerFeature(this._didOpenTextDocumentFeature);
17771947
this._didChangeTextDocumentFeature = new DidChangeTextDocumentFeature(this, pendingFullTextDocumentChanges);
17781948
this._didChangeTextDocumentFeature.onPendingChangeAdded(() => {
17791949
this.triggerPendingChangeDelivery();

0 commit comments

Comments
 (0)