Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts
5255 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 { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput } from '../../../common/editor.js';12import { EditorInput } from '../../../common/editor/editorInput.js';13import { IThemeService } from '../../../../platform/theme/common/themeService.js';14import { TAB_ACTIVE_FOREGROUND } from '../../../common/theme.js';15import { localize } from '../../../../nls.js';16import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';17import { IBrowserViewWorkbenchService, IBrowserViewModel } from '../common/browserView.js';18import { hasKey } from '../../../../base/common/types.js';19import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js';20import { BrowserEditor } from './browserEditor.js';21import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';22import { logBrowserOpen } from './browserViewTelemetry.js';2324const LOADING_SPINNER_SVG = (color: string | undefined) => `25<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">26<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"/>27<path d="M8 1a7 7 0 0 1 7 7h-1.5A5.5 5.5 0 0 0 8 2.5V1z" fill="${color}">28<animateTransform attributeName="transform" type="rotate" dur="1s" repeatCount="indefinite" values="0 8 8;360 8 8"/>29</path>30</svg>31`;3233/**34* Maximum length for browser page titles before truncation35*/36const MAX_TITLE_LENGTH = 30;3738/**39* JSON-serializable type used during browser state serialization/deserialization40*/41export interface IBrowserEditorInputData {42readonly id: string;43readonly url?: string;44readonly title?: string;45readonly favicon?: string;46}4748export class BrowserEditorInput extends EditorInput {49static readonly ID = 'workbench.editorinputs.browser';50private static readonly DEFAULT_LABEL = localize('browser.editorLabel', "Browser");5152private readonly _id: string;53private readonly _initialData: IBrowserEditorInputData;54private _model: IBrowserViewModel | undefined;55private _modelPromise: Promise<IBrowserViewModel> | undefined;5657constructor(58options: IBrowserEditorInputData,59@IThemeService private readonly themeService: IThemeService,60@IBrowserViewWorkbenchService private readonly browserViewWorkbenchService: IBrowserViewWorkbenchService,61@ILifecycleService private readonly lifecycleService: ILifecycleService,62@IInstantiationService private readonly instantiationService: IInstantiationService,63@ITelemetryService private readonly telemetryService: ITelemetryService64) {65super();66this._id = options.id;67this._initialData = options;6869this._register(this.lifecycleService.onWillShutdown((e) => {70if (this._model) {71// For reloads, we simply hide / re-show the view.72if (e.reason === ShutdownReason.RELOAD) {73void this._model.setVisible(false);74} else {75this._model.dispose();76this._model = undefined;77}78}79}));80}8182get id() {83return this._id;84}8586override async resolve(): Promise<IBrowserViewModel> {87if (!this._model && !this._modelPromise) {88this._modelPromise = (async () => {89this._model = await this.browserViewWorkbenchService.getOrCreateBrowserViewModel(this._id);90this._modelPromise = undefined;9192// Set up cleanup when the model is disposed93this._register(this._model.onWillDispose(() => {94this._model = undefined;95}));9697// Auto-close editor when webcontents closes98this._register(this._model.onDidClose(() => {99this.dispose();100}));101102// Listen for label-relevant changes to fire onDidChangeLabel103this._register(this._model.onDidChangeTitle(() => this._onDidChangeLabel.fire()));104this._register(this._model.onDidChangeFavicon(() => this._onDidChangeLabel.fire()));105this._register(this._model.onDidChangeLoadingState(() => this._onDidChangeLabel.fire()));106this._register(this._model.onDidNavigate(() => this._onDidChangeLabel.fire()));107108// Navigate to initial URL if provided109if (this._initialData.url && this._model.url !== this._initialData.url) {110void this._model.loadURL(this._initialData.url);111}112113return this._model;114})();115}116return this._model || this._modelPromise!;117}118119override get typeId(): string {120return BrowserEditorInput.ID;121}122123override get editorId(): string {124return BrowserEditor.ID;125}126127override get capabilities(): EditorInputCapabilities {128return EditorInputCapabilities.Singleton | EditorInputCapabilities.Readonly;129}130131override get resource(): URI {132if (this._resourceBeforeDisposal) {133return this._resourceBeforeDisposal;134}135136const url = this._model?.url ?? this._initialData.url ?? '';137return BrowserViewUri.forUrl(url, this._id);138}139140override getIcon(): ThemeIcon | URI | undefined {141// Use model data if available, otherwise fall back to initial data142if (this._model) {143if (this._model.loading) {144const color = this.themeService.getColorTheme().getColor(TAB_ACTIVE_FOREGROUND);145return URI.parse('data:image/svg+xml;utf8,' + encodeURIComponent(LOADING_SPINNER_SVG(color?.toString())));146}147if (this._model.favicon) {148return URI.parse(this._model.favicon);149}150// Model exists but no favicon yet, use default151return Codicon.globe;152}153// Model not created yet, use initial data if available154if (this._initialData.favicon) {155return URI.parse(this._initialData.favicon);156}157return Codicon.globe;158}159160override getName(): string {161return truncate(this.getTitle(), MAX_TITLE_LENGTH);162}163164override getTitle(): string {165// Use model data if available, otherwise fall back to initial data166if (this._model && this._model.url) {167if (this._model.title) {168return this._model.title;169}170// Model exists, use its URL for authority171const authority = URI.parse(this._model.url).authority;172return authority || BrowserEditorInput.DEFAULT_LABEL;173}174// Model not created yet, use initial data175if (this._initialData.title) {176return this._initialData.title;177}178const url = this._initialData.url ?? '';179const authority = URI.parse(url).authority;180return authority || BrowserEditorInput.DEFAULT_LABEL;181}182183override getDescription(): string | undefined {184// Use model URL if available, otherwise fall back to initial data185return this._model ? this._model.url : this._initialData.url;186}187188override canReopen(): boolean {189return true;190}191192override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {193if (super.matches(otherInput)) {194return true;195}196197if (otherInput instanceof BrowserEditorInput) {198return this._id === otherInput._id;199}200201// Check if it's an untyped input with a browser view resource202if (hasKey(otherInput, { resource: true }) && otherInput.resource?.scheme === BrowserViewUri.scheme) {203const parsed = BrowserViewUri.parse(otherInput.resource);204if (parsed) {205return this._id === parsed.id;206}207}208209return false;210}211212/**213* Creates a copy of this browser editor input with a new unique ID, creating an independent browser view with no linked state.214* This is used during Copy into New Window.215*/216override copy(): EditorInput {217logBrowserOpen(this.telemetryService, 'copyToNewWindow');218219const currentUrl = this._model?.url ?? this._initialData.url;220return this.instantiationService.createInstance(BrowserEditorInput, {221id: generateUuid(),222url: currentUrl,223title: this._model?.title ?? this._initialData.title,224favicon: this._model?.favicon ?? this._initialData.favicon225});226}227228override toUntyped(): IUntypedEditorInput {229return {230resource: this.resource,231options: {232override: BrowserEditorInput.ID233}234};235}236237// When closing the editor, toUntyped() is called after dispose().238// So we save a snapshot of the resource so we can still use it after the model is disposed.239private _resourceBeforeDisposal: URI | undefined;240override dispose(): void {241if (this._model) {242this._resourceBeforeDisposal = this.resource;243this._model.dispose();244this._model = undefined;245}246super.dispose();247}248249serialize(): IBrowserEditorInputData {250return {251id: this._id,252url: this._model ? this._model.url : this._initialData.url,253title: this._model ? this._model.title : this._initialData.title,254favicon: this._model ? this._model.favicon : this._initialData.favicon255};256}257}258259export class BrowserEditorSerializer implements IEditorSerializer {260canSerialize(editorInput: EditorInput): editorInput is BrowserEditorInput {261return editorInput instanceof BrowserEditorInput;262}263264serialize(editorInput: EditorInput): string | undefined {265if (!this.canSerialize(editorInput)) {266return undefined;267}268269return JSON.stringify(editorInput.serialize());270}271272deserialize(instantiationService: IInstantiationService, serializedEditor: string): EditorInput | undefined {273try {274const data: IBrowserEditorInputData = JSON.parse(serializedEditor);275return instantiationService.createInstance(BrowserEditorInput, data);276} catch {277return undefined;278}279}280}281282283