Repository: jupyterlab-contrib/jupyterlab-favorites (language: python)

Issue:
Add the ability to rename favorites
Since it's possible to have favorites that point to identically named files or directories living in different locations in the filesystem (e.g. `README.md` files tend to exist everywhere), it would be very useful to be able to rename the actual favorite so the user can pick a more mnemonic identifier.

All bookmark managers in browsers have this feature, and favorites is effectively a  Lab bookmark manager, so it seems like a reasonable addition.

A simple "rename favorite" entry on right-click would do the job, though ideally it would also allow the user to pick a name when creating the entry.


Discussion / hints:
Thanks for the suggestion @fperez 
I was about to log the same issue but with a different solution :)

For me, instead of renaming the favorite, it could give you more information about where it is located without needing to wait for the tooltip. For example, in VSCode, when two files with the same name are opened, it adds location information to the tab in a subtle way:

![image](https://user-images.githubusercontent.com/1813603/164082407-94ffb5c4-6789-4450-9ad0-e45c73262c24.png)

Just curious, are you (still) working on this enhancement? I like the idea by @mlucool, just adding the parent path after the favorite name (that info is already available in the link hover). E.g.

> Favorite name / (parent path)

Fix patch:
diff --git a/schema/favorites.json b/schema/favorites.json
index 7929381..3d79214 100644
--- a/schema/favorites.json
+++ b/schema/favorites.json
@@ -37,6 +37,11 @@
         "rank": 0,
         "selector": ".jp-Favorites-item"
       },
