@@ -38,16 +38,29 @@ interface ServerState {
3838 documentCount : number ;
3939}
4040
41+ type OpenDocumentSkippedReason = 'server-not-running' ;
42+
43+ export interface OpenDocumentResult {
44+ language : string ;
45+ opened : boolean ;
46+ skippedReason ?: OpenDocumentSkippedReason ;
47+ serverStatus ?: ServerState [ 'status' ] ;
48+ }
49+
50+ interface CachedServerStatus {
51+ status : ServerState [ 'status' ] ;
52+ updatedAt : number ;
53+ }
54+
4155export class WorkspaceLspManager {
4256 private static instances = new Map < string , WorkspaceLspManager > ( ) ;
4357
4458 private workspacePath : string ;
4559 private eventUnlisten ?: UnlistenFn ;
4660 private isInitialized = false ;
47-
48-
49- private startingLanguages = new Set < string > ( ) ;
50- private languageReadyPromises = new Map < string , Promise < void > > ( ) ;
61+ private serverStatusByLanguage = new Map < string , CachedServerStatus > ( ) ;
62+ private skippedOpenNoticeLanguages = new Set < string > ( ) ;
63+ private readonly SERVER_STATUS_CACHE_TTL_MS = 30000 ;
5164
5265
5366 private diagnosticsCallbacks = new Map < string , Array < ( diagnostics : any [ ] ) => void > > ( ) ;
@@ -155,7 +168,81 @@ export class WorkspaceLspManager {
155168 return uri . toLowerCase ( ) ;
156169 }
157170
158-
171+
172+ private normalizeLanguage ( language : string ) : string {
173+ return language . trim ( ) . toLowerCase ( ) ;
174+ }
175+
176+
177+ private isServerStatus ( status ?: string ) : status is ServerState [ 'status' ] {
178+ return status === 'stopped'
179+ || status === 'starting'
180+ || status === 'running'
181+ || status === 'failed'
182+ || status === 'restarting' ;
183+ }
184+
185+
186+ private rememberServerStatus ( language : string , status : ServerState [ 'status' ] ) : void {
187+ const key = this . normalizeLanguage ( language ) ;
188+ this . serverStatusByLanguage . set ( key , {
189+ status,
190+ updatedAt : Date . now ( )
191+ } ) ;
192+
193+ if ( status === 'running' ) {
194+ this . skippedOpenNoticeLanguages . delete ( key ) ;
195+ }
196+ }
197+
198+
199+ private getFreshCachedServerStatus ( language : string ) : ServerState [ 'status' ] | undefined {
200+ const cached = this . serverStatusByLanguage . get ( this . normalizeLanguage ( language ) ) ;
201+ if ( ! cached ) {
202+ return undefined ;
203+ }
204+
205+ if ( Date . now ( ) - cached . updatedAt > this . SERVER_STATUS_CACHE_TTL_MS ) {
206+ return undefined ;
207+ }
208+
209+ return cached . status ;
210+ }
211+
212+
213+ private async getDocumentOpenAvailability ( language : string ) : Promise < {
214+ canOpen : boolean ;
215+ status ?: ServerState [ 'status' ] ;
216+ skippedReason ?: OpenDocumentSkippedReason ;
217+ } > {
218+ const cachedStatus = this . getFreshCachedServerStatus ( language ) ;
219+ if ( cachedStatus ) {
220+ return cachedStatus === 'running'
221+ ? { canOpen : true , status : cachedStatus }
222+ : {
223+ canOpen : false ,
224+ status : cachedStatus ,
225+ skippedReason : 'server-not-running'
226+ } ;
227+ }
228+
229+ const state = await this . getServerState ( language ) ;
230+ if ( ! state ) {
231+ return { canOpen : true } ;
232+ }
233+
234+ this . rememberServerStatus ( language , state . status ) ;
235+
236+ return state . status === 'running'
237+ ? { canOpen : true , status : state . status }
238+ : {
239+ canOpen : false ,
240+ status : state . status ,
241+ skippedReason : 'server-not-running'
242+ } ;
243+ }
244+
245+
159246 private onDiagnosticsReceived ( data : LspEvent [ 'data' ] ) {
160247 const { uri, diagnostics } = data ;
161248
@@ -186,6 +273,9 @@ export class WorkspaceLspManager {
186273
187274 if ( ! language ) return ;
188275
276+ if ( this . isServerStatus ( status ) ) {
277+ this . rememberServerStatus ( language , status ) ;
278+ }
189279
190280 switch ( status ) {
191281 case 'starting' :
@@ -282,21 +372,10 @@ export class WorkspaceLspManager {
282372 progressNotif . complete ( ) ;
283373 this . indexingProgressNotifications . delete ( language ) ;
284374 }
285-
286-
287- this . startingLanguages . delete ( language ) ;
288375 }
289376
290377
291- async openDocument ( uri : string , language : string , content : string ) : Promise < string > {
292-
293- if ( this . startingLanguages . has ( language ) ) {
294- const readyPromise = this . languageReadyPromises . get ( language ) ;
295- if ( readyPromise ) {
296- await readyPromise ;
297- }
298- }
299-
378+ async openDocument ( uri : string , language : string , content : string ) : Promise < OpenDocumentResult > {
300379 if ( ! this . isInitialized ) {
301380 try {
302381 await this . initialize ( ) ;
@@ -305,21 +384,28 @@ export class WorkspaceLspManager {
305384 throw initError ;
306385 }
307386 }
308-
309387
310- if ( ! this . startingLanguages . has ( language ) ) {
311- this . startingLanguages . add ( language ) ;
312-
313-
314- const readyPromise = new Promise < void > ( ( resolve ) => {
388+ // didOpen is only meaningful when a language server is actually running.
389+ // Checking the state here prevents layout/remount churn (for example window
390+ // fullscreen transitions) from generating batches of backend no-op logs.
391+ const availability = await this . getDocumentOpenAvailability ( language ) ;
392+ if ( ! availability . canOpen ) {
393+ const key = this . normalizeLanguage ( language ) ;
394+ if ( ! this . skippedOpenNoticeLanguages . has ( key ) ) {
395+ log . debug ( 'Skipped LSP didOpen because language server is not running' , {
396+ workspacePath : this . workspacePath ,
397+ language,
398+ status : availability . status
399+ } ) ;
400+ this . skippedOpenNoticeLanguages . add ( key ) ;
401+ }
315402
316- setTimeout ( ( ) => {
317- this . startingLanguages . delete ( language ) ;
318- this . languageReadyPromises . delete ( language ) ;
319- resolve ( ) ;
320- } , 5000 ) ;
321- } ) ;
322- this . languageReadyPromises . set ( language , readyPromise ) ;
403+ return {
404+ language,
405+ opened : false ,
406+ skippedReason : availability . skippedReason ,
407+ serverStatus : availability . status
408+ } ;
323409 }
324410
325411 try {
@@ -341,7 +427,7 @@ export class WorkspaceLspManager {
341427
342428
343429
344- return language ;
430+ return { language, opened : true } ;
345431 } catch ( error ) {
346432 log . error ( 'Failed to open document' , { workspacePath : this . workspacePath , uri, language, error } ) ;
347433 throw error ;
0 commit comments