Repository: microsoft/vscode (TypeScript)
Date: 2022-10-31 19:11:14+00:00
Task type: Bug Fix

Issue:
Renaming a file is not reflected in the editor when renaming twice to same name different case
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes/No

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
- VS Code Version: 1.71.2
- OS Version: macOS Monterey 12.5.1 (21G83)

Steps to Reproduce:

1. create file named `Test.go`
2. rename the file to `test.go`
3. check the title of the file's editor tab
<img width="199" alt="image" src="https://user-images.githubusercontent.com/12084604/192210829-f6b0d8a3-fc61-4215-80e0-beb5296e39ee.png">

Is still `Test.go`, not the new name `test.go`. until reopen the file.
But `test.go` rename to `Test.go` is ok.
Also `Foo.go` reanme to `test.go` is ok.
Just `Test.go` rename to `test.go` is not ok.



Issue discussion:
Can you try to reproduce with our nightly insider builds? You can give our preview releases a try from: https://code.visualstudio.com/insiders/
> 

Ok, now it's changed when i use the insider build:
test.go rename to Test.go is not ok.
Test.go rename to test.go is ok.

It seems that some intersting things work wrong here.
Can you try to run `code --disable-extensions --user-data-dir <directory>` where `<directory>` is an empty folder? This will ensure Code is starting with a fresh data directory, e.g. no specific settings and without any extensions running.
> 

<img width="578" alt="image" src="https://user-images.githubusercontent.com/12084604/192284889-9fd91f04-098d-445e-92c2-74cb832b34e4.png">

As you see, nothing changed.
Hm yeah I think I see it too, the first rename works fine but then renaming back does not work, is that what you see?
@lramos15 this is a funny one and indeed a regression from our work to always go through the editor resolver when resolving editors and not go through the `ITextEditorService` as a shortcut.

**Steps**
* open an empty folder with 1 file `Test.go`
* open the file
* rename it from the explorer to `test.go`
* verify the tab label updates
* rename it from the explorer to `Test.go`
* 🐛 the label does not update

