@@ -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
1718import {
@@ -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
4244import * as c2p from './codeConverter' ;
@@ -48,7 +50,8 @@ import * as UUID from './utils/uuid';
4850import { ProgressPart } from './progressPart' ;
4951import {
5052 DynamicFeature , ensure , FeatureClient , LSPCancellationError , TextDocumentSendFeature , RegistrationData , StaticFeature ,
51- TextDocumentProviderFeature , WorkspaceProviderFeature
53+ TextDocumentProviderFeature , WorkspaceProviderFeature ,
54+ type TabsModel
5255} from './features' ;
5356
5457import { 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 > ;
410426namespace 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+
480617export 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