Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts
4779 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 { ThemeIcon } from '../../../../base/common/themables.js';7import { URI } from '../../../../base/common/uri.js';8import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js';9import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput } from '../../../common/editor.js';10import { EditorInput } from '../../../common/editor/editorInput.js';11import { IThemeService } from '../../../../platform/theme/common/themeService.js';12import { TAB_ACTIVE_FOREGROUND } from '../../../common/theme.js';13import { localize } from '../../../../nls.js';14import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';15import { IBrowserViewWorkbenchService, IBrowserViewModel } from '../common/browserView.js';16import { hasKey } from '../../../../base/common/types.js';17import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js';18import { BrowserEditor } from './browserEditor.js';1920const LOADING_SPINNER_SVG = (color: string | undefined) => `21<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">22<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"/>23<path d="M8 1a7 7 0 0 1 7 7h-1.5A5.5 5.5 0 0 0 8 2.5V1z" fill="${color}">24<animateTransform attributeName="transform" type="rotate" dur="1s" repeatCount="indefinite" values="0 8 8;360 8 8"/>25</path>26</svg>27`;2829/**30* JSON-serializable type used during browser state serialization/deserialization31*/32export interface IBrowserEditorInputData {33readonly id: string;34readonly url?: string;35readonly title?: string;36readonly favicon?: string;37}3839export class BrowserEditorInput extends EditorInput {40static readonly ID = 'workbench.editorinputs.browser';41private static readonly DEFAULT_LABEL = localize('browser.editorLabel', "Browser");4243private readonly _id: string;44private readonly _initialData: IBrowserEditorInputData;45private _model: IBrowserViewModel | undefined;46private _modelPromise: Promise<IBrowserViewModel> | undefined;4748constructor(49options: IBrowserEditorInputData,50@IThemeService private readonly themeService: IThemeService,51@IBrowserViewWorkbenchService private readonly browserViewWorkbenchService: IBrowserViewWorkbenchService,52@ILifecycleService private readonly lifecycleService: ILifecycleService53) {54super();55this._id = options.id;56this._initialData = options;5758this._register(this.lifecycleService.onWillShutdown((e) => {59if (this._model) {60// For reloads, we simply hide / re-show the view.61if (e.reason === ShutdownReason.RELOAD) {62void this._model.setVisible(false);63} else {64this._model.dispose();65this._model = undefined;66}67}68}));69}7071get id() {72return this._id;73}7475override async resolve(): Promise<IBrowserViewModel> {76if (!this._model && !this._modelPromise) {77this._modelPromise = (async () => {78this._model = await this.browserViewWorkbenchService.getOrCreateBrowserViewModel(this._id);79this._modelPromise = undefined;8081// Set up cleanup when the model is disposed82this._register(this._model.onWillDispose(() => {83this._model = undefined;84}));8586// Auto-close editor when webcontents closes87this._register(this._model.onDidClose(() => {88this.dispose();89}));9091// Listen for label-relevant changes to fire onDidChangeLabel92this._register(this._model.onDidChangeTitle(() => this._onDidChangeLabel.fire()));93this._register(this._model.onDidChangeFavicon(() => this._onDidChangeLabel.fire()));94this._register(this._model.onDidChangeLoadingState(() => this._onDidChangeLabel.fire()));95this._register(this._model.onDidNavigate(() => this._onDidChangeLabel.fire()));9697// Navigate to initial URL if provided98if (this._initialData.url && this._model.url !== this._initialData.url) {99void this._model.loadURL(this._initialData.url);100}101102return this._model;103})();104}105return this._model || this._modelPromise!;106}107108override get typeId(): string {109return BrowserEditorInput.ID;110}111112override get editorId(): string {113return BrowserEditor.ID;114}115116override get capabilities(): EditorInputCapabilities {117return EditorInputCapabilities.Singleton | EditorInputCapabilities.Readonly;118}119120override get resource(): URI {121if (this._resourceBeforeDisposal) {122return this._resourceBeforeDisposal;123}124125const url = this._model?.url ?? this._initialData.url ?? '';126return BrowserViewUri.forUrl(url, this._id);127}128129override getIcon(): ThemeIcon | URI | undefined {130// Use model data if available, otherwise fall back to initial data131if (this._model) {132if (this._model.loading) {133const color = this.themeService.getColorTheme().getColor(TAB_ACTIVE_FOREGROUND);134return URI.parse('data:image/svg+xml;utf8,' + encodeURIComponent(LOADING_SPINNER_SVG(color?.toString())));135}136if (this._model.favicon) {137return URI.parse(this._model.favicon);138}139// Model exists but no favicon yet, use default140return Codicon.globe;141}142// Model not created yet, use initial data if available143if (this._initialData.favicon) {144return URI.parse(this._initialData.favicon);145}146return Codicon.globe;147}148149override getName(): string {150// Use model data if available, otherwise fall back to initial data151if (this._model && this._model.url) {152if (this._model.title) {153return this._model.title;154}155// Model exists, use its URL for authority156const authority = URI.parse(this._model.url).authority;157return authority || BrowserEditorInput.DEFAULT_LABEL;158}159// Model not created yet, use initial data160if (this._initialData.title) {161return this._initialData.title;162}163const url = this._initialData.url ?? '';164const authority = URI.parse(url).authority;165return authority || BrowserEditorInput.DEFAULT_LABEL;166}167168override getDescription(): string | undefined {169// Use model URL if available, otherwise fall back to initial data170return this._model ? this._model.url : this._initialData.url;171}172173override canReopen(): boolean {174return true;175}176177override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {178if (super.matches(otherInput)) {179return true;180}181182if (otherInput instanceof BrowserEditorInput) {183return this._id === otherInput._id;184}185186// Check if it's an untyped input with a browser view resource187if (hasKey(otherInput, { resource: true }) && otherInput.resource?.scheme === BrowserViewUri.scheme) {188const parsed = BrowserViewUri.parse(otherInput.resource);189if (parsed) {190return this._id === parsed.id;191}192}193194return false;195}196197override toUntyped(): IUntypedEditorInput {198return {199resource: this.resource,200options: {201override: BrowserEditorInput.ID202}203};204}205206// When closing the editor, toUntyped() is called after dispose().207// So we save a snapshot of the resource so we can still use it after the model is disposed.208private _resourceBeforeDisposal: URI | undefined;209override dispose(): void {210if (this._model) {211this._resourceBeforeDisposal = this.resource;212this._model.dispose();213this._model = undefined;214}215super.dispose();216}217218serialize(): IBrowserEditorInputData {219return {220id: this._id,221url: this._model ? this._model.url : this._initialData.url,222title: this._model ? this._model.title : this._initialData.title,223favicon: this._model ? this._model.favicon : this._initialData.favicon224};225}226}227228export class BrowserEditorSerializer implements IEditorSerializer {229canSerialize(editorInput: EditorInput): editorInput is BrowserEditorInput {230return editorInput instanceof BrowserEditorInput;231}232233serialize(editorInput: EditorInput): string | undefined {234if (!this.canSerialize(editorInput)) {235return undefined;236}237238return JSON.stringify(editorInput.serialize());239}240241deserialize(instantiationService: IInstantiationService, serializedEditor: string): EditorInput | undefined {242try {243const data: IBrowserEditorInputData = JSON.parse(serializedEditor);244return instantiationService.createInstance(BrowserEditorInput, data);245} catch {246return undefined;247}248}249}250251252