Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/browserView/common/browserEditorInput.ts
13401 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 { BrowserViewSharingState, IBrowserEditorViewState, IBrowserViewWorkbenchService } from './browserView.js';
13
import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput, Verbosity } from '../../../common/editor.js';
14
import { EditorInput } from '../../../common/editor/editorInput.js';
15
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
16
import { TAB_ACTIVE_FOREGROUND } from '../../../common/theme.js';
17
import { localize } from '../../../../nls.js';
18
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { IBrowserViewModel } from '../common/browserView.js';
20
import { hasKey } from '../../../../base/common/types.js';
21
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
22
import { logBrowserOpen } from '../../../../platform/browserView/common/browserViewTelemetry.js';
23
import { LRUCachedFunction } from '../../../../base/common/cache.js';
24
import { DisposableStore } from '../../../../base/common/lifecycle.js';
25
import { Emitter, Event } from '../../../../base/common/event.js';
26
27
const LOADING_SPINNER_SVG = (color: string | undefined) => `
28
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
29
<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"/>
30
<path d="M8 1a7 7 0 0 1 7 7h-1.5A5.5 5.5 0 0 0 8 2.5V1z" fill="${color}">
31
<animateTransform attributeName="transform" type="rotate" dur="1s" repeatCount="indefinite" values="0 8 8;360 8 8"/>
32
</path>
33
</svg>
34
`;
35
36
/**
37
* Maximum length for browser page titles before truncation
38
*/
39
const MAX_TITLE_LENGTH = 30;
40
41
/**
42
* JSON-serializable type used during browser state serialization/deserialization
43
*/
44
export interface IBrowserEditorInputData extends IBrowserEditorViewState {
45
readonly id: string;
46
}
47
48
/**
49
* Fired before a {@link BrowserEditorInput} is disposed. Listeners may call
50
* {@link veto} to prevent disposal and keep the input and its model alive.
51
*/
52
export interface IBeforeDisposeBrowserEditorEvent {
53
veto(): void;
54
}
55
56
export class BrowserEditorInput extends EditorInput {
57
static readonly ID = 'workbench.editorinputs.browser';
58
static readonly EDITOR_ID = 'workbench.editor.browser';
59
static readonly DEFAULT_LABEL = localize('browser.editorLabel', "Browser");
60
61
private readonly _id: string;
62
private _initialData: IBrowserEditorInputData;
63
64
private _model: IBrowserViewModel | undefined;
65
private _modelPromise: Promise<IBrowserViewModel> | undefined;
66
private _modelStore = this._register(new DisposableStore());
67
68
private readonly _onBeforeDispose = this._register(new Emitter<IBeforeDisposeBrowserEditorEvent>());
69
readonly onBeforeDispose: Event<IBeforeDisposeBrowserEditorEvent> = this._onBeforeDispose.event;
70
71
private readonly _onDidResolveModel = this._register(new Emitter<IBrowserViewModel>());
72
readonly onDidResolveModel: Event<IBrowserViewModel> = this._onDidResolveModel.event;
73
74
constructor(
75
options: IBrowserEditorInputData,
76
private _resolveModel: () => Promise<IBrowserViewModel>,
77
@IThemeService private readonly themeService: IThemeService,
78
@IInstantiationService private readonly instantiationService: IInstantiationService,
79
@ITelemetryService private readonly telemetryService: ITelemetryService,
80
@IBrowserViewWorkbenchService private readonly browserViewWorkbenchService: IBrowserViewWorkbenchService,
81
) {
82
super();
83
this._id = options.id;
84
this._initialData = options;
85
}
86
87
get model(): IBrowserViewModel | undefined {
88
return this._model;
89
}
90
91
set model(model: IBrowserViewModel) {
92
if (this._model === model) {
93
return;
94
}
95
96
this._modelStore.clear();
97
this._model = model;
98
99
// Set up cleanup when the model is disposed
100
this._modelStore.add(this._model.onWillDispose(() => {
101
this._modelStore.clear();
102
this._model = undefined;
103
}));
104
105
// Auto-close editor when webcontents closes
106
this._modelStore.add(this._model.onDidClose(() => {
107
this.dispose(true);
108
}));
109
110
// Listen for label-relevant changes to fire onDidChangeLabel
111
this._modelStore.add(this._model.onDidChangeTitle(() => this._onDidChangeLabel.fire()));
112
this._modelStore.add(this._model.onDidChangeFavicon(() => this._onDidChangeLabel.fire()));
113
this._modelStore.add(this._model.onDidChangeLoadingState(() => this._onDidChangeLabel.fire()));
114
this._modelStore.add(this._model.onDidNavigate(() => this._onDidChangeLabel.fire()));
115
116
this._onDidChangeLabel.fire();
117
this._onDidResolveModel.fire(model);
118
}
119
120
get id() {
121
return this._id;
122
}
123
124
get url(): string | undefined {
125
// Use model URL if available, otherwise fall back to initial data
126
return this._model ? this._model.url : this._initialData.url;
127
}
128
129
get title(): string | undefined {
130
// Use model title if available, otherwise fall back to initial data
131
return this._model ? this._model.title : this._initialData.title;
132
}
133
134
get favicon(): string | undefined {
135
// Use model favicon if available, otherwise fall back to initial data
136
return this._model ? this._model.favicon : this._initialData.favicon;
137
}
138
139
/**
140
* Whether this editor was opened via a default localhost link open (setting
141
* not explicitly configured by the user). Transient — not serialized.
142
*/
143
get isDefaultLinkOpen(): boolean {
144
return !!this._initialData.isDefaultLinkOpen;
145
}
146
147
get isSharingAvailable(): boolean {
148
return this._model ? this._model.sharingState !== BrowserViewSharingState.Unavailable : this.browserViewWorkbenchService.isSharingAvailable;
149
}
150
151
navigate(url: string): void {
152
if (this._model) {
153
void this._model.loadURL(url);
154
} else {
155
// If the model isn't created yet, update the initial data so that the URL is correct when the model is created
156
this._initialData = {
157
id: this._id,
158
url
159
};
160
this._onDidChangeLabel.fire();
161
}
162
}
163
164
override async resolve(): Promise<IBrowserViewModel> {
165
if (!this._model && !this._modelPromise) {
166
this._modelPromise = (async () => {
167
this._model = await this._resolveModel();
168
this._modelPromise = undefined;
169
170
return this._model;
171
})();
172
}
173
return this._model || this._modelPromise!;
174
}
175
176
override get typeId(): string {
177
return BrowserEditorInput.ID;
178
}
179
180
override get editorId(): string {
181
return BrowserEditorInput.EDITOR_ID;
182
}
183
184
override get capabilities(): EditorInputCapabilities {
185
return EditorInputCapabilities.ForceReveal | EditorInputCapabilities.Readonly;
186
}
187
188
override get resource(): URI {
189
return BrowserViewUri.forId(this._id);
190
}
191
192
override getIcon(): ThemeIcon | URI | undefined {
193
// Use model data if available, otherwise fall back to initial data
194
if (this._model) {
195
if (this._model.loading) {
196
const color = this.themeService.getColorTheme().getColor(TAB_ACTIVE_FOREGROUND);
197
return URI.parse('data:image/svg+xml;utf8,' + encodeURIComponent(LOADING_SPINNER_SVG(color?.toString())));
198
}
199
if (this._model.favicon) {
200
return URI.parse(this._model.favicon);
201
}
202
// Model exists but no favicon yet, use default
203
return Codicon.globe;
204
}
205
// Model not created yet, use initial data if available
206
if (this._initialData.favicon) {
207
return URI.parse(this._initialData.favicon);
208
}
209
return Codicon.globe;
210
}
211
212
override getName(): string {
213
const hasTitle = this._model ? !!this._model.title : !!this._initialData.title;
214
const name = hasTitle ? this.title! : this.getDescription(Verbosity.SHORT) || BrowserEditorInput.DEFAULT_LABEL;
215
return truncate(name, MAX_TITLE_LENGTH);
216
}
217
218
override getTitle(verbosity = Verbosity.MEDIUM): string {
219
const hasTitle = this._model ? !!this._model.title : !!this._initialData.title;
220
const description = this.getDescription(verbosity);
221
const title = hasTitle ? `${this.title} (${description})` : description;
222
return title || BrowserEditorInput.DEFAULT_LABEL;
223
}
224
225
override getDescription(verbosity = Verbosity.MEDIUM): string | undefined {
226
return this.url && this.getURLTitles.get(this.url)[verbosity];
227
}
228
229
private readonly getURLTitles = new LRUCachedFunction((url: string) => {
230
let _parsed: URI | undefined = undefined;
231
let _short: string | undefined = undefined;
232
let _medium: string | undefined = undefined;
233
let _long: string | undefined = undefined;
234
function getParsed() {
235
if (!_parsed) {
236
_parsed = URI.parse(url);
237
}
238
return _parsed;
239
}
240
return {
241
get [Verbosity.SHORT]() {
242
if (!_short) {
243
_short = getParsed().authority;
244
}
245
return _short;
246
},
247
get [Verbosity.MEDIUM]() {
248
if (!_medium) {
249
_medium = getParsed().with({ query: '', fragment: '' }).toString();
250
}
251
return _medium;
252
},
253
get [Verbosity.LONG]() {
254
if (!_long) {
255
_long = getParsed().with({ fragment: '' }).toString();
256
}
257
return _long;
258
}
259
};
260
});
261
262
override canReopen(): boolean {
263
return true;
264
}
265
266
override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {
267
if (super.matches(otherInput)) {
268
return true;
269
}
270
271
if (otherInput instanceof BrowserEditorInput) {
272
return this._id === otherInput._id;
273
}
274
275
// Check if it's an untyped input with a browser view resource
276
if (hasKey(otherInput, { resource: true }) && otherInput.resource?.scheme === BrowserViewUri.scheme) {
277
const parsed = BrowserViewUri.parse(otherInput.resource);
278
if (parsed) {
279
return this._id === parsed.id;
280
}
281
}
282
283
return false;
284
}
285
286
/**
287
* Creates a copy of this browser editor input with a new unique ID, creating an independent browser view with no linked state.
288
* This is used during Copy into New Window.
289
*/
290
override copy(): EditorInput {
291
logBrowserOpen(this.telemetryService, 'copyToNewWindow');
292
293
return this.instantiationService.invokeFunction((accessor) => {
294
const browserViewWorkbenchService = accessor.get(IBrowserViewWorkbenchService);
295
return browserViewWorkbenchService.getOrCreateLazy(generateUuid(), {
296
url: this.url,
297
title: this.title,
298
favicon: this.favicon
299
});
300
});
301
}
302
303
override toUntyped(): IUntypedEditorInput {
304
const viewState: IBrowserEditorViewState = {
305
url: this.url,
306
title: this.title,
307
favicon: this.favicon
308
};
309
return {
310
resource: this.resource,
311
options: {
312
override: BrowserEditorInput.EDITOR_ID,
313
viewState
314
}
315
};
316
}
317
318
override dispose(force?: boolean): void {
319
if (!force) {
320
let vetoed = false;
321
this._onBeforeDispose.fire({ veto: () => { vetoed = true; } });
322
if (vetoed) {
323
return;
324
}
325
}
326
327
super.dispose(); // Emit `onWillDispose` event first, then clean up the model.
328
if (this._model) {
329
// `toUntyped()` is called after disposal. Store the latest data in `_initialData` so we can still get them there.
330
this._initialData = {
331
id: this._id,
332
url: this._model.url,
333
title: this._model.title,
334
favicon: this._model.favicon
335
};
336
this._model.dispose();
337
this._model = undefined;
338
}
339
}
340
341
serialize(): IBrowserEditorInputData {
342
return {
343
id: this._id,
344
url: this.url,
345
title: this.title,
346
favicon: this.favicon
347
};
348
}
349
}
350
351
export class BrowserEditorSerializer implements IEditorSerializer {
352
canSerialize(editorInput: EditorInput): editorInput is BrowserEditorInput {
353
return editorInput instanceof BrowserEditorInput;
354
}
355
356
serialize(editorInput: EditorInput): string | undefined {
357
if (!this.canSerialize(editorInput)) {
358
return undefined;
359
}
360
361
return JSON.stringify(editorInput.serialize());
362
}
363
364
deserialize(instantiationService: IInstantiationService, serializedEditor: string): EditorInput | undefined {
365
try {
366
const data: IBrowserEditorInputData = JSON.parse(serializedEditor);
367
return instantiationService.invokeFunction((accessor) => {
368
const browserViewWorkbenchService = accessor.get(IBrowserViewWorkbenchService);
369
return browserViewWorkbenchService.getOrCreateLazy(data.id, data);
370
});
371
} catch {
372
return undefined;
373
}
374
}
375
}
376
377