Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts
4779 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Codicon } from '../../../../base/common/codicons.js';
7
import { ThemeIcon } from '../../../../base/common/themables.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js';
10
import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput } from '../../../common/editor.js';
11
import { EditorInput } from '../../../common/editor/editorInput.js';
12
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
13
import { TAB_ACTIVE_FOREGROUND } from '../../../common/theme.js';
14
import { localize } from '../../../../nls.js';
15
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
16
import { IBrowserViewWorkbenchService, IBrowserViewModel } from '../common/browserView.js';
17
import { hasKey } from '../../../../base/common/types.js';
18
import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js';
19
import { BrowserEditor } from './browserEditor.js';
20
21
const LOADING_SPINNER_SVG = (color: string | undefined) => `
22
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
23
<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"/>
24
<path d="M8 1a7 7 0 0 1 7 7h-1.5A5.5 5.5 0 0 0 8 2.5V1z" fill="${color}">
25
<animateTransform attributeName="transform" type="rotate" dur="1s" repeatCount="indefinite" values="0 8 8;360 8 8"/>
26
</path>
27
</svg>
28
`;
29
30
/**
31
* JSON-serializable type used during browser state serialization/deserialization
32
*/
33
export interface IBrowserEditorInputData {
34
readonly id: string;
35
readonly url?: string;
36
readonly title?: string;
37
readonly favicon?: string;
38
}
39
40
export class BrowserEditorInput extends EditorInput {
41
static readonly ID = 'workbench.editorinputs.browser';
42
private static readonly DEFAULT_LABEL = localize('browser.editorLabel', "Browser");
43
44
private readonly _id: string;
45
private readonly _initialData: IBrowserEditorInputData;
46
private _model: IBrowserViewModel | undefined;
47
private _modelPromise: Promise<IBrowserViewModel> | undefined;
48
49
constructor(
50
options: IBrowserEditorInputData,
51
@IThemeService private readonly themeService: IThemeService,
52
@IBrowserViewWorkbenchService private readonly browserViewWorkbenchService: IBrowserViewWorkbenchService,
53
@ILifecycleService private readonly lifecycleService: ILifecycleService
54
) {
55
super();
56
this._id = options.id;
57
this._initialData = options;
58
59
this._register(this.lifecycleService.onWillShutdown((e) => {
60
if (this._model) {
61
// For reloads, we simply hide / re-show the view.
62
if (e.reason === ShutdownReason.RELOAD) {
63
void this._model.setVisible(false);
64
} else {
65
this._model.dispose();
66
this._model = undefined;
67
}
68
}
69
}));
70
}
71
72
get id() {
73
return this._id;
74
}
75
76
override async resolve(): Promise<IBrowserViewModel> {
77
if (!this._model && !this._modelPromise) {
78
this._modelPromise = (async () => {
79
this._model = await this.browserViewWorkbenchService.getOrCreateBrowserViewModel(this._id);
80
this._modelPromise = undefined;
81
82
// Set up cleanup when the model is disposed
83
this._register(this._model.onWillDispose(() => {
84
this._model = undefined;
85
}));
86
87
// Auto-close editor when webcontents closes
88
this._register(this._model.onDidClose(() => {
89
this.dispose();
90
}));
91
92
// Listen for label-relevant changes to fire onDidChangeLabel
93
this._register(this._model.onDidChangeTitle(() => this._onDidChangeLabel.fire()));
94
this._register(this._model.onDidChangeFavicon(() => this._onDidChangeLabel.fire()));
95
this._register(this._model.onDidChangeLoadingState(() => this._onDidChangeLabel.fire()));
96
this._register(this._model.onDidNavigate(() => this._onDidChangeLabel.fire()));
97
98
// Navigate to initial URL if provided
99
if (this._initialData.url && this._model.url !== this._initialData.url) {
100
void this._model.loadURL(this._initialData.url);
101
}
102
103
return this._model;
104
})();
105
}
106
return this._model || this._modelPromise!;
107
}
108
109
override get typeId(): string {
110
return BrowserEditorInput.ID;
111
}
112
113
override get editorId(): string {
114
return BrowserEditor.ID;
115
}
116
117
override get capabilities(): EditorInputCapabilities {
118
return EditorInputCapabilities.Singleton | EditorInputCapabilities.Readonly;
119
}
120
121
override get resource(): URI {
122
if (this._resourceBeforeDisposal) {
123
return this._resourceBeforeDisposal;
124
}
125
126
const url = this._model?.url ?? this._initialData.url ?? '';
127
return BrowserViewUri.forUrl(url, this._id);
128
}
129
130
override getIcon(): ThemeIcon | URI | undefined {
131
// Use model data if available, otherwise fall back to initial data
132
if (this._model) {
133
if (this._model.loading) {
134
const color = this.themeService.getColorTheme().getColor(TAB_ACTIVE_FOREGROUND);
135
return URI.parse('data:image/svg+xml;utf8,' + encodeURIComponent(LOADING_SPINNER_SVG(color?.toString())));
136
}
137
if (this._model.favicon) {
138
return URI.parse(this._model.favicon);
139
}
140
// Model exists but no favicon yet, use default
141
return Codicon.globe;
142
}
143
// Model not created yet, use initial data if available
144
if (this._initialData.favicon) {
145
return URI.parse(this._initialData.favicon);
146
}
147
return Codicon.globe;
148
}
149
150
override getName(): string {
151
// Use model data if available, otherwise fall back to initial data
152
if (this._model && this._model.url) {
153
if (this._model.title) {
154
return this._model.title;
155
}
156
// Model exists, use its URL for authority
157
const authority = URI.parse(this._model.url).authority;
158
return authority || BrowserEditorInput.DEFAULT_LABEL;
159
}
160
// Model not created yet, use initial data
161
if (this._initialData.title) {
162
return this._initialData.title;
163
}
164
const url = this._initialData.url ?? '';
165
const authority = URI.parse(url).authority;
166
return authority || BrowserEditorInput.DEFAULT_LABEL;
167
}
168
169
override getDescription(): string | undefined {
170
// Use model URL if available, otherwise fall back to initial data
171
return this._model ? this._model.url : this._initialData.url;
172
}
173
174
override canReopen(): boolean {
175
return true;
176
}
177
178
override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {
179
if (super.matches(otherInput)) {
180
return true;
181
}
182
183
if (otherInput instanceof BrowserEditorInput) {
184
return this._id === otherInput._id;
185
}
186
187
// Check if it's an untyped input with a browser view resource
188
if (hasKey(otherInput, { resource: true }) && otherInput.resource?.scheme === BrowserViewUri.scheme) {
189
const parsed = BrowserViewUri.parse(otherInput.resource);
190
if (parsed) {
191
return this._id === parsed.id;
192
}
193
}
194
195
return false;
196
}
197
198
override toUntyped(): IUntypedEditorInput {
199
return {
200
resource: this.resource,
201
options: {
202
override: BrowserEditorInput.ID
203
}
204
};
205
}
206
207
// When closing the editor, toUntyped() is called after dispose().
208
// So we save a snapshot of the resource so we can still use it after the model is disposed.
209
private _resourceBeforeDisposal: URI | undefined;
210
override dispose(): void {
211
if (this._model) {
212
this._resourceBeforeDisposal = this.resource;
213
this._model.dispose();
214
this._model = undefined;
215
}
216
super.dispose();
217
}
218
219
serialize(): IBrowserEditorInputData {
220
return {
221
id: this._id,
222
url: this._model ? this._model.url : this._initialData.url,
223
title: this._model ? this._model.title : this._initialData.title,
224
favicon: this._model ? this._model.favicon : this._initialData.favicon
225
};
226
}
227
}
228
229
export class BrowserEditorSerializer implements IEditorSerializer {
230
canSerialize(editorInput: EditorInput): editorInput is BrowserEditorInput {
231
return editorInput instanceof BrowserEditorInput;
232
}
233
234
serialize(editorInput: EditorInput): string | undefined {
235
if (!this.canSerialize(editorInput)) {
236
return undefined;
237
}
238
239
return JSON.stringify(editorInput.serialize());
240
}
241
242
deserialize(instantiationService: IInstantiationService, serializedEditor: string): EditorInput | undefined {
243
try {
244
const data: IBrowserEditorInputData = JSON.parse(serializedEditor);
245
return instantiationService.createInstance(BrowserEditorInput, data);
246
} catch {
247
return undefined;
248
}
249
}
250
}
251
252