+      {
+        "command": "jupyterlab-favorites:rename-favorite",
+        "rank": 1,
+        "selector": ".jp-Favorites-item"
+      },
       {
         "command": "jupyterlab-favorites:add-or-remove-favorite",
         "rank": 3,
diff --git a/src/components.tsx b/src/components.tsx
index 1274f16..7155b1c 100644
--- a/src/components.tsx
+++ b/src/components.tsx
@@ -76,9 +76,14 @@ namespace types {
 
 const FavoriteComponent = (props: types.FavoriteComponentProps) => {
   const { favorite, handleClick } = props;
-  let displayName = getName(favorite.path);
+
+  let [displayName, dirname] = getName(favorite.path);
   if (favorite.name) {
     displayName = favorite.name;
+    // Hide path name if a display name is defined
+    dirname = '';
+  } else {
+    dirname = '/' + dirname;
   }
 
   let FavoriteIcon: LabIcon;
@@ -100,9 +105,13 @@ const FavoriteComponent = (props: types.FavoriteComponentProps) => {
       onClick={e => {
         handleClick(favorite);
       }}
+      data-path={favorite.path}
     >
       <FavoriteIcon.react className={FAVORITE_ITEM_ICON_CLASS} tag="span" />
-      <span className={FAVORITE_ITEM_TEXT_CLASS}>{displayName}</span>
+      <span className={FAVORITE_ITEM_TEXT_CLASS}>
+        {displayName}
+        <span className="jp-Favorites-dirname">{dirname}</span>
+      </span>
     </div>
   );
 };
diff --git a/src/index.tsx b/src/index.tsx
index d4a8896..79f7c12 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -10,6 +10,7 @@ import {
 import { IMainMenu } from '@jupyterlab/mainmenu';
 import { ISettingRegistry } from '@jupyterlab/settingregistry';
 import { ReactWidget, UseSignal, folderIcon } from '@jupyterlab/ui-components';
+import { toArray } from '@lumino/algorithm';
 import { Throttler } from '@lumino/polling';
 import { Menu, PanelLayout, Widget } from '@lumino/widgets';
 import React from 'react';
@@ -22,6 +23,7 @@ import {
   getPinnerActionDescription,
   mergePaths
 } from './utils';
+import { InputDialog } from '@jupyterlab/apputils';
 
 export { IFavorites } from './token';
 
@@ -132,7 +134,7 @@ const favorites: JupyterFrontEndPlugin<IFavorites> = {
       if (!widget) {
         return [];
       }
-      return Array.from(widget.selectedItems());
+      return toArray(widget.selectedItems());
     };
     const { tracker } = factory;
 
@@ -178,17 +180,82 @@ const favorites: JupyterFrontEndPlugin<IFavorites> = {
         const contextNode: HTMLElement = app.contextMenuHitTest(node =>
           node.classList.contains('jp-Favorites-item')
         )!;
-        const fullPath = contextNode.getAttribute('title')!;
-        let path = fullPath.replace(favoritesManager.serverRoot, '');
-        if (path.startsWith('/')) {
-          path = path.slice(1);
-        }
+        const path = contextNode.dataset['path']!;
         favoritesManager.removeFavorite(path);
       },
       icon: starIcon,
       label: 'Remove Favorite'
     });
 
+    commands.addCommand(CommandIDs.renameFavorite, {
+      execute: async args => {
+        let { path, displayName } = args as {
+          path?: string;
+          displayName?: string;
+        };
+        if (!path) {
+          const contextNode: HTMLElement = app.contextMenuHitTest(node =>
+            node.classList.contains('jp-Favorites-item')
+          )!;
+          path = contextNode.dataset['path'];
+        }
+
+        if (!path) {
+          return;
+        }
+
+        if (!displayName) {
+          const result = await InputDialog.getText({
+            title: 'Rename favorite',
+            label: `Name for favorite at '${path}'`,
+            okLabel: 'Rename',
+            placeholder: 'Display name'
+          });
+
+          displayName = result.button.accept ? result.value ?? '' : '';
+        }
+
+        if (!displayName) {
+          return;
+        }
+
+        favoritesManager.renameFavorite(path, displayName);
+      },
+      label: 'Rename Favorite'
+    });
+
+    // Commands
+    commands.addCommand(CommandIDs.openFavorite, {
+      execute: async args => {
+        const favorite = args.favorite as IFavorites.Favorite;
+        const path = favorite.path === '' ? '/' : favorite.path;
+        await commands.execute('filebrowser:open-path', { path });
+      },
+      label: args => {
+        const favorite = args.favorite as IFavorites.Favorite;
+        return mergePaths(favorite.root, favorite.path);
+      }
+    });
+    commands.addCommand(CommandIDs.toggleFavoritesWidget, {
+      execute: async args => {
+        const showWidget = args.showWidget as boolean;
+        favoritesManager.saveSettings({ showWidget: !showWidget });
+      },
+      label: args => {
+        const showWidget = args.showWidget as boolean;
+        return `${showWidget ? 'Hide' : 'Show'} Favorites Widget`;
+      },
+      isVisible: () => favoritesManager.visibleFavorites().length > 0
+    });
+    commands.addCommand(CommandIDs.restoreDefaults, {
+      execute: () => favoritesManager.restoreDefaults(),
+      label: 'Restore Defaults'
+    });
+    commands.addCommand(CommandIDs.clearFavorites, {
+      execute: () => favoritesManager.clearFavorites(),
+      label: 'Clear Favorites'
+    });
+
     // Main Menu
     if (mainMenu) {
       mainMenu.fileMenu.addGroup(
@@ -240,37 +307,6 @@ const favorites: JupyterFrontEndPlugin<IFavorites> = {
         console.log(e);
       }
     }
-    // Commands
-    commands.addCommand(CommandIDs.openFavorite, {
-      execute: async args => {
-        const favorite = args.favorite as IFavorites.Favorite;
-        const path = favorite.path === '' ? '/' : favorite.path;
-        await commands.execute('filebrowser:open-path', { path });
-      },
-      label: args => {
-        const favorite = args.favorite as IFavorites.Favorite;
-        return mergePaths(favorite.root, favorite.path);
-      }
-    });
-    commands.addCommand(CommandIDs.toggleFavoritesWidget, {
-      execute: async args => {
-        const showWidget = args.showWidget as boolean;
-        favoritesManager.saveSettings({ showWidget: !showWidget });
-      },
-      label: args => {
-        const showWidget = args.showWidget as boolean;
-        return `${showWidget ? 'Hide' : 'Show'} Favorites Widget`;
-      },
-      isVisible: () => favoritesManager.visibleFavorites().length > 0
-    });
-    commands.addCommand(CommandIDs.restoreDefaults, {
-      execute: () => favoritesManager.restoreDefaults(),
-      label: 'Restore Defaults'
-    });
-    commands.addCommand(CommandIDs.clearFavorites, {
-      execute: () => favoritesManager.clearFavorites(),
-      label: 'Clear Favorites'
-    });
 
     return favoritesManager;
   }
diff --git a/src/manager.ts b/src/manager.ts
index 6c5db9b..83a4a08 100644
--- a/src/manager.ts
+++ b/src/manager.ts
@@ -97,6 +97,23 @@ export class FavoritesManager {
     this._settingsRegistry.upload(SettingIDs.favorites, newSettings);
   }
 
+  /**
+   * Rename a favorite
+   * @param path Favorite path
+   * @param displayName Favorite name
+   */
+  async renameFavorite(path: string, displayName: string): Promise<void> {
+    // Copy the favorites
+    const favorites = this._favorites.slice();
+    const favorite = favorites.find(
+      f => f.root === this.serverRoot && f.path === path
+    );
+    if (favorite) {
+      favorite.name = displayName;
+      this.saveSettings({ favorites });
+    }
+  }
+
   async removeFavorite(path: string): Promise<void> {
     const favorites = this._favorites.slice();
     const index = favorites.findIndex(
diff --git a/src/token.ts b/src/token.ts
index b52ed2f..c2e5a04 100644
--- a/src/token.ts
+++ b/src/token.ts
@@ -7,6 +7,7 @@ export namespace PluginIDs {
 export namespace CommandIDs {
   export const addOrRemoveFavorite = `${PluginIDs.favorites}:add-or-remove-favorite`;
   export const removeFavorite = `${PluginIDs.favorites}:remove-favorite`;
+  export const renameFavorite = `${PluginIDs.favorites}:rename-favorite`;
   export const openFavorite = `${PluginIDs.favorites}:open-favorite`;
   export const toggleFavoritesWidget = `${PluginIDs.favorites}:toggle-favorites-widget`;
   export const restoreDefaults = `${PluginIDs.favorites}:restore-defaults`;
diff --git a/src/utils.ts b/src/utils.ts
index a2ff1b1..303df06 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,17 +1,18 @@
 import { LabIcon } from '@jupyterlab/ui-components';
 import { filledStarIcon, starIcon } from './icons';
+import { PathExt } from '@jupyterlab/coreutils';
 
 export function getFavoritesIcon(filled: boolean): LabIcon {
   return filled ? filledStarIcon : starIcon;
 }
 
-export function getName(path: string): string {
-  let name = '';
-  const parts = path.split('/');
-  if (parts.length > 0) {
-    name = parts[parts.length - 1];
-  }
-  return name;
+/**
+ * Split the file path in two parts: the name and the folder
+ * @param path Path to display
+ * @returns [name, folder]
+ */
+export function getName(path: string): [string, string] {
+  return [PathExt.basename(path), PathExt.dirname(path)];
 }
 
 export function getPinnerActionDescription(showRemove: boolean): string {
@@ -19,11 +20,5 @@ export function getPinnerActionDescription(showRemove: boolean): string {
 }
 
 export function mergePaths(root: string, path: string): string {
-  if (root.endsWith('/')) {
-    root = root.slice(0, -1);
-  }
-  if (path.endsWith('/')) {
-    path = path.slice(1);
-  }
-  return `${root}/${path}`;
+  return PathExt.join(root, path);
 }
diff --git a/style/base.css b/style/base.css
index 16e7777..a9487f5 100644
--- a/style/base.css
+++ b/style/base.css
@@ -53,7 +53,7 @@
 }
 
 .jp-Favorites-itemText {
-  flex: 1 0 64px;
+  flex: 1 1 64px;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -62,3 +62,9 @@
   -ms-user-select: none;
   user-select: none;
 }
+
+.jp-Favorites-dirname {
+  color: var(--jp-ui-font-color2);
+  font-size: var(--jp-ui-font-size0);
+  margin: 0 4px;
+}


Test patch:
diff --git a/ui-tests/tests/jupyterlab_favorites.spec.ts b/ui-tests/tests/jupyterlab_favorites.spec.ts
index 8752111..8e343f0 100644
--- a/ui-tests/tests/jupyterlab_favorites.spec.ts
+++ b/ui-tests/tests/jupyterlab_favorites.spec.ts
@@ -62,3 +62,20 @@ test('should remove a favorite from breadcrumb', async ({ page }) => {
     page.locator('.jp-Favorites').getByText('Untitled Folder')
   ).toHaveCount(0);
 });
+
+test('should rename a favorite', async ({ page }) => {
+  await page.getByText('untitled.txt').click({ button: 'right' });
+  await page.getByRole('menuitem', { name: 'Add Favorite' }).click();
+
+  await page
+    .locator('.jp-Favorites')
+    .getByText('untitled.txt')
+    .click({ button: 'right' });
+  await page.getByRole('menuitem', { name: 'Rename Favorite' }).click();
+
+  await page.getByRole('textbox', { name: 'Name for favorite' }).fill('magics');
+
+  await page.getByRole('button', { name: 'Rename' }).click();
+
+  await expect(page.locator('.jp-Favorites').getByText('magics')).toBeVisible();
+});