-
Notifications
You must be signed in to change notification settings - Fork 30.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add option to always reuse open editors #21815
Conversation
The idea being that we never* open the same file in more than one editor at a time, regardless of the action which opens the editor. *never meaning whenever possible, for example splitting with a single file open or explicitly opening the same file "side by side" cannot work if we literally take the newly introduced option. Implements microsoft#7049 (note that the issue's title talks only about Ctrl/Cmd+P action but the consensus seems to be that we should apply the logic to all "open a document" actions).
@ddotlic, It will cover your contributions to all Microsoft-managed open source projects. |
@ddotlic, thanks for signing the contribution license agreement. We will now validate the agreement and then the pull request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, some feedback provided.
// Respect option to reveal an editor if it is open (not necessarily already visible) | ||
const skipReuse = (options && options.index) || arg1; | ||
if(!skipReuse && reuseIfOpen) { | ||
const groups = this.stacks.groups; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to extract this into a method findGroup(editor)
in the EditorStacksModel
and add tests for it. I would add an option to this method to only respect the active editor and then also use it for the revealIfVisible
option further below 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially went down this road, but the code became less readable. It's gonna be easier to show you by implementing it than to discuss, let's revisit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be surprised and the added value of being able to write tests is a big plus.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bpasero Sorry, my comment was related only to the value of extracting into a findGroup
method, not adding tests for it. I did not mean to imply there's no value in adding tests.
Let's do it like this: I'll extract into a method and if you think it's readable, I'll proceed with the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ddotlic awesome
const config = this.configurationService.getConfiguration<IWorkbenchEditorConfiguration>(); | ||
const reuseIfOpen = config.workbench.editor.reuseIfOpen; | ||
// Respect option to reveal an editor if it is open (not necessarily already visible) | ||
const skipReuse = (options && options.index) || arg1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are imho more cases where you need to skipReuse
: when revealIfVisible
is true and there is a visible editor matching the input you should prefer it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If so, wouldn't this be handled simply by moving the revealIfVisible code block before the newly introduced one? It exits early so we don't need to check for it later on (assuming I move the new code block below).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes!
@@ -107,6 +107,11 @@ configurationRegistry.registerConfiguration({ | |||
'default': 'right', | |||
'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorOpenPositioning' }, "Controls where editors open. Select 'left' or 'right' to open editors to the left or right of the current active one. Select 'first' or 'last' to open editors independently from the currently active one.") | |||
}, | |||
'workbench.editor.reuseIfOpen': { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name and description reads a bit weird, maybe this should be called revealIfOpen
? Because the editor is not reused if opened, it is being revealed. That also aligns with the revealIfVisible
option.
Description idea:
Controls if an editor is revealed in any of the visible groups if opened. If disabled, an editor will prefer to open in the currently active editor group. If enabled, an already opened editor will be revealed instead of opened again in the currently active editor group. Note that there are some cases where this setting is ignored, e.g. when forcing an editor to open in a specific group or to the side of the currently active group.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am in no way pretending I got the wording right 😄 I'm perfectly fine putting in whatever you feel is suitable (the above IMHO is good enough, if a bit long).
I struggled quite a bit with the option name and couldn't think of anything obviousy good, so thanks for the suggestion 👍
This being my first PR in a while, I'll need to first figure out how to update this PR instead of sending a new one 😳
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries. To update this PR you just push to your branch ddotlic:master
. Thats it.
Refactored according to @bpasero suggestions.
@bpasero Updated the PR with the changes you've requested. It's all there, including unit test for the code we've extracted into a common method. Let me know if I can do something more to make this acceptable. Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, first round of feedback provided.
@@ -944,6 +944,16 @@ export class EditorStacksModel implements IEditorStacksModel { | |||
return this._groups.indexOf(group); | |||
} | |||
|
|||
public findGroup(editorInput: EditorInput, activeOnly: Boolean): EditorGroup { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Boolean
=> boolean
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LOL, that was autocompleted by VS Code 😉
@@ -944,6 +944,16 @@ export class EditorStacksModel implements IEditorStacksModel { | |||
return this._groups.indexOf(group); | |||
} | |||
|
|||
public findGroup(editorInput: EditorInput, activeOnly: Boolean): EditorGroup { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
activeOnly
should probably be optional => activeOnly?: boolean
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, no problem
@@ -944,6 +944,16 @@ export class EditorStacksModel implements IEditorStacksModel { | |||
return this._groups.indexOf(group); | |||
} | |||
|
|||
public findGroup(editorInput: EditorInput, activeOnly: Boolean): EditorGroup { | |||
const found = this.groups.filter(group => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to change this logic a) for perf reasons and b) for logical reasons
a) Perf: I suggest to simply use 2 for loops here to avoid iterating over all groups and all editors even if a match has found. With the current logic, if the editor is found in group 1 you will still iterate over all other groups and editors even though we only return one result.
b) Logic: there is always an active group and that group might not be group 1. Wouldn't it make more sense to start with the currently active group to look for the editor and then check the others? This means we prefer to reveal an editor in the currently active group if we find it. Another solution would be to prefer an editor if it is already visible in one of the groups. You have to decide after all how this option should behave:
- does it prefer active editors over inactive
- does it prefer active groups over inactive
- does it prefer strict spatial order of the groups (left to right)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a) I initially wrote it as such, precisely for the reasons you listed, but it somehow looked ugly and the intent was obscured by lots of lines of "useless" code; I decided to err on the side of readability seeing how we're talking about 3 groups at most and what, couple dozen editors open? But if you prefer perf, that's what we'll do
b) Not really, it all depends on the activeOnly parameter; if I start catering for that (as it was previously), we'll end up with a mess - code which tries to do two things at the same time, which is how I originally ended up leaving it as-is and just adding a secondary 2 for loops (exactly what you wanted for this method btw)
If I understand what you're saying, the best approach would be to simply take my previous code and paste it into the method. It's gonna be much longer, less readable but faster in the most common case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bpasero The code before assumed that we prefer active editor and groups over inactive as it made sure to first check the active ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest you come up with something that you think makes most sense and I review it again 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bpasero It's all done since an hour ago - don't you see my changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ddotlic checking now...
return editor.matches(editorInput) | ||
&& (!activeOnly || isActive); | ||
})); | ||
return found[0] || null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is fine to return undefined
instead of null
, you do not need to do an explicit || null
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, understood.
const found = this.groups.filter(group => | ||
group.getEditors().some(editor => { | ||
const isActive = group.isActive(editor); | ||
return editor.matches(editorInput) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should first check for the isActive
condition and then do the editor.matches
call to avoid it unless needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough.
const skipReuse = (options && options.index) || arg1; | ||
if (!skipReuse && revealIfOpen) { | ||
const group = this.stacks.findGroup(input, false); | ||
if (null !== group) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> if (group)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, let me do the changes quickly as today I have a lot more bandwidth for this kind of work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bpasero Just to make sure it's understood, the changes have been done and the code pushed an hour ago, feel free to proceed with the review at your convenience.
return editorToCheck.position; | ||
} | ||
const group = this.stacks.findGroup(input, true); | ||
if (null !== group) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> if (group)
} | ||
} | ||
|
||
const config = this.configurationService.getConfiguration<IWorkbenchEditorConfiguration>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of getting the config each time we open an editor I suggest to store the value in a field of EditorPart
. You can see in the constructor that we resolve configuration and we also react to updates.
Note that at the expense of memory consumed, the portions of the code where we "order" groups and editors such that active one comes first could have been much more readable.
@bpasero Next time I'll make a branch for my changes, merging with master as we develop this PR is annoying at best 😳 |
@ddotlic I merged with your master to be able to apply the diff in my VS Code instance (using |
@bpasero I'm just saying that it's my fault for making your life difficult, sorry for that, having a separate branch in the future should help you see the diff much better. |
@ddotlic no problem at all, for me it was not harder compared to other PRs, I have my fixed list of steps to do to bring a PR up to speed with master to be able to create a patch I can apply :) |
@ddotlic thanks, merged. I will push some 💄 on top of it, nothing big. |
@bpasero Let's hope it's not like in the saying regarding 💄 on a 🐷 🤣 |
Hehe ;) |
The idea being that we never* open the same file in more than one editor at a time, regardless of the action which opens the editor.
*never meaning whenever possible, for example splitting with a single file open or explicitly opening the same file "side by side" cannot work if we literally take the newly introduced option.
Implements #7049 (note that the issue's title talks only about Ctrl/Cmd+P action but the consensus - at least with @bpasero and the issue creator - seems to be that we should apply the logic to all "open a document" actions).