Path: blob/main/src/vs/workbench/services/host/browser/browserHostService.ts
5253 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../../base/common/event.js';6import { IHostService, IToastOptions, IToastResult } from './host.js';7import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';8import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';9import { IEditorService } from '../../editor/common/editorService.js';10import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';11import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen, IOpenedMainWindow, IOpenedAuxiliaryWindow } from '../../../../platform/window/common/window.js';12import { isResourceEditorInput, pathsToEditors } from '../../../common/editor.js';13import { whenEditorClosed } from '../../../browser/editor.js';14import { IWorkspace, IWorkspaceProvider } from '../../../browser/web.api.js';15import { IFileService } from '../../../../platform/files/common/files.js';16import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';17import { EventType, ModifierKeyEmitter, addDisposableListener, addDisposableThrottledListener, detectFullscreen, disposableWindowInterval, getActiveDocument, getActiveWindow, getWindowId, onDidRegisterWindow, trackFocus, getWindows as getDOMWindows } from '../../../../base/browser/dom.js';18import { Disposable, DisposableSet, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';19import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';20import { memoize } from '../../../../base/common/decorators.js';21import { parseLineAndColumnAware } from '../../../../base/common/extpath.js';22import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/common/workspaces.js';23import { IWorkspaceEditingService } from '../../workspaces/common/workspaceEditing.js';24import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';25import { ILifecycleService, BeforeShutdownEvent, ShutdownReason } from '../../lifecycle/common/lifecycle.js';26import { BrowserLifecycleService } from '../../lifecycle/browser/lifecycleService.js';27import { ILogService } from '../../../../platform/log/common/log.js';28import { getWorkspaceIdentifier } from '../../workspaces/browser/workspaces.js';29import { localize } from '../../../../nls.js';30import Severity from '../../../../base/common/severity.js';31import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';32import { DomEmitter } from '../../../../base/browser/event.js';33import { isUndefined } from '../../../../base/common/types.js';34import { isTemporaryWorkspace, IWorkspaceContextService, toWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js';35import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';36import { Schemas } from '../../../../base/common/network.js';37import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js';38import { coalesce } from '../../../../base/common/arrays.js';39import { mainWindow, isAuxiliaryWindow } from '../../../../base/browser/window.js';40import { isIOS, isMacintosh } from '../../../../base/common/platform.js';41import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';42import { URI } from '../../../../base/common/uri.js';43import { VSBuffer } from '../../../../base/common/buffer.js';44import { MarkdownString } from '../../../../base/common/htmlContent.js';45import { CancellationToken } from '../../../../base/common/cancellation.js';46import { showBrowserToast } from './toasts.js';4748enum HostShutdownReason {4950/**51* An unknown shutdown reason.52*/53Unknown = 1,5455/**56* A shutdown that was potentially triggered by keyboard use.57*/58Keyboard = 2,5960/**61* An explicit shutdown via code.62*/63Api = 364}6566export class BrowserHostService extends Disposable implements IHostService {6768declare readonly _serviceBrand: undefined;6970private workspaceProvider: IWorkspaceProvider;7172private shutdownReason = HostShutdownReason.Unknown;7374constructor(75@ILayoutService private readonly layoutService: ILayoutService,76@IConfigurationService private readonly configurationService: IConfigurationService,77@IFileService private readonly fileService: IFileService,78@ILabelService private readonly labelService: ILabelService,79@IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService,80@IInstantiationService private readonly instantiationService: IInstantiationService,81@ILifecycleService private readonly lifecycleService: BrowserLifecycleService,82@ILogService private readonly logService: ILogService,83@IDialogService private readonly dialogService: IDialogService,84@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,85@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService86) {87super();8889if (environmentService.options?.workspaceProvider) {90this.workspaceProvider = environmentService.options.workspaceProvider;91} else {92this.workspaceProvider = new class implements IWorkspaceProvider {93readonly workspace = undefined;94readonly trusted = undefined;95async open() { return true; }96};97}9899this.registerListeners();100}101102103private registerListeners(): void {104105// Veto shutdown depending on `window.confirmBeforeClose` setting106this._register(this.lifecycleService.onBeforeShutdown(e => this.onBeforeShutdown(e)));107108// Track modifier keys to detect keybinding usage109this._register(ModifierKeyEmitter.getInstance().event(() => this.updateShutdownReasonFromEvent()));110111// Make sure to hide all toasts when the window gains focus112this._register(this.onDidChangeFocus(focus => {113if (focus) {114this.clearToasts();115}116}));117}118119private onBeforeShutdown(e: BeforeShutdownEvent): void {120121switch (this.shutdownReason) {122123// Unknown / Keyboard shows veto depending on setting124case HostShutdownReason.Unknown:125case HostShutdownReason.Keyboard: {126const confirmBeforeClose = this.configurationService.getValue('window.confirmBeforeClose');127if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && this.shutdownReason === HostShutdownReason.Keyboard)) {128e.veto(true, 'veto.confirmBeforeClose');129}130break;131}132// Api never shows veto133case HostShutdownReason.Api:134break;135}136137// Unset for next shutdown138this.shutdownReason = HostShutdownReason.Unknown;139}140141private updateShutdownReasonFromEvent(): void {142if (this.shutdownReason === HostShutdownReason.Api) {143return; // do not overwrite any explicitly set shutdown reason144}145146if (ModifierKeyEmitter.getInstance().isModifierPressed) {147this.shutdownReason = HostShutdownReason.Keyboard;148} else {149this.shutdownReason = HostShutdownReason.Unknown;150}151}152153//#region Focus154155@memoize156get onDidChangeFocus(): Event<boolean> {157const emitter = this._register(new Emitter<boolean>());158159this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {160const focusTracker = disposables.add(trackFocus(window));161const visibilityTracker = disposables.add(new DomEmitter(window.document, 'visibilitychange'));162163Event.any(164Event.map(focusTracker.onDidFocus, () => this.hasFocus, disposables),165Event.map(focusTracker.onDidBlur, () => this.hasFocus, disposables),166Event.map(visibilityTracker.event, () => this.hasFocus, disposables),167Event.map(this.onDidChangeActiveWindow, () => this.hasFocus, disposables),168)(focus => emitter.fire(focus), undefined, disposables);169}, { window: mainWindow, disposables: this._store }));170171return Event.latch(emitter.event, undefined, this._store);172}173174get hasFocus(): boolean {175return getActiveDocument().hasFocus();176}177178async hadLastFocus(): Promise<boolean> {179return true;180}181182async focus(targetWindow: Window): Promise<void> {183targetWindow.focus();184}185186//#endregion187188189//#region Window190191@memoize192get onDidChangeActiveWindow(): Event<number> {193const emitter = this._register(new Emitter<number>());194195this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {196const windowId = getWindowId(window);197198// Emit via focus tracking199const focusTracker = disposables.add(trackFocus(window));200disposables.add(focusTracker.onDidFocus(() => emitter.fire(windowId)));201202// Emit via interval: immediately when opening an auxiliary window,203// it is possible that document focus has not yet changed, so we204// poll for a while to ensure we catch the event.205if (isAuxiliaryWindow(window)) {206disposables.add(disposableWindowInterval(window, () => {207const hasFocus = window.document.hasFocus();208if (hasFocus) {209emitter.fire(windowId);210}211212return hasFocus;213}, 100, 20));214}215}, { window: mainWindow, disposables: this._store }));216217return Event.latch(emitter.event, undefined, this._store);218}219220@memoize221get onDidChangeFullScreen(): Event<{ windowId: number; fullscreen: boolean }> {222const emitter = this._register(new Emitter<{ windowId: number; fullscreen: boolean }>());223224this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {225const windowId = getWindowId(window);226const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */;227228// Fullscreen (Browser)229for (const event of [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE]) {230disposables.add(addDisposableListener(window.document, event, () => emitter.fire({ windowId, fullscreen: !!detectFullscreen(window) })));231}232233// Fullscreen (Native)234disposables.add(addDisposableThrottledListener(viewport, EventType.RESIZE, () => emitter.fire({ windowId, fullscreen: !!detectFullscreen(window) }), undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */));235}, { window: mainWindow, disposables: this._store }));236237return emitter.event;238}239240openWindow(options?: IOpenEmptyWindowOptions): Promise<void>;241openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void>;242openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise<void> {243if (Array.isArray(arg1)) {244return this.doOpenWindow(arg1, arg2);245}246247return this.doOpenEmptyWindow(arg1);248}249250private async doOpenWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void> {251const payload = this.preservePayload(false /* not an empty window */, options);252const fileOpenables: IFileToOpen[] = [];253254const foldersToAdd: IWorkspaceFolderCreationData[] = [];255const foldersToRemove: URI[] = [];256257for (const openable of toOpen) {258openable.label = openable.label || this.getRecentLabel(openable);259260// Folder261if (isFolderToOpen(openable)) {262if (options?.addMode) {263foldersToAdd.push({ uri: openable.folderUri });264} else if (options?.removeMode) {265foldersToRemove.push(openable.folderUri);266} else {267this.doOpen({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options, false /* no file */), payload });268}269}270271// Workspace272else if (isWorkspaceToOpen(openable)) {273this.doOpen({ workspaceUri: openable.workspaceUri }, { reuse: this.shouldReuse(options, false /* no file */), payload });274}275276// File (handled later in bulk)277else if (isFileToOpen(openable)) {278fileOpenables.push(openable);279}280}281282// Handle Folders to add or remove283if (foldersToAdd.length > 0 || foldersToRemove.length > 0) {284this.withServices(async accessor => {285const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService);286if (foldersToAdd.length > 0) {287await workspaceEditingService.addFolders(foldersToAdd);288}289290if (foldersToRemove.length > 0) {291await workspaceEditingService.removeFolders(foldersToRemove);292}293});294}295296// Handle Files297if (fileOpenables.length > 0) {298this.withServices(async accessor => {299const editorService = accessor.get(IEditorService);300301// Support mergeMode302if (options?.mergeMode && fileOpenables.length === 4) {303const editors = coalesce(await pathsToEditors(fileOpenables, this.fileService, this.logService));304if (editors.length !== 4 || !isResourceEditorInput(editors[0]) || !isResourceEditorInput(editors[1]) || !isResourceEditorInput(editors[2]) || !isResourceEditorInput(editors[3])) {305return; // invalid resources306}307308// Same Window: open via editor service in current window309if (this.shouldReuse(options, true /* file */)) {310editorService.openEditor({311input1: { resource: editors[0].resource },312input2: { resource: editors[1].resource },313base: { resource: editors[2].resource },314result: { resource: editors[3].resource },315options: { pinned: true }316});317}318319// New Window: open into empty window320else {321const environment = new Map<string, string>();322environment.set('mergeFile1', editors[0].resource.toString());323environment.set('mergeFile2', editors[1].resource.toString());324environment.set('mergeFileBase', editors[2].resource.toString());325environment.set('mergeFileResult', editors[3].resource.toString());326327this.doOpen(undefined, { payload: Array.from(environment.entries()) });328}329}330331// Support diffMode332else if (options?.diffMode && fileOpenables.length === 2) {333const editors = coalesce(await pathsToEditors(fileOpenables, this.fileService, this.logService));334if (editors.length !== 2 || !isResourceEditorInput(editors[0]) || !isResourceEditorInput(editors[1])) {335return; // invalid resources336}337338// Same Window: open via editor service in current window339if (this.shouldReuse(options, true /* file */)) {340editorService.openEditor({341original: { resource: editors[0].resource },342modified: { resource: editors[1].resource },343options: { pinned: true }344});345}346347// New Window: open into empty window348else {349const environment = new Map<string, string>();350environment.set('diffFileSecondary', editors[0].resource.toString());351environment.set('diffFilePrimary', editors[1].resource.toString());352353this.doOpen(undefined, { payload: Array.from(environment.entries()) });354}355}356357// Just open normally358else {359for (const openable of fileOpenables) {360361// Same Window: open via editor service in current window362if (this.shouldReuse(options, true /* file */)) {363let openables: IPathData<ITextEditorOptions>[] = [];364365// Support: --goto parameter to open on line/col366if (options?.gotoLineMode) {367const pathColumnAware = parseLineAndColumnAware(openable.fileUri.path);368openables = [{369fileUri: openable.fileUri.with({ path: pathColumnAware.path }),370options: {371selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined372}373}];374} else {375openables = [openable];376}377378editorService.openEditors(coalesce(await pathsToEditors(openables, this.fileService, this.logService)), undefined, { validateTrust: true });379}380381// New Window: open into empty window382else {383const environment = new Map<string, string>();384environment.set('openFile', openable.fileUri.toString());385386if (options?.gotoLineMode) {387environment.set('gotoLineMode', 'true');388}389390this.doOpen(undefined, { payload: Array.from(environment.entries()) });391}392}393}394395// Support wait mode396const waitMarkerFileURI = options?.waitMarkerFileURI;397if (waitMarkerFileURI) {398(async () => {399400// Wait for the resources to be closed in the text editor...401const filesToWaitFor: URI[] = [];402if (options.mergeMode) {403filesToWaitFor.push(fileOpenables[3].fileUri /* [3] is the resulting merge file */);404} else {405filesToWaitFor.push(...fileOpenables.map(fileOpenable => fileOpenable.fileUri));406}407await this.instantiationService.invokeFunction(accessor => whenEditorClosed(accessor, filesToWaitFor));408409// ...before deleting the wait marker file410await this.fileService.del(waitMarkerFileURI);411})();412}413});414}415}416417private withServices(fn: (accessor: ServicesAccessor) => unknown): void {418// Host service is used in a lot of contexts and some services419// need to be resolved dynamically to avoid cyclic dependencies420// (https://github.com/microsoft/vscode/issues/108522)421this.instantiationService.invokeFunction(accessor => fn(accessor));422}423424private preservePayload(isEmptyWindow: boolean, options?: IOpenWindowOptions): Array<unknown> | undefined {425426// Selectively copy payload: for now only extension debugging properties are considered427const newPayload: Array<unknown> = [];428if (!isEmptyWindow && this.environmentService.extensionDevelopmentLocationURI) {429newPayload.push(['extensionDevelopmentPath', this.environmentService.extensionDevelopmentLocationURI.toString()]);430431if (this.environmentService.debugExtensionHost.debugId) {432newPayload.push(['debugId', this.environmentService.debugExtensionHost.debugId]);433}434435if (this.environmentService.debugExtensionHost.port) {436newPayload.push(['inspect-brk-extensions', String(this.environmentService.debugExtensionHost.port)]);437}438}439440const newWindowProfile = options?.forceProfile441? this.userDataProfilesService.profiles.find(profile => profile.name === options?.forceProfile)442: undefined;443if (newWindowProfile && !newWindowProfile.isDefault) {444newPayload.push(['profile', newWindowProfile.name]);445}446447return newPayload.length ? newPayload : undefined;448}449450private getRecentLabel(openable: IWindowOpenable): string {451if (isFolderToOpen(openable)) {452return this.labelService.getWorkspaceLabel(openable.folderUri, { verbose: Verbosity.LONG });453}454455if (isWorkspaceToOpen(openable)) {456return this.labelService.getWorkspaceLabel(getWorkspaceIdentifier(openable.workspaceUri), { verbose: Verbosity.LONG });457}458459return this.labelService.getUriLabel(openable.fileUri, { appendWorkspaceSuffix: true });460}461462private shouldReuse(options: IOpenWindowOptions = Object.create(null), isFile: boolean): boolean {463if (options.waitMarkerFileURI) {464return true; // always handle --wait in same window465}466467const windowConfig = this.configurationService.getValue<IWindowSettings | undefined>('window');468const openInNewWindowConfig = isFile ? (windowConfig?.openFilesInNewWindow || 'off' /* default */) : (windowConfig?.openFoldersInNewWindow || 'default' /* default */);469470let openInNewWindow = (options.preferNewWindow || !!options.forceNewWindow) && !options.forceReuseWindow;471if (!options.forceNewWindow && !options.forceReuseWindow && (openInNewWindowConfig === 'on' || openInNewWindowConfig === 'off')) {472openInNewWindow = (openInNewWindowConfig === 'on');473}474475return !openInNewWindow;476}477478private async doOpenEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void> {479return this.doOpen(undefined, {480reuse: options?.forceReuseWindow,481payload: this.preservePayload(true /* empty window */, options)482});483}484485private async doOpen(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise<void> {486487// When we are in a temporary workspace and are asked to open a local folder488// we swap that folder into the workspace to avoid a window reload. Access489// to local resources is only possible without a window reload because it490// needs user activation.491if (workspace && isFolderToOpen(workspace) && workspace.folderUri.scheme === Schemas.file && isTemporaryWorkspace(this.contextService.getWorkspace())) {492this.withServices(async accessor => {493const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService);494495await workspaceEditingService.updateFolders(0, this.contextService.getWorkspace().folders.length, [{ uri: workspace.folderUri }]);496});497498return;499}500501// We know that `workspaceProvider.open` will trigger a shutdown502// with `options.reuse` so we handle this expected shutdown503if (options?.reuse) {504await this.handleExpectedShutdown(ShutdownReason.LOAD);505}506507const opened = await this.workspaceProvider.open(workspace, options);508if (!opened) {509await this.dialogService.prompt({510type: Severity.Warning,511message: workspace ?512localize('unableToOpenExternalWorkspace', "The browser blocked opening a new tab or window for '{0}'. Press 'Retry' to try again.", this.getRecentLabel(workspace)) :513localize('unableToOpenExternal', "The browser blocked opening a new tab or window. Press 'Retry' to try again."),514custom: {515markdownDetails: [{ markdown: new MarkdownString(localize('unableToOpenWindowDetail', "Please allow pop-ups for this website in your [browser settings]({0}).", 'https://aka.ms/allow-vscode-popup'), true) }]516},517buttons: [518{519label: localize({ key: 'retry', comment: ['&& denotes a mnemonic'] }, "&&Retry"),520run: () => this.workspaceProvider.open(workspace, options)521}522],523cancelButton: true524});525}526}527528async toggleFullScreen(targetWindow: Window): Promise<void> {529const target = this.layoutService.getContainer(targetWindow);530531// Chromium532if (targetWindow.document.fullscreen !== undefined) {533if (!targetWindow.document.fullscreen) {534try {535return await target.requestFullscreen();536} catch (error) {537this.logService.warn('toggleFullScreen(): requestFullscreen failed'); // https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen538}539} else {540try {541return await targetWindow.document.exitFullscreen();542} catch (error) {543this.logService.warn('toggleFullScreen(): exitFullscreen failed');544}545}546}547548// Safari and Edge 14 are all using webkit prefix549550interface WebkitDocument extends Document {551webkitFullscreenElement: Element | null;552webkitExitFullscreen(): Promise<void>;553webkitIsFullScreen: boolean;554}555556interface WebkitHTMLElement extends HTMLElement {557webkitRequestFullscreen(): Promise<void>;558}559560const webkitDocument = targetWindow.document as WebkitDocument;561const webkitElement = target as WebkitHTMLElement;562if (webkitDocument.webkitIsFullScreen !== undefined) {563try {564if (!webkitDocument.webkitIsFullScreen) {565webkitElement.webkitRequestFullscreen(); // it's async, but doesn't return a real promise566} else {567webkitDocument.webkitExitFullscreen(); // it's async, but doesn't return a real promise568}569} catch {570this.logService.warn('toggleFullScreen(): requestFullscreen/exitFullscreen failed');571}572}573}574575async moveTop(targetWindow: Window): Promise<void> {576// There seems to be no API to bring a window to front in browsers577}578579async getCursorScreenPoint(): Promise<undefined> {580return undefined;581}582583getWindows(options: { includeAuxiliaryWindows: true }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;584getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;585async getWindows(options: { includeAuxiliaryWindows: boolean }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>> {586const activeWindow = getActiveWindow();587const activeWindowId = getWindowId(activeWindow);588589// Main window590const result: Array<IOpenedMainWindow | IOpenedAuxiliaryWindow> = [{591id: activeWindowId,592title: activeWindow.document.title,593workspace: toWorkspaceIdentifier(this.contextService.getWorkspace()),594dirty: false595}];596597// Auxiliary windows598if (options.includeAuxiliaryWindows) {599for (const { window } of getDOMWindows()) {600const windowId = getWindowId(window);601if (windowId !== activeWindowId && isAuxiliaryWindow(window)) {602result.push({603id: windowId,604title: window.document.title,605parentId: activeWindowId606});607}608}609}610611return result;612}613614//#endregion615616//#region Lifecycle617618async restart(): Promise<void> {619this.reload();620}621622async reload(): Promise<void> {623await this.handleExpectedShutdown(ShutdownReason.RELOAD);624625mainWindow.location.reload();626}627628async close(): Promise<void> {629await this.handleExpectedShutdown(ShutdownReason.CLOSE);630631mainWindow.close();632}633634async withExpectedShutdown<T>(expectedShutdownTask: () => Promise<T>): Promise<T> {635const previousShutdownReason = this.shutdownReason;636try {637this.shutdownReason = HostShutdownReason.Api;638return await expectedShutdownTask();639} finally {640this.shutdownReason = previousShutdownReason;641}642}643644private async handleExpectedShutdown(reason: ShutdownReason): Promise<void> {645646// Update shutdown reason in a way that we do647// not show a dialog because this is a expected648// shutdown.649this.shutdownReason = HostShutdownReason.Api;650651// Signal shutdown reason to lifecycle652return this.lifecycleService.withExpectedShutdown(reason);653}654655//#endregion656657//#region Screenshots658659async getScreenshot(): Promise<VSBuffer | undefined> {660// Gets a screenshot from the browser. This gets the screenshot via the browser's display661// media API which will typically offer a picker of all available screens and windows for662// the user to select. Using the video stream provided by the display media API, this will663// capture a single frame of the video and convert it to a JPEG image.664const store = new DisposableStore();665666// Create a video element to play the captured screen source667const video = document.createElement('video');668store.add(toDisposable(() => video.remove()));669let stream: MediaStream | undefined;670try {671// Create a stream from the screen source (capture screen without audio)672stream = await navigator.mediaDevices.getDisplayMedia({673audio: false,674video: true675});676677// Set the stream as the source of the video element678video.srcObject = stream;679video.play();680681// Wait for the video to load properly before capturing the screenshot682await Promise.all([683new Promise<void>(r => store.add(addDisposableListener(video, 'loadedmetadata', () => r()))),684new Promise<void>(r => store.add(addDisposableListener(video, 'canplaythrough', () => r())))685]);686687const canvas = document.createElement('canvas');688canvas.width = video.videoWidth;689canvas.height = video.videoHeight;690691const ctx = canvas.getContext('2d');692if (!ctx) {693return undefined;694}695696// Draw the portion of the video (x, y) with the specified width and height697ctx.drawImage(video, 0, 0, canvas.width, canvas.height);698699// Convert the canvas to a Blob (JPEG format), use .95 for quality700const blob: Blob | null = await new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), 'image/jpeg', 0.95));701if (!blob) {702throw new Error('Failed to create blob from canvas');703}704705const buf = await blob.bytes();706return VSBuffer.wrap(buf);707708} catch (error) {709console.error('Error taking screenshot:', error);710return undefined;711} finally {712store.dispose();713if (stream) {714for (const track of stream.getTracks()) {715track.stop();716}717}718}719}720721async getBrowserId(): Promise<string | undefined> {722return undefined;723}724725//#endregion726727//#region Native Handle728729async getNativeWindowHandle(_windowId: number) {730return undefined;731}732733//#endregion734735//#region Toast Notifications736737private readonly activeToasts = this._register(new DisposableSet());738739async showToast(options: IToastOptions, token: CancellationToken): Promise<IToastResult> {740return showBrowserToast({741onDidCreateToast: disposable => this.activeToasts.add(disposable),742onDidDisposeToast: disposable => this.activeToasts.deleteAndDispose(disposable)743}, options, token);744}745746private async clearToasts(): Promise<void> {747this.activeToasts.clearAndDisposeAll();748}749750//#endregion751}752753registerSingleton(IHostService, BrowserHostService, InstantiationType.Delayed);754755756