Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/common/editor/sideBySideEditorInput.ts
3296 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 { Event } from '../../../base/common/event.js';
7
import { IMarkdownString } from '../../../base/common/htmlContent.js';
8
import { URI } from '../../../base/common/uri.js';
9
import { localize } from '../../../nls.js';
10
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
11
import { Registry } from '../../../platform/registry/common/platform.js';
12
import { EditorInputCapabilities, GroupIdentifier, ISaveOptions, IRevertOptions, EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, ISideBySideEditorInput, IUntypedEditorInput, isResourceSideBySideEditorInput, isDiffEditorInput, isResourceDiffEditorInput, IResourceSideBySideEditorInput, findViewStateForEditor, IMoveResult, isEditorInput, isResourceEditorInput, Verbosity, isResourceMergeEditorInput, isResourceMultiDiffEditorInput } from '../editor.js';
13
import { EditorInput, IUntypedEditorOptions } from './editorInput.js';
14
import { IEditorService } from '../../services/editor/common/editorService.js';
15
16
/**
17
* Side by side editor inputs that have a primary and secondary side.
18
*/
19
export class SideBySideEditorInput extends EditorInput implements ISideBySideEditorInput {
20
21
static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput';
22
23
override get typeId(): string {
24
return SideBySideEditorInput.ID;
25
}
26
27
override get capabilities(): EditorInputCapabilities {
28
29
// Use primary capabilities as main capabilities...
30
let capabilities = this.primary.capabilities;
31
32
// ...with the exception of `CanSplitInGroup` which
33
// is only relevant to single editors.
34
capabilities &= ~EditorInputCapabilities.CanSplitInGroup;
35
36
// Trust: should be considered for both sides
37
if (this.secondary.hasCapability(EditorInputCapabilities.RequiresTrust)) {
38
capabilities |= EditorInputCapabilities.RequiresTrust;
39
}
40
41
// Singleton: should be considered for both sides
42
if (this.secondary.hasCapability(EditorInputCapabilities.Singleton)) {
43
capabilities |= EditorInputCapabilities.Singleton;
44
}
45
46
// Indicate we show more than one editor
47
capabilities |= EditorInputCapabilities.MultipleEditors;
48
49
return capabilities;
50
}
51
52
get resource(): URI | undefined {
53
if (this.hasIdenticalSides) {
54
// pretend to be just primary side when being asked for a resource
55
// in case both sides are the same. this can help when components
56
// want to identify this input among others (e.g. in history).
57
return this.primary.resource;
58
}
59
60
return undefined;
61
}
62
63
private hasIdenticalSides: boolean;
64
65
constructor(
66
protected readonly preferredName: string | undefined,
67
protected readonly preferredDescription: string | undefined,
68
readonly secondary: EditorInput,
69
readonly primary: EditorInput,
70
@IEditorService private readonly editorService: IEditorService
71
) {
72
super();
73
74
this.hasIdenticalSides = this.primary.matches(this.secondary);
75
76
this.registerListeners();
77
}
78
79
private registerListeners(): void {
80
81
// When the primary or secondary input gets disposed, dispose this diff editor input
82
this._register(Event.once(Event.any(this.primary.onWillDispose, this.secondary.onWillDispose))(() => {
83
if (!this.isDisposed()) {
84
this.dispose();
85
}
86
}));
87
88
// Re-emit some events from the primary side to the outside
89
this._register(this.primary.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
90
91
// Re-emit some events from both sides to the outside
92
this._register(this.primary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire()));
93
this._register(this.secondary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire()));
94
this._register(this.primary.onDidChangeLabel(() => this._onDidChangeLabel.fire()));
95
this._register(this.secondary.onDidChangeLabel(() => this._onDidChangeLabel.fire()));
96
}
97
98
override getName(): string {
99
const preferredName = this.getPreferredName();
100
if (preferredName) {
101
return preferredName;
102
}
103
104
if (this.hasIdenticalSides) {
105
return this.primary.getName(); // keep name concise when same editor is opened side by side
106
}
107
108
return localize('sideBySideLabels', "{0} - {1}", this.secondary.getName(), this.primary.getName());
109
}
110
111
getPreferredName(): string | undefined {
112
return this.preferredName;
113
}
114
115
override getDescription(verbosity?: Verbosity): string | undefined {
116
const preferredDescription = this.getPreferredDescription();
117
if (preferredDescription) {
118
return preferredDescription;
119
}
120
121
if (this.hasIdenticalSides) {
122
return this.primary.getDescription(verbosity);
123
}
124
125
return super.getDescription(verbosity);
126
}
127
128
getPreferredDescription(): string | undefined {
129
return this.preferredDescription;
130
}
131
132
override getTitle(verbosity?: Verbosity): string {
133
let title: string;
134
if (this.hasIdenticalSides) {
135
title = this.primary.getTitle(verbosity) ?? this.getName();
136
} else {
137
title = super.getTitle(verbosity);
138
}
139
140
const preferredTitle = this.getPreferredTitle();
141
if (preferredTitle) {
142
title = `${preferredTitle} (${title})`;
143
}
144
145
return title;
146
}
147
148
protected getPreferredTitle(): string | undefined {
149
if (this.preferredName && this.preferredDescription) {
150
return `${this.preferredName} ${this.preferredDescription}`;
151
}
152
153
if (this.preferredName || this.preferredDescription) {
154
return this.preferredName ?? this.preferredDescription;
155
}
156
157
return undefined;
158
}
159
160
override getLabelExtraClasses(): string[] {
161
if (this.hasIdenticalSides) {
162
return this.primary.getLabelExtraClasses();
163
}
164
165
return super.getLabelExtraClasses();
166
}
167
168
override getAriaLabel(): string {
169
if (this.hasIdenticalSides) {
170
return this.primary.getAriaLabel();
171
}
172
173
return super.getAriaLabel();
174
}
175
176
override getTelemetryDescriptor(): { [key: string]: unknown } {
177
const descriptor = this.primary.getTelemetryDescriptor();
178
179
return { ...descriptor, ...super.getTelemetryDescriptor() };
180
}
181
182
override isDirty(): boolean {
183
return this.primary.isDirty();
184
}
185
186
override isSaving(): boolean {
187
return this.primary.isSaving();
188
}
189
190
override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> {
191
const primarySaveResult = await this.primary.save(group, options);
192
193
return this.saveResultToEditor(primarySaveResult);
194
}
195
196
override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> {
197
const primarySaveResult = await this.primary.saveAs(group, options);
198
199
return this.saveResultToEditor(primarySaveResult);
200
}
201
202
private saveResultToEditor(primarySaveResult: EditorInput | IUntypedEditorInput | undefined): EditorInput | IUntypedEditorInput | undefined {
203
if (!primarySaveResult || !this.hasIdenticalSides) {
204
return primarySaveResult;
205
}
206
207
if (this.primary.matches(primarySaveResult)) {
208
return this;
209
}
210
211
if (primarySaveResult instanceof EditorInput) {
212
return new SideBySideEditorInput(this.preferredName, this.preferredDescription, primarySaveResult, primarySaveResult, this.editorService);
213
}
214
215
if (!isResourceDiffEditorInput(primarySaveResult) && !isResourceMultiDiffEditorInput(primarySaveResult) && !isResourceSideBySideEditorInput(primarySaveResult) && !isResourceMergeEditorInput(primarySaveResult)) {
216
return {
217
primary: primarySaveResult,
218
secondary: primarySaveResult,
219
label: this.preferredName,
220
description: this.preferredDescription
221
};
222
}
223
224
return undefined;
225
}
226
227
override revert(group: GroupIdentifier, options?: IRevertOptions): Promise<void> {
228
return this.primary.revert(group, options);
229
}
230
231
override async rename(group: GroupIdentifier, target: URI): Promise<IMoveResult | undefined> {
232
if (!this.hasIdenticalSides) {
233
return; // currently only enabled when both sides are identical
234
}
235
236
// Forward rename to primary side
237
const renameResult = await this.primary.rename(group, target);
238
if (!renameResult) {
239
return undefined;
240
}
241
242
// Build a side-by-side result from the rename result
243
244
if (isEditorInput(renameResult.editor)) {
245
return {
246
editor: new SideBySideEditorInput(this.preferredName, this.preferredDescription, renameResult.editor, renameResult.editor, this.editorService),
247
options: {
248
...renameResult.options,
249
viewState: findViewStateForEditor(this, group, this.editorService)
250
}
251
};
252
}
253
254
if (isResourceEditorInput(renameResult.editor)) {
255
return {
256
editor: {
257
label: this.preferredName,
258
description: this.preferredDescription,
259
primary: renameResult.editor,
260
secondary: renameResult.editor,
261
options: {
262
...renameResult.options,
263
viewState: findViewStateForEditor(this, group, this.editorService)
264
}
265
}
266
};
267
}
268
269
return undefined;
270
}
271
272
override isReadonly(): boolean | IMarkdownString {
273
return this.primary.isReadonly();
274
}
275
276
override toUntyped(options?: IUntypedEditorOptions): IResourceSideBySideEditorInput | undefined {
277
const primaryResourceEditorInput = this.primary.toUntyped(options);
278
const secondaryResourceEditorInput = this.secondary.toUntyped(options);
279
280
// Prevent nested side by side editors which are unsupported
281
if (
282
primaryResourceEditorInput && secondaryResourceEditorInput &&
283
!isResourceDiffEditorInput(primaryResourceEditorInput) && !isResourceDiffEditorInput(secondaryResourceEditorInput) &&
284
!isResourceMultiDiffEditorInput(primaryResourceEditorInput) && !isResourceMultiDiffEditorInput(secondaryResourceEditorInput) &&
285
!isResourceSideBySideEditorInput(primaryResourceEditorInput) && !isResourceSideBySideEditorInput(secondaryResourceEditorInput) &&
286
!isResourceMergeEditorInput(primaryResourceEditorInput) && !isResourceMergeEditorInput(secondaryResourceEditorInput)
287
) {
288
const untypedInput: IResourceSideBySideEditorInput = {
289
label: this.preferredName,
290
description: this.preferredDescription,
291
primary: primaryResourceEditorInput,
292
secondary: secondaryResourceEditorInput
293
};
294
295
if (typeof options?.preserveViewState === 'number') {
296
untypedInput.options = {
297
viewState: findViewStateForEditor(this, options.preserveViewState, this.editorService)
298
};
299
}
300
301
return untypedInput;
302
}
303
304
return undefined;
305
}
306
307
override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {
308
if (this === otherInput) {
309
return true;
310
}
311
312
if (isDiffEditorInput(otherInput) || isResourceDiffEditorInput(otherInput)) {
313
return false; // prevent subclass from matching
314
}
315
316
if (otherInput instanceof SideBySideEditorInput) {
317
return this.primary.matches(otherInput.primary) && this.secondary.matches(otherInput.secondary);
318
}
319
320
if (isResourceSideBySideEditorInput(otherInput)) {
321
return this.primary.matches(otherInput.primary) && this.secondary.matches(otherInput.secondary);
322
}
323
324
return false;
325
}
326
}
327
328
// Register SideBySide/DiffEditor Input Serializer
329
interface ISerializedSideBySideEditorInput {
330
name: string | undefined;
331
description: string | undefined;
332
333
primarySerialized: string;
334
secondarySerialized: string;
335
336
primaryTypeId: string;
337
secondaryTypeId: string;
338
}
339
340
export abstract class AbstractSideBySideEditorInputSerializer implements IEditorSerializer {
341
342
canSerialize(editorInput: EditorInput): boolean {
343
const input = editorInput as SideBySideEditorInput;
344
345
if (input.primary && input.secondary) {
346
const [secondaryInputSerializer, primaryInputSerializer] = this.getSerializers(input.secondary.typeId, input.primary.typeId);
347
348
return !!(secondaryInputSerializer?.canSerialize(input.secondary) && primaryInputSerializer?.canSerialize(input.primary));
349
}
350
351
return false;
352
}
353
354
serialize(editorInput: EditorInput): string | undefined {
355
const input = editorInput as SideBySideEditorInput;
356
357
if (input.primary && input.secondary) {
358
const [secondaryInputSerializer, primaryInputSerializer] = this.getSerializers(input.secondary.typeId, input.primary.typeId);
359
if (primaryInputSerializer && secondaryInputSerializer) {
360
const primarySerialized = primaryInputSerializer.serialize(input.primary);
361
const secondarySerialized = secondaryInputSerializer.serialize(input.secondary);
362
363
if (primarySerialized && secondarySerialized) {
364
const serializedEditorInput: ISerializedSideBySideEditorInput = {
365
name: input.getPreferredName(),
366
description: input.getPreferredDescription(),
367
primarySerialized,
368
secondarySerialized,
369
primaryTypeId: input.primary.typeId,
370
secondaryTypeId: input.secondary.typeId
371
};
372
373
return JSON.stringify(serializedEditorInput);
374
}
375
}
376
}
377
378
return undefined;
379
}
380
381
deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined {
382
const deserialized: ISerializedSideBySideEditorInput = JSON.parse(serializedEditorInput);
383
384
const [secondaryInputSerializer, primaryInputSerializer] = this.getSerializers(deserialized.secondaryTypeId, deserialized.primaryTypeId);
385
if (primaryInputSerializer && secondaryInputSerializer) {
386
const primaryInput = primaryInputSerializer.deserialize(instantiationService, deserialized.primarySerialized);
387
const secondaryInput = secondaryInputSerializer.deserialize(instantiationService, deserialized.secondarySerialized);
388
389
if (primaryInput instanceof EditorInput && secondaryInput instanceof EditorInput) {
390
return this.createEditorInput(instantiationService, deserialized.name, deserialized.description, secondaryInput, primaryInput);
391
}
392
}
393
394
return undefined;
395
}
396
397
private getSerializers(secondaryEditorInputTypeId: string, primaryEditorInputTypeId: string): [IEditorSerializer | undefined, IEditorSerializer | undefined] {
398
const registry = Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory);
399
400
return [registry.getEditorSerializer(secondaryEditorInputTypeId), registry.getEditorSerializer(primaryEditorInputTypeId)];
401
}
402
403
protected abstract createEditorInput(instantiationService: IInstantiationService, name: string | undefined, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput;
404
}
405
406
export class SideBySideEditorInputSerializer extends AbstractSideBySideEditorInputSerializer {
407
408
protected createEditorInput(instantiationService: IInstantiationService, name: string | undefined, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput {
409
return instantiationService.createInstance(SideBySideEditorInput, name, description, secondaryInput, primaryInput);
410
}
411
}
412
413