diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 3a1bd949168..eca511ebe7b 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -256,7 +256,7 @@ export class BreadcrumbsControl { input = input.primary; } if (Registry.as(Extensions.EditorInputFactories).getFileEditorInputFactory().isFileEditorInput(input)) { - fileInfoUri = input.label; + fileInfoUri = input.preferredResource; } this.domNode.classList.toggle('hidden', false); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 16d38a9a18f..202f03996a9 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -167,7 +167,7 @@ export interface IFileEditorInputFactory { /** * Creates new new editor input capable of showing files. */ - createFileEditorInput(resource: URI, label: URI | undefined, encoding: string | undefined, mode: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; + createFileEditorInput(resource: URI, preferredResource: URI | undefined, encoding: string | undefined, mode: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; /** * Check if the provided object is a file editor input. @@ -649,20 +649,25 @@ export interface IModeSupport { export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeSupport { /** - * Gets the resource this file input is about. + * Gets the resource this file input is about. This will always be the + * canonical form of the resource, so it may differ from the original + * resource that was provided to create the input. Use `preferredResource` + * for the form as it was created. */ readonly resource: URI; /** - * Gets the label of the editor. In most cases this will - * be identical to the resource. + * Gets the preferred resource of the editor. In most cases this will + * be identical to the resource. But in some cases the preferredResource + * may differ in path casing to the actual resource because we keep + * canonical forms of resources in-memory. */ - readonly label: URI; + readonly preferredResource: URI; /** - * Sets the preferred label to use for this file input. + * Sets the preferred resource to use for this file input. */ - setLabel(label: URI): void; + setPreferredResource(preferredResource: URI): void; /** * Sets the preferred encoding to use for this file input. diff --git a/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts index ad427fbf461..92bb60db4ad 100644 --- a/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -22,12 +22,12 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput { private static readonly MEMOIZER = createMemoizer(); - private _label: URI; - get label(): URI { return this._label; } + private _preferredResource: URI; + get preferredResource(): URI { return this._preferredResource; } constructor( public readonly resource: URI, - preferredLabel: URI | undefined, + preferredResource: URI | undefined, @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @ITextFileService protected readonly textFileService: ITextFileService, @@ -37,7 +37,7 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput { ) { super(); - this._label = preferredLabel || resource; + this._preferredResource = preferredResource || resource; this.registerListeners(); } @@ -51,7 +51,7 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput { } private onLabelEvent(scheme: string): void { - if (scheme === this._label.scheme) { + if (scheme === this._preferredResource.scheme) { this.updateLabel(); } } @@ -65,25 +65,21 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput { this._onDidChangeLabel.fire(); } - setLabel(label: URI): void { - if (!extUri.isEqual(label, this._label)) { - this._label = label; + setPreferredResource(preferredResource: URI): void { + if (!extUri.isEqual(preferredResource, this._preferredResource)) { + this._preferredResource = preferredResource; this.updateLabel(); } } - getLabel(): URI { - return this._label; - } - getName(): string { return this.basename; } @AbstractTextResourceEditorInput.MEMOIZER private get basename(): string { - return this.labelService.getUriBasenameLabel(this._label); + return this.labelService.getUriBasenameLabel(this._preferredResource); } getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { @@ -100,17 +96,17 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput { @AbstractTextResourceEditorInput.MEMOIZER private get shortDescription(): string { - return this.labelService.getUriBasenameLabel(dirname(this._label)); + return this.labelService.getUriBasenameLabel(dirname(this._preferredResource)); } @AbstractTextResourceEditorInput.MEMOIZER private get mediumDescription(): string { - return this.labelService.getUriLabel(dirname(this._label), { relative: true }); + return this.labelService.getUriLabel(dirname(this._preferredResource), { relative: true }); } @AbstractTextResourceEditorInput.MEMOIZER private get longDescription(): string { - return this.labelService.getUriLabel(dirname(this._label)); + return this.labelService.getUriLabel(dirname(this._preferredResource)); } @AbstractTextResourceEditorInput.MEMOIZER @@ -120,12 +116,12 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput { @AbstractTextResourceEditorInput.MEMOIZER private get mediumTitle(): string { - return this.labelService.getUriLabel(this._label, { relative: true }); + return this.labelService.getUriLabel(this._preferredResource, { relative: true }); } @AbstractTextResourceEditorInput.MEMOIZER private get longTitle(): string { - return this.labelService.getUriLabel(this._label); + return this.labelService.getUriLabel(this._preferredResource); } getTitle(verbosity: Verbosity): string { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 52a93613d63..4346b997702 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -102,8 +102,8 @@ Registry.as(EditorExtensions.Editors).registerEditor( // Register default file input factory Registry.as(EditorInputExtensions.EditorInputFactories).registerFileEditorInputFactory({ - createFileEditorInput: (resource, label, encoding, mode, instantiationService): IFileEditorInput => { - return instantiationService.createInstance(FileEditorInput, resource, label, encoding, mode); + createFileEditorInput: (resource, preferredResource, encoding, mode, instantiationService): IFileEditorInput => { + return instantiationService.createInstance(FileEditorInput, resource, preferredResource, encoding, mode); }, isFileEditorInput: (obj): obj is IFileEditorInput => { @@ -113,7 +113,7 @@ Registry.as(EditorInputExtensions.EditorInputFactor interface ISerializedFileEditorInput { resourceJSON: UriComponents; - labelJSON?: UriComponents; + preferredResourceJSON?: UriComponents; encoding?: string; modeId?: string; } @@ -128,10 +128,10 @@ class FileEditorInputFactory implements IEditorInputFactory { serialize(editorInput: EditorInput): string { const fileEditorInput = editorInput; const resource = fileEditorInput.resource; - const label = fileEditorInput.getLabel(); + const preferredResource = fileEditorInput.preferredResource; const serializedFileEditorInput: ISerializedFileEditorInput = { resourceJSON: resource.toJSON(), - labelJSON: extUri.isEqual(resource, label) ? undefined : label, // only storing label if it differs from the resource + preferredResourceJSON: extUri.isEqual(resource, preferredResource) ? undefined : preferredResource, // only storing preferredResource if it differs from the resource encoding: fileEditorInput.getEncoding(), modeId: fileEditorInput.getPreferredMode() // only using the preferred user associated mode here if available to not store redundant data }; @@ -143,13 +143,18 @@ class FileEditorInputFactory implements IEditorInputFactory { return instantiationService.invokeFunction(accessor => { const serializedFileEditorInput: ISerializedFileEditorInput = JSON.parse(serializedEditorInput); const resource = URI.revive(serializedFileEditorInput.resourceJSON); - const label = URI.revive(serializedFileEditorInput.labelJSON); + const preferredResource = URI.revive(serializedFileEditorInput.preferredResourceJSON); const encoding = serializedFileEditorInput.encoding; const mode = serializedFileEditorInput.modeId; - const fileEditorInput = accessor.get(IEditorService).createEditorInput({ resource, encoding, mode, forceFile: true }) as FileEditorInput; - if (label) { - fileEditorInput.setLabel(label); + const fileEditorInput = accessor.get(IEditorService).createEditorInput({ + resource: preferredResource || resource, // prefer the preferred resource when creating the input again (https://github.com/microsoft/vscode/issues/102627) + encoding, + mode, + forceFile: true + }) as FileEditorInput; + if (preferredResource) { + fileEditorInput.setPreferredResource(preferredResource); } return fileEditorInput; diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index aaf749555d4..6d104cc918a 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -45,7 +45,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements constructor( resource: URI, - preferredLabel: URI | undefined, + preferredResource: URI | undefined, preferredEncoding: string | undefined, preferredMode: string | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -57,7 +57,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(resource, preferredLabel, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); + super(resource, preferredResource, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); this.model = this.textFileService.files.get(resource); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 98d437bf0b2..9bf3ad060f3 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -9,7 +9,7 @@ import { toResource } from 'vs/base/test/common/utils'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { workbenchInstantiationService, TestServiceAccessor, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EncodingMode, Verbosity } from 'vs/workbench/common/editor'; +import { EncodingMode, IEditorInputFactoryRegistry, Verbosity, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; @@ -18,6 +18,7 @@ import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRe import { DisposableStore } from 'vs/base/common/lifecycle'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { Registry } from 'vs/platform/registry/common/platform'; suite('Files - FileEditorInput', () => { let instantiationService: IInstantiationService; @@ -92,18 +93,32 @@ suite('Files - FileEditorInput', () => { } }); - test('label', function () { - const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), toResource.call(this, '/foo/bar/UPDATEFILE.js'), undefined, undefined); + test('preferred resource', function () { + const resource = toResource.call(this, '/foo/bar/updatefile.js'); + const preferredResource = toResource.call(this, '/foo/bar/UPDATEFILE.js'); + + const inputWithoutPreferredResource = instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined); + assert.equal(inputWithoutPreferredResource.resource.toString(), resource.toString()); + assert.equal(inputWithoutPreferredResource.preferredResource.toString(), resource.toString()); + + const inputWithPreferredResource = instantiationService.createInstance(FileEditorInput, resource, preferredResource, undefined, undefined); + + assert.equal(inputWithPreferredResource.resource.toString(), resource.toString()); + assert.equal(inputWithPreferredResource.preferredResource.toString(), preferredResource.toString()); let didChangeLabel = false; - const listener = input.onDidChangeLabel(e => { + const listener = inputWithPreferredResource.onDidChangeLabel(e => { didChangeLabel = true; }); - assert.equal(input.getName(), 'UPDATEFILE.js'); + assert.equal(inputWithPreferredResource.getName(), 'UPDATEFILE.js'); - input.setLabel(toResource.call(this, '/FOO/BAR/updateFILE.js')); - assert.equal(input.getName(), 'updateFILE.js'); + const otherPreferredResource = toResource.call(this, '/FOO/BAR/updateFILE.js'); + inputWithPreferredResource.setPreferredResource(otherPreferredResource); + + assert.equal(inputWithPreferredResource.resource.toString(), resource.toString()); + assert.equal(inputWithPreferredResource.preferredResource.toString(), otherPreferredResource.toString()); + assert.equal(inputWithPreferredResource.getName(), 'updateFILE.js'); assert.equal(didChangeLabel, true); listener.dispose(); @@ -239,4 +254,37 @@ suite('Files - FileEditorInput', () => { resolved.dispose(); }); + + test('file editor input factory', async function () { + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); + + const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined, undefined); + + const factory = Registry.as(EditorExtensions.EditorInputFactories).getEditorInputFactory(input.getTypeId()); + if (!factory) { + assert.fail('File Editor Input Factory missing'); + } + + assert.equal(factory.canSerialize(input), true); + + const inputSerialized = factory.serialize(input); + if (!inputSerialized) { + assert.fail('Unexpected serialized file input'); + } + + const inputDeserialized = factory.deserialize(instantiationService, inputSerialized); + assert.equal(input.matches(inputDeserialized), true); + + const preferredResource = toResource.call(this, '/foo/bar/UPDATEfile.js'); + const inputWithPreferredResource = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), preferredResource, undefined, undefined); + + const inputWithPreferredResourceSerialized = factory.serialize(inputWithPreferredResource); + if (!inputWithPreferredResourceSerialized) { + assert.fail('Unexpected serialized file input'); + } + + const inputWithPreferredResourceDeserialized = factory.deserialize(instantiationService, inputWithPreferredResourceSerialized) as FileEditorInput; + assert.equal(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.resource.toString()); + assert.equal(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.preferredResource.toString()); + }); }); diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 4cd489bac56..88800a2c196 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -874,16 +874,21 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Derive the label from the path if not provided explicitly const label = resourceEditorInput.label || basename(resourceEditorInput.resource); + // We keep track of the preferred resource this input is to be created + // with but it may be different from the canonical resource (see below) + const preferredResource = resourceEditorInput.resource; + // From this moment on, only operate on the canonical resource // to ensure we reduce the chance of opening the same resource // with different resource forms (e.g. path casing on Windows) - const canonicalResource = this.asCanonicalEditorResource(resourceEditorInput.resource); + const canonicalResource = this.asCanonicalEditorResource(preferredResource); + return this.createOrGetCached(canonicalResource, () => { // File - if (resourceEditorInput.forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(canonicalResource)) { - return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, resourceEditorInput.resource, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService); + if (resourceEditorInput.forceFile || this.fileService.canHandleResource(canonicalResource)) { + return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, preferredResource, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService); } // Resource @@ -897,7 +902,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Files else if (!(cachedInput instanceof ResourceEditorInput)) { - cachedInput.setLabel(resourceEditorInput.resource); + cachedInput.setPreferredResource(preferredResource); if (resourceEditorInput.encoding) { cachedInput.setPreferredEncoding(resourceEditorInput.encoding); diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts index 6c41c464c08..1e618eb1943 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -150,14 +150,14 @@ class NonSerializableTestEditorInput extends EditorInput { class TestFileEditorInput extends EditorInput implements IFileEditorInput { - readonly label = this.resource; + readonly preferredResource = this.resource; constructor(public id: string, public resource: URI) { super(); } getTypeId() { return 'testFileEditorInputForGroups'; } resolve(): Promise { return Promise.resolve(null!); } - setLabel(label: URI): void { } + setPreferredResource(resource: URI): void { } setEncoding(encoding: string) { } getEncoding() { return undefined; } setPreferredEncoding(encoding: string) { } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 92ab7bf7ef7..8840867e739 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1123,7 +1123,7 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); } matches(other: EditorInput): boolean { return !!(other?.resource && this.resource.toString() === other.resource.toString() && other instanceof TestFileEditorInput && other.getTypeId() === this.typeId); } - setLabel(label: URI): void { } + setPreferredResource(resource: URI): void { } setEncoding(encoding: string) { } getEncoding() { return undefined; } setPreferredEncoding(encoding: string) { }