Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/host/browser/browserHostService.ts
5253 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 { Emitter, Event } from '../../../../base/common/event.js';
7
import { IHostService, IToastOptions, IToastResult } from './host.js';
8
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
9
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
10
import { IEditorService } from '../../editor/common/editorService.js';
11
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
12
import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen, IOpenedMainWindow, IOpenedAuxiliaryWindow } from '../../../../platform/window/common/window.js';
13
import { isResourceEditorInput, pathsToEditors } from '../../../common/editor.js';
14
import { whenEditorClosed } from '../../../browser/editor.js';
15
import { IWorkspace, IWorkspaceProvider } from '../../../browser/web.api.js';
16
import { IFileService } from '../../../../platform/files/common/files.js';
17
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
18
import { EventType, ModifierKeyEmitter, addDisposableListener, addDisposableThrottledListener, detectFullscreen, disposableWindowInterval, getActiveDocument, getActiveWindow, getWindowId, onDidRegisterWindow, trackFocus, getWindows as getDOMWindows } from '../../../../base/browser/dom.js';
19
import { Disposable, DisposableSet, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
20
import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';
21
import { memoize } from '../../../../base/common/decorators.js';
22
import { parseLineAndColumnAware } from '../../../../base/common/extpath.js';
23
import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/common/workspaces.js';
24
import { IWorkspaceEditingService } from '../../workspaces/common/workspaceEditing.js';
25
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
26
import { ILifecycleService, BeforeShutdownEvent, ShutdownReason } from '../../lifecycle/common/lifecycle.js';
27
import { BrowserLifecycleService } from '../../lifecycle/browser/lifecycleService.js';
28
import { ILogService } from '../../../../platform/log/common/log.js';
29
import { getWorkspaceIdentifier } from '../../workspaces/browser/workspaces.js';
30
import { localize } from '../../../../nls.js';
31
import Severity from '../../../../base/common/severity.js';
32
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
33
import { DomEmitter } from '../../../../base/browser/event.js';
34
import { isUndefined } from '../../../../base/common/types.js';
35
import { isTemporaryWorkspace, IWorkspaceContextService, toWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js';
36
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
37
import { Schemas } from '../../../../base/common/network.js';
38
import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js';
39
import { coalesce } from '../../../../base/common/arrays.js';
40
import { mainWindow, isAuxiliaryWindow } from '../../../../base/browser/window.js';
41
import { isIOS, isMacintosh } from '../../../../base/common/platform.js';
42
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
43
import { URI } from '../../../../base/common/uri.js';
44
import { VSBuffer } from '../../../../base/common/buffer.js';
45
import { MarkdownString } from '../../../../base/common/htmlContent.js';
46
import { CancellationToken } from '../../../../base/common/cancellation.js';
47
import { showBrowserToast } from './toasts.js';
48
49
enum HostShutdownReason {
50
51
/**
52
* An unknown shutdown reason.
53
*/
54
Unknown = 1,
55
56
/**
57
* A shutdown that was potentially triggered by keyboard use.
58
*/
59
Keyboard = 2,
60
61
/**
62
* An explicit shutdown via code.
63
*/
64
Api = 3
65
}
66
67
export class BrowserHostService extends Disposable implements IHostService {
68
69
declare readonly _serviceBrand: undefined;
70
71
private workspaceProvider: IWorkspaceProvider;
72
73
private shutdownReason = HostShutdownReason.Unknown;
74
75
constructor(
76
@ILayoutService private readonly layoutService: ILayoutService,
77
@IConfigurationService private readonly configurationService: IConfigurationService,
78
@IFileService private readonly fileService: IFileService,
79
@ILabelService private readonly labelService: ILabelService,
80
@IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService,
81
@IInstantiationService private readonly instantiationService: IInstantiationService,
82
@ILifecycleService private readonly lifecycleService: BrowserLifecycleService,
83
@ILogService private readonly logService: ILogService,
84
@IDialogService private readonly dialogService: IDialogService,
85
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
86
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService
87
) {
88
super();
89
90
if (environmentService.options?.workspaceProvider) {
91
this.workspaceProvider = environmentService.options.workspaceProvider;
92
} else {
93
this.workspaceProvider = new class implements IWorkspaceProvider {
94
readonly workspace = undefined;
95
readonly trusted = undefined;
96
async open() { return true; }
97
};
98
}
99
100
this.registerListeners();
101
}
102
103
104
private registerListeners(): void {
105
106
// Veto shutdown depending on `window.confirmBeforeClose` setting
107
this._register(this.lifecycleService.onBeforeShutdown(e => this.onBeforeShutdown(e)));
108
109
// Track modifier keys to detect keybinding usage
110
this._register(ModifierKeyEmitter.getInstance().event(() => this.updateShutdownReasonFromEvent()));
111
112
// Make sure to hide all toasts when the window gains focus
113
this._register(this.onDidChangeFocus(focus => {
114
if (focus) {
115
this.clearToasts();
116
}
117
}));
118
}
119
120
private onBeforeShutdown(e: BeforeShutdownEvent): void {
121
122
switch (this.shutdownReason) {
123
124
// Unknown / Keyboard shows veto depending on setting
125
case HostShutdownReason.Unknown:
126
case HostShutdownReason.Keyboard: {
127
const confirmBeforeClose = this.configurationService.getValue('window.confirmBeforeClose');
128
if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && this.shutdownReason === HostShutdownReason.Keyboard)) {
129
e.veto(true, 'veto.confirmBeforeClose');
130
}
131
break;
132
}
133
// Api never shows veto
134
case HostShutdownReason.Api:
135
break;
136
}
137
138
// Unset for next shutdown
139
this.shutdownReason = HostShutdownReason.Unknown;
140
}
141
142
private updateShutdownReasonFromEvent(): void {
143
if (this.shutdownReason === HostShutdownReason.Api) {
144
return; // do not overwrite any explicitly set shutdown reason
145
}
146
147
if (ModifierKeyEmitter.getInstance().isModifierPressed) {
148
this.shutdownReason = HostShutdownReason.Keyboard;
149
} else {
150
this.shutdownReason = HostShutdownReason.Unknown;
151
}
152
}
153
154
//#region Focus
155
156
@memoize
157
get onDidChangeFocus(): Event<boolean> {
158
const emitter = this._register(new Emitter<boolean>());
159
160
this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {
161
const focusTracker = disposables.add(trackFocus(window));
162
const visibilityTracker = disposables.add(new DomEmitter(window.document, 'visibilitychange'));
163
164
Event.any(
165
Event.map(focusTracker.onDidFocus, () => this.hasFocus, disposables),
166
Event.map(focusTracker.onDidBlur, () => this.hasFocus, disposables),
167
Event.map(visibilityTracker.event, () => this.hasFocus, disposables),
168
Event.map(this.onDidChangeActiveWindow, () => this.hasFocus, disposables),
169
)(focus => emitter.fire(focus), undefined, disposables);
170
}, { window: mainWindow, disposables: this._store }));
171
172
return Event.latch(emitter.event, undefined, this._store);
173
}
174
175
get hasFocus(): boolean {
176
return getActiveDocument().hasFocus();
177
}
178
179
async hadLastFocus(): Promise<boolean> {
180
return true;
181
}
182
183
async focus(targetWindow: Window): Promise<void> {
184
targetWindow.focus();
185
}
186
187
//#endregion
188
189
190
//#region Window
191
192
@memoize
193
get onDidChangeActiveWindow(): Event<number> {
194
const emitter = this._register(new Emitter<number>());
195
196
this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {
197
const windowId = getWindowId(window);
198
199
// Emit via focus tracking
200
const focusTracker = disposables.add(trackFocus(window));
201
disposables.add(focusTracker.onDidFocus(() => emitter.fire(windowId)));
202
203
// Emit via interval: immediately when opening an auxiliary window,
204
// it is possible that document focus has not yet changed, so we
205
// poll for a while to ensure we catch the event.
206
if (isAuxiliaryWindow(window)) {
207
disposables.add(disposableWindowInterval(window, () => {
208
const hasFocus = window.document.hasFocus();
209
if (hasFocus) {
210
emitter.fire(windowId);
211
}
212
213
return hasFocus;
214
}, 100, 20));
215
}
216
}, { window: mainWindow, disposables: this._store }));
217
218
return Event.latch(emitter.event, undefined, this._store);
219
}
220
221
@memoize
222
get onDidChangeFullScreen(): Event<{ windowId: number; fullscreen: boolean }> {
223
const emitter = this._register(new Emitter<{ windowId: number; fullscreen: boolean }>());
224
225
this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {
226
const windowId = getWindowId(window);
227
const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */;
228
229
// Fullscreen (Browser)
230
for (const event of [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE]) {
231
disposables.add(addDisposableListener(window.document, event, () => emitter.fire({ windowId, fullscreen: !!detectFullscreen(window) })));
232
}
233
234
// Fullscreen (Native)
235
disposables.add(addDisposableThrottledListener(viewport, EventType.RESIZE, () => emitter.fire({ windowId, fullscreen: !!detectFullscreen(window) }), undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */));
236
}, { window: mainWindow, disposables: this._store }));
237
238
return emitter.event;
239
}
240
241
openWindow(options?: IOpenEmptyWindowOptions): Promise<void>;
242
openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void>;
243
openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise<void> {
244
if (Array.isArray(arg1)) {
245
return this.doOpenWindow(arg1, arg2);
246
}
247
248
return this.doOpenEmptyWindow(arg1);
249
}
250
251
private async doOpenWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void> {
252
const payload = this.preservePayload(false /* not an empty window */, options);
253
const fileOpenables: IFileToOpen[] = [];
254
255
const foldersToAdd: IWorkspaceFolderCreationData[] = [];
256
const foldersToRemove: URI[] = [];
257
258
for (const openable of toOpen) {
259
openable.label = openable.label || this.getRecentLabel(openable);
260
261
// Folder
262
if (isFolderToOpen(openable)) {
263
if (options?.addMode) {
264
foldersToAdd.push({ uri: openable.folderUri });
265
} else if (options?.removeMode) {
266
foldersToRemove.push(openable.folderUri);
267
} else {
268
this.doOpen({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options, false /* no file */), payload });
269
}
270
}
271
272
// Workspace
273
else if (isWorkspaceToOpen(openable)) {
274
this.doOpen({ workspaceUri: openable.workspaceUri }, { reuse: this.shouldReuse(options, false /* no file */), payload });
275
}
276
277
// File (handled later in bulk)
278
else if (isFileToOpen(openable)) {
279
fileOpenables.push(openable);
280
}
281
}
282
283
// Handle Folders to add or remove
284
if (foldersToAdd.length > 0 || foldersToRemove.length > 0) {
285
this.withServices(async accessor => {
286
const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService);
287
if (foldersToAdd.length > 0) {
288
await workspaceEditingService.addFolders(foldersToAdd);
289
}
290
291
if (foldersToRemove.length > 0) {
292
await workspaceEditingService.removeFolders(foldersToRemove);
293
}
294
});
295
}
296
297
// Handle Files
298
if (fileOpenables.length > 0) {
299
this.withServices(async accessor => {
300
const editorService = accessor.get(IEditorService);
301
302
// Support mergeMode
303
if (options?.mergeMode && fileOpenables.length === 4) {
304
const editors = coalesce(await pathsToEditors(fileOpenables, this.fileService, this.logService));
305
if (editors.length !== 4 || !isResourceEditorInput(editors[0]) || !isResourceEditorInput(editors[1]) || !isResourceEditorInput(editors[2]) || !isResourceEditorInput(editors[3])) {
306
return; // invalid resources
307
}
308
309
// Same Window: open via editor service in current window
310
if (this.shouldReuse(options, true /* file */)) {
311
editorService.openEditor({
312
input1: { resource: editors[0].resource },
313
input2: { resource: editors[1].resource },
314
base: { resource: editors[2].resource },
315
result: { resource: editors[3].resource },
316
options: { pinned: true }
317
});
318
}
319
320
// New Window: open into empty window
321
else {
322
const environment = new Map<string, string>();
323
environment.set('mergeFile1', editors[0].resource.toString());
324
environment.set('mergeFile2', editors[1].resource.toString());
325
environment.set('mergeFileBase', editors[2].resource.toString());
326
environment.set('mergeFileResult', editors[3].resource.toString());
327
328
this.doOpen(undefined, { payload: Array.from(environment.entries()) });
329
}
330
}
331
332
// Support diffMode
333
else if (options?.diffMode && fileOpenables.length === 2) {
334
const editors = coalesce(await pathsToEditors(fileOpenables, this.fileService, this.logService));
335
if (editors.length !== 2 || !isResourceEditorInput(editors[0]) || !isResourceEditorInput(editors[1])) {
336
return; // invalid resources
337
}
338
339
// Same Window: open via editor service in current window
340
if (this.shouldReuse(options, true /* file */)) {
341
editorService.openEditor({
342
original: { resource: editors[0].resource },
343
modified: { resource: editors[1].resource },
344
options: { pinned: true }
345
});
346
}
347
348
// New Window: open into empty window
349
else {
350
const environment = new Map<string, string>();
351
environment.set('diffFileSecondary', editors[0].resource.toString());
352
environment.set('diffFilePrimary', editors[1].resource.toString());
353
354
this.doOpen(undefined, { payload: Array.from(environment.entries()) });
355
}
356
}
357
358
// Just open normally
359
else {
360
for (const openable of fileOpenables) {
361
362
// Same Window: open via editor service in current window
363
if (this.shouldReuse(options, true /* file */)) {
364
let openables: IPathData<ITextEditorOptions>[] = [];
365
366
// Support: --goto parameter to open on line/col
367
if (options?.gotoLineMode) {
368
const pathColumnAware = parseLineAndColumnAware(openable.fileUri.path);
369
openables = [{
370
fileUri: openable.fileUri.with({ path: pathColumnAware.path }),
371
options: {
372
selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined
373
}
374
}];
375
} else {
376
openables = [openable];
377
}
378
379
editorService.openEditors(coalesce(await pathsToEditors(openables, this.fileService, this.logService)), undefined, { validateTrust: true });
380
}
381
382
// New Window: open into empty window
383
else {
384
const environment = new Map<string, string>();
385
environment.set('openFile', openable.fileUri.toString());
386
387
if (options?.gotoLineMode) {
388
environment.set('gotoLineMode', 'true');
389
}
390
391
this.doOpen(undefined, { payload: Array.from(environment.entries()) });
392
}
393
}
394
}
395
396
// Support wait mode
397
const waitMarkerFileURI = options?.waitMarkerFileURI;
398
if (waitMarkerFileURI) {
399
(async () => {
400
401
// Wait for the resources to be closed in the text editor...
402
const filesToWaitFor: URI[] = [];
403
if (options.mergeMode) {
404
filesToWaitFor.push(fileOpenables[3].fileUri /* [3] is the resulting merge file */);
405
} else {
406
filesToWaitFor.push(...fileOpenables.map(fileOpenable => fileOpenable.fileUri));
407
}
408
await this.instantiationService.invokeFunction(accessor => whenEditorClosed(accessor, filesToWaitFor));
409
410
// ...before deleting the wait marker file
411
await this.fileService.del(waitMarkerFileURI);
412
})();
413
}
414
});
415
}
416
}
417
418
private withServices(fn: (accessor: ServicesAccessor) => unknown): void {
419
// Host service is used in a lot of contexts and some services
420
// need to be resolved dynamically to avoid cyclic dependencies
421
// (https://github.com/microsoft/vscode/issues/108522)
422
this.instantiationService.invokeFunction(accessor => fn(accessor));
423
}
424
425
private preservePayload(isEmptyWindow: boolean, options?: IOpenWindowOptions): Array<unknown> | undefined {
426
427
// Selectively copy payload: for now only extension debugging properties are considered
428
const newPayload: Array<unknown> = [];
429
if (!isEmptyWindow && this.environmentService.extensionDevelopmentLocationURI) {
430
newPayload.push(['extensionDevelopmentPath', this.environmentService.extensionDevelopmentLocationURI.toString()]);
431
432
if (this.environmentService.debugExtensionHost.debugId) {
433
newPayload.push(['debugId', this.environmentService.debugExtensionHost.debugId]);
434
}
435
436
if (this.environmentService.debugExtensionHost.port) {
437
newPayload.push(['inspect-brk-extensions', String(this.environmentService.debugExtensionHost.port)]);
438
}
439
}
440
441
const newWindowProfile = options?.forceProfile
442
? this.userDataProfilesService.profiles.find(profile => profile.name === options?.forceProfile)
443
: undefined;
444
if (newWindowProfile && !newWindowProfile.isDefault) {
445
newPayload.push(['profile', newWindowProfile.name]);
446
}
447
448
return newPayload.length ? newPayload : undefined;
449
}
450
451
private getRecentLabel(openable: IWindowOpenable): string {
452
if (isFolderToOpen(openable)) {
453
return this.labelService.getWorkspaceLabel(openable.folderUri, { verbose: Verbosity.LONG });
454
}
455
456
if (isWorkspaceToOpen(openable)) {
457
return this.labelService.getWorkspaceLabel(getWorkspaceIdentifier(openable.workspaceUri), { verbose: Verbosity.LONG });
458
}
459
460
return this.labelService.getUriLabel(openable.fileUri, { appendWorkspaceSuffix: true });
461
}
462
463
private shouldReuse(options: IOpenWindowOptions = Object.create(null), isFile: boolean): boolean {
464
if (options.waitMarkerFileURI) {
465
return true; // always handle --wait in same window
466
}
467
468
const windowConfig = this.configurationService.getValue<IWindowSettings | undefined>('window');
469
const openInNewWindowConfig = isFile ? (windowConfig?.openFilesInNewWindow || 'off' /* default */) : (windowConfig?.openFoldersInNewWindow || 'default' /* default */);
470
471
let openInNewWindow = (options.preferNewWindow || !!options.forceNewWindow) && !options.forceReuseWindow;
472
if (!options.forceNewWindow && !options.forceReuseWindow && (openInNewWindowConfig === 'on' || openInNewWindowConfig === 'off')) {
473
openInNewWindow = (openInNewWindowConfig === 'on');
474
}
475
476
return !openInNewWindow;
477
}
478
479
private async doOpenEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void> {
480
return this.doOpen(undefined, {
481
reuse: options?.forceReuseWindow,
482
payload: this.preservePayload(true /* empty window */, options)
483
});
484
}
485
486
private async doOpen(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise<void> {
487
488
// When we are in a temporary workspace and are asked to open a local folder
489
// we swap that folder into the workspace to avoid a window reload. Access
490
// to local resources is only possible without a window reload because it
491
// needs user activation.
492
if (workspace && isFolderToOpen(workspace) && workspace.folderUri.scheme === Schemas.file && isTemporaryWorkspace(this.contextService.getWorkspace())) {
493
this.withServices(async accessor => {
494
const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService);
495
496
await workspaceEditingService.updateFolders(0, this.contextService.getWorkspace().folders.length, [{ uri: workspace.folderUri }]);
497
});
498
499
return;
500
}
501
502
// We know that `workspaceProvider.open` will trigger a shutdown
503
// with `options.reuse` so we handle this expected shutdown
504
if (options?.reuse) {
505
await this.handleExpectedShutdown(ShutdownReason.LOAD);
506
}
507
508
const opened = await this.workspaceProvider.open(workspace, options);
509
if (!opened) {
510
await this.dialogService.prompt({
511
type: Severity.Warning,
512
message: workspace ?
513
localize('unableToOpenExternalWorkspace', "The browser blocked opening a new tab or window for '{0}'. Press 'Retry' to try again.", this.getRecentLabel(workspace)) :
514
localize('unableToOpenExternal', "The browser blocked opening a new tab or window. Press 'Retry' to try again."),
515
custom: {
516
markdownDetails: [{ markdown: new MarkdownString(localize('unableToOpenWindowDetail', "Please allow pop-ups for this website in your [browser settings]({0}).", 'https://aka.ms/allow-vscode-popup'), true) }]
517
},
518
buttons: [
519
{
520
label: localize({ key: 'retry', comment: ['&& denotes a mnemonic'] }, "&&Retry"),
521
run: () => this.workspaceProvider.open(workspace, options)
522
}
523
],
524
cancelButton: true
525
});
526
}
527
}
528
529
async toggleFullScreen(targetWindow: Window): Promise<void> {
530
const target = this.layoutService.getContainer(targetWindow);
531
532
// Chromium
533
if (targetWindow.document.fullscreen !== undefined) {
534
if (!targetWindow.document.fullscreen) {
535
try {
536
return await target.requestFullscreen();
537
} catch (error) {
538
this.logService.warn('toggleFullScreen(): requestFullscreen failed'); // https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen
539
}
540
} else {
541
try {
542
return await targetWindow.document.exitFullscreen();
543
} catch (error) {
544
this.logService.warn('toggleFullScreen(): exitFullscreen failed');
545
}
546
}
547
}
548
549
// Safari and Edge 14 are all using webkit prefix
550
551
interface WebkitDocument extends Document {
552
webkitFullscreenElement: Element | null;
553
webkitExitFullscreen(): Promise<void>;
554
webkitIsFullScreen: boolean;
555
}
556
557
interface WebkitHTMLElement extends HTMLElement {
558
webkitRequestFullscreen(): Promise<void>;
559
}
560
561
const webkitDocument = targetWindow.document as WebkitDocument;
562
const webkitElement = target as WebkitHTMLElement;
563
if (webkitDocument.webkitIsFullScreen !== undefined) {
564
try {
565
if (!webkitDocument.webkitIsFullScreen) {
566
webkitElement.webkitRequestFullscreen(); // it's async, but doesn't return a real promise
567
} else {
568
webkitDocument.webkitExitFullscreen(); // it's async, but doesn't return a real promise
569
}
570
} catch {
571
this.logService.warn('toggleFullScreen(): requestFullscreen/exitFullscreen failed');
572
}
573
}
574
}
575
576
async moveTop(targetWindow: Window): Promise<void> {
577
// There seems to be no API to bring a window to front in browsers
578
}
579
580
async getCursorScreenPoint(): Promise<undefined> {
581
return undefined;
582
}
583
584
getWindows(options: { includeAuxiliaryWindows: true }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;
585
getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;
586
async getWindows(options: { includeAuxiliaryWindows: boolean }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>> {
587
const activeWindow = getActiveWindow();
588
const activeWindowId = getWindowId(activeWindow);
589
590
// Main window
591
const result: Array<IOpenedMainWindow | IOpenedAuxiliaryWindow> = [{
592
id: activeWindowId,
593
title: activeWindow.document.title,
594
workspace: toWorkspaceIdentifier(this.contextService.getWorkspace()),
595
dirty: false
596
}];
597
598
// Auxiliary windows
599
if (options.includeAuxiliaryWindows) {
600
for (const { window } of getDOMWindows()) {
601
const windowId = getWindowId(window);
602
if (windowId !== activeWindowId && isAuxiliaryWindow(window)) {
603
result.push({
604
id: windowId,
605
title: window.document.title,
606
parentId: activeWindowId
607
});
608
}
609
}
610
}
611
612
return result;
613
}
614
615
//#endregion
616
617
//#region Lifecycle
618
619
async restart(): Promise<void> {
620
this.reload();
621
}
622
623
async reload(): Promise<void> {
624
await this.handleExpectedShutdown(ShutdownReason.RELOAD);
625
626
mainWindow.location.reload();
627
}
628
629
async close(): Promise<void> {
630
await this.handleExpectedShutdown(ShutdownReason.CLOSE);
631
632
mainWindow.close();
633
}
634
635
async withExpectedShutdown<T>(expectedShutdownTask: () => Promise<T>): Promise<T> {
636
const previousShutdownReason = this.shutdownReason;
637
try {
638
this.shutdownReason = HostShutdownReason.Api;
639
return await expectedShutdownTask();
640
} finally {
641
this.shutdownReason = previousShutdownReason;
642
}
643
}
644
645
private async handleExpectedShutdown(reason: ShutdownReason): Promise<void> {
646
647
// Update shutdown reason in a way that we do
648
// not show a dialog because this is a expected
649
// shutdown.
650
this.shutdownReason = HostShutdownReason.Api;
651
652
// Signal shutdown reason to lifecycle
653
return this.lifecycleService.withExpectedShutdown(reason);
654
}
655
656
//#endregion
657
658
//#region Screenshots
659
660
async getScreenshot(): Promise<VSBuffer | undefined> {
661
// Gets a screenshot from the browser. This gets the screenshot via the browser's display
662
// media API which will typically offer a picker of all available screens and windows for
663
// the user to select. Using the video stream provided by the display media API, this will
664
// capture a single frame of the video and convert it to a JPEG image.
665
const store = new DisposableStore();
666
667
// Create a video element to play the captured screen source
668
const video = document.createElement('video');
669
store.add(toDisposable(() => video.remove()));
670
let stream: MediaStream | undefined;
671
try {
672
// Create a stream from the screen source (capture screen without audio)
673
stream = await navigator.mediaDevices.getDisplayMedia({
674
audio: false,
675
video: true
676
});
677
678
// Set the stream as the source of the video element
679
video.srcObject = stream;
680
video.play();
681
682
// Wait for the video to load properly before capturing the screenshot
683
await Promise.all([
684
new Promise<void>(r => store.add(addDisposableListener(video, 'loadedmetadata', () => r()))),
685
new Promise<void>(r => store.add(addDisposableListener(video, 'canplaythrough', () => r())))
686
]);
687
688
const canvas = document.createElement('canvas');
689
canvas.width = video.videoWidth;
690
canvas.height = video.videoHeight;
691
692
const ctx = canvas.getContext('2d');
693
if (!ctx) {
694
return undefined;
695
}
696
697
// Draw the portion of the video (x, y) with the specified width and height
698
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
699
700
// Convert the canvas to a Blob (JPEG format), use .95 for quality
701
const blob: Blob | null = await new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), 'image/jpeg', 0.95));
702
if (!blob) {
703
throw new Error('Failed to create blob from canvas');
704
}
705
706
const buf = await blob.bytes();
707
return VSBuffer.wrap(buf);
708
709
} catch (error) {
710
console.error('Error taking screenshot:', error);
711
return undefined;
712
} finally {
713
store.dispose();
714
if (stream) {
715
for (const track of stream.getTracks()) {
716
track.stop();
717
}
718
}
719
}
720
}
721
722
async getBrowserId(): Promise<string | undefined> {
723
return undefined;
724
}
725
726
//#endregion
727
728
//#region Native Handle
729
730
async getNativeWindowHandle(_windowId: number) {
731
return undefined;
732
}
733
734
//#endregion
735
736
//#region Toast Notifications
737
738
private readonly activeToasts = this._register(new DisposableSet());
739
740
async showToast(options: IToastOptions, token: CancellationToken): Promise<IToastResult> {
741
return showBrowserToast({
742
onDidCreateToast: disposable => this.activeToasts.add(disposable),
743
onDidDisposeToast: disposable => this.activeToasts.deleteAndDispose(disposable)
744
}, options, token);
745
}
746
747
private async clearToasts(): Promise<void> {
748
this.activeToasts.clearAndDisposeAll();
749
}
750
751
//#endregion
752
}
753
754
registerSingleton(IHostService, BrowserHostService, InstantiationType.Delayed);
755
756