Path: blob/main/src/vs/workbench/contrib/browserView/common/browserView.ts
5240 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,22IBrowserViewCaptureScreenshotOptions,23IBrowserViewFindInPageOptions,24IBrowserViewFindInPageResult,25IBrowserViewVisibilityEvent26} from '../../../../platform/browserView/common/browserView.js';27import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';28import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';29import { isLocalhostAuthority } from '../../../../platform/url/common/trustedDomains.js';30import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';31import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';3233type IntegratedBrowserNavigationEvent = {34navigationType: 'urlInput' | 'goBack' | 'goForward' | 'reload';35isLocalhost: boolean;36};3738type IntegratedBrowserNavigationClassification = {39navigationType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the navigation was triggered' };40isLocalhost: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the URL is a localhost address' };41owner: 'kycutler';42comment: 'Tracks navigation patterns in integrated browser';43};4445export const IBrowserViewWorkbenchService = createDecorator<IBrowserViewWorkbenchService>('browserViewWorkbenchService');4647/**48* Workbench-level service for browser views that provides model-based access to browser views.49* This service manages browser view models that proxy to the main process browser view service.50*/51export interface IBrowserViewWorkbenchService {52readonly _serviceBrand: undefined;5354/**55* Get or create a browser view model for the given ID56* @param id The browser view identifier57* @returns A browser view model that proxies to the main process58*/59getOrCreateBrowserViewModel(id: string): Promise<IBrowserViewModel>;6061/**62* Clear all storage data for the global browser session63*/64clearGlobalStorage(): Promise<void>;6566/**67* Clear all storage data for the current workspace browser session68*/69clearWorkspaceStorage(): Promise<void>;70}717273/**74* A browser view model that represents a single browser view instance in the workbench.75* This model proxies calls to the main process browser view service using its unique ID.76*/77export interface IBrowserViewModel extends IDisposable {78readonly id: string;79readonly url: string;80readonly title: string;81readonly favicon: string | undefined;82readonly screenshot: VSBuffer | undefined;83readonly loading: boolean;84readonly focused: boolean;85readonly visible: boolean;86readonly canGoBack: boolean;87readonly isDevToolsOpen: boolean;88readonly canGoForward: boolean;89readonly error: IBrowserViewLoadError | undefined;9091readonly storageScope: BrowserViewStorageScope;9293readonly onDidNavigate: Event<IBrowserViewNavigationEvent>;94readonly onDidChangeLoadingState: Event<IBrowserViewLoadingEvent>;95readonly onDidChangeFocus: Event<IBrowserViewFocusEvent>;96readonly onDidChangeDevToolsState: Event<IBrowserViewDevToolsStateEvent>;97readonly onDidKeyCommand: Event<IBrowserViewKeyDownEvent>;98readonly onDidChangeTitle: Event<IBrowserViewTitleChangeEvent>;99readonly onDidChangeFavicon: Event<IBrowserViewFaviconChangeEvent>;100readonly onDidRequestNewPage: Event<IBrowserViewNewPageRequest>;101readonly onDidFindInPage: Event<IBrowserViewFindInPageResult>;102readonly onDidChangeVisibility: Event<IBrowserViewVisibilityEvent>;103readonly onDidClose: Event<void>;104readonly onWillDispose: Event<void>;105106initialize(): Promise<void>;107108layout(bounds: IBrowserViewBounds): Promise<void>;109setVisible(visible: boolean): Promise<void>;110loadURL(url: string): Promise<void>;111goBack(): Promise<void>;112goForward(): Promise<void>;113reload(): Promise<void>;114toggleDevTools(): Promise<void>;115captureScreenshot(options?: IBrowserViewCaptureScreenshotOptions): Promise<VSBuffer>;116dispatchKeyEvent(keyEvent: IBrowserViewKeyDownEvent): Promise<void>;117focus(): Promise<void>;118findInPage(text: string, options?: IBrowserViewFindInPageOptions): Promise<void>;119stopFindInPage(keepSelection?: boolean): Promise<void>;120getSelectedText(): Promise<string>;121clearStorage(): Promise<void>;122}123124export class BrowserViewModel extends Disposable implements IBrowserViewModel {125private _url: string = '';126private _title: string = '';127private _favicon: string | undefined = undefined;128private _screenshot: VSBuffer | undefined = undefined;129private _loading: boolean = false;130private _focused: boolean = false;131private _visible: boolean = false;132private _isDevToolsOpen: boolean = false;133private _canGoBack: boolean = false;134private _canGoForward: boolean = false;135private _error: IBrowserViewLoadError | undefined = undefined;136private _storageScope: BrowserViewStorageScope = BrowserViewStorageScope.Ephemeral;137138private readonly _onWillDispose = this._register(new Emitter<void>());139readonly onWillDispose: Event<void> = this._onWillDispose.event;140141constructor(142readonly id: string,143private readonly browserViewService: IBrowserViewService,144@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,145@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,146@ITelemetryService private readonly telemetryService: ITelemetryService,147@IConfigurationService private readonly configurationService: IConfigurationService148) {149super();150}151152get url(): string { return this._url; }153get title(): string { return this._title; }154get favicon(): string | undefined { return this._favicon; }155get loading(): boolean { return this._loading; }156get focused(): boolean { return this._focused; }157get visible(): boolean { return this._visible; }158get isDevToolsOpen(): boolean { return this._isDevToolsOpen; }159get canGoBack(): boolean { return this._canGoBack; }160get canGoForward(): boolean { return this._canGoForward; }161get screenshot(): VSBuffer | undefined { return this._screenshot; }162get error(): IBrowserViewLoadError | undefined { return this._error; }163get storageScope(): BrowserViewStorageScope { return this._storageScope; }164165get onDidNavigate(): Event<IBrowserViewNavigationEvent> {166return this.browserViewService.onDynamicDidNavigate(this.id);167}168169get onDidChangeLoadingState(): Event<IBrowserViewLoadingEvent> {170return this.browserViewService.onDynamicDidChangeLoadingState(this.id);171}172173get onDidChangeFocus(): Event<IBrowserViewFocusEvent> {174return this.browserViewService.onDynamicDidChangeFocus(this.id);175}176177get onDidChangeDevToolsState(): Event<IBrowserViewDevToolsStateEvent> {178return this.browserViewService.onDynamicDidChangeDevToolsState(this.id);179}180181get onDidKeyCommand(): Event<IBrowserViewKeyDownEvent> {182return this.browserViewService.onDynamicDidKeyCommand(this.id);183}184185get onDidChangeTitle(): Event<IBrowserViewTitleChangeEvent> {186return this.browserViewService.onDynamicDidChangeTitle(this.id);187}188189get onDidChangeFavicon(): Event<IBrowserViewFaviconChangeEvent> {190return this.browserViewService.onDynamicDidChangeFavicon(this.id);191}192193get onDidRequestNewPage(): Event<IBrowserViewNewPageRequest> {194return this.browserViewService.onDynamicDidRequestNewPage(this.id);195}196197get onDidFindInPage(): Event<IBrowserViewFindInPageResult> {198return this.browserViewService.onDynamicDidFindInPage(this.id);199}200201get onDidChangeVisibility(): Event<IBrowserViewVisibilityEvent> {202return this.browserViewService.onDynamicDidChangeVisibility(this.id);203}204205get onDidClose(): Event<void> {206return this.browserViewService.onDynamicDidClose(this.id);207}208209/**210* Initialize the model with the current state from the main process211*/212async initialize(): Promise<void> {213const dataStorageSetting = this.configurationService.getValue<BrowserViewStorageScope>(214'workbench.browser.dataStorage'215) ?? BrowserViewStorageScope.Global;216217// Wait for trust initialization before determining storage scope218await this.workspaceTrustManagementService.workspaceTrustInitialized;219const isWorkspaceUntrusted =220this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY &&221!this.workspaceTrustManagementService.isWorkspaceTrusted();222223// Always use ephemeral sessions for untrusted workspaces224const dataStorage = isWorkspaceUntrusted ? BrowserViewStorageScope.Ephemeral : dataStorageSetting;225226const workspaceId = this.workspaceContextService.getWorkspace().id;227const state = await this.browserViewService.getOrCreateBrowserView(this.id, dataStorage, workspaceId);228229this._url = state.url;230this._title = state.title;231this._loading = state.loading;232this._focused = state.focused;233this._visible = state.visible;234this._isDevToolsOpen = state.isDevToolsOpen;235this._canGoBack = state.canGoBack;236this._canGoForward = state.canGoForward;237this._screenshot = state.lastScreenshot;238this._favicon = state.lastFavicon;239this._error = state.lastError;240this._storageScope = state.storageScope;241242// Set up state synchronization243244this._register(this.onDidNavigate(e => {245// Clear favicon on navigation to a different host246if (URL.parse(e.url)?.host !== URL.parse(this._url)?.host) {247this._favicon = undefined;248}249250this._url = e.url;251this._canGoBack = e.canGoBack;252this._canGoForward = e.canGoForward;253}));254255this._register(this.onDidChangeLoadingState(e => {256this._loading = e.loading;257this._error = e.error;258}));259260this._register(this.onDidChangeDevToolsState(e => {261this._isDevToolsOpen = e.isDevToolsOpen;262}));263264this._register(this.onDidChangeTitle(e => {265this._title = e.title;266}));267268this._register(this.onDidChangeFavicon(e => {269this._favicon = e.favicon;270}));271272this._register(this.onDidChangeFocus(({ focused }) => {273this._focused = focused;274}));275276this._register(this.onDidChangeVisibility(({ visible }) => {277this._visible = visible;278}));279}280281async layout(bounds: IBrowserViewBounds): Promise<void> {282return this.browserViewService.layout(this.id, bounds);283}284285async setVisible(visible: boolean): Promise<void> {286this._visible = visible; // Set optimistically so model is in sync immediately287return this.browserViewService.setVisible(this.id, visible);288}289290async loadURL(url: string): Promise<void> {291this.logNavigationTelemetry('urlInput', url);292return this.browserViewService.loadURL(this.id, url);293}294295async goBack(): Promise<void> {296this.logNavigationTelemetry('goBack', this._url);297return this.browserViewService.goBack(this.id);298}299300async goForward(): Promise<void> {301this.logNavigationTelemetry('goForward', this._url);302return this.browserViewService.goForward(this.id);303}304305async reload(): Promise<void> {306this.logNavigationTelemetry('reload', this._url);307return this.browserViewService.reload(this.id);308}309310async toggleDevTools(): Promise<void> {311return this.browserViewService.toggleDevTools(this.id);312}313314async captureScreenshot(options?: IBrowserViewCaptureScreenshotOptions): Promise<VSBuffer> {315const result = await this.browserViewService.captureScreenshot(this.id, options);316// Store full-page screenshots for display in UI as placeholders317if (!options?.rect) {318this._screenshot = result;319}320return result;321}322323async dispatchKeyEvent(keyEvent: IBrowserViewKeyDownEvent): Promise<void> {324return this.browserViewService.dispatchKeyEvent(this.id, keyEvent);325}326327async focus(): Promise<void> {328return this.browserViewService.focus(this.id);329}330331async findInPage(text: string, options?: IBrowserViewFindInPageOptions): Promise<void> {332return this.browserViewService.findInPage(this.id, text, options);333}334335async stopFindInPage(keepSelection?: boolean): Promise<void> {336return this.browserViewService.stopFindInPage(this.id, keepSelection);337}338339async getSelectedText(): Promise<string> {340return this.browserViewService.getSelectedText(this.id);341}342343async clearStorage(): Promise<void> {344return this.browserViewService.clearStorage(this.id);345}346347/**348* Log navigation telemetry event349*/350private logNavigationTelemetry(navigationType: IntegratedBrowserNavigationEvent['navigationType'], url: string): void {351let localhost: boolean;352try {353localhost = isLocalhostAuthority(new URL(url).host);354} catch {355localhost = false;356}357358this.telemetryService.publicLog2<IntegratedBrowserNavigationEvent, IntegratedBrowserNavigationClassification>(359'integratedBrowser.navigation',360{361navigationType,362isLocalhost: localhost363}364);365}366367override dispose(): void {368this._onWillDispose.fire();369370// Clean up the browser view when the model is disposed371void this.browserViewService.destroyBrowserView(this.id);372373super.dispose();374}375}376377378