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 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
+ [![GitHub Actions](https://github.com/stellarshenson/jupyterlab_file_browser_sorting_extension/actions/workflows/build.yml/badge.svg)](https://github.com/stellarshenson/jupyterlab_file_browser_sorting_extension/actions/workflows/build.yml)
4
+ [![npm version](https://img.shields.io/npm/v/jupyterlab_file_browser_sorting_extension.svg)](https://www.npmjs.com/package/jupyterlab_file_browser_sorting_extension)
5
+ [![PyPI version](https://img.shields.io/pypi/v/jupyterlab-file-browser-sorting-extension.svg)](https://pypi.org/project/jupyterlab-file-browser-sorting-extension/)
6
+ [![Total PyPI downloads](https://static.pepy.tech/badge/jupyterlab-file-browser-sorting-extension)](https://pepy.tech/project/jupyterlab-file-browser-sorting-extension)
7
+ [![JupyterLab 4](https://img.shields.io/badge/JupyterLab-4-orange.svg)](https://jupyterlab.readthedocs.io/en/stable/)
8
+ [![Brought To You By KOLOMOLO](https://img.shields.io/badge/Brought%20To%20You%20By-KOLOMOLO-00ffff?style=flat)](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
@@ -0,0 +1,3 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ declare const plugin: JupyterFrontEndPlugin<void>;
3
+ export default plugin;
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
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
3
+ */
4
+
5
+ describe('jupyterlab_file_browser_sorting_extension', () => {
6
+ it('should be tested', () => {
7
+ expect(1 + 1).toEqual(2);
8
+ });
9
+ });
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
@@ -0,0 +1,5 @@
1
+ /*
2
+ See the JupyterLab Developer Guide for useful CSS Patterns:
3
+
4
+ https://jupyterlab.readthedocs.io/en/stable/developer/css.html
5
+ */
@@ -0,0 +1 @@
1
+ @import url('base.css');
package/style/index.js ADDED
@@ -0,0 +1 @@
1
+ import './base.css';