Path: blob/main/src/vs/workbench/contrib/browserView/common/browserEditorInput.ts
13401 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 { Codicon } from '../../../../base/common/codicons.js';6import { truncate } from '../../../../base/common/strings.js';7import { ThemeIcon } from '../../../../base/common/themables.js';8import { URI } from '../../../../base/common/uri.js';9import { generateUuid } from '../../../../base/common/uuid.js';10import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js';11import { BrowserViewSharingState, IBrowserEditorViewState, IBrowserViewWorkbenchService } from './browserView.js';12import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput, Verbosity } from '../../../common/editor.js';13import { EditorInput } from '../../../common/editor/editorInput.js';14import { IThemeService } from '../../../../platform/theme/common/themeService.js';15import { TAB_ACTIVE_FOREGROUND } from '../../../common/theme.js';16import { localize } from '../../../../nls.js';17import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';18import { IBrowserViewModel } from '../common/browserView.js';19import { hasKey } from '../../../../base/common/types.js';20import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';21import { logBrowserOpen } from '../../../../platform/browserView/common/browserViewTelemetry.js';22import { LRUCachedFunction } from '../../../../base/common/cache.js';23import { DisposableStore } from '../../../../base/common/lifecycle.js';24import { Emitter, Event } from '../../../../base/common/event.js';2526const LOADING_SPINNER_SVG = (color: string | undefined) => `27<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">28<path d="M8 1a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm0 1.5a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11z" fill="${color}" opacity="0.3"/>29<path d="M8 1a7 7 0 0 1 7 7h-1.5A5.5 5.5 0 0 0 8 2.5V1z" fill="${color}">30<animateTransform attributeName="transform" type="rotate" dur="1s" repeatCount="indefinite" values="0 8 8;360 8 8"/>31</path>32</svg>33`;3435/**36* Maximum length for browser page titles before truncation37*/38const MAX_TITLE_LENGTH = 30;3940/**41* JSON-serializable type used during browser state serialization/deserialization42*/43export interface IBrowserEditorInputData extends IBrowserEditorViewState {44readonly id: string;45}4647/**48* Fired before a {@link BrowserEditorInput} is disposed. Listeners may call49* {@link veto} to prevent disposal and keep the input and its model alive.50*/51export interface IBeforeDisposeBrowserEditorEvent {52veto(): void;53}5455export class BrowserEditorInput extends EditorInput {56static readonly ID = 'workbench.editorinputs.browser';57static readonly EDITOR_ID = 'workbench.editor.browser';58static readonly DEFAULT_LABEL = localize('browser.editorLabel', "Browser");5960private readonly _id: string;61private _initialData: IBrowserEditorInputData;6263private _model: IBrowserViewModel | undefined;64private _modelPromise: Promise<IBrowserViewModel> | undefined;65private _modelStore = this._register(new DisposableStore());6667private readonly _onBeforeDispose = this._register(new Emitter<IBeforeDisposeBrowserEditorEvent>());68readonly onBeforeDispose: Event<IBeforeDisposeBrowserEditorEvent> = this._onBeforeDispose.event;6970private readonly _onDidResolveModel = this._register(new Emitter<IBrowserViewModel>());71readonly onDidResolveModel: Event<IBrowserViewModel> = this._onDidResolveModel.event;7273constructor(74options: IBrowserEditorInputData,75private _resolveModel: () => Promise<IBrowserViewModel>,76@IThemeService private readonly themeService: IThemeService,77@IInstantiationService private readonly instantiationService: IInstantiationService,78@ITelemetryService private readonly telemetryService: ITelemetryService,79@IBrowserViewWorkbenchService private readonly browserViewWorkbenchService: IBrowserViewWorkbenchService,80) {81super();82this._id = options.id;83this._initialData = options;84}8586get model(): IBrowserViewModel | undefined {87return this._model;88}8990set model(model: IBrowserViewModel) {91if (this._model === model) {92return;93}9495this._modelStore.clear();96this._model = model;9798// Set up cleanup when the model is disposed99this._modelStore.add(this._model.onWillDispose(() => {100this._modelStore.clear();101this._model = undefined;102}));103104// Auto-close editor when webcontents closes105this._modelStore.add(this._model.onDidClose(() => {106this.dispose(true);107}));108109// Listen for label-relevant changes to fire onDidChangeLabel110this._modelStore.add(this._model.onDidChangeTitle(() => this._onDidChangeLabel.fire()));111this._modelStore.add(this._model.onDidChangeFavicon(() => this._onDidChangeLabel.fire()));112this._modelStore.add(this._model.onDidChangeLoadingState(() => this._onDidChangeLabel.fire()));113this._modelStore.add(this._model.onDidNavigate(() => this._onDidChangeLabel.fire()));114115this._onDidChangeLabel.fire();116this._onDidResolveModel.fire(model);117}118119get id() {120return this._id;121}122123get url(): string | undefined {124// Use model URL if available, otherwise fall back to initial data125return this._model ? this._model.url : this._initialData.url;126}127128get title(): string | undefined {129// Use model title if available, otherwise fall back to initial data130return this._model ? this._model.title : this._initialData.title;131}132133get favicon(): string | undefined {134// Use model favicon if available, otherwise fall back to initial data135return this._model ? this._model.favicon : this._initialData.favicon;136}137138/**139* Whether this editor was opened via a default localhost link open (setting140* not explicitly configured by the user). Transient — not serialized.141*/142get isDefaultLinkOpen(): boolean {143return !!this._initialData.isDefaultLinkOpen;144}145146get isSharingAvailable(): boolean {147return this._model ? this._model.sharingState !== BrowserViewSharingState.Unavailable : this.browserViewWorkbenchService.isSharingAvailable;148}149150navigate(url: string): void {151if (this._model) {152void this._model.loadURL(url);153} else {154// If the model isn't created yet, update the initial data so that the URL is correct when the model is created155this._initialData = {156id: this._id,157url158};159this._onDidChangeLabel.fire();160}161}162163override async resolve(): Promise<IBrowserViewModel> {164if (!this._model && !this._modelPromise) {165this._modelPromise = (async () => {166this._model = await this._resolveModel();167this._modelPromise = undefined;168169return this._model;170})();171}172return this._model || this._modelPromise!;173}174175override get typeId(): string {176return BrowserEditorInput.ID;177}178179override get editorId(): string {180return BrowserEditorInput.EDITOR_ID;181}182183override get capabilities(): EditorInputCapabilities {184return EditorInputCapabilities.ForceReveal | EditorInputCapabilities.Readonly;185}186187override get resource(): URI {188return BrowserViewUri.forId(this._id);189}190191override getIcon(): ThemeIcon | URI | undefined {192// Use model data if available, otherwise fall back to initial data193if (this._model) {194if (this._model.loading) {195const color = this.themeService.getColorTheme().getColor(TAB_ACTIVE_FOREGROUND);196return URI.parse('data:image/svg+xml;utf8,' + encodeURIComponent(LOADING_SPINNER_SVG(color?.toString())));197}198if (this._model.favicon) {199return URI.parse(this._model.favicon);200}201// Model exists but no favicon yet, use default202return Codicon.globe;203}204// Model not created yet, use initial data if available205if (this._initialData.favicon) {206return URI.parse(this._initialData.favicon);207}208return Codicon.globe;209}210211override getName(): string {212const hasTitle = this._model ? !!this._model.title : !!this._initialData.title;213const name = hasTitle ? this.title! : this.getDescription(Verbosity.SHORT) || BrowserEditorInput.DEFAULT_LABEL;214return truncate(name, MAX_TITLE_LENGTH);215}216217override getTitle(verbosity = Verbosity.MEDIUM): string {218const hasTitle = this._model ? !!this._model.title : !!this._initialData.title;219const description = this.getDescription(verbosity);220const title = hasTitle ? `${this.title} (${description})` : description;221return title || BrowserEditorInput.DEFAULT_LABEL;222}223224override getDescription(verbosity = Verbosity.MEDIUM): string | undefined {225return this.url && this.getURLTitles.get(this.url)[verbosity];226}227228private readonly getURLTitles = new LRUCachedFunction((url: string) => {229let _parsed: URI | undefined = undefined;230let _short: string | undefined = undefined;231let _medium: string | undefined = undefined;232let _long: string | undefined = undefined;233function getParsed() {234if (!_parsed) {235_parsed = URI.parse(url);236}237return _parsed;238}239return {240get [Verbosity.SHORT]() {241if (!_short) {242_short = getParsed().authority;243}244return _short;245},246get [Verbosity.MEDIUM]() {247if (!_medium) {248_medium = getParsed().with({ query: '', fragment: '' }).toString();249}250return _medium;251},252get [Verbosity.LONG]() {253if (!_long) {254_long = getParsed().with({ fragment: '' }).toString();255}256return _long;257}258};259});260261override canReopen(): boolean {262return true;263}264265override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {266if (super.matches(otherInput)) {267return true;268}269270if (otherInput instanceof BrowserEditorInput) {271return this._id === otherInput._id;272}273274// Check if it's an untyped input with a browser view resource275if (hasKey(otherInput, { resource: true }) && otherInput.resource?.scheme === BrowserViewUri.scheme) {276const parsed = BrowserViewUri.parse(otherInput.resource);277if (parsed) {278return this._id === parsed.id;279}280}281282return false;283}284285/**286* Creates a copy of this browser editor input with a new unique ID, creating an independent browser view with no linked state.287* This is used during Copy into New Window.288*/289override copy(): EditorInput {290logBrowserOpen(this.telemetryService, 'copyToNewWindow');291292return this.instantiationService.invokeFunction((accessor) => {293const browserViewWorkbenchService = accessor.get(IBrowserViewWorkbenchService);294return browserViewWorkbenchService.getOrCreateLazy(generateUuid(), {295url: this.url,296title: this.title,297favicon: this.favicon298});299});300}301302override toUntyped(): IUntypedEditorInput {303const viewState: IBrowserEditorViewState = {304url: this.url,305title: this.title,306favicon: this.favicon307};308return {309resource: this.resource,310options: {311override: BrowserEditorInput.EDITOR_ID,312viewState313}314};315}316317override dispose(force?: boolean): void {318if (!force) {319let vetoed = false;320this._onBeforeDispose.fire({ veto: () => { vetoed = true; } });321if (vetoed) {322return;323}324}325326super.dispose(); // Emit `onWillDispose` event first, then clean up the model.327if (this._model) {328// `toUntyped()` is called after disposal. Store the latest data in `_initialData` so we can still get them there.329this._initialData = {330id: this._id,331url: this._model.url,332title: this._model.title,333favicon: this._model.favicon334};335this._model.dispose();336this._model = undefined;337}338}339340serialize(): IBrowserEditorInputData {341return {342id: this._id,343url: this.url,344title: this.title,345favicon: this.favicon346};347}348}349350export class BrowserEditorSerializer implements IEditorSerializer {351canSerialize(editorInput: EditorInput): editorInput is BrowserEditorInput {352return editorInput instanceof BrowserEditorInput;353}354355serialize(editorInput: EditorInput): string | undefined {356if (!this.canSerialize(editorInput)) {357return undefined;358}359360return JSON.stringify(editorInput.serialize());361}362363deserialize(instantiationService: IInstantiationService, serializedEditor: string): EditorInput | undefined {364try {365const data: IBrowserEditorInputData = JSON.parse(serializedEditor);366return instantiationService.invokeFunction((accessor) => {367const browserViewWorkbenchService = accessor.get(IBrowserViewWorkbenchService);368return browserViewWorkbenchService.getOrCreateLazy(data.id, data);369});370} catch {371return undefined;372}373}374}375376377