forked from microsoft/vscode
-
Notifications
You must be signed in to change notification settings - Fork 3
/
windows.ts
2120 lines (1756 loc) · 76.9 KB
/
windows.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { basename, normalize, join, dirname } from 'vs/base/common/path';
import { localize } from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { assign, mixin } from 'vs/base/common/objects';
import { IBackupMainService, IEmptyWindowBackupInfo } from 'vs/platform/backup/common/backup';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { IStateService } from 'vs/platform/state/common/state';
import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window';
import { hasArgs, asArray } from 'vs/platform/environment/node/argv';
import { ipcMain as ipc, screen, BrowserWindow, dialog, systemPreferences, FileFilter } from 'electron';
import { parseLineAndColumnAware } from 'vs/code/node/paths';
import { ILifecycleService, UnloadReason, LifecycleService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, IPathsToWaitFor, IEnterWorkspaceResult, IMessageBoxResult, INewWindowOptions, IURIToOpen, isFileToOpen, isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows';
import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri } from 'vs/code/node/windowsFinder';
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
import product from 'vs/platform/product/node/product';
import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
import { IHistoryMainService, IRecent } from 'vs/platform/history/common/history';
import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform';
import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { Schemas } from 'vs/base/common/network';
import { normalizeNFC } from 'vs/base/common/normalization';
import { URI } from 'vs/base/common/uri';
import { Queue } from 'vs/base/common/async';
import { exists, dirExists } from 'vs/base/node/pfs';
import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, originalFSPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources';
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/code/electron-main/windowsStateStorage';
import { getWorkspaceIdentifier } from 'vs/platform/workspaces/electron-main/workspacesMainService';
import { once } from 'vs/base/common/functional';
import { Disposable } from 'vs/base/common/lifecycle';
const enum WindowError {
UNRESPONSIVE = 1,
CRASHED = 2
}
export interface IWindowState {
workspace?: IWorkspaceIdentifier;
folderUri?: URI;
backupPath?: string;
remoteAuthority?: string;
uiState: ISingleWindowState;
}
export interface IWindowsState {
lastActiveWindow?: IWindowState;
lastPluginDevelopmentHostWindow?: IWindowState;
openedWindows: IWindowState[];
}
interface INewWindowState extends ISingleWindowState {
hasDefaultState?: boolean;
}
type RestoreWindowsSetting = 'all' | 'folders' | 'one' | 'none';
interface IOpenBrowserWindowOptions {
userEnv?: IProcessEnvironment;
cli?: ParsedArgs;
workspace?: IWorkspaceIdentifier;
folderUri?: URI;
remoteAuthority?: string;
initialStartup?: boolean;
fileInputs?: IFileInputs;
forceNewWindow?: boolean;
forceNewTabbedWindow?: boolean;
windowToUse?: ICodeWindow;
emptyWindowBackupInfo?: IEmptyWindowBackupInfo;
}
interface IPathParseOptions {
ignoreFileNotFound?: boolean;
gotoLineMode?: boolean;
remoteAuthority?: string;
}
interface IFileInputs {
filesToOpenOrCreate: IPath[];
filesToDiff: IPath[];
filesToWait?: IPathsToWaitFor;
remoteAuthority?: string;
}
interface IPathToOpen extends IPath {
// the workspace for a Code instance to open
workspace?: IWorkspaceIdentifier;
// the folder path for a Code instance to open
folderUri?: URI;
// the backup path for a Code instance to use
backupPath?: string;
// the remote authority for the Code instance to open. Undefined if not remote.
remoteAuthority?: string;
// optional label for the recent history
label?: string;
}
function isFolderPathToOpen(path: IPathToOpen): path is IFolderPathToOpen {
return !!path.folderUri;
}
interface IFolderPathToOpen {
// the folder path for a Code instance to open
folderUri: URI;
// the backup path for a Code instance to use
backupPath?: string;
// the remote authority for the Code instance to open. Undefined if not remote.
remoteAuthority?: string;
// optional label for the recent history
label?: string;
}
function isWorkspacePathToOpen(path: IPathToOpen): path is IWorkspacePathToOpen {
return !!path.workspace;
}
interface IWorkspacePathToOpen {
// the workspace for a Code instance to open
workspace: IWorkspaceIdentifier;
// the backup path for a Code instance to use
backupPath?: string;
// the remote authority for the Code instance to open. Undefined if not remote.
remoteAuthority?: string;
// optional label for the recent history
label?: string;
}
export class WindowsManager extends Disposable implements IWindowsMainService {
_serviceBrand: any;
private static readonly windowsStateStorageKey = 'windowsState';
private static readonly WINDOWS: ICodeWindow[] = [];
private readonly windowsState: IWindowsState;
private lastClosedWindowState?: IWindowState;
private readonly dialogs: Dialogs;
private readonly workspacesManager: WorkspacesManager;
private readonly _onWindowReady = this._register(new Emitter<ICodeWindow>());
readonly onWindowReady: CommonEvent<ICodeWindow> = this._onWindowReady.event;
private readonly _onWindowClose = this._register(new Emitter<number>());
readonly onWindowClose: CommonEvent<number> = this._onWindowClose.event;
private readonly _onWindowLoad = this._register(new Emitter<number>());
readonly onWindowLoad: CommonEvent<number> = this._onWindowLoad.event;
private readonly _onWindowsCountChanged = this._register(new Emitter<IWindowsCountChangedEvent>());
readonly onWindowsCountChanged: CommonEvent<IWindowsCountChangedEvent> = this._onWindowsCountChanged.event;
constructor(
private readonly machineId: string,
private readonly initialUserEnv: IProcessEnvironment,
@ILogService private readonly logService: ILogService,
@IStateService private readonly stateService: IStateService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IBackupMainService private readonly backupMainService: IBackupMainService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IHistoryMainService private readonly historyMainService: IHistoryMainService,
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
const windowsStateStoreData = this.stateService.getItem<WindowsStateStorageData>(WindowsManager.windowsStateStorageKey);
this.windowsState = restoreWindowsState(windowsStateStoreData);
if (!Array.isArray(this.windowsState.openedWindows)) {
this.windowsState.openedWindows = [];
}
this.dialogs = new Dialogs(stateService, this);
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, this);
this.lifecycleService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners());
this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.installWindowsMutex());
}
private installWindowsMutex(): void {
if (isWindows) {
try {
const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex;
const mutex = new WindowsMutex(product.win32MutexName);
once(this.lifecycleService.onWillShutdown)(() => mutex.release());
} catch (e) {
this.logService.error(e);
}
}
}
private registerListeners(): void {
// React to workbench ready events from windows
ipc.on('vscode:workbenchReady', (event: Electron.Event, windowId: number) => {
this.logService.trace('IPC#vscode-workbenchReady');
const win = this.getWindowById(windowId);
if (win) {
win.setReady();
// Event
this._onWindowReady.fire(win);
}
});
// React to HC color scheme changes (Windows)
if (isWindows) {
systemPreferences.on('inverted-color-scheme-changed', () => {
if (systemPreferences.isInvertedColorScheme()) {
this.sendToAll('vscode:enterHighContrast');
} else {
this.sendToAll('vscode:leaveHighContrast');
}
});
}
// Handle various lifecycle events around windows
this.lifecycleService.onBeforeWindowClose(window => this.onBeforeWindowClose(window));
this.lifecycleService.onBeforeShutdown(() => this.onBeforeShutdown());
this.onWindowsCountChanged(e => {
if (e.newCount - e.oldCount > 0) {
// clear last closed window state when a new window opens. this helps on macOS where
// otherwise closing the last window, opening a new window and then quitting would
// use the state of the previously closed window when restarting.
this.lastClosedWindowState = undefined;
}
});
}
// Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS:
// - macOS: since the app will not quit when closing the last window, you will always first get
// the onBeforeShutdown() event followed by N onbeforeWindowClose() events for each window
// - other: on other OS, closing the last window will quit the app so the order depends on the
// user interaction: closing the last window will first trigger onBeforeWindowClose()
// and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown()
// and then onBeforeWindowClose().
//
// Here is the behaviour on different OS dependig on action taken (Electron 1.7.x):
//
// Legend
// - quit(N): quit application with N windows opened
// - close(1): close one window via the window close button
// - closeAll: close all windows via the taskbar command
// - onBeforeShutdown(N): number of windows reported in this event handler
// - onBeforeWindowClose(N, M): number of windows reported and quitRequested boolean in this event handler
//
// macOS
// - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true)
// - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true)
// - quit(0): onBeforeShutdown(0)
// - close(1): onBeforeWindowClose(1, false)
//
// Windows
// - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true)
// - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true)
// - close(1): onBeforeWindowClose(2, false)[not last window]
// - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window]
// - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0)
//
// Linux
// - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true)
// - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true)
// - close(1): onBeforeWindowClose(2, false)[not last window]
// - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window]
// - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0)
//
private onBeforeShutdown(): void {
const currentWindowsState: IWindowsState = {
openedWindows: [],
lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow,
lastActiveWindow: this.lastClosedWindowState
};
// 1.) Find a last active window (pick any other first window otherwise)
if (!currentWindowsState.lastActiveWindow) {
let activeWindow = this.getLastActiveWindow();
if (!activeWindow || activeWindow.isExtensionDevelopmentHost) {
activeWindow = WindowsManager.WINDOWS.filter(w => !w.isExtensionDevelopmentHost)[0];
}
if (activeWindow) {
currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow);
}
}
// 2.) Find extension host window
const extensionHostWindow = WindowsManager.WINDOWS.filter(w => w.isExtensionDevelopmentHost && !w.isExtensionTestHost)[0];
if (extensionHostWindow) {
currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow);
}
// 3.) All windows (except extension host) for N >= 2 to support restoreWindows: all or for auto update
//
// Carefull here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0)
// so if we ever want to persist the UI state of the last closed window (window count === 1), it has
// to come from the stored lastClosedWindowState on Win/Linux at least
if (this.getWindowCount() > 1) {
currentWindowsState.openedWindows = WindowsManager.WINDOWS.filter(w => !w.isExtensionDevelopmentHost).map(w => this.toWindowState(w));
}
// Persist
this.stateService.setItem(WindowsManager.windowsStateStorageKey, getWindowsStateStoreData(currentWindowsState));
}
// See note on #onBeforeShutdown() for details how these events are flowing
private onBeforeWindowClose(win: ICodeWindow): void {
if (this.lifecycleService.quitRequested) {
return; // during quit, many windows close in parallel so let it be handled in the before-quit handler
}
// On Window close, update our stored UI state of this window
const state: IWindowState = this.toWindowState(win);
if (win.isExtensionDevelopmentHost && !win.isExtensionTestHost) {
this.windowsState.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state
}
// Any non extension host window with same workspace or folder
else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderUri)) {
this.windowsState.openedWindows.forEach(o => {
const sameWorkspace = win.openedWorkspace && o.workspace && o.workspace.id === win.openedWorkspace.id;
const sameFolder = win.openedFolderUri && o.folderUri && isEqual(o.folderUri, win.openedFolderUri);
if (sameWorkspace || sameFolder) {
o.uiState = state.uiState;
}
});
}
// On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state
// before quitting, we need to remember the UI state of this window to be able to persist it.
// On macOS we keep the last closed window state ready in case the user wants to quit right after or
// wants to open another window, in which case we use this state over the persisted one.
if (this.getWindowCount() === 1) {
this.lastClosedWindowState = state;
}
}
private toWindowState(win: ICodeWindow): IWindowState {
return {
workspace: win.openedWorkspace,
folderUri: win.openedFolderUri,
backupPath: win.backupPath,
remoteAuthority: win.remoteAuthority,
uiState: win.serializeWindowState()
};
}
open(openConfig: IOpenConfiguration): ICodeWindow[] {
setTimeout(() => {
this.logService.trace('windowsManager#open');
openConfig = this.validateOpenConfig(openConfig);
const pathsToOpen = this.getPathsToOpen(openConfig);
const foldersToAdd: IFolderPathToOpen[] = [];
const foldersToOpen: IFolderPathToOpen[] = [];
const workspacesToOpen: IWorkspacePathToOpen[] = [];
const emptyToRestore: IEmptyWindowBackupInfo[] = []; // empty windows with backupPath
let emptyToOpen: number = 0;
let fileInputs: IFileInputs | undefined; // collect all file inputs
for (const path of pathsToOpen) {
if (isFolderPathToOpen(path)) {
if (openConfig.addMode) {
// When run with --add, take the folders that are to be opened as
// folders that should be added to the currently active window.
foldersToAdd.push(path);
} else {
foldersToOpen.push(path);
}
} else if (isWorkspacePathToOpen(path)) {
workspacesToOpen.push(path);
} else if (path.fileUri) {
if (!fileInputs) {
fileInputs = { filesToOpenOrCreate: [], filesToDiff: [], remoteAuthority: path.remoteAuthority };
}
fileInputs.filesToOpenOrCreate.push(path);
} else if (path.backupPath) {
emptyToRestore.push({ backupFolder: basename(path.backupPath), remoteAuthority: path.remoteAuthority });
} else {
emptyToOpen++;
}
}
// When run with --diff, take the files to open as files to diff
// if there are exactly two files provided.
if (fileInputs && openConfig.diffMode && fileInputs.filesToOpenOrCreate.length === 2) {
fileInputs.filesToDiff = fileInputs.filesToOpenOrCreate;
fileInputs.filesToOpenOrCreate = [];
}
// When run with --wait, make sure we keep the paths to wait for
if (fileInputs && openConfig.waitMarkerFileURI) {
fileInputs.filesToWait = { paths: [...fileInputs.filesToDiff, ...fileInputs.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI };
}
//
// These are windows to restore because of hot-exit or from previous session (only performed once on startup!)
//
let foldersToRestore: URI[] = [];
let workspacesToRestore: IWorkspacePathToOpen[] = [];
if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) {
let foldersToRestore = this.backupMainService.getFolderBackupPaths();
foldersToAdd.push(...foldersToRestore.map(f => ({ folderUri: f, remoteAuhority: getRemoteAuthority(f), isRestored: true })));
// collect from workspaces with hot-exit backups and from previous window session
workspacesToRestore = [...this.backupMainService.getWorkspaceBackups(), ...this.workspacesMainService.getUntitledWorkspacesSync()];
workspacesToOpen.push(...workspacesToRestore);
emptyToRestore.push(...this.backupMainService.getEmptyWindowBackupPaths());
} else {
emptyToRestore.length = 0;
}
// Open based on config
const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, fileInputs, foldersToAdd);
// Make sure to pass focus to the most relevant of the windows if we open multiple
if (usedWindows.length > 1) {
const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !hasArgs(openConfig.cli._) && !hasArgs(openConfig.cli['file-uri']) && !hasArgs(openConfig.cli['folder-uri']) && !(openConfig.urisToOpen && openConfig.urisToOpen.length);
let focusLastOpened = true;
let focusLastWindow = true;
// 1.) focus last active window if we are not instructed to open any paths
if (focusLastActive) {
const lastActiveWindow = usedWindows.filter(w => w.backupPath === this.windowsState.lastActiveWindow!.backupPath);
if (lastActiveWindow.length) {
lastActiveWindow[0].focus();
focusLastOpened = false;
focusLastWindow = false;
}
}
// 2.) if instructed to open paths, focus last window which is not restored
if (focusLastOpened) {
for (let i = usedWindows.length - 1; i >= 0; i--) {
const usedWindow = usedWindows[i];
if (
(usedWindow.openedWorkspace && workspacesToRestore.some(workspace => workspace.workspace.id === usedWindow.openedWorkspace!.id)) || // skip over restored workspace
(usedWindow.openedFolderUri && foldersToRestore.some(uri => isEqual(uri, usedWindow.openedFolderUri))) || // skip over restored folder
(usedWindow.backupPath && emptyToRestore.some(empty => empty.backupFolder === basename(usedWindow.backupPath!))) // skip over restored empty window
) {
continue;
}
usedWindow.focus();
focusLastWindow = false;
break;
}
}
// 3.) finally, always ensure to have at least last used window focused
if (focusLastWindow) {
usedWindows[usedWindows.length - 1].focus();
}
}
// Remember in recent document list (unless this opens for extension development)
// Also do not add paths when files are opened for diffing, only if opened individually
if (!usedWindows.some(w => w.isExtensionDevelopmentHost) && !openConfig.diffMode && !openConfig.noRecentEntry) {
const recents: IRecent[] = [];
for (let pathToOpen of pathsToOpen) {
if (pathToOpen.workspace) {
recents.push({ label: pathToOpen.label, workspace: pathToOpen.workspace });
} else if (pathToOpen.folderUri) {
recents.push({ label: pathToOpen.label, folderUri: pathToOpen.folderUri });
} else if (pathToOpen.fileUri) {
recents.push({ label: pathToOpen.label, fileUri: pathToOpen.fileUri });
}
}
this.historyMainService.addRecentlyOpened(recents);
}
// If we got started with --wait from the CLI, we need to signal to the outside when the window
// used for the edit operation is closed or loaded to a different folder so that the waiting
// process can continue. We do this by deleting the waitMarkerFilePath.
const waitMarkerFileURI = openConfig.waitMarkerFileURI;
if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) {
this.waitForWindowCloseOrLoad(usedWindows[0].id).then(() => fs.unlink(waitMarkerFileURI.fsPath, _error => undefined));
}
}, 100);
return [];
}
private validateOpenConfig(config: IOpenConfiguration): IOpenConfiguration {
// Make sure addMode is only enabled if we have an active window
if (config.addMode && (config.initialStartup || !this.getLastActiveWindow())) {
config.addMode = false;
}
return config;
}
private doOpen(
openConfig: IOpenConfiguration,
workspacesToOpen: IWorkspacePathToOpen[],
foldersToOpen: IFolderPathToOpen[],
emptyToRestore: IEmptyWindowBackupInfo[],
emptyToOpen: number,
fileInputs: IFileInputs | undefined,
foldersToAdd: IFolderPathToOpen[]
) {
const usedWindows: ICodeWindow[] = [];
// Settings can decide if files/folders open in new window or not
let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig);
// Handle folders to add by looking for the last active workspace (not on initial startup)
if (!openConfig.initialStartup && foldersToAdd.length > 0) {
const authority = foldersToAdd[0].remoteAuthority;
const lastActiveWindow = this.getLastActiveWindowForAuthority(authority);
if (lastActiveWindow) {
usedWindows.push(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(f => f.folderUri)));
}
}
// Handle files to open/diff or to create when we dont open a folder and we do not restore any folder/untitled from hot-exit
const potentialWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length;
if (potentialWindowsCount === 0 && fileInputs) {
// Find suitable window or folder path to open files in
const fileToCheck = fileInputs.filesToOpenOrCreate[0] || fileInputs.filesToDiff[0];
// only look at the windows with correct authority
const windows = WindowsManager.WINDOWS.filter(w => w.remoteAuthority === fileInputs!.remoteAuthority);
const bestWindowOrFolder = findBestWindowOrFolderForFile({
windows,
newWindow: openFilesInNewWindow,
context: openConfig.context,
fileUri: fileToCheck && fileToCheck.fileUri,
localWorkspaceResolver: workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath) : null
});
// We found a window to open the files in
if (bestWindowOrFolder instanceof CodeWindow) {
// Window is workspace
if (bestWindowOrFolder.openedWorkspace) {
workspacesToOpen.push({ workspace: bestWindowOrFolder.openedWorkspace, remoteAuthority: bestWindowOrFolder.remoteAuthority });
}
// Window is single folder
else if (bestWindowOrFolder.openedFolderUri) {
foldersToOpen.push({ folderUri: bestWindowOrFolder.openedFolderUri, remoteAuthority: bestWindowOrFolder.remoteAuthority });
}
// Window is empty
else {
// Do open files
usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, fileInputs));
// Reset these because we handled them
fileInputs = undefined;
}
}
// Finally, if no window or folder is found, just open the files in an empty window
else {
usedWindows.push(this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
fileInputs,
forceNewWindow: true,
remoteAuthority: fileInputs.remoteAuthority,
forceNewTabbedWindow: openConfig.forceNewTabbedWindow
}));
// Reset these because we handled them
fileInputs = undefined;
}
}
// Handle workspaces to open (instructed and to restore)
const allWorkspacesToOpen = arrays.distinct(workspacesToOpen, workspace => workspace.workspace.id); // prevent duplicates
if (allWorkspacesToOpen.length > 0) {
// Check for existing instances
const windowsOnWorkspace = arrays.coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsManager.WINDOWS, workspaceToOpen.workspace)));
if (windowsOnWorkspace.length > 0) {
const windowOnWorkspace = windowsOnWorkspace[0];
const fileInputsForWindow = (fileInputs && fileInputs.remoteAuthority === windowOnWorkspace.remoteAuthority) ? fileInputs : undefined;
// Do open files
usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, fileInputsForWindow));
// Reset these because we handled them
if (fileInputsForWindow) {
fileInputs = undefined;
}
openFolderInNewWindow = true; // any other folders to open must open in new window then
}
// Open remaining ones
allWorkspacesToOpen.forEach(workspaceToOpen => {
if (windowsOnWorkspace.some(win => win.openedWorkspace!.id === workspaceToOpen.workspace.id)) {
return; // ignore folders that are already open
}
const remoteAuthority = workspaceToOpen.remoteAuthority;
const fileInputsForWindow = (fileInputs && fileInputs.remoteAuthority === remoteAuthority) ? fileInputs : undefined;
// Do open folder
usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, fileInputsForWindow));
// Reset these because we handled them
if (fileInputsForWindow) {
fileInputs = undefined;
}
openFolderInNewWindow = true; // any other folders to open must open in new window then
});
}
// Handle folders to open (instructed and to restore)
const allFoldersToOpen = arrays.distinct(foldersToOpen, folder => getComparisonKey(folder.folderUri)); // prevent duplicates
if (allFoldersToOpen.length > 0) {
// Check for existing instances
const windowsOnFolderPath = arrays.coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsManager.WINDOWS, folderToOpen.folderUri)));
if (windowsOnFolderPath.length > 0) {
const windowOnFolderPath = windowsOnFolderPath[0];
const fileInputsForWindow = fileInputs && fileInputs.remoteAuthority === windowOnFolderPath.remoteAuthority ? fileInputs : undefined;
// Do open files
usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, fileInputsForWindow));
// Reset these because we handled them
if (fileInputsForWindow) {
fileInputs = undefined;
}
openFolderInNewWindow = true; // any other folders to open must open in new window then
}
// Open remaining ones
allFoldersToOpen.forEach(folderToOpen => {
if (windowsOnFolderPath.some(win => isEqual(win.openedFolderUri, folderToOpen.folderUri))) {
return; // ignore folders that are already open
}
const remoteAuthority = folderToOpen.remoteAuthority;
const fileInputsForWindow = (fileInputs && fileInputs.remoteAuthority === remoteAuthority) ? fileInputs : undefined;
// Do open folder
usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, fileInputsForWindow));
// Reset these because we handled them
if (fileInputsForWindow) {
fileInputs = undefined;
}
openFolderInNewWindow = true; // any other folders to open must open in new window then
});
}
// Handle empty to restore
const allEmptyToRestore = arrays.distinct(emptyToRestore, info => info.backupFolder); // prevent duplicates
if (allEmptyToRestore.length > 0) {
allEmptyToRestore.forEach(emptyWindowBackupInfo => {
const remoteAuthority = emptyWindowBackupInfo.remoteAuthority;
const fileInputsForWindow = (fileInputs && fileInputs.remoteAuthority === remoteAuthority) ? fileInputs : undefined;
usedWindows.push(this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
fileInputs: fileInputsForWindow,
remoteAuthority,
forceNewWindow: true,
forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
emptyWindowBackupInfo
}));
// Reset these because we handled them
if (fileInputsForWindow) {
fileInputs = undefined;
}
openFolderInNewWindow = true; // any other folders to open must open in new window then
});
}
// Handle empty to open (only if no other window opened)
if (usedWindows.length === 0 || fileInputs) {
if (fileInputs && !emptyToOpen) {
emptyToOpen++;
}
const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined);
for (let i = 0; i < emptyToOpen; i++) {
usedWindows.push(this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
remoteAuthority,
forceNewWindow: openFolderInNewWindow,
forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
fileInputs
}));
// Reset these because we handled them
fileInputs = undefined;
openFolderInNewWindow = true; // any other window to open must open in new window then
}
}
return arrays.distinct(usedWindows);
}
private doOpenFilesInExistingWindow(configuration: IOpenConfiguration, window: ICodeWindow, fileInputs?: IFileInputs): ICodeWindow {
window.focus(); // make sure window has focus
const params: { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[], filesToWait?: IPathsToWaitFor, termProgram?: string } = {};
if (fileInputs) {
params.filesToOpenOrCreate = fileInputs.filesToOpenOrCreate;
params.filesToDiff = fileInputs.filesToDiff;
params.filesToWait = fileInputs.filesToWait;
}
if (configuration.userEnv) {
params.termProgram = configuration.userEnv['TERM_PROGRAM'];
}
window.sendWhenReady('vscode:openFiles', params);
return window;
}
private doAddFoldersToExistingWindow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow {
window.focus(); // make sure window has focus
window.sendWhenReady('vscode:addFolders', { foldersToAdd });
return window;
}
private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow {
if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') {
windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/Microsoft/vscode/issues/49587
}
const browserWindow = this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
workspace: folderOrWorkspace.workspace,
folderUri: folderOrWorkspace.folderUri,
fileInputs,
remoteAuthority: folderOrWorkspace.remoteAuthority,
forceNewWindow,
forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
windowToUse
});
return browserWindow;
}
private getPathsToOpen(openConfig: IOpenConfiguration): IPathToOpen[] {
let windowsToOpen: IPathToOpen[];
let isCommandLineOrAPICall = false;
// Extract paths: from API
if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) {
windowsToOpen = this.doExtractPathsFromAPI(openConfig);
isCommandLineOrAPICall = true;
}
// Check for force empty
else if (openConfig.forceEmpty) {
windowsToOpen = [Object.create(null)];
}
// Extract paths: from CLI
else if (hasArgs(openConfig.cli._) || hasArgs(openConfig.cli['folder-uri']) || hasArgs(openConfig.cli['file-uri'])) {
windowsToOpen = this.doExtractPathsFromCLI(openConfig.cli);
isCommandLineOrAPICall = true;
}
// Extract windows: from previous session
else {
windowsToOpen = this.doGetWindowsFromLastSession();
}
// Convert multiple folders into workspace (if opened via API or CLI)
// This will ensure to open these folders in one window instead of multiple
// If we are in addMode, we should not do this because in that case all
// folders should be added to the existing window.
if (!openConfig.addMode && isCommandLineOrAPICall) {
const foldersToOpen = windowsToOpen.filter(path => !!path.folderUri);
if (foldersToOpen.length > 1) {
const remoteAuthority = foldersToOpen[0].remoteAuthority;
if (foldersToOpen.every(f => f.remoteAuthority === remoteAuthority)) { // only if all folder have the same authority
const workspace = this.workspacesMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.folderUri! })));
// Add workspace and remove folders thereby
windowsToOpen.push({ workspace, remoteAuthority });
windowsToOpen = windowsToOpen.filter(path => !path.folderUri);
}
}
}
return windowsToOpen;
}
private doExtractPathsFromAPI(openConfig: IOpenConfiguration): IPathToOpen[] {
const pathsToOpen: IPathToOpen[] = [];
const parseOptions: IPathParseOptions = { gotoLineMode: openConfig.gotoLineMode };
for (const pathToOpen of openConfig.urisToOpen || []) {
if (!pathToOpen) {
continue;
}
const path = this.parseUri(pathToOpen, parseOptions);
if (path) {
path.label = pathToOpen.label;
pathsToOpen.push(path);
} else {
const uri = resourceFromURIToOpen(pathToOpen);
// Warn about the invalid URI or path
let message, detail;
if (uri.scheme === Schemas.file) {
message = localize('pathNotExistTitle', "Path does not exist");
detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", uri.fsPath);
} else {
message = localize('uriInvalidTitle', "URI can not be opened");
detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString());
}
const options: Electron.MessageBoxOptions = {
title: product.nameLong,
type: 'info',
buttons: [localize('ok', "OK")],
message,
detail,
noLink: true
};
this.dialogs.showMessageBox(options, this.getFocusedWindow());
}
}
return pathsToOpen;
}
private doExtractPathsFromCLI(cli: ParsedArgs): IPath[] {
const pathsToOpen: IPathToOpen[] = [];
const parseOptions: IPathParseOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto, remoteAuthority: cli.remote || undefined };
// folder uris
const folderUris = asArray(cli['folder-uri']);
for (let f of folderUris) {
const folderUri = this.argToUri(f);
if (folderUri) {
const path = this.parseUri({ folderUri }, parseOptions);
if (path) {
pathsToOpen.push(path);
}
}
}
// file uris
const fileUris = asArray(cli['file-uri']);
for (let f of fileUris) {
const fileUri = this.argToUri(f);
if (fileUri) {
const path = this.parseUri(hasWorkspaceFileExtension(f) ? { workspaceUri: fileUri } : { fileUri }, parseOptions);
if (path) {
pathsToOpen.push(path);
}
}
}
// folder or file paths
const cliArgs = asArray(cli._);
for (let cliArg of cliArgs) {
const path = this.parsePath(cliArg, parseOptions);
if (path) {
pathsToOpen.push(path);
}
}
if (pathsToOpen.length) {
return pathsToOpen;
}
// No path provided, return empty to open empty
return [Object.create(null)];
}
private doGetWindowsFromLastSession(): IPathToOpen[] {
const restoreWindows = this.getRestoreWindowsSetting();
switch (restoreWindows) {
// none: we always open an empty window
case 'none':
return [Object.create(null)];
// one: restore last opened workspace/folder or empty window
// all: restore all windows
// folders: restore last opened folders only
case 'one':
case 'all':
case 'folders':
const openedWindows: IWindowState[] = [];
if (restoreWindows !== 'one') {
openedWindows.push(...this.windowsState.openedWindows);
}
if (this.windowsState.lastActiveWindow) {
openedWindows.push(this.windowsState.lastActiveWindow);
}
const windowsToOpen: IPathToOpen[] = [];
for (const openedWindow of openedWindows) {
if (openedWindow.workspace) { // Workspaces
const pathToOpen = this.parseUri({ workspaceUri: openedWindow.workspace.configPath }, { remoteAuthority: openedWindow.remoteAuthority });
if (pathToOpen && pathToOpen.workspace) {
windowsToOpen.push(pathToOpen);
}
} else if (openedWindow.folderUri) { // Folders
const pathToOpen = this.parseUri({ folderUri: openedWindow.folderUri }, { remoteAuthority: openedWindow.remoteAuthority });
if (pathToOpen && pathToOpen.folderUri) {
windowsToOpen.push(pathToOpen);
}
} else if (restoreWindows !== 'folders' && openedWindow.backupPath && !openedWindow.remoteAuthority) { // Local windows that were empty. Empty windows with backups will always be restored in open()
windowsToOpen.push({ backupPath: openedWindow.backupPath, remoteAuthority: openedWindow.remoteAuthority });
}
}
if (windowsToOpen.length > 0) {
return windowsToOpen;
}
break;
}
// Always fallback to empty window
return [Object.create(null)];
}
private getRestoreWindowsSetting(): RestoreWindowsSetting {
let restoreWindows: RestoreWindowsSetting;
if (this.lifecycleService.wasRestarted) {
restoreWindows = 'all'; // always reopen all windows when an update was applied
} else {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
restoreWindows = ((windowConfig && windowConfig.restoreWindows) || 'one');
if (['all', 'folders', 'one', 'none'].indexOf(restoreWindows) === -1) {
restoreWindows = 'one';
}
}
return restoreWindows;
}
private argToUri(arg: string): URI | undefined {
try {
const uri = URI.parse(arg);
if (!uri.scheme) {
this.logService.error(`Invalid URI input string, scheme missing: ${arg}`);