Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/actions/windowActions.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize, localize2 } from '../../../nls.js';
7
import { IWindowOpenable } from '../../../platform/window/common/window.js';
8
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
9
import { MenuRegistry, MenuId, Action2, registerAction2, IAction2Options } from '../../../platform/actions/common/actions.js';
10
import { KeyChord, KeyCode, KeyMod } from '../../../base/common/keyCodes.js';
11
import { IsMainWindowFullscreenContext } from '../../common/contextkeys.js';
12
import { IsMacNativeContext, IsDevelopmentContext, IsWebContext, IsIOSContext } from '../../../platform/contextkey/common/contextkeys.js';
13
import { Categories } from '../../../platform/action/common/actionCommonCategories.js';
14
import { KeybindingsRegistry, KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js';
15
import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js';
16
import { IWorkspaceContextService, IWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js';
17
import { ILabelService, Verbosity } from '../../../platform/label/common/label.js';
18
import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';
19
import { IModelService } from '../../../editor/common/services/model.js';
20
import { ILanguageService } from '../../../editor/common/languages/language.js';
21
import { IRecent, isRecentFolder, isRecentWorkspace, IWorkspacesService } from '../../../platform/workspaces/common/workspaces.js';
22
import { URI } from '../../../base/common/uri.js';
23
import { getIconClasses } from '../../../editor/common/services/getIconClasses.js';
24
import { FileKind } from '../../../platform/files/common/files.js';
25
import { splitRecentLabel } from '../../../base/common/labels.js';
26
import { isMacintosh, isWeb, isWindows } from '../../../base/common/platform.js';
27
import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js';
28
import { inQuickPickContext, getQuickNavigateHandler } from '../quickaccess.js';
29
import { IHostService } from '../../services/host/browser/host.js';
30
import { ResourceMap } from '../../../base/common/map.js';
31
import { Codicon } from '../../../base/common/codicons.js';
32
import { ThemeIcon } from '../../../base/common/themables.js';
33
import { CommandsRegistry } from '../../../platform/commands/common/commands.js';
34
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
35
import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';
36
import { isFolderBackupInfo, isWorkspaceBackupInfo } from '../../../platform/backup/common/backup.js';
37
import { getActiveElement, getActiveWindow, isHTMLElement } from '../../../base/browser/dom.js';
38
39
export const inRecentFilesPickerContextKey = 'inRecentFilesPicker';
40
41
interface IRecentlyOpenedPick extends IQuickPickItem {
42
resource: URI;
43
openable: IWindowOpenable;
44
remoteAuthority: string | undefined;
45
}
46
47
abstract class BaseOpenRecentAction extends Action2 {
48
49
private readonly removeFromRecentlyOpened: IQuickInputButton = {
50
iconClass: ThemeIcon.asClassName(Codicon.removeClose),
51
tooltip: localize('remove', "Remove from Recently Opened")
52
};
53
54
private readonly dirtyRecentlyOpenedFolder: IQuickInputButton = {
55
iconClass: 'dirty-workspace ' + ThemeIcon.asClassName(Codicon.closeDirty),
56
tooltip: localize('dirtyRecentlyOpenedFolder', "Folder With Unsaved Files"),
57
alwaysVisible: true
58
};
59
60
private readonly dirtyRecentlyOpenedWorkspace: IQuickInputButton = {
61
...this.dirtyRecentlyOpenedFolder,
62
tooltip: localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"),
63
};
64
65
constructor(desc: Readonly<IAction2Options>) {
66
super(desc);
67
}
68
69
protected abstract isQuickNavigate(): boolean;
70
71
override async run(accessor: ServicesAccessor): Promise<void> {
72
const workspacesService = accessor.get(IWorkspacesService);
73
const quickInputService = accessor.get(IQuickInputService);
74
const contextService = accessor.get(IWorkspaceContextService);
75
const labelService = accessor.get(ILabelService);
76
const keybindingService = accessor.get(IKeybindingService);
77
const modelService = accessor.get(IModelService);
78
const languageService = accessor.get(ILanguageService);
79
const hostService = accessor.get(IHostService);
80
const dialogService = accessor.get(IDialogService);
81
82
const recentlyOpened = await workspacesService.getRecentlyOpened();
83
const dirtyWorkspacesAndFolders = await workspacesService.getDirtyWorkspaces();
84
85
let hasWorkspaces = false;
86
87
// Identify all folders and workspaces with unsaved files
88
const dirtyFolders = new ResourceMap<boolean>();
89
const dirtyWorkspaces = new ResourceMap<IWorkspaceIdentifier>();
90
for (const dirtyWorkspace of dirtyWorkspacesAndFolders) {
91
if (isFolderBackupInfo(dirtyWorkspace)) {
92
dirtyFolders.set(dirtyWorkspace.folderUri, true);
93
} else {
94
dirtyWorkspaces.set(dirtyWorkspace.workspace.configPath, dirtyWorkspace.workspace);
95
hasWorkspaces = true;
96
}
97
}
98
99
// Identify all recently opened folders and workspaces
100
const recentFolders = new ResourceMap<boolean>();
101
const recentWorkspaces = new ResourceMap<IWorkspaceIdentifier>();
102
for (const recent of recentlyOpened.workspaces) {
103
if (isRecentFolder(recent)) {
104
recentFolders.set(recent.folderUri, true);
105
} else {
106
recentWorkspaces.set(recent.workspace.configPath, recent.workspace);
107
hasWorkspaces = true;
108
}
109
}
110
111
// Fill in all known recently opened workspaces
112
const workspacePicks: IRecentlyOpenedPick[] = [];
113
for (const recent of recentlyOpened.workspaces) {
114
const isDirty = isRecentFolder(recent) ? dirtyFolders.has(recent.folderUri) : dirtyWorkspaces.has(recent.workspace.configPath);
115
116
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, recent, isDirty));
117
}
118
119
// Fill any backup workspace that is not yet shown at the end
120
for (const dirtyWorkspaceOrFolder of dirtyWorkspacesAndFolders) {
121
if (isFolderBackupInfo(dirtyWorkspaceOrFolder) && !recentFolders.has(dirtyWorkspaceOrFolder.folderUri)) {
122
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, true));
123
} else if (isWorkspaceBackupInfo(dirtyWorkspaceOrFolder) && !recentWorkspaces.has(dirtyWorkspaceOrFolder.workspace.configPath)) {
124
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, true));
125
}
126
}
127
128
const filePicks = recentlyOpened.files.map(p => this.toQuickPick(modelService, languageService, labelService, p, false));
129
130
// focus second entry if the first recent workspace is the current workspace
131
const firstEntry = recentlyOpened.workspaces[0];
132
const autoFocusSecondEntry: boolean = firstEntry && contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri);
133
134
let keyMods: IKeyMods | undefined;
135
136
const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: hasWorkspaces ? localize('workspacesAndFolders', "folders & workspaces") : localize('folders', "folders") };
137
const fileSeparator: IQuickPickSeparator = { type: 'separator', label: localize('files', "files") };
138
const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks];
139
140
const pick = await quickInputService.pick(picks, {
141
contextKey: inRecentFilesPickerContextKey,
142
activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0],
143
placeHolder: isMacintosh ? localize('openRecentPlaceholderMac', "Select to open (hold Cmd-key to force new window or Option-key for same window)") : localize('openRecentPlaceholder', "Select to open (hold Ctrl-key to force new window or Alt-key for same window)"),
144
matchOnDescription: true,
145
onKeyMods: mods => keyMods = mods,
146
quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined,
147
hideInput: this.isQuickNavigate(),
148
onDidTriggerItemButton: async context => {
149
150
// Remove
151
if (context.button === this.removeFromRecentlyOpened) {
152
await workspacesService.removeRecentlyOpened([context.item.resource]);
153
context.removeItem();
154
}
155
156
// Dirty Folder/Workspace
157
else if (context.button === this.dirtyRecentlyOpenedFolder || context.button === this.dirtyRecentlyOpenedWorkspace) {
158
const isDirtyWorkspace = context.button === this.dirtyRecentlyOpenedWorkspace;
159
const { confirmed } = await dialogService.confirm({
160
title: isDirtyWorkspace ? localize('dirtyWorkspace', "Workspace with Unsaved Files") : localize('dirtyFolder', "Folder with Unsaved Files"),
161
message: isDirtyWorkspace ? localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the unsaved files?") : localize('dirtyFolderConfirm', "Do you want to open the folder to review the unsaved files?"),
162
detail: isDirtyWorkspace ? localize('dirtyWorkspaceConfirmDetail', "Workspaces with unsaved files cannot be removed until all unsaved files have been saved or reverted.") : localize('dirtyFolderConfirmDetail', "Folders with unsaved files cannot be removed until all unsaved files have been saved or reverted.")
163
});
164
165
if (confirmed) {
166
hostService.openWindow(
167
[context.item.openable], {
168
remoteAuthority: context.item.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
169
});
170
quickInputService.cancel();
171
}
172
}
173
}
174
});
175
176
if (pick) {
177
return hostService.openWindow([pick.openable], {
178
forceNewWindow: keyMods?.ctrlCmd,
179
forceReuseWindow: keyMods?.alt,
180
remoteAuthority: pick.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
181
});
182
}
183
}
184
185
private toQuickPick(modelService: IModelService, languageService: ILanguageService, labelService: ILabelService, recent: IRecent, isDirty: boolean): IRecentlyOpenedPick {
186
let openable: IWindowOpenable | undefined;
187
let iconClasses: string[];
188
let fullLabel: string | undefined;
189
let resource: URI | undefined;
190
let isWorkspace = false;
191
192
// Folder
193
if (isRecentFolder(recent)) {
194
resource = recent.folderUri;
195
iconClasses = getIconClasses(modelService, languageService, resource, FileKind.FOLDER);
196
openable = { folderUri: resource };
197
fullLabel = recent.label || labelService.getWorkspaceLabel(resource, { verbose: Verbosity.LONG });
198
}
199
200
// Workspace
201
else if (isRecentWorkspace(recent)) {
202
resource = recent.workspace.configPath;
203
iconClasses = getIconClasses(modelService, languageService, resource, FileKind.ROOT_FOLDER);
204
openable = { workspaceUri: resource };
205
fullLabel = recent.label || labelService.getWorkspaceLabel(recent.workspace, { verbose: Verbosity.LONG });
206
isWorkspace = true;
207
}
208
209
// File
210
else {
211
resource = recent.fileUri;
212
iconClasses = getIconClasses(modelService, languageService, resource, FileKind.FILE);
213
openable = { fileUri: resource };
214
fullLabel = recent.label || labelService.getUriLabel(resource, { appendWorkspaceSuffix: true });
215
}
216
217
const { name, parentPath } = splitRecentLabel(fullLabel);
218
219
return {
220
iconClasses,
221
label: name,
222
ariaLabel: isDirty ? isWorkspace ? localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name,
223
description: parentPath,
224
buttons: isDirty ? [isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder] : [this.removeFromRecentlyOpened],
225
openable,
226
resource,
227
remoteAuthority: recent.remoteAuthority
228
};
229
}
230
}
231
232
export class OpenRecentAction extends BaseOpenRecentAction {
233
234
static ID = 'workbench.action.openRecent';
235
236
constructor() {
237
super({
238
id: OpenRecentAction.ID,
239
title: {
240
...localize2('openRecent', "Open Recent..."),
241
mnemonicTitle: localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More..."),
242
},
243
category: Categories.File,
244
f1: true,
245
keybinding: {
246
weight: KeybindingWeight.WorkbenchContrib,
247
primary: KeyMod.CtrlCmd | KeyCode.KeyR,
248
mac: { primary: KeyMod.WinCtrl | KeyCode.KeyR }
249
},
250
menu: {
251
id: MenuId.MenubarRecentMenu,
252
group: 'y_more',
253
order: 1
254
}
255
});
256
}
257
258
protected isQuickNavigate(): boolean {
259
return false;
260
}
261
}
262
263
class QuickPickRecentAction extends BaseOpenRecentAction {
264
265
constructor() {
266
super({
267
id: 'workbench.action.quickOpenRecent',
268
title: localize2('quickOpenRecent', 'Quick Open Recent...'),
269
category: Categories.File,
270
f1: false // hide quick pickers from command palette to not confuse with the other entry that shows a input field
271
});
272
}
273
274
protected isQuickNavigate(): boolean {
275
return true;
276
}
277
}
278
279
class ToggleFullScreenAction extends Action2 {
280
281
constructor() {
282
super({
283
id: 'workbench.action.toggleFullScreen',
284
title: {
285
...localize2('toggleFullScreen', "Toggle Full Screen"),
286
mnemonicTitle: localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "&&Full Screen"),
287
},
288
category: Categories.View,
289
f1: true,
290
keybinding: {
291
weight: KeybindingWeight.WorkbenchContrib,
292
primary: KeyCode.F11,
293
mac: {
294
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyF
295
}
296
},
297
precondition: IsIOSContext.toNegated(),
298
toggled: IsMainWindowFullscreenContext,
299
menu: [{
300
id: MenuId.MenubarAppearanceMenu,
301
group: '1_toggle_view',
302
order: 1
303
}]
304
});
305
}
306
307
override run(accessor: ServicesAccessor): Promise<void> {
308
const hostService = accessor.get(IHostService);
309
310
return hostService.toggleFullScreen(getActiveWindow());
311
}
312
}
313
314
export class ReloadWindowAction extends Action2 {
315
316
static readonly ID = 'workbench.action.reloadWindow';
317
318
constructor() {
319
super({
320
id: ReloadWindowAction.ID,
321
title: localize2('reloadWindow', 'Reload Window'),
322
category: Categories.Developer,
323
f1: true,
324
keybinding: {
325
weight: KeybindingWeight.WorkbenchContrib + 50,
326
when: IsDevelopmentContext,
327
primary: KeyMod.CtrlCmd | KeyCode.KeyR
328
}
329
});
330
}
331
332
override async run(accessor: ServicesAccessor): Promise<void> {
333
const hostService = accessor.get(IHostService);
334
335
return hostService.reload();
336
}
337
}
338
339
class ShowAboutDialogAction extends Action2 {
340
341
constructor() {
342
super({
343
id: 'workbench.action.showAboutDialog',
344
title: {
345
...localize2('about', "About"),
346
mnemonicTitle: localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About"),
347
},
348
category: Categories.Help,
349
f1: true,
350
menu: {
351
id: MenuId.MenubarHelpMenu,
352
group: 'z_about',
353
order: 1,
354
when: IsMacNativeContext.toNegated()
355
}
356
});
357
}
358
359
override run(accessor: ServicesAccessor): Promise<void> {
360
const dialogService = accessor.get(IDialogService);
361
362
return dialogService.about();
363
}
364
}
365
366
class NewWindowAction extends Action2 {
367
368
constructor() {
369
super({
370
id: 'workbench.action.newWindow',
371
title: {
372
...localize2('newWindow', "New Window"),
373
mnemonicTitle: localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window"),
374
},
375
f1: true,
376
keybinding: {
377
weight: KeybindingWeight.WorkbenchContrib,
378
primary: isWeb ? (isWindows ? KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.KeyN) : KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyN) : KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN,
379
secondary: isWeb ? [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN] : undefined
380
},
381
menu: {
382
id: MenuId.MenubarFileMenu,
383
group: '1_new',
384
order: 3
385
}
386
});
387
}
388
389
override run(accessor: ServicesAccessor): Promise<void> {
390
const hostService = accessor.get(IHostService);
391
392
return hostService.openWindow({ remoteAuthority: null });
393
}
394
}
395
396
class BlurAction extends Action2 {
397
398
constructor() {
399
super({
400
id: 'workbench.action.blur',
401
title: localize2('blur', 'Remove keyboard focus from focused element')
402
});
403
}
404
405
run(): void {
406
const activeElement = getActiveElement();
407
if (isHTMLElement(activeElement)) {
408
activeElement.blur();
409
}
410
}
411
}
412
413
// --- Actions Registration
414
415
registerAction2(NewWindowAction);
416
registerAction2(ToggleFullScreenAction);
417
registerAction2(QuickPickRecentAction);
418
registerAction2(OpenRecentAction);
419
registerAction2(ReloadWindowAction);
420
registerAction2(ShowAboutDialogAction);
421
registerAction2(BlurAction);
422
423
// --- Commands/Keybindings Registration
424
425
const recentFilesPickerContext = ContextKeyExpr.and(inQuickPickContext, ContextKeyExpr.has(inRecentFilesPickerContextKey));
426
427
const quickPickNavigateNextInRecentFilesPickerId = 'workbench.action.quickOpenNavigateNextInRecentFilesPicker';
428
KeybindingsRegistry.registerCommandAndKeybindingRule({
429
id: quickPickNavigateNextInRecentFilesPickerId,
430
weight: KeybindingWeight.WorkbenchContrib + 50,
431
handler: getQuickNavigateHandler(quickPickNavigateNextInRecentFilesPickerId, true),
432
when: recentFilesPickerContext,
433
primary: KeyMod.CtrlCmd | KeyCode.KeyR,
434
mac: { primary: KeyMod.WinCtrl | KeyCode.KeyR }
435
});
436
437
const quickPickNavigatePreviousInRecentFilesPicker = 'workbench.action.quickOpenNavigatePreviousInRecentFilesPicker';
438
KeybindingsRegistry.registerCommandAndKeybindingRule({
439
id: quickPickNavigatePreviousInRecentFilesPicker,
440
weight: KeybindingWeight.WorkbenchContrib + 50,
441
handler: getQuickNavigateHandler(quickPickNavigatePreviousInRecentFilesPicker, false),
442
when: recentFilesPickerContext,
443
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyR,
444
mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyR }
445
});
446
447
CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', accessor => {
448
const configurationService = accessor.get(IConfigurationService);
449
const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue;
450
451
return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never');
452
});
453
454
// --- Menu Registration
455
456
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
457
group: 'z_ConfirmClose',
458
command: {
459
id: 'workbench.action.toggleConfirmBeforeClose',
460
title: localize('miConfirmClose', "Confirm Before Close"),
461
toggled: ContextKeyExpr.notEquals('config.window.confirmBeforeClose', 'never')
462
},
463
order: 1,
464
when: IsWebContext
465
});
466
467
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
468
title: localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent"),
469
submenu: MenuId.MenubarRecentMenu,
470
group: '2_open',
471
order: 4
472
});
473
474