**Here is how it used to be**
* the rename operation will eventually hit `IEditorService.replaceEditors`
* since this is a default text editor we would directly use `ITextEditorService.createTextEditorInput`
* ⭐ we figure out that we have an existing editor input so we do not create a new one however we update its preferred resource [here](https://github.com/microsoft/vscode/blob/36b1398cfbabbe91d695717d76b818a788f7b7d9/src/vs/workbench/services/textfile/common/textEditorService.ts#L199)
* this triggers a label change event that will update the tab label

=> this works every time you rename, irrespective of going back and forward of the name

**Here is how it is today**
* we also end up in `IEditorService.replaceEditors`
* however we always now go to the editor resolver (which imho is good)
* the editor resolver however does not consider the input to be a new input after the first rename because the canonical resource is still how it was in the begining, it will do an early return [here](https://github.com/microsoft/vscode/blob/36b1398cfbabbe91d695717d76b818a788f7b7d9/src/vs/workbench/services/editor/browser/editorResolverService.ts#L183-L186)
* 🐛 as such, the inputs preferred resource is never updated

Now I am actually not so sure how to fix this. I am a bit worried that updating the `FileEditorInput.matches()` method to respect resource path casing may break other behaviour that relies on the current behaviour.

Maybe the resolver should not be so clever to do an early return? Why do we do this?
> Maybe the resolver should not be so clever to do an early return? Why do we do this?

Yeah maybe, I think it was to prevent creating too many inputs and calling unnecessary factories when things are not actually changing at all. I guess we could always remove the early return and see what breaks. 
At least for text file editors, there is caching so that we do not end up with an editor input each time, but I think the same issue already exists today: if you do not have a tab as active and the editor resolver is used to bring up that editor, we would delegate the decision whether to create an editor input or not to the provider and so its up to the provider imho to do the caching.

https://github.com/microsoft/vscode/blob/8b8767eadefaf4898fffa7942e9918ca446275fc/src/vs/workbench/services/textfile/common/textEditorService.ts#L246-L266
Sorry, got caught up in other work and missed this. Will attempt a fix early in debt week for the November release as it is too risky to go in this late

Solution patch:
diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts
--- a/src/vs/workbench/services/editor/browser/editorResolverService.ts
+++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts
@@ -114,7 +114,6 @@ export class EditorResolverService extends Disposable implements IEditorResolver
 		}
 
 		let resource = EditorResourceAccessor.getCanonicalUri(untypedEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
-		const options = untypedEditor.options;
 
 		// If it was resolved before we await for the extensions to activate and then proceed with resolution or else the backing extensions won't be registered
 		if (this.cache && resource && this.resourceMatchesCache(resource)) {
@@ -178,12 +177,6 @@ export class EditorResolverService extends Disposable implements IEditorResolver
 			return ResolvedStatus.NONE;
 		}
 
-		// If it's the currently active editor we shouldn't do anything
-		const activeEditor = group.activeEditor;
-		const isActive = activeEditor ? activeEditor.matches(untypedEditor) : false;
-		if (activeEditor && isActive) {
-			return { editor: activeEditor, options, group };
-		}
 		const input = await this.doResolveEditor(untypedEditor, group, selectedEditor);
 		if (conflictingDefault && input) {
 			// Show the conflicting default dialog


Test patch:
diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts
--- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts
+++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts
@@ -7,7 +7,7 @@ import * as assert from 'assert';
 import { EditorActivation, IResourceEditorInput } from 'vs/platform/editor/common/editor';
 import { URI } from 'vs/base/common/uri';
 import { Event } from 'vs/base/common/event';
-import { DEFAULT_EDITOR_ASSOCIATION, EditorCloseContext, EditorsOrder, IEditorCloseEvent, EditorInputWithOptions, IEditorPane, IResourceDiffEditorInput, isEditorInputWithOptions, IUntitledTextResourceEditorInput, IUntypedEditorInput, SideBySideEditor } from 'vs/workbench/common/editor';
+import { DEFAULT_EDITOR_ASSOCIATION, EditorCloseContext, EditorsOrder, IEditorCloseEvent, EditorInputWithOptions, IEditorPane, IResourceDiffEditorInput, isEditorInputWithOptions, IUntitledTextResourceEditorInput, IUntypedEditorInput, SideBySideEditor, isEditorInput } from 'vs/workbench/common/editor';
 import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput, ITestInstantiationService, registerTestResourceEditor, registerTestSideBySideEditor, createEditorPart, registerTestFileEditor, TestTextFileEditor, TestSingletonFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices';
 import { EditorService } from 'vs/workbench/services/editor/browser/editorService';
 import { IEditorGroup, IEditorGroupsService, GroupDirection, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService';
@@ -602,8 +602,13 @@ suite('EditorService', () => {
 
 		async function openEditor(editor: EditorInputWithOptions | IUntypedEditorInput, group?: PreferredGroup): Promise<IEditorPane | undefined> {
 			if (useOpenEditors) {
+				// The type safety isn't super good here, so we assist with runtime checks
+				// Open editors expects untyped or editor input with options, you cannot pass a typed editor input
+				// without options
+				if (!isEditorInputWithOptions(editor) && isEditorInput(editor)) {
+					editor = { editor: editor, options: {} };
+				}
 				const panes = await service.openEditors([editor], group);
-
 				return panes[0];
 			}
 
@@ -651,7 +656,7 @@ suite('EditorService', () => {
 				assert.ok(typedEditor instanceof TestFileEditorInput);
 				assert.strictEqual(typedEditor?.resource?.toString(), untypedEditorReplacement.resource.toString());
 
-				assert.strictEqual(editorFactoryCalled, 2);
+				assert.strictEqual(editorFactoryCalled, 3);
 				assert.strictEqual(untitledEditorFactoryCalled, 0);
 				assert.strictEqual(diffEditorFactoryCalled, 0);
 
@@ -881,7 +886,6 @@ suite('EditorService', () => {
 				assert.strictEqual(untitledEditorFactoryCalled, 0);
 				assert.strictEqual(diffEditorFactoryCalled, 0);
 
-				assert.ok(!lastEditorFactoryEditor);
 				assert.ok(!lastUntitledEditorFactoryEditor);
 				assert.ok(!lastDiffEditorFactoryEditor);