Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/electron-browser/actions/windowActions.ts
5220 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 './media/actions.css';
7
import { URI } from '../../../base/common/uri.js';
8
import { localize, localize2 } from '../../../nls.js';
9
import { ApplyZoomTarget, MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, applyZoom } from '../../../platform/window/electron-browser/window.js';
10
import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';
11
import { getZoomLevel } from '../../../base/browser/browser.js';
12
import { FileKind } from '../../../platform/files/common/files.js';
13
import { IModelService } from '../../../editor/common/services/model.js';
14
import { ILanguageService } from '../../../editor/common/languages/language.js';
15
import { IQuickInputService, IQuickInputButton, IQuickPickItem, QuickPickInput } from '../../../platform/quickinput/common/quickInput.js';
16
import { getIconClasses } from '../../../editor/common/services/getIconClasses.js';
17
import { ICommandHandler } from '../../../platform/commands/common/commands.js';
18
import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';
19
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
20
import { INativeHostService } from '../../../platform/native/common/native.js';
21
import { Codicon } from '../../../base/common/codicons.js';
22
import { ThemeIcon } from '../../../base/common/themables.js';
23
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js';
24
import { Action2, MenuId } from '../../../platform/actions/common/actions.js';
25
import { Categories } from '../../../platform/action/common/actionCommonCategories.js';
26
import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js';
27
import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js';
28
import { isMacintosh } from '../../../base/common/platform.js';
29
import { getActiveWindow } from '../../../base/browser/dom.js';
30
import { IOpenedAuxiliaryWindow, IOpenedMainWindow, isOpenedAuxiliaryWindow } from '../../../platform/window/common/window.js';
31
import { IsAuxiliaryWindowContext, IsAuxiliaryWindowFocusedContext, IsWindowAlwaysOnTopContext } from '../../common/contextkeys.js';
32
import { isAuxiliaryWindow } from '../../../base/browser/window.js';
33
import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js';
34
35
export class CloseWindowAction extends Action2 {
36
37
static readonly ID = 'workbench.action.closeWindow';
38
39
constructor() {
40
super({
41
id: CloseWindowAction.ID,
42
title: {
43
...localize2('closeWindow', "Close Window"),
44
mnemonicTitle: localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window"),
45
},
46
f1: true,
47
keybinding: {
48
weight: KeybindingWeight.WorkbenchContrib,
49
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyW },
50
linux: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyW] },
51
win: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyW] }
52
},
53
menu: {
54
id: MenuId.MenubarFileMenu,
55
group: '6_close',
56
order: 4
57
}
58
});
59
}
60
61
override async run(accessor: ServicesAccessor): Promise<void> {
62
const nativeHostService = accessor.get(INativeHostService);
63
64
return nativeHostService.closeWindow({ targetWindowId: getActiveWindow().vscodeWindowId });
65
}
66
}
67
68
export class CloseOtherWindowsAction extends Action2 {
69
70
private static readonly ID = 'workbench.action.closeOtherWindows';
71
72
constructor() {
73
super({
74
id: CloseOtherWindowsAction.ID,
75
title: localize2('closeOtherWindows', "Close Other Windows"),
76
f1: true
77
});
78
}
79
80
override async run(accessor: ServicesAccessor): Promise<void> {
81
const nativeHostService = accessor.get(INativeHostService);
82
83
const currentWindowId = getActiveWindow().vscodeWindowId;
84
const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: false });
85
86
for (const window of windows) {
87
if (window.id !== currentWindowId) {
88
nativeHostService.closeWindow({ targetWindowId: window.id });
89
}
90
}
91
}
92
}
93
94
abstract class BaseZoomAction extends Action2 {
95
96
private static readonly ZOOM_LEVEL_SETTING_KEY = 'window.zoomLevel';
97
private static readonly ZOOM_PER_WINDOW_SETTING_KEY = 'window.zoomPerWindow';
98
99
protected async setZoomLevel(accessor: ServicesAccessor, levelOrReset: number | true): Promise<void> {
100
const configurationService = accessor.get(IConfigurationService);
101
102
let target: ApplyZoomTarget;
103
if (configurationService.getValue(BaseZoomAction.ZOOM_PER_WINDOW_SETTING_KEY) !== false) {
104
target = ApplyZoomTarget.ACTIVE_WINDOW;
105
} else {
106
target = ApplyZoomTarget.ALL_WINDOWS;
107
}
108
109
let level: number;
110
if (typeof levelOrReset === 'number') {
111
level = Math.round(levelOrReset); // prevent fractional zoom levels
112
} else {
113
114
// reset to 0 when we apply to all windows
115
if (target === ApplyZoomTarget.ALL_WINDOWS) {
116
level = 0;
117
}
118
119
// otherwise, reset to the default zoom level
120
else {
121
const defaultLevel = configurationService.getValue(BaseZoomAction.ZOOM_LEVEL_SETTING_KEY);
122
if (typeof defaultLevel === 'number') {
123
level = defaultLevel;
124
} else {
125
level = 0;
126
}
127
}
128
}
129
130
if (level > MAX_ZOOM_LEVEL || level < MIN_ZOOM_LEVEL) {
131
return; // https://github.com/microsoft/vscode/issues/48357
132
}
133
134
if (target === ApplyZoomTarget.ALL_WINDOWS) {
135
await configurationService.updateValue(BaseZoomAction.ZOOM_LEVEL_SETTING_KEY, level);
136
}
137
138
applyZoom(level, target);
139
}
140
}
141
142
export class ZoomInAction extends BaseZoomAction {
143
144
constructor() {
145
super({
146
id: 'workbench.action.zoomIn',
147
title: {
148
...localize2('zoomIn', "Zoom In"),
149
mnemonicTitle: localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"),
150
},
151
category: Categories.View,
152
f1: true,
153
keybinding: {
154
weight: KeybindingWeight.WorkbenchContrib,
155
primary: KeyMod.CtrlCmd | KeyCode.Equal,
156
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Equal, KeyMod.CtrlCmd | KeyCode.NumpadAdd]
157
},
158
menu: {
159
id: MenuId.MenubarAppearanceMenu,
160
group: '5_zoom',
161
order: 1
162
}
163
});
164
}
165
166
override run(accessor: ServicesAccessor): Promise<void> {
167
return super.setZoomLevel(accessor, getZoomLevel(getActiveWindow()) + 1);
168
}
169
}
170
171
export class ZoomOutAction extends BaseZoomAction {
172
173
constructor() {
174
super({
175
id: 'workbench.action.zoomOut',
176
title: {
177
...localize2('zoomOut', "Zoom Out"),
178
mnemonicTitle: localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out"),
179
},
180
category: Categories.View,
181
f1: true,
182
keybinding: {
183
weight: KeybindingWeight.WorkbenchContrib,
184
primary: KeyMod.CtrlCmd | KeyCode.Minus,
185
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus, KeyMod.CtrlCmd | KeyCode.NumpadSubtract],
186
linux: {
187
primary: KeyMod.CtrlCmd | KeyCode.Minus,
188
secondary: [KeyMod.CtrlCmd | KeyCode.NumpadSubtract]
189
}
190
},
191
menu: {
192
id: MenuId.MenubarAppearanceMenu,
193
group: '5_zoom',
194
order: 2
195
}
196
});
197
}
198
199
override run(accessor: ServicesAccessor): Promise<void> {
200
return super.setZoomLevel(accessor, getZoomLevel(getActiveWindow()) - 1);
201
}
202
}
203
204
export class ZoomResetAction extends BaseZoomAction {
205
206
constructor() {
207
super({
208
id: 'workbench.action.zoomReset',
209
title: {
210
...localize2('zoomReset', "Reset Zoom"),
211
mnemonicTitle: localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"),
212
},
213
category: Categories.View,
214
f1: true,
215
keybinding: {
216
weight: KeybindingWeight.WorkbenchContrib,
217
primary: KeyMod.CtrlCmd | KeyCode.Numpad0
218
},
219
menu: {
220
id: MenuId.MenubarAppearanceMenu,
221
group: '5_zoom',
222
order: 3
223
}
224
});
225
}
226
227
override run(accessor: ServicesAccessor): Promise<void> {
228
return super.setZoomLevel(accessor, true);
229
}
230
}
231
232
abstract class BaseSwitchWindow extends Action2 {
233
234
private readonly closeWindowAction: IQuickInputButton = {
235
iconClass: ThemeIcon.asClassName(Codicon.removeClose),
236
tooltip: localize('close', "Close Window")
237
};
238
239
private readonly closeDirtyWindowAction: IQuickInputButton = {
240
iconClass: 'dirty-window ' + ThemeIcon.asClassName(Codicon.closeDirty),
241
tooltip: localize('close', "Close Window"),
242
alwaysVisible: true
243
};
244
245
private readonly closeActiveWindowAction: IQuickInputButton = {
246
iconClass: 'active-window ' + ThemeIcon.asClassName(Codicon.windowActive),
247
tooltip: localize('closeActive', "Close Active Window"),
248
alwaysVisible: true
249
};
250
251
protected abstract isQuickNavigate(): boolean;
252
253
override async run(accessor: ServicesAccessor): Promise<void> {
254
const quickInputService = accessor.get(IQuickInputService);
255
const keybindingService = accessor.get(IKeybindingService);
256
const modelService = accessor.get(IModelService);
257
const languageService = accessor.get(ILanguageService);
258
const nativeHostService = accessor.get(INativeHostService);
259
260
const currentWindowId = getActiveWindow().vscodeWindowId;
261
262
const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: true });
263
264
const mainWindows = new Set<IOpenedMainWindow>();
265
const mapMainWindowToAuxiliaryWindows = new Map<number, Set<IOpenedAuxiliaryWindow>>();
266
for (const window of windows) {
267
if (isOpenedAuxiliaryWindow(window)) {
268
let auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.parentId);
269
if (!auxiliaryWindows) {
270
auxiliaryWindows = new Set<IOpenedAuxiliaryWindow>();
271
mapMainWindowToAuxiliaryWindows.set(window.parentId, auxiliaryWindows);
272
}
273
auxiliaryWindows.add(window);
274
} else {
275
mainWindows.add(window);
276
}
277
}
278
279
interface IWindowPickItem extends IQuickPickItem {
280
readonly windowId: number;
281
}
282
283
function isWindowPickItem(candidate: unknown): candidate is IWindowPickItem {
284
const windowPickItem = candidate as IWindowPickItem | undefined;
285
286
return typeof windowPickItem?.windowId === 'number';
287
}
288
289
const picks: Array<QuickPickInput<IWindowPickItem>> = [];
290
for (const window of mainWindows) {
291
const auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.id);
292
if (mapMainWindowToAuxiliaryWindows.size > 0) {
293
picks.push({ type: 'separator', label: auxiliaryWindows ? localize('windowGroup', "window group") : undefined });
294
}
295
296
const resource = window.filename ? URI.file(window.filename) : isSingleFolderWorkspaceIdentifier(window.workspace) ? window.workspace.uri : isWorkspaceIdentifier(window.workspace) ? window.workspace.configPath : undefined;
297
const fileKind = window.filename ? FileKind.FILE : isSingleFolderWorkspaceIdentifier(window.workspace) ? FileKind.FOLDER : isWorkspaceIdentifier(window.workspace) ? FileKind.ROOT_FOLDER : FileKind.FILE;
298
const pick: IWindowPickItem = {
299
windowId: window.id,
300
label: window.title,
301
ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, window with unsaved changes", window.title) : window.title,
302
iconClasses: getIconClasses(modelService, languageService, resource, fileKind),
303
description: (currentWindowId === window.id) ? localize('current', "Current Window") : undefined,
304
buttons: window.dirty ? [this.closeDirtyWindowAction] : currentWindowId === window.id ? [this.closeActiveWindowAction] : [this.closeWindowAction]
305
};
306
picks.push(pick);
307
308
if (auxiliaryWindows) {
309
for (const auxiliaryWindow of auxiliaryWindows) {
310
const pick: IWindowPickItem = {
311
windowId: auxiliaryWindow.id,
312
label: auxiliaryWindow.title,
313
iconClasses: getIconClasses(modelService, languageService, auxiliaryWindow.filename ? URI.file(auxiliaryWindow.filename) : undefined, FileKind.FILE),
314
description: (currentWindowId === auxiliaryWindow.id) ? localize('current', "Current Window") : undefined,
315
buttons: currentWindowId === auxiliaryWindow.id ? [this.closeActiveWindowAction] : [this.closeWindowAction]
316
};
317
picks.push(pick);
318
}
319
}
320
}
321
322
const pick = await quickInputService.pick(picks, {
323
contextKey: 'inWindowsPicker',
324
activeItem: (() => {
325
for (let i = 0; i < picks.length; i++) {
326
const pick = picks[i];
327
if (isWindowPickItem(pick) && pick.windowId === currentWindowId) {
328
let nextPick = picks[i + 1]; // try to select next window unless it's a separator
329
if (isWindowPickItem(nextPick)) {
330
return nextPick;
331
}
332
333
nextPick = picks[i + 2]; // otherwise try to select the next window after the separator
334
if (isWindowPickItem(nextPick)) {
335
return nextPick;
336
}
337
}
338
}
339
340
return undefined;
341
})(),
342
placeHolder: localize('switchWindowPlaceHolder', "Select a window to switch to"),
343
quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined,
344
hideInput: this.isQuickNavigate(),
345
onDidTriggerItemButton: async context => {
346
await nativeHostService.closeWindow({ targetWindowId: context.item.windowId });
347
context.removeItem();
348
}
349
});
350
351
if (pick) {
352
nativeHostService.focusWindow({ targetWindowId: pick.windowId });
353
}
354
}
355
}
356
357
export class SwitchWindowAction extends BaseSwitchWindow {
358
359
constructor() {
360
super({
361
id: 'workbench.action.switchWindow',
362
title: localize2('switchWindow', 'Switch Window...'),
363
f1: true,
364
keybinding: {
365
weight: KeybindingWeight.WorkbenchContrib,
366
primary: 0,
367
mac: { primary: KeyMod.WinCtrl | KeyCode.KeyW }
368
}
369
});
370
}
371
372
protected isQuickNavigate(): boolean {
373
return false;
374
}
375
}
376
377
export class QuickSwitchWindowAction extends BaseSwitchWindow {
378
379
constructor() {
380
super({
381
id: 'workbench.action.quickSwitchWindow',
382
title: localize2('quickSwitchWindow', 'Quick Switch Window...'),
383
f1: false // hide quick pickers from command palette to not confuse with the other entry that shows a input field
384
});
385
}
386
387
protected isQuickNavigate(): boolean {
388
return true;
389
}
390
}
391
392
function canRunNativeTabsHandler(accessor: ServicesAccessor): boolean {
393
if (!isMacintosh) {
394
return false;
395
}
396
397
const configurationService = accessor.get(IConfigurationService);
398
return configurationService.getValue<unknown>('window.nativeTabs') === true;
399
}
400
401
export const NewWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {
402
if (!canRunNativeTabsHandler(accessor)) {
403
return;
404
}
405
406
return accessor.get(INativeHostService).newWindowTab();
407
};
408
409
export const ShowPreviousWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {
410
if (!canRunNativeTabsHandler(accessor)) {
411
return;
412
}
413
414
return accessor.get(INativeHostService).showPreviousWindowTab();
415
};
416
417
export const ShowNextWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {
418
if (!canRunNativeTabsHandler(accessor)) {
419
return;
420
}
421
422
return accessor.get(INativeHostService).showNextWindowTab();
423
};
424
425
export const MoveWindowTabToNewWindowHandler: ICommandHandler = function (accessor: ServicesAccessor) {
426
if (!canRunNativeTabsHandler(accessor)) {
427
return;
428
}
429
430
return accessor.get(INativeHostService).moveWindowTabToNewWindow();
431
};
432
433
export const MergeWindowTabsHandlerHandler: ICommandHandler = function (accessor: ServicesAccessor) {
434
if (!canRunNativeTabsHandler(accessor)) {
435
return;
436
}
437
438
return accessor.get(INativeHostService).mergeAllWindowTabs();
439
};
440
441
export const ToggleWindowTabsBarHandler: ICommandHandler = function (accessor: ServicesAccessor) {
442
if (!canRunNativeTabsHandler(accessor)) {
443
return;
444
}
445
446
return accessor.get(INativeHostService).toggleWindowTabsBar();
447
};
448
449
export class ToggleWindowAlwaysOnTopAction extends Action2 {
450
451
static readonly ID = 'workbench.action.toggleWindowAlwaysOnTop';
452
453
constructor() {
454
super({
455
id: ToggleWindowAlwaysOnTopAction.ID,
456
title: localize2('toggleWindowAlwaysOnTop', "Toggle Window Always on Top"),
457
f1: true,
458
precondition: IsAuxiliaryWindowFocusedContext
459
});
460
}
461
462
override async run(accessor: ServicesAccessor): Promise<void> {
463
const nativeHostService = accessor.get(INativeHostService);
464
465
const targetWindow = getActiveWindow();
466
if (!isAuxiliaryWindow(targetWindow.window)) {
467
return; // Currently, we only support toggling always on top for auxiliary windows
468
}
469
470
return nativeHostService.toggleWindowAlwaysOnTop({ targetWindowId: getActiveWindow().vscodeWindowId });
471
}
472
}
473
474
export class EnableWindowAlwaysOnTopAction extends Action2 {
475
476
static readonly ID = 'workbench.action.enableWindowAlwaysOnTop';
477
478
constructor() {
479
super({
480
id: EnableWindowAlwaysOnTopAction.ID,
481
title: localize('enableWindowAlwaysOnTop', "Turn On Always on Top"),
482
icon: Codicon.pin,
483
menu: {
484
id: MenuId.LayoutControlMenu,
485
when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext.toNegated(), IsAuxiliaryWindowContext),
486
order: 1
487
}
488
});
489
}
490
491
override async run(accessor: ServicesAccessor): Promise<void> {
492
const nativeHostService = accessor.get(INativeHostService);
493
494
const targetWindow = getActiveWindow();
495
if (!isAuxiliaryWindow(targetWindow.window)) {
496
return; // Currently, we only support toggling always on top for auxiliary windows
497
}
498
499
return nativeHostService.setWindowAlwaysOnTop(true, { targetWindowId: targetWindow.vscodeWindowId });
500
}
501
}
502
503
export class DisableWindowAlwaysOnTopAction extends Action2 {
504
505
static readonly ID = 'workbench.action.disableWindowAlwaysOnTop';
506
507
constructor() {
508
super({
509
id: DisableWindowAlwaysOnTopAction.ID,
510
title: localize('disableWindowAlwaysOnTop', "Turn Off Always on Top"),
511
icon: Codicon.pinned,
512
menu: {
513
id: MenuId.LayoutControlMenu,
514
when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext, IsAuxiliaryWindowContext),
515
order: 1
516
}
517
});
518
}
519
520
override async run(accessor: ServicesAccessor): Promise<void> {
521
const nativeHostService = accessor.get(INativeHostService);
522
523
const targetWindow = getActiveWindow();
524
if (!isAuxiliaryWindow(targetWindow.window)) {
525
return; // Currently, we only support toggling always on top for auxiliary windows
526
}
527
528
return nativeHostService.setWindowAlwaysOnTop(false, { targetWindowId: targetWindow.vscodeWindowId });
529
}
530
}
531
532