Skip to content

Commit 8398e84

Browse files
Allow marking and viewing favorite cells in notebooks (#32)
* add-cell-fav * fix-lint * refactor-code * add-icon-in-input-prompt * apply-suggestions * apply-linting * make-show-stars-configurable * apply-linting * bump-version * add-tests * fix-lint * apply-suggestions * lint * apply-feedbacks * update-snapshots * update-tests * `onlyfavoriteCells` → `onlyFavoriteCells` * disconnect-listener-on-disposal --------- Co-authored-by: Michał Krassowski <[email protected]>
1 parent 7443c1c commit 8398e84

File tree

13 files changed

+2781
-474
lines changed

13 files changed

+2781
-474
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
### Bugs fixed
3030

31-
- Prevent listing/favourite icon jitter [#21](https://github.com/jupyterlab-contrib/jupyterlab-favorites/pull/21) ([@krassowski](https://github.com/krassowski))
31+
- Prevent listing/favorite icon jitter [#21](https://github.com/jupyterlab-contrib/jupyterlab-favorites/pull/21) ([@krassowski](https://github.com/krassowski))
3232

3333
### Documentation improvements
3434

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@
7070
"dependencies": {
7171
"@jupyterlab/application": "^4.0.5",
7272
"@jupyterlab/apputils": "^4.1.5",
73+
"@jupyterlab/cells": "^4.0.5",
7374
"@jupyterlab/coreutils": "^6.0.5",
7475
"@jupyterlab/docregistry": "^4.0.5",
7576
"@jupyterlab/filebrowser": "^4.0.5",
7677
"@jupyterlab/mainmenu": "^4.0.5",
78+
"@jupyterlab/notebook": "^4.0.5",
7779
"@jupyterlab/services": "^7.0.5",
7880
"@jupyterlab/settingregistry": "^4.0.5",
7981
"@jupyterlab/ui-components": "^4.0.5",

schema/favorites.json

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
},
2424
"root": {
2525
"type": "string",
26-
"description": "Root path used for filtering out favourites from instances of JupyterLab launched in a different root directory. If not given the favourite will be shown if the associated file exists."
26+
"description": "Root path used for filtering out favorites from instances of JupyterLab launched in a different root directory. If not given the favorite will be shown if the associated file exists."
2727
}
2828
},
2929
"required": ["path"],
@@ -48,6 +48,25 @@
4848
"rank": 3,
4949
"selector": ".jp-DirListing-item[data-isdir]"
5050
}
51+
],
52+
"main": [
53+
{
54+
"id": "jp-mainmenu-view",
55+
"rank": 10,
56+
"items": [
57+
{
58+
"command": "jupyterlab-favorites:toggle-cell-visibility"
59+
}
60+
]
61+
}
62+
]
63+
},
64+
"jupyter.lab.toolbars": {
65+
"Cell": [
66+
{
67+
"name": "cellFavoriteToggle",
68+
"rank": 110
69+
}
5170
]
5271
},
5372
"jupyter.lab.setting-icon": "jupyterlab-favorites:filledStar",
@@ -67,6 +86,17 @@
6786
"description": "Toggles the favorites widget above the filebrowser breadcrumbs.",
6887
"title": "Show Widget",
6988
"type": "boolean"
89+
},
90+
"showStarsOnCells": {
91+
"type": "string",
92+
"title": "Show Stars on Cells",
93+
"description": "Controls the visibility of star icons on cells.",
94+
"default": "onlyFavoriteCells",
95+
"oneOf": [
96+
{ "const": "allCells", "title": "All Cells" },
97+
{ "const": "onlyFavoriteCells", "title": "Only favorite Cells" },
98+
{ "const": "never", "title": "Never" }
99+
]
70100
}
71101
},
72102
"title": "Favorites",

src/index.tsx

Lines changed: 204 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,49 @@ import {
77
IDefaultFileBrowser,
88
IFileBrowserFactory
99
} from '@jupyterlab/filebrowser';
10+
import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
11+
import { Cell, ICellModel } from '@jupyterlab/cells';
1012
import { IMainMenu } from '@jupyterlab/mainmenu';
1113
import { ISettingRegistry } from '@jupyterlab/settingregistry';
12-
import { ReactWidget, UseSignal, folderIcon } from '@jupyterlab/ui-components';
14+
import {
15+
ReactWidget,
16+
ToolbarButton,
17+
UseSignal,
18+
folderIcon
19+
} from '@jupyterlab/ui-components';
1320
import { Menu, PanelLayout, Widget } from '@lumino/widgets';
1421
import React from 'react';
1522
import { FavoritesBreadCrumbs, FavoritesWidget } from './components';
1623
import { starIcon } from './icons';
1724
import { FavoritesManager } from './manager';
18-
import { IFavorites, PluginIDs, CommandIDs } from './token';
1925
import {
26+
IFavorites,
27+
PluginIDs,
28+
CommandIDs,
29+
SettingIDs,
30+
ShowStarsTypes,
31+
FAVORITE_TAG
32+
} from './token';
33+
import {
34+
changeShowStarsOnCells,
2035
getFavoritesIcon,
2136
getPinnerActionDescription,
22-
mergePaths
37+
mergePaths,
38+
toggleCellFavorite,
39+
updateCellClasses,
40+
updateCellFavoriteButton,
41+
updateSingleCellClass
2342
} from './utils';
24-
import { InputDialog } from '@jupyterlab/apputils';
43+
import {
44+
ICommandPalette,
45+
InputDialog,
46+
IToolbarWidgetRegistry
47+
} from '@jupyterlab/apputils';
48+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
49+
import { IEditorServices } from '@jupyterlab/codeeditor';
50+
import { StarredNotebookContentFactory } from './starPrompt';
2551

