Path: blob/main/src/vs/workbench/common/editor/sideBySideEditorInput.ts
3296 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 { Event } from '../../../base/common/event.js';6import { IMarkdownString } from '../../../base/common/htmlContent.js';7import { URI } from '../../../base/common/uri.js';8import { localize } from '../../../nls.js';9import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';10import { Registry } from '../../../platform/registry/common/platform.js';11import { EditorInputCapabilities, GroupIdentifier, ISaveOptions, IRevertOptions, EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, ISideBySideEditorInput, IUntypedEditorInput, isResourceSideBySideEditorInput, isDiffEditorInput, isResourceDiffEditorInput, IResourceSideBySideEditorInput, findViewStateForEditor, IMoveResult, isEditorInput, isResourceEditorInput, Verbosity, isResourceMergeEditorInput, isResourceMultiDiffEditorInput } from '../editor.js';12import { EditorInput, IUntypedEditorOptions } from './editorInput.js';13import { IEditorService } from '../../services/editor/common/editorService.js';1415/**16* Side by side editor inputs that have a primary and secondary side.17*/18export class SideBySideEditorInput extends EditorInput implements ISideBySideEditorInput {1920static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput';2122override get typeId(): string {23return SideBySideEditorInput.ID;24}2526override get capabilities(): EditorInputCapabilities {2728// Use primary capabilities as main capabilities...29let capabilities = this.primary.capabilities;3031// ...with the exception of `CanSplitInGroup` which32// is only relevant to single editors.33capabilities &= ~EditorInputCapabilities.CanSplitInGroup;3435// Trust: should be considered for both sides36if (this.secondary.hasCapability(EditorInputCapabilities.RequiresTrust)) {37capabilities |= EditorInputCapabilities.RequiresTrust;38}3940// Singleton: should be considered for both sides41if (this.secondary.hasCapability(EditorInputCapabilities.Singleton)) {42capabilities |= EditorInputCapabilities.Singleton;43}4445// Indicate we show more than one editor46capabilities |= EditorInputCapabilities.MultipleEditors;4748return capabilities;49}5051get resource(): URI | undefined {52if (this.hasIdenticalSides) {53// pretend to be just primary side when being asked for a resource54// in case both sides are the same. this can help when components55// want to identify this input among others (e.g. in history).56return this.primary.resource;57}5859return undefined;60}6162private hasIdenticalSides: boolean;6364constructor(65protected readonly preferredName: string | undefined,66protected readonly preferredDescription: string | undefined,67readonly secondary: EditorInput,68readonly primary: EditorInput,69@IEditorService private readonly editorService: IEditorService70) {71super();7273this.hasIdenticalSides = this.primary.matches(this.secondary);7475this.registerListeners();76}7778private registerListeners(): void {7980// When the primary or secondary input gets disposed, dispose this diff editor input81this._register(Event.once(Event.any(this.primary.onWillDispose, this.secondary.onWillDispose))(() => {82if (!this.isDisposed()) {83this.dispose();84}85}));8687// Re-emit some events from the primary side to the outside88this._register(this.primary.onDidChangeDirty(() => this._onDidChangeDirty.fire()));8990// Re-emit some events from both sides to the outside91this._register(this.primary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire()));92this._register(this.secondary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire()));93this._register(this.primary.onDidChangeLabel(() => this._onDidChangeLabel.fire()));94this._register(this.secondary.onDidChangeLabel(() => this._onDidChangeLabel.fire()));95}9697override getName(): string {98const preferredName = this.getPreferredName();99if (preferredName) {100return preferredName;101}102103if (this.hasIdenticalSides) {104return this.primary.getName(); // keep name concise when same editor is opened side by side105}106107return localize('sideBySideLabels', "{0} - {1}", this.secondary.getName(), this.primary.getName());108}109110getPreferredName(): string | undefined {111return this.preferredName;112}113114override getDescription(verbosity?: Verbosity): string | undefined {115const preferredDescription = this.getPreferredDescription();116if (preferredDescription) {117return preferredDescription;118}119120if (this.hasIdenticalSides) {121return this.primary.getDescription(verbosity);122}123124return super.getDescription(verbosity);125}126127getPreferredDescription(): string | undefined {128return this.preferredDescription;129}130131override getTitle(verbosity?: Verbosity): string {132let title: string;133if (this.hasIdenticalSides) {134title = this.primary.getTitle(verbosity) ?? this.getName();135} else {136title = super.getTitle(verbosity);137}138139const preferredTitle = this.getPreferredTitle();140if (preferredTitle) {141title = `${preferredTitle} (${title})`;142}143144return title;145}146147protected getPreferredTitle(): string | undefined {148if (this.preferredName && this.preferredDescription) {149return `${this.preferredName} ${this.preferredDescription}`;150}151152if (this.preferredName || this.preferredDescription) {153return this.preferredName ?? this.preferredDescription;154}155156return undefined;157}158159override getLabelExtraClasses(): string[] {160if (this.hasIdenticalSides) {161return this.primary.getLabelExtraClasses();162}163164return super.getLabelExtraClasses();165}166167override getAriaLabel(): string {168if (this.hasIdenticalSides) {169return this.primary.getAriaLabel();170}171172return super.getAriaLabel();173}174175override getTelemetryDescriptor(): { [key: string]: unknown } {176const descriptor = this.primary.getTelemetryDescriptor();177178return { ...descriptor, ...super.getTelemetryDescriptor() };179}180181override isDirty(): boolean {182return this.primary.isDirty();183}184185override isSaving(): boolean {186return this.primary.isSaving();187}188189override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> {190const primarySaveResult = await this.primary.save(group, options);191192return this.saveResultToEditor(primarySaveResult);193}194195override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> {196const primarySaveResult = await this.primary.saveAs(group, options);197198return this.saveResultToEditor(primarySaveResult);199}200201private saveResultToEditor(primarySaveResult: EditorInput | IUntypedEditorInput | undefined): EditorInput | IUntypedEditorInput | undefined {202if (!primarySaveResult || !this.hasIdenticalSides) {203return primarySaveResult;204}205206if (this.primary.matches(primarySaveResult)) {207return this;208}209210if (primarySaveResult instanceof EditorInput) {211return new SideBySideEditorInput(this.preferredName, this.preferredDescription, primarySaveResult, primarySaveResult, this.editorService);212}213214if (!isResourceDiffEditorInput(primarySaveResult) && !isResourceMultiDiffEditorInput(primarySaveResult) && !isResourceSideBySideEditorInput(primarySaveResult) && !isResourceMergeEditorInput(primarySaveResult)) {215return {216primary: primarySaveResult,217secondary: primarySaveResult,218label: this.preferredName,219description: this.preferredDescription220};221}222223return undefined;224}225226override revert(group: GroupIdentifier, options?: IRevertOptions): Promise<void> {227return this.primary.revert(group, options);228}229230override async rename(group: GroupIdentifier, target: URI): Promise<IMoveResult | undefined> {231if (!this.hasIdenticalSides) {232return; // currently only enabled when both sides are identical233}234235// Forward rename to primary side236const renameResult = await this.primary.rename(group, target);237if (!renameResult) {238return undefined;239}240241// Build a side-by-side result from the rename result242243if (isEditorInput(renameResult.editor)) {244return {245editor: new SideBySideEditorInput(this.preferredName, this.preferredDescription, renameResult.editor, renameResult.editor, this.editorService),246options: {247...renameResult.options,248viewState: findViewStateForEditor(this, group, this.editorService)249}250};251}252253if (isResourceEditorInput(renameResult.editor)) {254return {255editor: {256label: this.preferredName,257description: this.preferredDescription,258primary: renameResult.editor,259secondary: renameResult.editor,260options: {261...renameResult.options,262viewState: findViewStateForEditor(this, group, this.editorService)263}264}265};266}267268return undefined;269}270271override isReadonly(): boolean | IMarkdownString {272return this.primary.isReadonly();273}274275override toUntyped(options?: IUntypedEditorOptions): IResourceSideBySideEditorInput | undefined {276const primaryResourceEditorInput = this.primary.toUntyped(options);277const secondaryResourceEditorInput = this.secondary.toUntyped(options);278279// Prevent nested side by side editors which are unsupported280if (281primaryResourceEditorInput && secondaryResourceEditorInput &&282!isResourceDiffEditorInput(primaryResourceEditorInput) && !isResourceDiffEditorInput(secondaryResourceEditorInput) &&283!isResourceMultiDiffEditorInput(primaryResourceEditorInput) && !isResourceMultiDiffEditorInput(secondaryResourceEditorInput) &&284!isResourceSideBySideEditorInput(primaryResourceEditorInput) && !isResourceSideBySideEditorInput(secondaryResourceEditorInput) &&285!isResourceMergeEditorInput(primaryResourceEditorInput) && !isResourceMergeEditorInput(secondaryResourceEditorInput)286) {287const untypedInput: IResourceSideBySideEditorInput = {288label: this.preferredName,289description: this.preferredDescription,290primary: primaryResourceEditorInput,291secondary: secondaryResourceEditorInput292};293294if (typeof options?.preserveViewState === 'number') {295untypedInput.options = {296viewState: findViewStateForEditor(this, options.preserveViewState, this.editorService)297};298}299300return untypedInput;301}302303return undefined;304}305306override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {307if (this === otherInput) {308return true;309}310311if (isDiffEditorInput(otherInput) || isResourceDiffEditorInput(otherInput)) {312return false; // prevent subclass from matching313}314315if (otherInput instanceof SideBySideEditorInput) {316return this.primary.matches(otherInput.primary) && this.secondary.matches(otherInput.secondary);317}318319if (isResourceSideBySideEditorInput(otherInput)) {320return this.primary.matches(otherInput.primary) && this.secondary.matches(otherInput.secondary);321}322323return false;324}325}326327// Register SideBySide/DiffEditor Input Serializer328interface ISerializedSideBySideEditorInput {329name: string | undefined;330description: string | undefined;331332primarySerialized: string;333secondarySerialized: string;334335primaryTypeId: string;336secondaryTypeId: string;337}338339export abstract class AbstractSideBySideEditorInputSerializer implements IEditorSerializer {340341canSerialize(editorInput: EditorInput): boolean {342const input = editorInput as SideBySideEditorInput;343344if (input.primary && input.secondary) {345const [secondaryInputSerializer, primaryInputSerializer] = this.getSerializers(input.secondary.typeId, input.primary.typeId);346347return !!(secondaryInputSerializer?.canSerialize(input.secondary) && primaryInputSerializer?.canSerialize(input.primary));348}349350return false;351}352353serialize(editorInput: EditorInput): string | undefined {354const input = editorInput as SideBySideEditorInput;355356if (input.primary && input.secondary) {357const [secondaryInputSerializer, primaryInputSerializer] = this.getSerializers(input.secondary.typeId, input.primary.typeId);358if (primaryInputSerializer && secondaryInputSerializer) {359const primarySerialized = primaryInputSerializer.serialize(input.primary);360const secondarySerialized = secondaryInputSerializer.serialize(input.secondary);361362if (primarySerialized && secondarySerialized) {363const serializedEditorInput: ISerializedSideBySideEditorInput = {364name: input.getPreferredName(),365description: input.getPreferredDescription(),366primarySerialized,367secondarySerialized,368primaryTypeId: input.primary.typeId,369secondaryTypeId: input.secondary.typeId370};371372return JSON.stringify(serializedEditorInput);373}374}375}376377return undefined;378}379380deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined {381const deserialized: ISerializedSideBySideEditorInput = JSON.parse(serializedEditorInput);382383const [secondaryInputSerializer, primaryInputSerializer] = this.getSerializers(deserialized.secondaryTypeId, deserialized.primaryTypeId);384if (primaryInputSerializer && secondaryInputSerializer) {385const primaryInput = primaryInputSerializer.deserialize(instantiationService, deserialized.primarySerialized);386const secondaryInput = secondaryInputSerializer.deserialize(instantiationService, deserialized.secondarySerialized);387388if (primaryInput instanceof EditorInput && secondaryInput instanceof EditorInput) {389return this.createEditorInput(instantiationService, deserialized.name, deserialized.description, secondaryInput, primaryInput);390}391}392393return undefined;394}395396private getSerializers(secondaryEditorInputTypeId: string, primaryEditorInputTypeId: string): [IEditorSerializer | undefined, IEditorSerializer | undefined] {397const registry = Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory);398399return [registry.getEditorSerializer(secondaryEditorInputTypeId), registry.getEditorSerializer(primaryEditorInputTypeId)];400}401402protected abstract createEditorInput(instantiationService: IInstantiationService, name: string | undefined, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput;403}404405export class SideBySideEditorInputSerializer extends AbstractSideBySideEditorInputSerializer {406407protected createEditorInput(instantiationService: IInstantiationService, name: string | undefined, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput {408return instantiationService.createInstance(SideBySideEditorInput, name, description, secondaryInput, primaryInput);409}410}411412413