-
Notifications
You must be signed in to change notification settings - Fork 30.1k
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
Various kinds of sorting in explorer #29509
Conversation
@foreverest, thanks for your PR! By analyzing the history of the files in this pull request, we identified @egamma and @bpasero to be potential reviewers. |
@foreverest, It will cover your contributions to all Microsoft-managed open source projects. |
@foreverest, thanks for signing the contribution license agreement. We will now validate the agreement and then the pull request. |
@foreverest good start but I think we can do more. There is a lot of people that have asked for sorting the explorer and I think instead of introducing a setting for each we should have one setting
What do you think? They should all be straight forward, except for maybe the |
@bpasero sounds great! Let me do this. |
@bpasero I believe we should enhance sorting config by expanding in three dimensions. To cover most needs we can have something like this:
What do you think? |
@foreverest what is the motivation for allowing to change sorting from ascending/descending? I have not heard a feature request for that so far. I think that rather belongs to a pure file explorer than an editor. Arguably, even sorting by type and modified date is already stretch for an editor. |
@bpasero as of now, the ascending order is used in all sortings. But for |
@foreverest ah yeah, I was assuming that the order for "modified" actually is descending by default without an extra option to change that. overall I was hoping we could get this into a single option and not start to introduce many. |
@bpasero I'm done with this |
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.
Very good start, some comments to make this a little bit better 👍
@@ -60,6 +60,7 @@ export interface IFilesConfiguration extends IFilesConfiguration, IWorkbenchEdit | |||
}; | |||
autoReveal: boolean; | |||
enableDragAndDrop: boolean; | |||
sortOrder: string; |
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.
This should be sortOrder: 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified'
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.
This type will appear in three places:
IFilesConfiguration
interfaceExplorerView
class- The sorter class
Should I define a type alias like thistype SortOrder = 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified';
?
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, that makes sense to me.
@@ -0,0 +1,13 @@ | |||
/*--------------------------------------------------------------------------------------------- |
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 move this out of this new file and into src/vs/workbench/parts/files/common/files.ts
// Explorer Sorter | ||
export class FileSorter implements ISorter { | ||
// Default Sorter | ||
export class DefaultSorter implements ISorter { |
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 is lots of duplication across all sorters, specifically this code:
if (statA instanceof NewStatPlaceholder) {
return -1;
}
if (statB instanceof NewStatPlaceholder) {
return 1;
}
// Do not sort roots
if (statA.isRoot) {
return -1;
}
if (statB.isRoot) {
return 1;
}
I suggest to reduce the number of filters to just one which listens to configuration changes and updates itself based on the config. That avoids the extra call to setSorter()
in the tree and makes the sorter self contained based on the configuration.
@@ -82,6 +83,8 @@ export class ExplorerView extends CollapsibleView { | |||
|
|||
private autoReveal: boolean; | |||
|
|||
private sortOrder: string; |
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.
Make this sortOrder: 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified'
if (this.sortOrder !== configSortOrder) { | ||
this.sortOrder = configSortOrder; | ||
const sorter = this.getSorter(this.sortOrder); | ||
(<Tree>this.explorerViewer).setSorter(sorter); |
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 setting a different sorter after the configuration changed, I suggest to actually let the sorter implement the listening and update itself based on the config. You still need this code to decide to do the refresh, which is fine.
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 still need this code to decide to do the refresh
Do you mean to leave this code here, or move it to the sorter class?
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 mean, move it to the sorter class. But you still need that code to trigger a refresh of the explorer when the sorting config changed and imho it is fine that the explorerView triggers this and not the sorter.
@@ -364,6 +364,10 @@ export class Tree extends Events.EventEmitter implements _.ITree { | |||
return this.model.hasTrait(trait, element); | |||
} | |||
|
|||
public setSorter(sorter: _.ISorter): void { |
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.
This would not be needed once the sorter is configuring itself.
@@ -430,6 +443,21 @@ export class ExplorerView extends CollapsibleView { | |||
return this.explorerViewer; | |||
} | |||
|
|||
private getSorter(sortOrder: string): ISorter { |
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.
Not needed once the sorter configures itself based on the config.
if (e.gotUpdated()) { | ||
// Check updated only if sortOrder === MODIFIED | ||
const config = this.configurationService.getConfiguration<IFilesConfiguration>(); | ||
if (config && config.explorer && config.explorer.sortOrder === SortOrderConfiguration.MODIFIED) { |
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.
Why do you need to resolve the config here again? You should be able to just use this.sortOrder
?
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, you're right! 👍
} | ||
|
||
if (statA.mtime === statB.mtime) { | ||
return 0; |
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.
Wouldn't it make sense here to fallback to the default sorting by file name in case the modified time is the same?
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, sure!
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 I've made the changes. Please review.
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.
Almost, good stuff 👍
} | ||
|
||
if (oneName === otherName) { | ||
return 0; |
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.
Would it not make sense to use the filename compare fallback here as well?
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, and I do use it here. First, I compare extensions on line 104. And if they are equal, I compare filenames on lines 108-112.
nls.localize('sortOrder.type', 'Files and directories are sorted by their extensions, in alphabetical order. Directories are displayed before files.'), | ||
nls.localize('sortOrder.modified', 'Files and directories are sorted by last modified date, in descending order. Directories are displayed before files.') | ||
], | ||
'description': nls.localize('sortOrder', "Controls the way of sorting files and directories.") |
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.
Suggest: Controls the way of sorting files and directories in the explorer.
if (statA.isDirectory && !statB.isDirectory) { | ||
return -1; | ||
} | ||
switch (this.sortOrder) { |
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 appreciate that we now can do this all from a single compare implementation, that is very cool. However, I am having some difficulties that we have the switch (this.sortOrder)
twice in the method, makes it very hard to understand the flow imho. Can we:
- move the things which are independent from the sort order up (
NewStatPlaceholder
androots
) - have a single switch statement and not two?
I know that having a single switch statement might mean some duplication of sorting logic, but I think it would still be easier to understand. We can still in that model extract logic into methods if we wanted to share.
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.
But the order matters here. If I do this, it will affect the position of the new file/dir placeholder. It will appear at the very top of the current dir, not after all subdirs as currently.
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.
To implement the approach you described and keep current behavior of placeholders, the code may look like this:
public compare(tree: ITree, statA: FileStat, statB: FileStat): number {
// Do not sort roots
if (statA.isRoot) {
return -1;
}
if (statB.isRoot) {
return 1;
}
switch (this.sortOrder) {
case 'default':
if (statA.isDirectory && !statB.isDirectory) {
return -1;
}
if (statB.isDirectory && !statA.isDirectory) {
return 1;
}
if (statA instanceof NewStatPlaceholder) {
return -1;
}
if (statB instanceof NewStatPlaceholder) {
return 1;
}
return comparers.compareFileNames(statA.name, statB.name);
case 'type':
if (statA.isDirectory && !statB.isDirectory) {
return -1;
}
if (statB.isDirectory && !statA.isDirectory) {
return 1;
}
if (statA instanceof NewStatPlaceholder) {
return -1;
}
if (statB instanceof NewStatPlaceholder) {
return 1;
}
return comparers.compareFileExtensions(statA.name, statB.name);
case 'modified':
if (statA.isDirectory && !statB.isDirectory) {
return -1;
}
if (statB.isDirectory && !statA.isDirectory) {
return 1;
}
if (statA instanceof NewStatPlaceholder) {
return -1;
}
if (statB instanceof NewStatPlaceholder) {
return 1;
}
if (statA.mtime !== statB.mtime) {
return statA.mtime < statB.mtime ? 1 : -1;
} else {
return comparers.compareFileNames(statA.name, statB.name);
}
case 'filesFirst':
if (statA.isDirectory && !statB.isDirectory) {
return 1;
}
if (statB.isDirectory && !statA.isDirectory) {
return -1;
}
if (statA instanceof NewStatPlaceholder) {
return -1;
}
if (statB instanceof NewStatPlaceholder) {
return 1;
}
return comparers.compareFileNames(statA.name, statB.name);
case 'mixed':
if (statA instanceof NewStatPlaceholder) {
return -1;
}
if (statB instanceof NewStatPlaceholder) {
return 1;
}
return comparers.compareFileNames(statA.name, statB.name);
}
}
Yeah, it looks more verbose but definitely less confusing. Should I commit this?
@foreverest nah, it's fine, I will merge it, thanks. |
@bpasero Which is a bit unconventional. While dot files come first. |
@whitecolor good catch, can you file an issue? @foreverest do you want to take a look through a PR? |
@whitecolor That's because currently folders are handled in the same way as files. Namely, the algorithm first looks at extensions, then at names. In your case P.S.
|
IMHO, extensions within folder name should not matter. They should be ordered by name alphabatecally. |
@foreverest yes I think folders with a leading "." should show before folders that do not have any extension. Thanks 👍 |
I believe sometimes it may be useful to have folders sorted by extension too. |
Well, another sort type would be great : the default one, except it would be case sensitive. Every folders starting with a capital letter first. Github follow this rule if I'm not mistaken :) I would love to have this in VScode (which is awesome BTW). |
Addresses #29329, #5222, #22529, #11823