26-
export { IFavorites } from './token';
52+
export { IFavorites, FAVORITE_TAG } from './token';
2753

2854
const BREADCRUMBS_CLASS = 'jp-FileBrowser-crumbs';
2955

@@ -33,27 +59,47 @@ const BREADCRUMBS_CLASS = 'jp-FileBrowser-crumbs';
3359
const FAVORITE_ITEM_PINNER_CLASS = 'jp-Favorites-pinner';
3460

3561
/**
36-
* Modifier class added to breadcrumbs to ensure the favourite icon has enough spacing.
62+
* Modifier class added to breadcrumbs to ensure the favorite icon has enough spacing.
3763
*/
3864
const BREADCUMBS_FAVORITES_CLASS = 'jp-Favorites-crumbs';
3965

66+
/**
67+
* Class set to notebook when filtering cells by favorite is enabled
68+
*/
69+
const FAVORITE_FILTER_CLASS = 'jp-favorites-filter-active';
70+
71+
/**
72+
* Setting key for toggling the visibility of star icons on cells.
73+
*/
74+
const SHOW_STARS_ON_CELLS = 'showStarsOnCells';
4075
/**
4176
* Initialization data for the jupyterlab-favorites extension.
4277
*/
4378
const favorites: JupyterFrontEndPlugin<IFavorites> = {
4479
id: PluginIDs.favorites,
4580
autoStart: true,
46-
requires: [IFileBrowserFactory, ISettingRegistry],
81+
requires: [IFileBrowserFactory, ISettingRegistry, INotebookTracker],
4782
provides: IFavorites,
48-
optional: [IDefaultFileBrowser, IMainMenu],
49-
activate: (
83+
optional: [
84+
ITranslator,
85+
IDefaultFileBrowser,
86+
IMainMenu,
87+
IToolbarWidgetRegistry,
88+
ICommandPalette
89+
],
90+
activate: async (
5091
app: JupyterFrontEnd,
5192
factory: IFileBrowserFactory,
5293
settingsRegistry: ISettingRegistry,
94+
notebookTracker: INotebookTracker,
95+
translator: ITranslator | null,
5396
filebrowser: IDefaultFileBrowser | null,
54-
mainMenu: IMainMenu | null
55-
): IFavorites => {
97+
mainMenu: IMainMenu | null,
98+
toolbarRegistry: IToolbarWidgetRegistry | null,
99+
palette: ICommandPalette | null
100+
): Promise<IFavorites> => {
56101
console.log('JupyterLab extension jupyterlab-favorites is activated!');
102+
const trans = (translator ?? nullTranslator).load('jupyterlab');
57103
const docRegistry = app.docRegistry;
58104
const { commands, serviceManager } = app;
59105
const favoritesManager = new FavoritesManager(
@@ -63,6 +109,7 @@ const favorites: JupyterFrontEndPlugin<IFavorites> = {
63109
serviceManager.contents
64110
);
65111
favoritesManager.init();
112+
const favoriteSettings = await settingsRegistry.load(SettingIDs.favorites);
66113

67114
if (filebrowser) {
68115
const favoritesWidget = new FavoritesWidget(
@@ -129,6 +176,29 @@ const favorites: JupyterFrontEndPlugin<IFavorites> = {
129176
};
130177
const { tracker } = factory;
131178

179+
toolbarRegistry?.addFactory<Cell<ICellModel>>(
180+
'Cell',
181+
'cellFavoriteToggle',
182+
args => {
183+
const cell = args.model;
184+
const tags = cell.getMetadata('tags');
185+
const isFavorite = Array.isArray(tags) && tags.includes(FAVORITE_TAG);
186+
187+
const button = new ToolbarButton({
188+
tooltip: isFavorite ? 'Unfavorite cell' : 'Favorite cell',
189+
icon: getFavoritesIcon(isFavorite),
190+
onClick: () => toggleCellFavorite(cell)
191+
});
192+
193+
// Connect metadataChanged signal to update the icon and tooltip dynamically
194+
cell.metadataChanged.connect(() => {
195+
updateCellFavoriteButton(button, args);
196+
});
197+
198+
return button;
199+
}
200+
);
201+
132202
commands.addCommand(CommandIDs.addOrRemoveFavorite, {
133203
execute: () => {
134204
const selectedItems = getSelectedItems();
@@ -178,6 +248,100 @@ const favorites: JupyterFrontEndPlugin<IFavorites> = {
178248
label: 'Remove Favorite'
179249
});
180250

251+
commands.addCommand(CommandIDs.toggleCellFavorite, {
252+
execute: () => {
253+
const notebookPanel = notebookTracker.currentWidget;
254+
if (!notebookPanel || !notebookPanel.content) {
255+
return;
256+
}
257+
const notebook = notebookPanel.content;
258+
const activeCell = notebook.activeCell;
259+
if (!activeCell) {
260+
console.warn('No active cell found to toggle favorite');
261+
return;
262+
}
263+
toggleCellFavorite(activeCell.model);
264+
}
265+
});
266+
267+
const attachViewportListener = (notebook: NotebookPanel) => {
268+
if (!notebook || !notebook.content) {
269+
return;
270+
}
271+
changeShowStarsOnCells(
272+
favoriteSettings.get(SHOW_STARS_ON_CELLS).composite as ShowStarsTypes,
273+
notebook
274+
);
275+
notebook.content.cellInViewportChanged.connect((_, cell) => {
276+
updateSingleCellClass(cell);
277+
});
278+
notebook.disposed.connect(() => {
279+
notebook.content.cellInViewportChanged.disconnect((_, cell) => {
280+
updateSingleCellClass(cell);
281+
});
282+
});
283+
};
284+
285+
// Attach to currently open notebooks
286+
notebookTracker.forEach(notebook => {
287+
attachViewportListener(notebook);
288+
});
289+
290+
// Attach to any newly opened notebooks
291+
notebookTracker.widgetAdded.connect((_, notebook) => {
292+
attachViewportListener(notebook);
293+
});
294+
295+
notebookTracker.currentChanged.connect((_, notebook) => {
296+
changeShowStarsOnCells(
297+
favoriteSettings.get(SHOW_STARS_ON_CELLS).composite as ShowStarsTypes,
298+
notebook
299+
);
300+
});
301+
302+
favoriteSettings.changed.connect(() => {
303+
changeShowStarsOnCells(
304+
favoriteSettings.get(SHOW_STARS_ON_CELLS).composite as ShowStarsTypes,
305+
notebookTracker.currentWidget
306+
);
307+
});
308+
309+
commands.addCommand(CommandIDs.toggleCellsVisibility, {
310+
isToggled: () => {
311+
const notebookPanel = notebookTracker.currentWidget;
312+
if (!notebookPanel || !notebookPanel.content) {
313+
return false;
314+
}
315+
return notebookPanel.content.node.classList.contains(
316+
FAVORITE_FILTER_CLASS
317+
);
318+
},
319+
label: 'Show Only Favorite Cells',
320+
execute: () => {
321+
const notebookPanel = notebookTracker.currentWidget;
322+
if (!notebookPanel || !notebookPanel.content) {
323+
return;
324+
}
325+
326+
const notebook = notebookPanel.content;
327+
const isFavoriteMode = notebook.node.classList.contains(
328+
FAVORITE_FILTER_CLASS
329+
);
330+
331+
if (isFavoriteMode) {
332+
notebook.node.classList.remove(FAVORITE_FILTER_CLASS);
333+
} else {
334+
notebook.node.classList.add(FAVORITE_FILTER_CLASS);
335+
}
336+
updateCellClasses(notebook);
337+
},
338+
isEnabled: () => {
339+
const currentWidget = app.shell.currentWidget;
340+
const notebook = notebookTracker.currentWidget;
341+
return !!(notebook !== null && notebook === currentWidget);
342+
}
343+
});
344+
181345
commands.addCommand(CommandIDs.renameFavorite, {
182346
execute: async args => {
183347
let { path, displayName } = args as {
@@ -247,6 +411,12 @@ const favorites: JupyterFrontEndPlugin<IFavorites> = {
247411
label: 'Clear Favorites'
248412
});
249413

414+
// Add commands to palette
415+
palette?.addItem({
416+
command: CommandIDs.toggleCellsVisibility,
417+
category: trans.__('Notebook Operations')
418+
});
419+
250420
// Main Menu
251421
if (mainMenu) {
252422
mainMenu.fileMenu.addGroup(
@@ -303,4 +473,26 @@ const favorites: JupyterFrontEndPlugin<IFavorites> = {
303473
}
304474
};
305475

306-
export default favorites;
476+
/**
477+
* Plugin that provides the custom notebook factory with star icons
478+
*/
479+
export const notebookFactoryPlugin: JupyterFrontEndPlugin<NotebookPanel.IContentFactory> =
480+
{
481+
id: PluginIDs.notebookFactory,
482+
description: 'Provides notebook cell factory with star icons',
483+
provides: NotebookPanel.IContentFactory,
484+
requires: [IEditorServices],
485+
autoStart: true,
486+
activate: (app: JupyterFrontEnd, editorServices: IEditorServices) => {
487+
const editorFactory = editorServices.factoryService.newInlineEditor;
488+
489+
const factory = new StarredNotebookContentFactory({
490+
editorFactory,
491+
app
492+
});
493+
494+
return factory;
495+
}
496+
};
497+
498+
export default [favorites, notebookFactoryPlugin];

0 commit comments

Comments
 (0)