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