Path: blob/main/src/vs/workbench/api/common/extHostBrowsers.ts
13397 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 { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';7import { URI } from '../../../base/common/uri.js';8import { Codicon } from '../../../base/common/codicons.js';9import type * as vscode from 'vscode';10import { BrowserTabDto, ExtHostBrowsersShape, IMainContext, MainContext, MainThreadBrowsersShape } from './extHost.protocol.js';11import { generateUuid } from '../../../base/common/uuid.js';12import * as extHostTypes from './extHostTypes.js';13import * as typeConverters from './extHostTypeConverters.js';14import { CDPEvent, CDPRequest, CDPResponse } from '../../../platform/browserView/common/cdp/types.js';1516// #region Internal browser tab object1718class ExtHostBrowserTab {19private _url: string;20private _title: string;21private _favicon: string | undefined;2223readonly value: vscode.BrowserTab;2425constructor(26readonly id: string,27private readonly _proxy: MainThreadBrowsersShape,28private readonly _sessions: DisposableMap<string, ExtHostBrowserCDPSession>,29data: BrowserTabDto,30) {31this._url = data.url;32this._title = data.title;33this._favicon = data.favicon;3435const that = this;36this.value = {37get url(): string { return that._url; },38get title(): string { return that._title; },39get icon(): vscode.IconPath {40return that._favicon41? URI.parse(that._favicon)42: new extHostTypes.ThemeIcon(Codicon.globe.id) as vscode.ThemeIcon;43},44startCDPSession(): Promise<vscode.BrowserCDPSession> {45return that._startCDPSession();46},47close(): Promise<void> {48return that._close();49}50};51}5253update(data: BrowserTabDto): boolean {54let changed = false;55if (data.url !== this._url) {56this._url = data.url;57changed = true;58}59if (data.title !== this._title) {60this._title = data.title;61changed = true;62}63if (data.favicon !== this._favicon) {64this._favicon = data.favicon;65changed = true;66}67return changed;68}6970private async _startCDPSession(): Promise<vscode.BrowserCDPSession> {71const sessionId = generateUuid();72await this._proxy.$startCDPSession(sessionId, this.id);73const session = new ExtHostBrowserCDPSession(sessionId, this._proxy);74this._sessions.set(sessionId, session);75return session.value;76}7778private async _close(): Promise<void> {79await this._proxy.$closeBrowserTab(this.id);80}81}8283// #endregion8485// #region CDP Session8687class ExtHostBrowserCDPSession {88private readonly _onDidReceiveMessage = new Emitter<unknown>();89private readonly _onDidClose = new Emitter<void>();9091private _closed = false;9293readonly value: vscode.BrowserCDPSession;9495constructor(96readonly id: string,97private readonly _proxy: MainThreadBrowsersShape,98) {99const that = this;100this.value = {101get onDidReceiveMessage(): Event<unknown> { return that._onDidReceiveMessage.event; },102get onDidClose(): Event<void> { return that._onDidClose.event; },103sendMessage(message: unknown): Promise<void> {104return that._sendMessage(message as CDPRequest);105},106close(): Promise<void> {107return that._close();108}109};110}111112dispose(): void {113this._onDidReceiveMessage.dispose();114this._onDidClose.dispose();115}116117private async _sendMessage(message: CDPRequest): Promise<void> {118if (this._closed) {119throw new Error('Session is closed');120}121if (!message || typeof message !== 'object') {122throw new Error('Message must be an object');123}124if (typeof message.id !== 'number') {125throw new Error('Message must have a numeric id');126}127if (typeof message.method !== 'string') {128throw new Error('Message must have a method string');129}130if (message.params !== undefined && typeof message.params !== 'object') {131throw new Error('Message params must be an object');132}133if (message.sessionId !== undefined && typeof message.sessionId !== 'string') {134throw new Error('Message sessionId must be a string');135}136await this._proxy.$sendCDPMessage(this.id, { id: message.id, method: message.method, params: message.params, sessionId: message.sessionId });137}138139private async _close(): Promise<void> {140this._closed = true;141await this._proxy.$closeCDPSession(this.id);142}143144// Called from main thread145_acceptMessage(message: unknown): void {146this._onDidReceiveMessage.fire(message);147}148149_acceptClosed(): void {150this._closed = true;151this._onDidClose.fire();152}153}154155// #endregion156157export class ExtHostBrowsers extends Disposable implements ExtHostBrowsersShape {158private readonly _proxy: MainThreadBrowsersShape;159private readonly _browserTabs = new Map<string, ExtHostBrowserTab>();160private readonly _sessions = this._register(new DisposableMap<string, ExtHostBrowserCDPSession>());161162private _activeBrowserTabId: string | undefined;163164private readonly _onDidOpenBrowserTab = this._register(new Emitter<vscode.BrowserTab>());165readonly onDidOpenBrowserTab: Event<vscode.BrowserTab> = this._onDidOpenBrowserTab.event;166167private readonly _onDidCloseBrowserTab = this._register(new Emitter<vscode.BrowserTab>());168readonly onDidCloseBrowserTab: Event<vscode.BrowserTab> = this._onDidCloseBrowserTab.event;169170private readonly _onDidChangeActiveBrowserTab = this._register(new Emitter<vscode.BrowserTab | undefined>());171readonly onDidChangeActiveBrowserTab: Event<vscode.BrowserTab | undefined> = this._onDidChangeActiveBrowserTab.event;172173private readonly _onDidChangeBrowserTabState = this._register(new Emitter<vscode.BrowserTab>());174readonly onDidChangeBrowserTabState: Event<vscode.BrowserTab> = this._onDidChangeBrowserTabState.event;175176constructor(mainContext: IMainContext) {177super();178this._proxy = mainContext.getProxy(MainContext.MainThreadBrowsers);179}180181// #region Public API (called from extension code)182183get browserTabs(): readonly vscode.BrowserTab[] {184return [...this._browserTabs.values()].map(t => t.value);185}186187get activeBrowserTab(): vscode.BrowserTab | undefined {188if (this._activeBrowserTabId) {189return this._browserTabs.get(this._activeBrowserTabId)?.value;190}191return undefined;192}193194async openBrowserTab(url: string, options?: vscode.BrowserTabShowOptions): Promise<vscode.BrowserTab> {195const viewColumn = typeConverters.ViewColumn.from(options?.viewColumn);196const dto = await this._proxy.$openBrowserTab(url, viewColumn, {197preserveFocus: options?.preserveFocus,198inactive: options?.background,199});200201return this._getOrCreateTab(dto).value;202}203204// #endregion205206// #region Internal helpers207208private _getOrCreateTab(dto: BrowserTabDto): ExtHostBrowserTab {209let tab = this._browserTabs.get(dto.id);210if (!tab) {211tab = new ExtHostBrowserTab(dto.id, this._proxy, this._sessions, dto);212this._browserTabs.set(dto.id, tab);213this._onDidOpenBrowserTab.fire(tab.value);214} else {215tab.update(dto);216}217return tab;218}219220// #endregion221222// #region Main thread callbacks223224$onDidOpenBrowserTab(dto: BrowserTabDto): void {225this._getOrCreateTab(dto);226}227228$onDidCloseBrowserTab(browserId: string): void {229const tab = this._browserTabs.get(browserId);230if (tab) {231this._browserTabs.delete(browserId);232if (this._activeBrowserTabId === browserId) {233this._activeBrowserTabId = undefined;234}235this._onDidCloseBrowserTab.fire(tab.value);236}237}238239$onDidChangeActiveBrowserTab(browserId: string | undefined): void {240this._activeBrowserTabId = browserId;241this._onDidChangeActiveBrowserTab.fire(this.activeBrowserTab);242}243244$onDidChangeBrowserTabState(data: BrowserTabDto): void {245const tab = this._browserTabs.get(data.id);246if (tab && tab.update(data)) {247this._onDidChangeBrowserTabState.fire(tab.value);248}249}250251$onCDPSessionMessage(sessionId: string, message: CDPResponse | CDPEvent): void {252const session = this._sessions.get(sessionId);253if (session) {254session._acceptMessage(message);255}256}257258$onCDPSessionClosed(sessionId: string): void {259const session = this._sessions.get(sessionId);260if (session) {261session._acceptClosed();262this._sessions.deleteAndDispose(sessionId);263}264}265266// #endregion267}268269270