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