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