jupyterlab_file_browser_sorting_extension 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +61 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +232 -0
- package/package.json +200 -0
- package/src/__tests__/jupyterlab_file_browser_sorting_extension.spec.ts +9 -0
- package/src/index.ts +274 -0
- package/style/base.css +5 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Stellars Henson
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# jupyterlab_file_browser_sorting_extension
|
|
2
|
+
|
|
3
|
+
[](https://github.com/stellarshenson/jupyterlab_file_browser_sorting_extension/actions/workflows/build.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/jupyterlab_file_browser_sorting_extension)
|
|
5
|
+
[](https://pypi.org/project/jupyterlab-file-browser-sorting-extension/)
|
|
6
|
+
[](https://pepy.tech/project/jupyterlab-file-browser-sorting-extension)
|
|
7
|
+
[](https://jupyterlab.readthedocs.io/en/stable/)
|
|
8
|
+
[](https://kolomolo.com)
|
|
9
|
+
|
|
10
|
+
JupyterLab extension that implements LC_COLLATE=C (ASCIIbetical) sorting for the file browser.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **LC_COLLATE=C sorting** - Standard Unix sorting order used by `ls` with C locale
|
|
15
|
+
- **Dot files first** - Hidden files (starting with `.`) appear before other files
|
|
16
|
+
- **Uppercase before lowercase** - Capital letters (A-Z) sort before lowercase (a-z)
|
|
17
|
+
- **ASCII order** - Characters sorted by their ASCII code values
|
|
18
|
+
- **Toggle via context menu** - Right-click in the file browser to enable/disable
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Right-click anywhere in the file browser content area and select **"Use Unix-Style Sorting (LC_COLLATE=C)"** to toggle the sorting mode. The setting is persisted across sessions.
|
|
23
|
+
|
|
24
|
+
When enabled, the extension sorts files using the standard Unix C locale collation sequence:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
. (dot) -> 0-9 -> A-Z -> _ (underscore) -> a-z
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Example sort order:**
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
.hidden
|
|
34
|
+
.profile
|
|
35
|
+
123file
|
|
36
|
+
ABC
|
|
37
|
+
Makefile
|
|
38
|
+
README
|
|
39
|
+
_config
|
|
40
|
+
abc
|
|
41
|
+
readme
|
|
42
|
+
zebra
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This matches the behavior of `LC_COLLATE=C ls` on Unix systems.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- JupyterLab >= 4.0.0
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install jupyterlab_file_browser_sorting_extension
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Uninstall
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip uninstall jupyterlab_file_browser_sorting_extension
|
|
61
|
+
```
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { IFileBrowserFactory } from '@jupyterlab/filebrowser';
|
|
2
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
3
|
+
const PLUGIN_ID = 'jupyterlab_file_browser_sorting_extension:plugin';
|
|
4
|
+
const COMMAND_TOGGLE_UNIX_SORT = 'filebrowser:toggle-unix-sorting';
|
|
5
|
+
/**
|
|
6
|
+
* LC_COLLATE=C (ASCIIbetical) comparison function.
|
|
7
|
+
* Sort order: . -> 0-9 -> A-Z -> _ -> a-z
|
|
8
|
+
*/
|
|
9
|
+
function cLocaleCompare(a, b) {
|
|
10
|
+
const lenA = a.length;
|
|
11
|
+
const lenB = b.length;
|
|
12
|
+
const minLen = Math.min(lenA, lenB);
|
|
13
|
+
for (let i = 0; i < minLen; i++) {
|
|
14
|
+
const charCodeA = a.charCodeAt(i);
|
|
15
|
+
const charCodeB = b.charCodeAt(i);
|
|
16
|
+
if (charCodeA !== charCodeB) {
|
|
17
|
+
return charCodeA - charCodeB;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return lenA - lenB;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Default locale comparison (case-insensitive).
|
|
24
|
+
*/
|
|
25
|
+
function defaultLocaleCompare(a, b) {
|
|
26
|
+
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
27
|
+
}
|
|
28
|
+
// Global settings
|
|
29
|
+
let sortNotebooksFirst = false;
|
|
30
|
+
let useCLocaleSorting = true;
|
|
31
|
+
/**
|
|
32
|
+
* Sort items using the specified comparison function.
|
|
33
|
+
*/
|
|
34
|
+
function sortItems(items, state) {
|
|
35
|
+
const copy = [...items];
|
|
36
|
+
const reverse = state.direction === 'descending' ? -1 : 1;
|
|
37
|
+
function getPriority(item) {
|
|
38
|
+
if (item.type === 'directory') {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (sortNotebooksFirst && item.type === 'notebook') {
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
function compareByName(a, b) {
|
|
47
|
+
return useCLocaleSorting
|
|
48
|
+
? cLocaleCompare(a.name, b.name)
|
|
49
|
+
: defaultLocaleCompare(a.name, b.name);
|
|
50
|
+
}
|
|
51
|
+
copy.sort((a, b) => {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
const priorityA = getPriority(a);
|
|
54
|
+
const priorityB = getPriority(b);
|
|
55
|
+
if (priorityA !== priorityB) {
|
|
56
|
+
return priorityA - priorityB;
|
|
57
|
+
}
|
|
58
|
+
let result;
|
|
59
|
+
if (state.key === 'last_modified') {
|
|
60
|
+
result =
|
|
61
|
+
new Date(a.last_modified).getTime() -
|
|
62
|
+
new Date(b.last_modified).getTime();
|
|
63
|
+
}
|
|
64
|
+
else if (state.key === 'file_size') {
|
|
65
|
+
result = ((_a = b.size) !== null && _a !== void 0 ? _a : 0) - ((_b = a.size) !== null && _b !== void 0 ? _b : 0);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
result = compareByName(a, b);
|
|
69
|
+
}
|
|
70
|
+
return result * reverse;
|
|
71
|
+
});
|
|
72
|
+
return copy;
|
|
73
|
+
}
|
|
74
|
+
const plugin = {
|
|
75
|
+
id: PLUGIN_ID,
|
|
76
|
+
description: 'LC_COLLATE=C file browser sorting',
|
|
77
|
+
autoStart: true,
|
|
78
|
+
requires: [IFileBrowserFactory],
|
|
79
|
+
optional: [ISettingRegistry],
|
|
80
|
+
activate: (app, factory, settingRegistry) => {
|
|
81
|
+
const { commands } = app;
|
|
82
|
+
const { tracker } = factory;
|
|
83
|
+
const patchedListings = new WeakSet();
|
|
84
|
+
/**
|
|
85
|
+
* Force re-sort on a single listing
|
|
86
|
+
*/
|
|
87
|
+
function resortListing(listing) {
|
|
88
|
+
if (!listing || !listing.sortState) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Use patched sort method which handles customSortedItems
|
|
92
|
+
listing.sort(listing.sortState);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Force re-sort on all browsers
|
|
96
|
+
*/
|
|
97
|
+
function resortAllBrowsers() {
|
|
98
|
+
tracker.forEach((browser) => {
|
|
99
|
+
const listing = browser.listing;
|
|
100
|
+
resortListing(listing);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Patch a DirListing to use custom sorting.
|
|
105
|
+
*/
|
|
106
|
+
function patchListing(listing) {
|
|
107
|
+
if (!listing || patchedListings.has(listing)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
patchedListings.add(listing);
|
|
111
|
+
// Store reference to our custom sorted items
|
|
112
|
+
let customSortedItems = [];
|
|
113
|
+
// Override sortedItems getter to return our custom sorted items
|
|
114
|
+
Object.defineProperty(listing, 'sortedItems', {
|
|
115
|
+
get: function () {
|
|
116
|
+
return customSortedItems;
|
|
117
|
+
},
|
|
118
|
+
configurable: true
|
|
119
|
+
});
|
|
120
|
+
// Store original sort
|
|
121
|
+
const originalSort = listing.sort.bind(listing);
|
|
122
|
+
// Override sort method
|
|
123
|
+
listing.sort = function (state) {
|
|
124
|
+
const model = this.model;
|
|
125
|
+
if (!model) {
|
|
126
|
+
originalSort(state);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const itemsArray = Array.from(model.items());
|
|
130
|
+
if (itemsArray.length === 0) {
|
|
131
|
+
this._sortState = state;
|
|
132
|
+
customSortedItems = [];
|
|
133
|
+
this._sortedItems = [];
|
|
134
|
+
originalSort(state);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const sorted = sortItems(itemsArray, state);
|
|
138
|
+
customSortedItems = sorted;
|
|
139
|
+
this._sortedItems = sorted;
|
|
140
|
+
this._sortState = state;
|
|
141
|
+
this.update();
|
|
142
|
+
};
|
|
143
|
+
// Hook into model refresh to re-sort
|
|
144
|
+
const model = listing.model;
|
|
145
|
+
if (model && model.refreshed) {
|
|
146
|
+
model.refreshed.connect(() => {
|
|
147
|
+
if (listing.sortState) {
|
|
148
|
+
listing.sort(listing.sortState);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// Trigger initial sort
|
|
153
|
+
if (listing.sortState) {
|
|
154
|
+
listing.sort(listing.sortState);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function patchFileBrowser(browser) {
|
|
158
|
+
const listing = browser.listing;
|
|
159
|
+
if (listing) {
|
|
160
|
+
patchListing(listing);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Load settings
|
|
164
|
+
if (settingRegistry) {
|
|
165
|
+
settingRegistry
|
|
166
|
+
.load('@jupyterlab/filebrowser-extension:browser')
|
|
167
|
+
.then(settings => {
|
|
168
|
+
var _a;
|
|
169
|
+
sortNotebooksFirst =
|
|
170
|
+
(_a = settings.get('sortNotebooksFirst').composite) !== null && _a !== void 0 ? _a : false;
|
|
171
|
+
settings.changed.connect(() => {
|
|
172
|
+
var _a;
|
|
173
|
+
sortNotebooksFirst =
|
|
174
|
+
(_a = settings.get('sortNotebooksFirst').composite) !== null && _a !== void 0 ? _a : false;
|
|
175
|
+
});
|
|
176
|
+
})
|
|
177
|
+
.catch(() => { });
|
|
178
|
+
settingRegistry
|
|
179
|
+
.load(PLUGIN_ID)
|
|
180
|
+
.then(settings => {
|
|
181
|
+
var _a;
|
|
182
|
+
useCLocaleSorting =
|
|
183
|
+
(_a = settings.get('useCLocaleSorting').composite) !== null && _a !== void 0 ? _a : true;
|
|
184
|
+
settings.changed.connect(() => {
|
|
185
|
+
var _a;
|
|
186
|
+
const newValue = (_a = settings.get('useCLocaleSorting').composite) !== null && _a !== void 0 ? _a : true;
|
|
187
|
+
useCLocaleSorting = newValue;
|
|
188
|
+
resortAllBrowsers();
|
|
189
|
+
});
|
|
190
|
+
})
|
|
191
|
+
.catch(() => { });
|
|
192
|
+
}
|
|
193
|
+
// Register toggle command
|
|
194
|
+
commands.addCommand(COMMAND_TOGGLE_UNIX_SORT, {
|
|
195
|
+
label: 'Unix Style Sorting',
|
|
196
|
+
caption: 'Sort: dot files first, uppercase before lowercase',
|
|
197
|
+
isToggleable: true,
|
|
198
|
+
isToggled: () => useCLocaleSorting,
|
|
199
|
+
execute: async () => {
|
|
200
|
+
if (settingRegistry) {
|
|
201
|
+
const settings = await settingRegistry.load(PLUGIN_ID);
|
|
202
|
+
await settings.set('useCLocaleSorting', !useCLocaleSorting);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
useCLocaleSorting = !useCLocaleSorting;
|
|
206
|
+
resortAllBrowsers();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// Add to context menu at bottom
|
|
211
|
+
app.contextMenu.addItem({
|
|
212
|
+
command: COMMAND_TOGGLE_UNIX_SORT,
|
|
213
|
+
selector: '.jp-DirListing-item',
|
|
214
|
+
rank: 100
|
|
215
|
+
});
|
|
216
|
+
app.contextMenu.addItem({
|
|
217
|
+
command: COMMAND_TOGGLE_UNIX_SORT,
|
|
218
|
+
selector: '.jp-DirListing',
|
|
219
|
+
rank: 100
|
|
220
|
+
});
|
|
221
|
+
// Wait for app to be fully restored before patching
|
|
222
|
+
app.restored.then(() => {
|
|
223
|
+
// Patch existing browsers
|
|
224
|
+
tracker.forEach(patchFileBrowser);
|
|
225
|
+
// Patch new browsers when added
|
|
226
|
+
tracker.widgetAdded.connect((_, browser) => {
|
|
227
|
+
patchFileBrowser(browser);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jupyterlab_file_browser_sorting_extension",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Jupyterlab extension to help with sorting of the files in file browser (i.e. case sensitive sorting)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jupyter",
|
|
7
|
+
"jupyterlab",
|
|
8
|
+
"jupyterlab-extension"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/stellarshenson/jupyterlab_file_browser_sorting_extension",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/stellarshenson/jupyterlab_file_browser_sorting_extension/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "BSD-3-Clause",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Stellars Henson",
|
|
17
|
+
"email": "konrad.jelen@gmail.com"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
21
|
+
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
22
|
+
"src/**/*.{ts,tsx}"
|
|
23
|
+
],
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"types": "lib/index.d.ts",
|
|
26
|
+
"style": "style/index.css",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/stellarshenson/jupyterlab_file_browser_sorting_extension.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "jlpm build:lib && jlpm build:labextension:dev",
|
|
33
|
+
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
34
|
+
"build:labextension": "jupyter labextension build .",
|
|
35
|
+
"build:labextension:dev": "jupyter labextension build --development True .",
|
|
36
|
+
"build:lib": "tsc --sourceMap",
|
|
37
|
+
"build:lib:prod": "tsc",
|
|
38
|
+
"clean": "jlpm clean:lib",
|
|
39
|
+
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
40
|
+
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|
|
41
|
+
"clean:labextension": "rimraf jupyterlab_file_browser_sorting_extension/labextension jupyterlab_file_browser_sorting_extension/_version.py",
|
|
42
|
+
"clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
|
|
43
|
+
"eslint": "jlpm eslint:check --fix",
|
|
44
|
+
"eslint:check": "eslint . --cache --ext .ts,.tsx",
|
|
45
|
+
"install:extension": "jlpm build",
|
|
46
|
+
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
47
|
+
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
48
|
+
"prettier": "jlpm prettier:base --write --list-different",
|
|
49
|
+
"prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
|
|
50
|
+
"prettier:check": "jlpm prettier:base --check",
|
|
51
|
+
"stylelint": "jlpm stylelint:check --fix",
|
|
52
|
+
"stylelint:check": "stylelint --cache \"style/**/*.css\"",
|
|
53
|
+
"test": "jest --coverage",
|
|
54
|
+
"watch": "run-p watch:src watch:labextension",
|
|
55
|
+
"watch:src": "tsc -w --sourceMap",
|
|
56
|
+
"watch:labextension": "jupyter labextension watch ."
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@jupyterlab/application": "^4.0.0",
|
|
60
|
+
"@jupyterlab/filebrowser": "^4.0.0",
|
|
61
|
+
"@jupyterlab/settingregistry": "^4.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@jupyterlab/builder": "^4.0.0",
|
|
65
|
+
"@jupyterlab/testutils": "^4.0.0",
|
|
66
|
+
"@types/jest": "^29.2.0",
|
|
67
|
+
"@types/json-schema": "^7.0.11",
|
|
68
|
+
"@types/react": "^18.0.26",
|
|
69
|
+
"@types/react-addons-linked-state-mixin": "^0.14.22",
|
|
70
|
+
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
71
|
+
"@typescript-eslint/parser": "^6.1.0",
|
|
72
|
+
"css-loader": "^6.7.1",
|
|
73
|
+
"eslint": "^8.36.0",
|
|
74
|
+
"eslint-config-prettier": "^8.8.0",
|
|
75
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
76
|
+
"jest": "^29.2.0",
|
|
77
|
+
"npm-run-all2": "^7.0.1",
|
|
78
|
+
"prettier": "^3.0.0",
|
|
79
|
+
"rimraf": "^5.0.1",
|
|
80
|
+
"source-map-loader": "^1.0.2",
|
|
81
|
+
"style-loader": "^3.3.1",
|
|
82
|
+
"stylelint": "^15.10.1",
|
|
83
|
+
"stylelint-config-recommended": "^13.0.0",
|
|
84
|
+
"stylelint-config-standard": "^34.0.0",
|
|
85
|
+
"stylelint-csstree-validator": "^3.0.0",
|
|
86
|
+
"stylelint-prettier": "^4.0.0",
|
|
87
|
+
"typescript": "~5.5.4",
|
|
88
|
+
"yjs": "^13.5.0"
|
|
89
|
+
},
|
|
90
|
+
"resolutions": {
|
|
91
|
+
"lib0": "0.2.97"
|
|
92
|
+
},
|
|
93
|
+
"sideEffects": [
|
|
94
|
+
"style/*.css",
|
|
95
|
+
"style/index.js"
|
|
96
|
+
],
|
|
97
|
+
"styleModule": "style/index.js",
|
|
98
|
+
"publishConfig": {
|
|
99
|
+
"access": "public"
|
|
100
|
+
},
|
|
101
|
+
"jupyterlab": {
|
|
102
|
+
"extension": true,
|
|
103
|
+
"outputDir": "jupyterlab_file_browser_sorting_extension/labextension",
|
|
104
|
+
"schemaDir": "schema"
|
|
105
|
+
},
|
|
106
|
+
"eslintIgnore": [
|
|
107
|
+
"node_modules",
|
|
108
|
+
"dist",
|
|
109
|
+
"coverage",
|
|
110
|
+
"**/*.d.ts",
|
|
111
|
+
"tests",
|
|
112
|
+
"**/__tests__",
|
|
113
|
+
"ui-tests"
|
|
114
|
+
],
|
|
115
|
+
"eslintConfig": {
|
|
116
|
+
"extends": [
|
|
117
|
+
"eslint:recommended",
|
|
118
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
119
|
+
"plugin:@typescript-eslint/recommended",
|
|
120
|
+
"plugin:prettier/recommended"
|
|
121
|
+
],
|
|
122
|
+
"parser": "@typescript-eslint/parser",
|
|
123
|
+
"parserOptions": {
|
|
124
|
+
"project": "tsconfig.json",
|
|
125
|
+
"sourceType": "module"
|
|
126
|
+
},
|
|
127
|
+
"plugins": [
|
|
128
|
+
"@typescript-eslint"
|
|
129
|
+
],
|
|
130
|
+
"rules": {
|
|
131
|
+
"@typescript-eslint/naming-convention": [
|
|
132
|
+
"error",
|
|
133
|
+
{
|
|
134
|
+
"selector": "interface",
|
|
135
|
+
"format": [
|
|
136
|
+
"PascalCase"
|
|
137
|
+
],
|
|
138
|
+
"custom": {
|
|
139
|
+
"regex": "^I[A-Z]",
|
|
140
|
+
"match": true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"@typescript-eslint/no-unused-vars": [
|
|
145
|
+
"warn",
|
|
146
|
+
{
|
|
147
|
+
"args": "none"
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
151
|
+
"@typescript-eslint/no-namespace": "off",
|
|
152
|
+
"@typescript-eslint/no-use-before-define": "off",
|
|
153
|
+
"@typescript-eslint/quotes": [
|
|
154
|
+
"error",
|
|
155
|
+
"single",
|
|
156
|
+
{
|
|
157
|
+
"avoidEscape": true,
|
|
158
|
+
"allowTemplateLiterals": false
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"curly": [
|
|
162
|
+
"error",
|
|
163
|
+
"all"
|
|
164
|
+
],
|
|
165
|
+
"eqeqeq": "error",
|
|
166
|
+
"prefer-arrow-callback": "error"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"prettier": {
|
|
170
|
+
"singleQuote": true,
|
|
171
|
+
"trailingComma": "none",
|
|
172
|
+
"arrowParens": "avoid",
|
|
173
|
+
"endOfLine": "auto",
|
|
174
|
+
"overrides": [
|
|
175
|
+
{
|
|
176
|
+
"files": "package.json",
|
|
177
|
+
"options": {
|
|
178
|
+
"tabWidth": 4
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
"stylelint": {
|
|
184
|
+
"extends": [
|
|
185
|
+
"stylelint-config-recommended",
|
|
186
|
+
"stylelint-config-standard",
|
|
187
|
+
"stylelint-prettier/recommended"
|
|
188
|
+
],
|
|
189
|
+
"plugins": [
|
|
190
|
+
"stylelint-csstree-validator"
|
|
191
|
+
],
|
|
192
|
+
"rules": {
|
|
193
|
+
"csstree/validator": true,
|
|
194
|
+
"property-no-vendor-prefix": null,
|
|
195
|
+
"selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
|
|
196
|
+
"selector-no-vendor-prefix": null,
|
|
197
|
+
"value-no-vendor-prefix": null
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
import { IFileBrowserFactory, FileBrowser } from '@jupyterlab/filebrowser';
|
|
6
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
7
|
+
import { Contents } from '@jupyterlab/services';
|
|
8
|
+
|
|
9
|
+
const PLUGIN_ID = 'jupyterlab_file_browser_sorting_extension:plugin';
|
|
10
|
+
const COMMAND_TOGGLE_UNIX_SORT = 'filebrowser:toggle-unix-sorting';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* LC_COLLATE=C (ASCIIbetical) comparison function.
|
|
14
|
+
* Sort order: . -> 0-9 -> A-Z -> _ -> a-z
|
|
15
|
+
*/
|
|
16
|
+
function cLocaleCompare(a: string, b: string): number {
|
|
17
|
+
const lenA = a.length;
|
|
18
|
+
const lenB = b.length;
|
|
19
|
+
const minLen = Math.min(lenA, lenB);
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < minLen; i++) {
|
|
22
|
+
const charCodeA = a.charCodeAt(i);
|
|
23
|
+
const charCodeB = b.charCodeAt(i);
|
|
24
|
+
if (charCodeA !== charCodeB) {
|
|
25
|
+
return charCodeA - charCodeB;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return lenA - lenB;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default locale comparison (case-insensitive).
|
|
33
|
+
*/
|
|
34
|
+
function defaultLocaleCompare(a: string, b: string): number {
|
|
35
|
+
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Global settings
|
|
39
|
+
let sortNotebooksFirst = false;
|
|
40
|
+
let useCLocaleSorting = true;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sort items using the specified comparison function.
|
|
44
|
+
*/
|
|
45
|
+
function sortItems(
|
|
46
|
+
items: Contents.IModel[],
|
|
47
|
+
state: { direction: 'ascending' | 'descending'; key: string }
|
|
48
|
+
): Contents.IModel[] {
|
|
49
|
+
const copy = [...items];
|
|
50
|
+
const reverse = state.direction === 'descending' ? -1 : 1;
|
|
51
|
+
|
|
52
|
+
function getPriority(item: Contents.IModel): number {
|
|
53
|
+
if (item.type === 'directory') {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
if (sortNotebooksFirst && item.type === 'notebook') {
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function compareByName(a: Contents.IModel, b: Contents.IModel): number {
|
|
63
|
+
return useCLocaleSorting
|
|
64
|
+
? cLocaleCompare(a.name, b.name)
|
|
65
|
+
: defaultLocaleCompare(a.name, b.name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
copy.sort((a, b) => {
|
|
69
|
+
const priorityA = getPriority(a);
|
|
70
|
+
const priorityB = getPriority(b);
|
|
71
|
+
if (priorityA !== priorityB) {
|
|
72
|
+
return priorityA - priorityB;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let result: number;
|
|
76
|
+
if (state.key === 'last_modified') {
|
|
77
|
+
result =
|
|
78
|
+
new Date(a.last_modified).getTime() -
|
|
79
|
+
new Date(b.last_modified).getTime();
|
|
80
|
+
} else if (state.key === 'file_size') {
|
|
81
|
+
result = (b.size ?? 0) - (a.size ?? 0);
|
|
82
|
+
} else {
|
|
83
|
+
result = compareByName(a, b);
|
|
84
|
+
}
|
|
85
|
+
return result * reverse;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return copy;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
92
|
+
id: PLUGIN_ID,
|
|
93
|
+
description: 'LC_COLLATE=C file browser sorting',
|
|
94
|
+
autoStart: true,
|
|
95
|
+
requires: [IFileBrowserFactory],
|
|
96
|
+
optional: [ISettingRegistry],
|
|
97
|
+
activate: (
|
|
98
|
+
app: JupyterFrontEnd,
|
|
99
|
+
factory: IFileBrowserFactory,
|
|
100
|
+
settingRegistry: ISettingRegistry | null
|
|
101
|
+
) => {
|
|
102
|
+
const { commands } = app;
|
|
103
|
+
const { tracker } = factory;
|
|
104
|
+
const patchedListings = new WeakSet<object>();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Force re-sort on a single listing
|
|
108
|
+
*/
|
|
109
|
+
function resortListing(listing: any): void {
|
|
110
|
+
if (!listing || !listing.sortState) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Use patched sort method which handles customSortedItems
|
|
114
|
+
listing.sort(listing.sortState);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Force re-sort on all browsers
|
|
119
|
+
*/
|
|
120
|
+
function resortAllBrowsers(): void {
|
|
121
|
+
tracker.forEach((browser: FileBrowser) => {
|
|
122
|
+
const listing = (browser as any).listing;
|
|
123
|
+
resortListing(listing);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Patch a DirListing to use custom sorting.
|
|
129
|
+
*/
|
|
130
|
+
function patchListing(listing: any): void {
|
|
131
|
+
if (!listing || patchedListings.has(listing)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
patchedListings.add(listing);
|
|
135
|
+
|
|
136
|
+
// Store reference to our custom sorted items
|
|
137
|
+
let customSortedItems: Contents.IModel[] = [];
|
|
138
|
+
|
|
139
|
+
// Override sortedItems getter to return our custom sorted items
|
|
140
|
+
Object.defineProperty(listing, 'sortedItems', {
|
|
141
|
+
get: function () {
|
|
142
|
+
return customSortedItems;
|
|
143
|
+
},
|
|
144
|
+
configurable: true
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Store original sort
|
|
148
|
+
const originalSort = listing.sort.bind(listing);
|
|
149
|
+
|
|
150
|
+
// Override sort method
|
|
151
|
+
listing.sort = function (state: {
|
|
152
|
+
direction: 'ascending' | 'descending';
|
|
153
|
+
key: string;
|
|
154
|
+
}): void {
|
|
155
|
+
const model = this.model;
|
|
156
|
+
if (!model) {
|
|
157
|
+
originalSort(state);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const itemsArray = Array.from(model.items()) as Contents.IModel[];
|
|
162
|
+
if (itemsArray.length === 0) {
|
|
163
|
+
this._sortState = state;
|
|
164
|
+
customSortedItems = [];
|
|
165
|
+
this._sortedItems = [];
|
|
166
|
+
originalSort(state);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const sorted = sortItems(itemsArray, state);
|
|
171
|
+
customSortedItems = sorted;
|
|
172
|
+
this._sortedItems = sorted;
|
|
173
|
+
this._sortState = state;
|
|
174
|
+
this.update();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Hook into model refresh to re-sort
|
|
178
|
+
const model = listing.model;
|
|
179
|
+
if (model && model.refreshed) {
|
|
180
|
+
model.refreshed.connect(() => {
|
|
181
|
+
if (listing.sortState) {
|
|
182
|
+
listing.sort(listing.sortState);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Trigger initial sort
|
|
188
|
+
if (listing.sortState) {
|
|
189
|
+
listing.sort(listing.sortState);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function patchFileBrowser(browser: FileBrowser): void {
|
|
194
|
+
const listing = (browser as any).listing;
|
|
195
|
+
if (listing) {
|
|
196
|
+
patchListing(listing);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Load settings
|
|
201
|
+
if (settingRegistry) {
|
|
202
|
+
settingRegistry
|
|
203
|
+
.load('@jupyterlab/filebrowser-extension:browser')
|
|
204
|
+
.then(settings => {
|
|
205
|
+
sortNotebooksFirst =
|
|
206
|
+
(settings.get('sortNotebooksFirst').composite as boolean) ?? false;
|
|
207
|
+
settings.changed.connect(() => {
|
|
208
|
+
sortNotebooksFirst =
|
|
209
|
+
(settings.get('sortNotebooksFirst').composite as boolean) ??
|
|
210
|
+
false;
|
|
211
|
+
});
|
|
212
|
+
})
|
|
213
|
+
.catch(() => {});
|
|
214
|
+
|
|
215
|
+
settingRegistry
|
|
216
|
+
.load(PLUGIN_ID)
|
|
217
|
+
.then(settings => {
|
|
218
|
+
useCLocaleSorting =
|
|
219
|
+
(settings.get('useCLocaleSorting').composite as boolean) ?? true;
|
|
220
|
+
|
|
221
|
+
settings.changed.connect(() => {
|
|
222
|
+
const newValue =
|
|
223
|
+
(settings.get('useCLocaleSorting').composite as boolean) ?? true;
|
|
224
|
+
useCLocaleSorting = newValue;
|
|
225
|
+
resortAllBrowsers();
|
|
226
|
+
});
|
|
227
|
+
})
|
|
228
|
+
.catch(() => {});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Register toggle command
|
|
232
|
+
commands.addCommand(COMMAND_TOGGLE_UNIX_SORT, {
|
|
233
|
+
label: 'Unix Style Sorting',
|
|
234
|
+
caption: 'Sort: dot files first, uppercase before lowercase',
|
|
235
|
+
isToggleable: true,
|
|
236
|
+
isToggled: () => useCLocaleSorting,
|
|
237
|
+
execute: async () => {
|
|
238
|
+
if (settingRegistry) {
|
|
239
|
+
const settings = await settingRegistry.load(PLUGIN_ID);
|
|
240
|
+
await settings.set('useCLocaleSorting', !useCLocaleSorting);
|
|
241
|
+
} else {
|
|
242
|
+
useCLocaleSorting = !useCLocaleSorting;
|
|
243
|
+
resortAllBrowsers();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Add to context menu at bottom
|
|
249
|
+
app.contextMenu.addItem({
|
|
250
|
+
command: COMMAND_TOGGLE_UNIX_SORT,
|
|
251
|
+
selector: '.jp-DirListing-item',
|
|
252
|
+
rank: 100
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
app.contextMenu.addItem({
|
|
256
|
+
command: COMMAND_TOGGLE_UNIX_SORT,
|
|
257
|
+
selector: '.jp-DirListing',
|
|
258
|
+
rank: 100
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Wait for app to be fully restored before patching
|
|
262
|
+
app.restored.then(() => {
|
|
263
|
+
// Patch existing browsers
|
|
264
|
+
tracker.forEach(patchFileBrowser);
|
|
265
|
+
|
|
266
|
+
// Patch new browsers when added
|
|
267
|
+
tracker.widgetAdded.connect((_, browser) => {
|
|
268
|
+
patchFileBrowser(browser);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export default plugin;
|
package/style/base.css
ADDED
package/style/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url('base.css');
|
package/style/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './base.css';
|