@@ -14,6 +14,13 @@ import {
14
14
} from "./common" ;
15
15
import { WorkspaceChannel } from "./workspaceChannel" ;
16
16
17
+ const WATCHED_FILES = [
18
+ "Gemfile.lock" ,
19
+ "gems.locked" ,
20
+ ".rubocop.yml" ,
21
+ ".rubocop" ,
22
+ ] ;
23
+
17
24
export class Workspace implements WorkspaceInterface {
18
25
public lspClient ?: Client ;
19
26
public readonly ruby : Ruby ;
@@ -58,8 +65,6 @@ export class Workspace implements WorkspaceInterface {
58
65
this . createTestItems = createTestItems ;
59
66
this . isMainWorkspace = isMainWorkspace ;
60
67
this . virtualDocuments = virtualDocuments ;
61
-
62
- this . registerRestarts ( context ) ;
63
68
}
64
69
65
70
// Activate this workspace. This method is intended to be invoked only once, unlikely `start` which may be invoked
@@ -82,6 +87,20 @@ export class Workspace implements WorkspaceInterface {
82
87
rootGitUri ,
83
88
".git/{rebase-merge,rebase-apply,BISECT_START,CHERRY_PICK_HEAD}" ,
84
89
) ;
90
+
91
+ this . registerRestarts ( ) ;
92
+
93
+ // Eagerly calculate SHAs for the watched files to avoid unnecessary restarts
94
+ for ( const file of WATCHED_FILES ) {
95
+ const uri = vscode . Uri . joinPath ( this . workspaceFolder . uri , file ) ;
96
+ const currentSha = await this . fileContentsSha ( uri ) ;
97
+
98
+ if ( ! currentSha ) {
99
+ continue ;
100
+ }
101
+
102
+ this . restartDocumentShas . set ( uri . fsPath , currentSha ) ;
103
+ }
85
104
}
86
105
87
106
async start ( debugMode ?: boolean ) {
@@ -164,7 +183,9 @@ export class Workspace implements WorkspaceInterface {
164
183
// server can now handle shutdown requests
165
184
if ( this . needsRestart ) {
166
185
this . needsRestart = false ;
167
- await this . restart ( ) ;
186
+ await this . debouncedRestart (
187
+ "a restart was requested while the server was still booting" ,
188
+ ) ;
168
189
}
169
190
} catch ( error : any ) {
170
191
this . error = true ;
@@ -342,67 +363,49 @@ export class Workspace implements WorkspaceInterface {
342
363
return result ;
343
364
}
344
365
345
- private registerRestarts ( context : vscode . ExtensionContext ) {
346
- this . createRestartWatcher ( context , "Gemfile.lock" ) ;
347
- this . createRestartWatcher ( context , "gems.locked" ) ;
348
- this . createRestartWatcher ( context , "**/.rubocop.yml" ) ;
349
- this . createRestartWatcher ( context , ".rubocop" ) ;
366
+ private registerRestarts ( ) {
367
+ this . createRestartWatcher ( `{${ WATCHED_FILES . join ( "," ) } }` ) ;
350
368
351
369
// If a configuration that affects the Ruby LSP has changed, update the client options using the latest
352
370
// configuration and restart the server
353
- context . subscriptions . push (
371
+ this . context . subscriptions . push (
354
372
vscode . workspace . onDidChangeConfiguration ( async ( event ) => {
355
373
if ( event . affectsConfiguration ( "rubyLsp" ) ) {
356
- // Re-activate Ruby if the version manager changed
357
- if (
358
- event . affectsConfiguration ( "rubyLsp.rubyVersionManager" ) ||
359
- event . affectsConfiguration ( "rubyLsp.bundleGemfile" ) ||
360
- event . affectsConfiguration ( "rubyLsp.customRubyCommand" )
361
- ) {
362
- await this . ruby . activateRuby ( ) ;
363
- }
364
-
365
- this . outputChannel . info (
366
- "Restarting the Ruby LSP because configuration changed" ,
367
- ) ;
368
- await this . restart ( ) ;
374
+ await this . debouncedRestart ( "configuration changed" ) ;
369
375
}
370
376
} ) ,
371
377
) ;
372
378
}
373
379
374
- private createRestartWatcher (
375
- context : vscode . ExtensionContext ,
376
- pattern : string ,
377
- ) {
380
+ private createRestartWatcher ( pattern : string ) {
378
381
const watcher = vscode . workspace . createFileSystemWatcher (
379
382
new vscode . RelativePattern ( this . workspaceFolder , pattern ) ,
380
383
) ;
381
384
382
385
// Handler for only triggering restart if the contents of the file have been modified. If the file was just touched,
383
386
// but the contents are the same, we don't want to restart
384
387
const debouncedRestartWithHashCheck = async ( uri : vscode . Uri ) => {
385
- const fileContents = await vscode . workspace . fs . readFile ( uri ) ;
386
388
const fsPath = uri . fsPath ;
389
+ const currentSha = await this . fileContentsSha ( uri ) ;
387
390
388
- const hash = createHash ( "sha256" ) ;
389
- hash . update ( fileContents . toString ( ) ) ;
390
- const currentSha = hash . digest ( "hex" ) ;
391
-
392
- if ( this . restartDocumentShas . get ( fsPath ) !== currentSha ) {
391
+ if ( currentSha && this . restartDocumentShas . get ( fsPath ) !== currentSha ) {
393
392
this . restartDocumentShas . set ( fsPath , currentSha ) ;
394
- await this . debouncedRestart ( `${ pattern } changed` ) ;
393
+ await this . debouncedRestart ( `${ fsPath } changed, matching ${ pattern } ` ) ;
395
394
}
396
395
} ;
397
396
398
- const debouncedRestart = async ( ) => {
399
- await this . debouncedRestart ( `${ pattern } changed` ) ;
397
+ const debouncedRestart = async ( uri : vscode . Uri ) => {
398
+ this . restartDocumentShas . delete ( uri . fsPath ) ;
399
+ await this . debouncedRestart ( `${ uri . fsPath } changed, matching ${ pattern } ` ) ;
400
400
} ;
401
401
402
- context . subscriptions . push (
402
+ this . context . subscriptions . push (
403
403
watcher ,
404
404
watcher . onDidChange ( debouncedRestartWithHashCheck ) ,
405
- watcher . onDidCreate ( debouncedRestart ) ,
405
+ // Interestingly, we are seeing create events being fired even when the file already exists. If a create event is
406
+ // fired for an update to an existing file, then we need to check if the contents still match to prevent unwanted
407
+ // restarts
408
+ watcher . onDidCreate ( debouncedRestartWithHashCheck ) ,
406
409
watcher . onDidDelete ( debouncedRestart ) ,
407
410
) ;
408
411
}
@@ -416,9 +419,9 @@ export class Workspace implements WorkspaceInterface {
416
419
this . #inhibitRestart = true ;
417
420
} ;
418
421
419
- const stop = async ( ) => {
422
+ const stop = async ( uri : vscode . Uri ) => {
420
423
this . #inhibitRestart = false ;
421
- await this . debouncedRestart ( `${ glob } changed` ) ;
424
+ await this . debouncedRestart ( `${ uri . fsPath } changed, matching ${ glob } ` ) ;
422
425
} ;
423
426
424
427
this . context . subscriptions . push (
@@ -429,4 +432,18 @@ export class Workspace implements WorkspaceInterface {
429
432
workspaceWatcher . onDidDelete ( stop ) ,
430
433
) ;
431
434
}
435
+
436
+ private async fileContentsSha ( uri : vscode . Uri ) : Promise < string | undefined > {
437
+ let fileContents ;
438
+
439
+ try {
440
+ fileContents = await vscode . workspace . fs . readFile ( uri ) ;
441
+ } catch ( error : any ) {
442
+ return undefined ;
443
+ }
444
+
445
+ const hash = createHash ( "sha256" ) ;
446
+ hash . update ( fileContents . toString ( ) ) ;
447
+ return hash . digest ( "hex" ) ;
448
+ }
432
449
}
0 commit comments