Path: blob/main/src/vs/workbench/contrib/browserView/common/browserView.ts
4780 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 { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';8import { VSBuffer } from '../../../../base/common/buffer.js';9import {10IBrowserViewBounds,11IBrowserViewNavigationEvent,12IBrowserViewLoadingEvent,13IBrowserViewLoadError,14IBrowserViewFocusEvent,15IBrowserViewKeyDownEvent,16IBrowserViewTitleChangeEvent,17IBrowserViewFaviconChangeEvent,18IBrowserViewNewPageRequest,19IBrowserViewDevToolsStateEvent,20IBrowserViewService,21BrowserViewStorageScope,22IBrowserViewCaptureScreenshotOptions23} from '../../../../platform/browserView/common/browserView.js';24import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';25import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';26import { isLocalhost } from '../../../../platform/tunnel/common/tunnel.js';27import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';28import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';2930type IntegratedBrowserNavigationEvent = {31navigationType: 'urlInput' | 'goBack' | 'goForward' | 'reload';32isLocalhost: boolean;33};3435type IntegratedBrowserNavigationClassification = {36navigationType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the navigation was triggered' };37isLocalhost: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the URL is a localhost address' };38owner: 'kycutler';39comment: 'Tracks navigation patterns in integrated browser';40};4142export const IBrowserViewWorkbenchService = createDecorator<IBrowserViewWorkbenchService>('browserViewWorkbenchService');4344/**45* Workbench-level service for browser views that provides model-based access to browser views.46* This service manages browser view models that proxy to the main process browser view service.47*/48export interface IBrowserViewWorkbenchService {49readonly _serviceBrand: undefined;5051/**52* Get or create a browser view model for the given ID53* @param id The browser view identifier54* @returns A browser view model that proxies to the main process55*/56getOrCreateBrowserViewModel(id: string): Promise<IBrowserViewModel>;5758/**59* Clear all storage data for the global browser session60*/61clearGlobalStorage(): Promise<void>;6263/**64* Clear all storage data for the current workspace browser session65*/66clearWorkspaceStorage(): Promise<void>;67}686970/**71* A browser view model that represents a single browser view instance in the workbench.72* This model proxies calls to the main process browser view service using its unique ID.73*/74export interface IBrowserViewModel extends IDisposable {75readonly id: string;76readonly url: string;77readonly title: string;78readonly favicon: string | undefined;79readonly screenshot: VSBuffer | undefined;80readonly loading: boolean;81readonly canGoBack: boolean;82readonly isDevToolsOpen: boolean;83readonly canGoForward: boolean;84readonly error: IBrowserViewLoadError | undefined;8586readonly storageScope: BrowserViewStorageScope;8788readonly onDidNavigate: Event<IBrowserViewNavigationEvent>;89readonly onDidChangeLoadingState: Event<IBrowserViewLoadingEvent>;90readonly onDidChangeFocus: Event<IBrowserViewFocusEvent>;91readonly onDidChangeDevToolsState: Event<IBrowserViewDevToolsStateEvent>;92readonly onDidKeyCommand: Event<IBrowserViewKeyDownEvent>;93readonly onDidChangeTitle: Event<IBrowserViewTitleChangeEvent>;94readonly onDidChangeFavicon: Event<IBrowserViewFaviconChangeEvent>;95readonly onDidRequestNewPage: Event<IBrowserViewNewPageRequest>;96readonly onDidClose: Event<void>;97readonly onWillDispose: Event<void>;9899initialize(): Promise<void>;100101layout(bounds: IBrowserViewBounds): Promise<void>;102setVisible(visible: boolean): Promise<void>;103loadURL(url: string): Promise<void>;104goBack(): Promise<void>;105goForward(): Promise<void>;106reload(): Promise<void>;107toggleDevTools(): Promise<void>;108captureScreenshot(options?: IBrowserViewCaptureScreenshotOptions): Promise<VSBuffer>;109dispatchKeyEvent(keyEvent: IBrowserViewKeyDownEvent): Promise<void>;110focus(): Promise<void>;111}112113export class BrowserViewModel extends Disposable implements IBrowserViewModel {114private _url: string = '';115private _title: string = '';116private _favicon: string | undefined = undefined;117private _screenshot: VSBuffer | undefined = undefined;118private _loading: boolean = false;119private _isDevToolsOpen: boolean = false;120private _canGoBack: boolean = false;121private _canGoForward: boolean = false;122private _error: IBrowserViewLoadError | undefined = undefined;123private _storageScope: BrowserViewStorageScope = BrowserViewStorageScope.Ephemeral;124125private readonly _onWillDispose = this._register(new Emitter<void>());126readonly onWillDispose: Event<void> = this._onWillDispose.event;127128constructor(129readonly id: string,130private readonly browserViewService: IBrowserViewService,131@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,132@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,133@ITelemetryService private readonly telemetryService: ITelemetryService,134@IConfigurationService private readonly configurationService: IConfigurationService135) {136super();137}138139get url(): string { return this._url; }140get title(): string { return this._title; }141get favicon(): string | undefined { return this._favicon; }142get loading(): boolean { return this._loading; }143get isDevToolsOpen(): boolean { return this._isDevToolsOpen; }144get canGoBack(): boolean { return this._canGoBack; }145get canGoForward(): boolean { return this._canGoForward; }146get screenshot(): VSBuffer | undefined { return this._screenshot; }147get error(): IBrowserViewLoadError | undefined { return this._error; }148get storageScope(): BrowserViewStorageScope { return this._storageScope; }149150get onDidNavigate(): Event<IBrowserViewNavigationEvent> {151return this.browserViewService.onDynamicDidNavigate(this.id);152}153154get onDidChangeLoadingState(): Event<IBrowserViewLoadingEvent> {155return this.browserViewService.onDynamicDidChangeLoadingState(this.id);156}157158get onDidChangeFocus(): Event<IBrowserViewFocusEvent> {159return this.browserViewService.onDynamicDidChangeFocus(this.id);160}161162get onDidChangeDevToolsState(): Event<IBrowserViewDevToolsStateEvent> {163return this.browserViewService.onDynamicDidChangeDevToolsState(this.id);164}165166get onDidKeyCommand(): Event<IBrowserViewKeyDownEvent> {167return this.browserViewService.onDynamicDidKeyCommand(this.id);168}169170get onDidChangeTitle(): Event<IBrowserViewTitleChangeEvent> {171return this.browserViewService.onDynamicDidChangeTitle(this.id);172}173174get onDidChangeFavicon(): Event<IBrowserViewFaviconChangeEvent> {175return this.browserViewService.onDynamicDidChangeFavicon(this.id);176}177178get onDidRequestNewPage(): Event<IBrowserViewNewPageRequest> {179return this.browserViewService.onDynamicDidRequestNewPage(this.id);180}181182get onDidClose(): Event<void> {183return this.browserViewService.onDynamicDidClose(this.id);184}185186/**187* Initialize the model with the current state from the main process188*/189async initialize(): Promise<void> {190const dataStorageSetting = this.configurationService.getValue<BrowserViewStorageScope>(191'workbench.browser.dataStorage'192) ?? BrowserViewStorageScope.Global;193194// Wait for trust initialization before determining storage scope195await this.workspaceTrustManagementService.workspaceTrustInitialized;196const isWorkspaceUntrusted =197this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY &&198!this.workspaceTrustManagementService.isWorkspaceTrusted();199200// Always use ephemeral sessions for untrusted workspaces201const dataStorage = isWorkspaceUntrusted ? BrowserViewStorageScope.Ephemeral : dataStorageSetting;202203const workspaceId = this.workspaceContextService.getWorkspace().id;204const state = await this.browserViewService.getOrCreateBrowserView(this.id, dataStorage, workspaceId);205206this._url = state.url;207this._title = state.title;208this._loading = state.loading;209this._isDevToolsOpen = state.isDevToolsOpen;210this._canGoBack = state.canGoBack;211this._canGoForward = state.canGoForward;212this._screenshot = state.lastScreenshot;213this._favicon = state.lastFavicon;214this._error = state.lastError;215this._storageScope = state.storageScope;216217// Set up state synchronization218219this._register(this.onDidNavigate(e => {220// Clear favicon on navigation to a different host221if (URL.parse(e.url)?.host !== URL.parse(this._url)?.host) {222this._favicon = undefined;223}224225this._url = e.url;226this._canGoBack = e.canGoBack;227this._canGoForward = e.canGoForward;228}));229230this._register(this.onDidChangeLoadingState(e => {231this._loading = e.loading;232this._error = e.error;233}));234235this._register(this.onDidChangeDevToolsState(e => {236this._isDevToolsOpen = e.isDevToolsOpen;237}));238239this._register(this.onDidChangeTitle(e => {240this._title = e.title;241}));242243this._register(this.onDidChangeFavicon(e => {244this._favicon = e.favicon;245}));246}247248async layout(bounds: IBrowserViewBounds): Promise<void> {249return this.browserViewService.layout(this.id, bounds);250}251252async setVisible(visible: boolean): Promise<void> {253return this.browserViewService.setVisible(this.id, visible);254}255256async loadURL(url: string): Promise<void> {257this.logNavigationTelemetry('urlInput', url);258return this.browserViewService.loadURL(this.id, url);259}260261async goBack(): Promise<void> {262this.logNavigationTelemetry('goBack', this._url);263return this.browserViewService.goBack(this.id);264}265266async goForward(): Promise<void> {267this.logNavigationTelemetry('goForward', this._url);268return this.browserViewService.goForward(this.id);269}270271async reload(): Promise<void> {272this.logNavigationTelemetry('reload', this._url);273return this.browserViewService.reload(this.id);274}275276async toggleDevTools(): Promise<void> {277return this.browserViewService.toggleDevTools(this.id);278}279280async captureScreenshot(options?: IBrowserViewCaptureScreenshotOptions): Promise<VSBuffer> {281const result = await this.browserViewService.captureScreenshot(this.id, options);282// Store full-page screenshots for display in UI as placeholders283if (!options?.rect) {284this._screenshot = result;285}286return result;287}288289async dispatchKeyEvent(keyEvent: IBrowserViewKeyDownEvent): Promise<void> {290return this.browserViewService.dispatchKeyEvent(this.id, keyEvent);291}292293async focus(): Promise<void> {294return this.browserViewService.focus(this.id);295}296297/**298* Log navigation telemetry event299*/300private logNavigationTelemetry(navigationType: IntegratedBrowserNavigationEvent['navigationType'], url: string): void {301let localhost: boolean;302try {303localhost = isLocalhost(new URL(url).hostname);304} catch {305localhost = false;306}307308this.telemetryService.publicLog2<IntegratedBrowserNavigationEvent, IntegratedBrowserNavigationClassification>(309'integratedBrowser.navigation',310{311navigationType,312isLocalhost: localhost313}314);315}316317override dispose(): void {318this._onWillDispose.fire();319320// Clean up the browser view when the model is disposed321void this.browserViewService.destroyBrowserView(this.id);322323super.dispose();324}325}326327328